feat: add fear
This commit is contained in:
@@ -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;
|
||||
vec2 windSamplePos = (worldPos.xz * 0.05) - (mainWindDir * windTime * 0.2);
|
||||
|
||||
|
||||
float windBase = fbm(windSamplePos * 0.8) * 0.4 + 0.2;
|
||||
float gustNoise = fbm(windSamplePos * 0.4);
|
||||
float gust = pow(gustNoise, 3.0) * 1.8;
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ function Content() {
|
||||
<>
|
||||
<div className="main-frame">
|
||||
<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>
|
||||
<h1>neru</h1>
|
||||
|
||||
Reference in New Issue
Block a user