feat: add fear
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 799 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 8.6 MiB |
@@ -0,0 +1,4 @@
|
|||||||
|
.canvas {
|
||||||
|
width: 100vw !important;
|
||||||
|
height: 100vh !important;
|
||||||
|
}
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import './page.css';
|
||||||
|
|
||||||
|
import { PointerLockControls, useTexture } from "@react-three/drei";
|
||||||
|
import { Canvas, useFrame, useThree } from "@react-three/fiber";
|
||||||
|
import { BrightnessContrast, EffectComposer, Noise, Pixelation, Vignette } from "@react-three/postprocessing";
|
||||||
|
import { Suspense, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { AmbientSound } from './scene-components/ambient-sound';
|
||||||
|
import { MeshStandardNodeMaterial } from 'three/webgpu';
|
||||||
|
|
||||||
|
const HALLWAY_LENGTH = 40;
|
||||||
|
const HALLWAY_WIDTH = 6;
|
||||||
|
const HALLWAY_HEIGHT = 5;
|
||||||
|
|
||||||
|
const PLAYER_HEIGHT = 3;
|
||||||
|
|
||||||
|
function Player() {
|
||||||
|
const { camera } = useThree();
|
||||||
|
const controls = usePlayerControls();
|
||||||
|
|
||||||
|
const flashlightRef = useRef<THREE.SpotLight>(null);
|
||||||
|
const movementCounter = useRef<number>(0);
|
||||||
|
|
||||||
|
const frontVector = new THREE.Vector3();
|
||||||
|
const sideVector = new THREE.Vector3();
|
||||||
|
const direction = new THREE.Vector3();
|
||||||
|
|
||||||
|
const viewDirection = new THREE.Vector3();
|
||||||
|
|
||||||
|
useFrame((state, delta) => {
|
||||||
|
frontVector.set(0, 0, Number(controls.Backward) - Number(controls.Forward));
|
||||||
|
sideVector.set(Number(controls.Left) - Number(controls.Right), 0, 0);
|
||||||
|
|
||||||
|
direction
|
||||||
|
.subVectors(frontVector, sideVector)
|
||||||
|
.normalize()
|
||||||
|
.multiplyScalar(4 * delta)
|
||||||
|
.applyEuler(camera.rotation);
|
||||||
|
|
||||||
|
camera.position.x += direction.x;
|
||||||
|
camera.position.z += direction.z;
|
||||||
|
|
||||||
|
const isMoving = controls.Forward || controls.Backward || controls.Left || controls.Right;
|
||||||
|
if (isMoving) {
|
||||||
|
movementCounter.current += delta * 10;
|
||||||
|
camera.position.y = PLAYER_HEIGHT + Math.sin(movementCounter.current) * 0.08;
|
||||||
|
camera.position.x += Math.cos(movementCounter.current / 2) * 0.006;
|
||||||
|
} else
|
||||||
|
camera.position.y = THREE.MathUtils.lerp(camera.position.y, PLAYER_HEIGHT, 5 * delta);
|
||||||
|
|
||||||
|
|
||||||
|
if (flashlightRef.current) {
|
||||||
|
flashlightRef.current.position.copy(camera.position);
|
||||||
|
|
||||||
|
camera.getWorldDirection(viewDirection);
|
||||||
|
|
||||||
|
flashlightRef.current.target.position
|
||||||
|
.copy(camera.position)
|
||||||
|
.addScaledVector(viewDirection, 10);
|
||||||
|
flashlightRef.current.target.updateMatrixWorld();
|
||||||
|
flashlightRef.current.intensity = 5 + Math.sin(state.clock.elapsedTime * 30) * 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wallBuffer = 0.6;
|
||||||
|
const HALLWAY_WIDTH = 6;
|
||||||
|
const HALLWAY_LENGTH = 40;
|
||||||
|
|
||||||
|
if (camera.position.x < -HALLWAY_WIDTH / 2 + wallBuffer) camera.position.x = -HALLWAY_WIDTH / 2 + wallBuffer;
|
||||||
|
if (camera.position.x > HALLWAY_WIDTH / 2 - wallBuffer) camera.position.x = HALLWAY_WIDTH / 2 - wallBuffer;
|
||||||
|
|
||||||
|
if (camera.position.z < -HALLWAY_LENGTH) camera.position.z += HALLWAY_LENGTH;
|
||||||
|
if (camera.position.z > 0) camera.position.z -= HALLWAY_LENGTH;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PointerLockControls />
|
||||||
|
<spotLight
|
||||||
|
ref={flashlightRef}
|
||||||
|
distance={22}
|
||||||
|
angle={0.35}
|
||||||
|
penumbra={0.7}
|
||||||
|
intensity={5}
|
||||||
|
color="#fffaed"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function usePlayerControls() {
|
||||||
|
const keys = useRef({ Forward: false, Backward: false, Left: false, Right: false });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e) => {
|
||||||
|
if (e.code === 'KeyW' || e.code === 'ArrowUp') keys.current.Forward = true;
|
||||||
|
if (e.code === 'KeyS' || e.code === 'ArrowDown') keys.current.Backward = true;
|
||||||
|
if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.current.Left = true;
|
||||||
|
if (e.code === 'KeyD' || e.code === 'ArrowRight') keys.current.Right = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyUp = (e) => {
|
||||||
|
if (e.code === 'KeyW' || e.code === 'ArrowUp') keys.current.Forward = false;
|
||||||
|
if (e.code === 'KeyS' || e.code === 'ArrowDown') keys.current.Backward = false;
|
||||||
|
if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.current.Left = false;
|
||||||
|
if (e.code === 'KeyD' || e.code === 'ArrowRight') keys.current.Right = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
window.addEventListener('keyup', handleKeyUp);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
window.removeEventListener('keyup', handleKeyUp);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return keys.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DoorProps {
|
||||||
|
position: [number, number, number];
|
||||||
|
rotation: [number, number, number];
|
||||||
|
}
|
||||||
|
function Door({ position, rotation }: DoorProps) {
|
||||||
|
return (
|
||||||
|
<group position={position} rotation={rotation}>
|
||||||
|
<mesh position={[0, 2, -0.14]}>
|
||||||
|
<boxGeometry args={[2.4, 4.0, 0.2]} />
|
||||||
|
<meshStandardMaterial color="#8a8585" roughness={0.8} metalness={0.2} />
|
||||||
|
</mesh>
|
||||||
|
|
||||||
|
<mesh position={[0, 1.95, -0.08]}>
|
||||||
|
<boxGeometry args={[2.1, 3.8, 0.1]} />
|
||||||
|
<meshStandardMaterial color="#4e4b4b" roughness={0.7} metalness={0.2} />
|
||||||
|
</mesh>
|
||||||
|
|
||||||
|
<mesh position={[0.9, 1.8, 0.08]}>
|
||||||
|
<boxGeometry args={[0.08, 0.08, 0.15]} />
|
||||||
|
<meshStandardMaterial color="#4e4b4b" roughness={0.4} metalness={0.2} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Hallway() {
|
||||||
|
const [floorTex, wallTex] = useTexture([
|
||||||
|
'fear/img/concrete-floor.jpg',
|
||||||
|
'fear/img/concrete-wall.jpg'
|
||||||
|
]);
|
||||||
|
|
||||||
|
floorTex.wrapS = floorTex.wrapT = THREE.RepeatWrapping;
|
||||||
|
floorTex.repeat.set(1, 10);
|
||||||
|
floorTex.minFilter = THREE.NearestFilter;
|
||||||
|
floorTex.magFilter = THREE.NearestFilter;
|
||||||
|
|
||||||
|
|
||||||
|
wallTex.wrapS = wallTex.wrapT = THREE.RepeatWrapping;
|
||||||
|
wallTex.repeat.set(10, 1);
|
||||||
|
wallTex.minFilter = THREE.NearestFilter;
|
||||||
|
wallTex.magFilter = THREE.NearestFilter;
|
||||||
|
|
||||||
|
const segments = [0, -1, 1, -2, 2];
|
||||||
|
|
||||||
|
const lightRef1 = useRef<THREE.PointLight>(null);
|
||||||
|
const lightRef2 = useRef<THREE.PointLight>(null);
|
||||||
|
|
||||||
|
useFrame((state) => {
|
||||||
|
const time = state.clock.elapsedTime;
|
||||||
|
const flicker = Math.sin(time * 10) * Math.cos(time * 3) > 0.4 ? 0 : 0.8;
|
||||||
|
if (lightRef1.current) lightRef1.current.intensity = flicker;
|
||||||
|
if (lightRef2.current) lightRef2.current.intensity = flicker * 0.5;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* <ambientLight intensity={1} /> */}
|
||||||
|
<ambientLight intensity={0.02} />
|
||||||
|
|
||||||
|
{segments.map((i) => (
|
||||||
|
<group key={i} position={[0, 0, i * HALLWAY_LENGTH]}>
|
||||||
|
<pointLight ref={i === 0 ? lightRef1 : null} position={[0, HALLWAY_HEIGHT - 0.5, -HALLWAY_LENGTH / 4]} intensity={0.9} distance={15} color="#a8a1a1" />
|
||||||
|
<pointLight ref={i === 0 ? lightRef2 : null} position={[0, HALLWAY_HEIGHT - 0.5, -HALLWAY_LENGTH * 0.75]} intensity={0.9} distance={15} color="#a8a1a1" />
|
||||||
|
|
||||||
|
{/* floor */}
|
||||||
|
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, -HALLWAY_LENGTH / 2]}>
|
||||||
|
<planeGeometry args={[HALLWAY_WIDTH, HALLWAY_LENGTH]} />
|
||||||
|
<meshStandardMaterial map={floorTex} roughness={0.8} metalness={0.2} />
|
||||||
|
</mesh>
|
||||||
|
|
||||||
|
{/* roof */}
|
||||||
|
<mesh rotation={[Math.PI / 2, 0, 0]} position={[0, HALLWAY_HEIGHT, -HALLWAY_LENGTH / 2]}>
|
||||||
|
<planeGeometry args={[HALLWAY_WIDTH, HALLWAY_LENGTH]} />
|
||||||
|
<meshStandardMaterial map={floorTex} roughness={0.8} metalness={0.2} />
|
||||||
|
</mesh>
|
||||||
|
|
||||||
|
{/* l wall */}
|
||||||
|
<mesh rotation={[0, Math.PI / 2, 0]} position={[-HALLWAY_WIDTH / 2, HALLWAY_HEIGHT / 2, -HALLWAY_LENGTH / 2]}>
|
||||||
|
<planeGeometry args={[HALLWAY_LENGTH, HALLWAY_HEIGHT]} />
|
||||||
|
<meshStandardMaterial map={wallTex} roughness={0.7} metalness={0.1} />
|
||||||
|
</mesh>
|
||||||
|
|
||||||
|
{/* r wall */}
|
||||||
|
<mesh rotation={[0, -Math.PI / 2, 0]} position={[HALLWAY_WIDTH / 2, HALLWAY_HEIGHT / 2, -HALLWAY_LENGTH / 2]}>
|
||||||
|
<planeGeometry args={[HALLWAY_LENGTH, HALLWAY_HEIGHT]} />
|
||||||
|
<meshStandardMaterial map={wallTex} roughness={0.7} metalness={0.1} />
|
||||||
|
</mesh>
|
||||||
|
|
||||||
|
{/* doors */}
|
||||||
|
<Door
|
||||||
|
position={[-HALLWAY_WIDTH / 2 + 0.05, 0, -HALLWAY_LENGTH * 0.25]}
|
||||||
|
rotation={[0, Math.PI / 2, 0]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Door
|
||||||
|
position={[HALLWAY_WIDTH / 2 - 0.05, 0, -HALLWAY_LENGTH * 0.65]}
|
||||||
|
rotation={[0, -Math.PI / 2, 0]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Door
|
||||||
|
position={[-HALLWAY_WIDTH / 2 + 0.05, 0, -HALLWAY_LENGTH * 0.85]}
|
||||||
|
rotation={[0, Math.PI / 2, 0]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* pipes */}
|
||||||
|
|
||||||
|
{Array.from({ length: 3 }).map((_, idx) => (
|
||||||
|
<mesh
|
||||||
|
key={idx}
|
||||||
|
rotation={[Math.PI / 2, 0, 0]}
|
||||||
|
position={[-HALLWAY_WIDTH / 2 + 0.4 + (idx * 0.20), HALLWAY_HEIGHT - 0.2, -HALLWAY_LENGTH / 2]}
|
||||||
|
>
|
||||||
|
<cylinderGeometry args={[0.06, 0.06, HALLWAY_LENGTH, 4]} />
|
||||||
|
<meshStandardMaterial color="#a5aca8" roughness={0.0} metalness={0.4} />
|
||||||
|
</mesh>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{Array.from({ length: 5 }).map((_, idx) => {
|
||||||
|
const zOffset = -(idx * 8 + 4);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<mesh
|
||||||
|
key={`bracket-${idx}`}
|
||||||
|
position={[-HALLWAY_WIDTH / 2 + 0.6, HALLWAY_HEIGHT - 0.15, zOffset]}
|
||||||
|
>
|
||||||
|
<boxGeometry args={[0.7, 0.3, 0.15]} />
|
||||||
|
<meshStandardMaterial color="#b3adad" roughness={0.8} metalness={0.2} />
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PostProcessing() {
|
||||||
|
return (<EffectComposer>
|
||||||
|
<Pixelation granularity={10} />
|
||||||
|
<Vignette />
|
||||||
|
<Noise opacity={0.005} />
|
||||||
|
<BrightnessContrast
|
||||||
|
brightness={-0.01}
|
||||||
|
contrast={0.05}
|
||||||
|
/>
|
||||||
|
</EffectComposer>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Fear() {
|
||||||
|
return (<>
|
||||||
|
<Canvas
|
||||||
|
shadows
|
||||||
|
gl={{ antialias: true }}
|
||||||
|
className='canvas'
|
||||||
|
camera={{ position: [0, 3, -5], fov: 65, far: 100 }}
|
||||||
|
>
|
||||||
|
<color attach="background" args={['#050505']} />
|
||||||
|
|
||||||
|
<fogExp2 attach='fog' args={[0x050505, 0.035]} />
|
||||||
|
<PostProcessing />
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<Hallway />
|
||||||
|
</Suspense>
|
||||||
|
|
||||||
|
<AmbientSound url='fear/snd/ambience.mp3' />
|
||||||
|
|
||||||
|
<Player />
|
||||||
|
</Canvas>
|
||||||
|
</>)
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
interface AmbientSoundProps {
|
||||||
|
url: string
|
||||||
|
volume?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AmbientSound({ url, volume = 0.5 }: AmbientSoundProps) {
|
||||||
|
const audioRef = useRef<HTMLAudioElement | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const audio = new Audio(url)
|
||||||
|
audio.loop = true
|
||||||
|
audio.volume = volume
|
||||||
|
audioRef.current = audio
|
||||||
|
|
||||||
|
const startAudio = () => {
|
||||||
|
audio.play().catch((err) => {
|
||||||
|
console.warn('Autoplay blocked. Waiting for user interaction.', err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
startAudio()
|
||||||
|
|
||||||
|
window.addEventListener('click', startAudio, { once: true })
|
||||||
|
window.addEventListener('keydown', startAudio, { once: true })
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('click', startAudio)
|
||||||
|
window.removeEventListener('keydown', startAudio)
|
||||||
|
audio.pause()
|
||||||
|
audioRef.current = null
|
||||||
|
}
|
||||||
|
}, [url])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (audioRef.current) {
|
||||||
|
audioRef.current.volume = volume
|
||||||
|
}
|
||||||
|
}, [volume])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ float windSpeed = 1.5;
|
|||||||
float windTime = uTime * windSpeed;
|
float windTime = uTime * windSpeed;
|
||||||
vec2 windSamplePos = (worldPos.xz * 0.05) - (mainWindDir * windTime * 0.2);
|
vec2 windSamplePos = (worldPos.xz * 0.05) - (mainWindDir * windTime * 0.2);
|
||||||
|
|
||||||
|
|
||||||
float windBase = fbm(windSamplePos * 0.8) * 0.4 + 0.2;
|
float windBase = fbm(windSamplePos * 0.8) * 0.4 + 0.2;
|
||||||
float gustNoise = fbm(windSamplePos * 0.4);
|
float gustNoise = fbm(windSamplePos * 0.4);
|
||||||
float gust = pow(gustNoise, 3.0) * 1.8;
|
float gust = pow(gustNoise, 3.0) * 1.8;
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@ function Content() {
|
|||||||
<>
|
<>
|
||||||
<div className="main-frame">
|
<div className="main-frame">
|
||||||
<a href="/niko" className="decorative-sparkle" title="✧" style={{ left: '10px' }}>✧</a>
|
<a href="/niko" className="decorative-sparkle" title="✧" style={{ left: '10px' }}>✧</a>
|
||||||
<a href="/img/boom.gif" className="decorative-sparkle" title="✧" style={{ right: '10px' }}>✧</a>
|
<a href="/fear" className="decorative-sparkle" title="✧" style={{ right: '10px' }}>✧</a>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<h1>neru</h1>
|
<h1>neru</h1>
|
||||||
|
|||||||
Reference in New Issue
Block a user