Compare commits
43 Commits
339c660bcb
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ab3bf047d4 | |||
| f1ab2b692d | |||
| 673aabce50 | |||
| b3a5712c85 | |||
| 4691a9fbf4 | |||
| 0fca4db440 | |||
| 930139d1df | |||
| 4120e5ec72 | |||
| fd314cf2ec | |||
| e3ab974988 | |||
| e4a0c57e79 | |||
| ebda4b281e | |||
| 8dcc888d5c | |||
| b7e61b4240 | |||
| 23c39a71a6 | |||
| a0ee50703c | |||
| 8c4080f10c | |||
| b9eeed848b | |||
| d506071ce2 | |||
| cad47f07bd | |||
| 10543bba89 | |||
| aeee2158ba | |||
| 569a4f29fb | |||
| 9a67a800fa | |||
| 719a75d393 | |||
| f583cfdc57 | |||
| 5665804b8f | |||
| 67bf6325fa | |||
| 6d7651dec9 | |||
| ee2eb45527 | |||
| 079986ebec | |||
| a0b416c412 | |||
| c582d6b745 | |||
| beff5e3265 | |||
| c04d8536c0 | |||
| cb15cc3d95 | |||
| 0d72d49d7b | |||
| d66c898f23 | |||
| 566a684bfa | |||
| df81fc1ee0 | |||
| dd5e8a2ae2 | |||
| 20b6a559fd | |||
| eec01440f9 |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,10 @@
|
|||||||
.canvas {
|
.canvas {
|
||||||
width: 100vw !important;
|
width: 100vw !important;
|
||||||
height: 100vh !important;
|
height: 100vh !important;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
outline: none;
|
||||||
|
user-select: none;
|
||||||
|
touch-action: none;
|
||||||
}
|
}
|
||||||
+62
-15
@@ -2,46 +2,73 @@
|
|||||||
|
|
||||||
import './page.css';
|
import './page.css';
|
||||||
|
|
||||||
import { Canvas } from "@react-three/fiber";
|
import { Canvas, useFrame, useThree } from "@react-three/fiber";
|
||||||
import { BrightnessContrast, EffectComposer, HueSaturation, Noise, Pixelation, Vignette } from "@react-three/postprocessing";
|
import { BrightnessContrast, EffectComposer, HueSaturation, Noise, Pixelation, Vignette } from "@react-three/postprocessing";
|
||||||
import { Suspense, useEffect, useState } from "react";
|
import { Suspense, useEffect, useState } from "react";
|
||||||
|
|
||||||
import { AmbientSound } from './scene-components/ambient-sound';
|
import { AmbientSound } from './scene-components/ambient-sound';
|
||||||
|
|
||||||
import { fearState } from './state';
|
import { FEAR_SETTINGS, fearState } from './state';
|
||||||
|
|
||||||
import TheCreature from './scene-components/creature';
|
import TheCreature from './scene-components/creature';
|
||||||
import Player from './scene-components/player';
|
import Player from './scene-components/player';
|
||||||
import Hallway from './scene-components/hallway';
|
import Hallway from './scene-components/hallway';
|
||||||
|
|
||||||
|
import { AudioListener } from 'three';
|
||||||
|
import FinaleText from './scene-components/finale-text';
|
||||||
|
|
||||||
function PostProcessing() {
|
function PostProcessing() {
|
||||||
const [getCaught, setCaught] = useState(fearState.wasCaught);
|
const [wasCaught, setWasCaught] = useState(fearState.wasCaught);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = fearState.subscribe(() => {
|
const unsubscribe = fearState.subscribe(() => {
|
||||||
setCaught(fearState.wasCaught);
|
setWasCaught(fearState.wasCaught);
|
||||||
});
|
});
|
||||||
return () => unsubscribe();
|
return () => unsubscribe();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (<EffectComposer>
|
return (<EffectComposer>
|
||||||
<Pixelation granularity={getCaught ? 18 : 12} />
|
<Pixelation granularity={wasCaught ? 18 : 10} />
|
||||||
<Vignette />
|
<Vignette />
|
||||||
<Noise opacity={getCaught ? 0.01 : 0.005} />
|
<Noise opacity={wasCaught ? 0.01 : 0.003} />
|
||||||
<BrightnessContrast
|
<BrightnessContrast
|
||||||
brightness={-0.01}
|
brightness={-0.01}
|
||||||
contrast={0.05}
|
contrast={0.05}
|
||||||
/>
|
/>
|
||||||
<HueSaturation saturation={getCaught ? 1 : 0} />
|
<HueSaturation saturation={wasCaught ? 1 : 0} />
|
||||||
</EffectComposer>)
|
</EffectComposer>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ListenerCreator() {
|
||||||
|
const { camera } = useThree();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const listener = new AudioListener();
|
||||||
|
camera.add(listener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
camera.remove(listener);
|
||||||
|
};
|
||||||
|
}, [camera]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FearStateUpdater() {
|
||||||
|
useFrame((state, delta) => {
|
||||||
|
fearState.update(delta);
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Fear() {
|
export default function Fear() {
|
||||||
const [isRustActive, setIsRustActive] = useState(fearState.isRustActive);
|
const [isRustActive, setIsRustActive] = useState(fearState.isRustActive);
|
||||||
|
const [wasCaught, setWasCaught] = useState(fearState.isRustActive);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = fearState.subscribe(() => {
|
const unsubscribe = fearState.subscribe(() => {
|
||||||
setIsRustActive(fearState.isRustActive);
|
setIsRustActive(fearState.isRustActive);
|
||||||
|
setWasCaught(fearState.wasCaught)
|
||||||
});
|
});
|
||||||
return () => unsubscribe();
|
return () => unsubscribe();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -51,23 +78,43 @@ export default function Fear() {
|
|||||||
shadows
|
shadows
|
||||||
gl={{ antialias: true }}
|
gl={{ antialias: true }}
|
||||||
className='canvas'
|
className='canvas'
|
||||||
camera={{ position: [0, 3, -5], fov: 65, far: 100 }}
|
camera={{ position: [0, 3, -5], fov: 55, far: 100 }}
|
||||||
>
|
>
|
||||||
|
<FearStateUpdater />
|
||||||
|
|
||||||
|
<ListenerCreator />
|
||||||
|
|
||||||
<color attach="background" args={['#050505']} />
|
<color attach="background" args={['#050505']} />
|
||||||
|
|
||||||
<fogExp2 attach='fog' args={[0x050505, 0.035]} />
|
{FEAR_SETTINGS.TEST_MODE ? <ambientLight intensity={2} /> : <ambientLight intensity={0.0225} />}
|
||||||
<PostProcessing />
|
{FEAR_SETTINGS.TEST_MODE ? null : <fogExp2 attach='fog' args={[0x050505, 0.035]} />}
|
||||||
|
{FEAR_SETTINGS.TEST_MODE ? null : < PostProcessing />}
|
||||||
|
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<Hallway />
|
<Hallway />
|
||||||
<TheCreature />
|
<TheCreature />
|
||||||
<Player />
|
<Player />
|
||||||
|
|
||||||
<AmbientSound
|
|
||||||
url='fear/snd/ambience.mp3'
|
|
||||||
volume={isRustActive ? 0 : 1}
|
|
||||||
/>
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
|
<AmbientSound
|
||||||
|
key="ambient-1"
|
||||||
|
url='fear/snd/ambience.mp3'
|
||||||
|
volume={isRustActive ? 0 : 0.5}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AmbientSound
|
||||||
|
key="ambient-2"
|
||||||
|
url='fear/snd/ambience2.mp3'
|
||||||
|
volume={isRustActive ? 1 : 0}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{wasCaught ? <AmbientSound
|
||||||
|
key="ambient-glitch"
|
||||||
|
url='fear/snd/glitch.mp3'
|
||||||
|
volume={1}
|
||||||
|
/> : null}
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|
||||||
|
<FinaleText />
|
||||||
</>)
|
</>)
|
||||||
}
|
}
|
||||||
@@ -7,13 +7,39 @@ interface AmbientSoundProps {
|
|||||||
|
|
||||||
export function AmbientSound({ url, volume = 0.5 }: AmbientSoundProps) {
|
export function AmbientSound({ url, volume = 0.5 }: AmbientSoundProps) {
|
||||||
const audioRef = useRef<HTMLAudioElement | null>(null)
|
const audioRef = useRef<HTMLAudioElement | null>(null)
|
||||||
|
const targetVolumeRef = useRef<number>(volume)
|
||||||
|
|
||||||
|
targetVolumeRef.current = volume
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const audio = new Audio(url)
|
const audio = new Audio(url)
|
||||||
audio.loop = true
|
audio.loop = true
|
||||||
|
audio.volume = 0
|
||||||
audioRef.current = audio
|
audioRef.current = audio
|
||||||
|
|
||||||
|
let componentsMounted = true
|
||||||
|
|
||||||
|
const attemptPlay = () => {
|
||||||
|
if (!audioRef.current || !componentsMounted) return
|
||||||
|
|
||||||
|
audio.volume = targetVolumeRef.current
|
||||||
|
|
||||||
|
if (audio.volume > 0 && audio.paused) {
|
||||||
|
audio.play().catch((err) => {
|
||||||
|
console.warn('Autoplay management holding clip playback execution.', err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attemptPlay()
|
||||||
|
|
||||||
|
window.addEventListener('click', attemptPlay)
|
||||||
|
window.addEventListener('keydown', attemptPlay)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
componentsMounted = false
|
||||||
|
window.removeEventListener('click', attemptPlay)
|
||||||
|
window.removeEventListener('keydown', attemptPlay)
|
||||||
audio.pause()
|
audio.pause()
|
||||||
audio.src = ''
|
audio.src = ''
|
||||||
audioRef.current = null
|
audioRef.current = null
|
||||||
@@ -24,30 +50,15 @@ export function AmbientSound({ url, volume = 0.5 }: AmbientSoundProps) {
|
|||||||
const audio = audioRef.current
|
const audio = audioRef.current
|
||||||
if (!audio) return
|
if (!audio) return
|
||||||
|
|
||||||
audio.volume = volume
|
if (volume === 0) {
|
||||||
|
if (!audio.paused) audio.pause()
|
||||||
const startAudio = () => {
|
} else {
|
||||||
if (audio.volume > 0) {
|
audio.volume = volume
|
||||||
audio.play().catch((err) => {
|
if (audio.paused) {
|
||||||
console.warn('Autoplay blocked. Waiting for user interaction.', err)
|
audio.play().catch(() => {})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}, [volume])
|
||||||
if (volume === 0) {
|
|
||||||
audio.pause()
|
|
||||||
} else {
|
|
||||||
startAudio()
|
|
||||||
|
|
||||||
window.addEventListener('click', startAudio, { once: true })
|
|
||||||
window.addEventListener('keydown', startAudio, { once: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('click', startAudio)
|
|
||||||
window.removeEventListener('keydown', startAudio)
|
|
||||||
}
|
|
||||||
}, [volume])
|
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -1,37 +1,62 @@
|
|||||||
import { useTexture } from "@react-three/drei";
|
import { useTexture, PositionalAudio } from "@react-three/drei";
|
||||||
import { useFrame, useThree } from "@react-three/fiber";
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { fearState } from "../state";
|
import { FEAR_SETTINGS, fearState } from "../state";
|
||||||
|
import { ShaderPatch } from "../shader-patch";
|
||||||
|
|
||||||
|
useTexture.preload('fear/img/creature.png');
|
||||||
|
|
||||||
export default function TheCreature() {
|
export default function TheCreature() {
|
||||||
const texture = useTexture('fear/img/creature.png');
|
const baseTexture = useTexture('fear/img/creature.png');
|
||||||
|
|
||||||
|
const texture = useMemo(() => {
|
||||||
|
const t = baseTexture.clone();
|
||||||
|
t.needsUpdate = true;
|
||||||
|
return t;
|
||||||
|
}, [baseTexture]);
|
||||||
|
|
||||||
const meshRef = useRef<THREE.Mesh>(null);
|
const meshRef = useRef<THREE.Mesh>(null);
|
||||||
|
const audioRef = useRef<THREE.PositionalAudio>(null);
|
||||||
const { camera } = useThree();
|
const { camera } = useThree();
|
||||||
|
|
||||||
const [hasTriggered, setHasTriggered] = useState(false);
|
const [hasTriggered, setHasTriggered] = useState(false);
|
||||||
const [isSpawned, setIsSpawned] = useState(false);
|
const [isSpawned, setIsSpawned] = useState(false);
|
||||||
|
|
||||||
const speed = 15;
|
|
||||||
const globalDistance = useRef<number>(32);
|
const globalDistance = useRef<number>(32);
|
||||||
const [currentLoop, setCurrentLoop] = useState(fearState.loopCount);
|
const [finaleTriggered, setFinaleTriggered] = useState(fearState.finaleTriggered);
|
||||||
|
|
||||||
|
const audioPlaying = useRef<boolean>(false);
|
||||||
|
|
||||||
|
const movePhase = useRef<'frozen' | 'lurching'>('frozen');
|
||||||
|
const phaseTimer = useRef<number>(1.5);
|
||||||
|
|
||||||
|
const glitchCooldown = useRef<number>(0);
|
||||||
|
const isGlitchSpiking = useRef<boolean>(false);
|
||||||
|
const flickerCooldown = useRef<number>(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = fearState.subscribe(() => {
|
const unsubscribe = fearState.subscribe(() => {
|
||||||
setCurrentLoop(fearState.loopCount);
|
setFinaleTriggered(fearState.finaleTriggered);
|
||||||
|
|
||||||
if (fearState.loopCount < 4) {
|
if (!fearState.finaleTriggered) {
|
||||||
setIsSpawned(false);
|
setIsSpawned(false);
|
||||||
setHasTriggered(false);
|
setHasTriggered(false);
|
||||||
globalDistance.current = 32;
|
globalDistance.current = 32;
|
||||||
|
audioPlaying.current = false;
|
||||||
|
movePhase.current = 'frozen';
|
||||||
|
phaseTimer.current = 1.5;
|
||||||
|
|
||||||
|
if (audioRef.current && audioRef.current.isPlaying)
|
||||||
|
audioRef.current.stop();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return () => unsubscribe();
|
return () => unsubscribe();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useFrame((state, delta) => {
|
useFrame((state, delta) => {
|
||||||
if (fearState.loopCount < 4) return;
|
if (!fearState.finaleTriggered) return;
|
||||||
|
|
||||||
const creature = meshRef.current;
|
const creature = meshRef.current;
|
||||||
if (!creature) return;
|
if (!creature) return;
|
||||||
@@ -39,13 +64,40 @@ export default function TheCreature() {
|
|||||||
if (!isSpawned) {
|
if (!isSpawned) {
|
||||||
setIsSpawned(true);
|
setIsSpawned(true);
|
||||||
globalDistance.current = 32;
|
globalDistance.current = 32;
|
||||||
|
movePhase.current = 'frozen';
|
||||||
|
phaseTimer.current = 1.0 + Math.random() * 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasTriggered)
|
if (!hasTriggered) {
|
||||||
if (globalDistance.current < 40) setHasTriggered(true);
|
if (globalDistance.current < 40)
|
||||||
|
setHasTriggered(true);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
if (hasTriggered) {
|
if (hasTriggered) {
|
||||||
globalDistance.current -= speed * delta;
|
phaseTimer.current -= delta;
|
||||||
|
|
||||||
|
if (phaseTimer.current <= 0) {
|
||||||
|
if (movePhase.current === 'frozen') {
|
||||||
|
movePhase.current = 'lurching';
|
||||||
|
phaseTimer.current = 0.05 + Math.random() * 0.2;
|
||||||
|
} else {
|
||||||
|
movePhase.current = 'frozen';
|
||||||
|
const proximityFactor = Math.max(0.05, globalDistance.current / 32);
|
||||||
|
phaseTimer.current = (0.2 + Math.random() * 1.0) * proximityFactor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (movePhase.current === 'lurching') {
|
||||||
|
globalDistance.current -= FEAR_SETTINGS.CREATURE_SPEED * 3 * delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioRef.current && !audioPlaying.current) {
|
||||||
|
audioPlaying.current = true;
|
||||||
|
if (audioRef.current.context.state === 'suspended')
|
||||||
|
audioRef.current.context.resume();
|
||||||
|
audioRef.current.play();
|
||||||
|
}
|
||||||
|
|
||||||
const shakeIntensity = Math.max(0, 1 - (globalDistance.current / 32)) * 0.22;
|
const shakeIntensity = Math.max(0, 1 - (globalDistance.current / 32)) * 0.22;
|
||||||
camera.position.x += (Math.random() - 0.5) * shakeIntensity;
|
camera.position.x += (Math.random() - 0.5) * shakeIntensity;
|
||||||
@@ -66,20 +118,79 @@ export default function TheCreature() {
|
|||||||
|
|
||||||
creature.position.set(0, 1.6, calculatedZ);
|
creature.position.set(0, 1.6, calculatedZ);
|
||||||
creature.lookAt(camera.position.x, creature.position.y, camera.position.z);
|
creature.lookAt(camera.position.x, creature.position.y, camera.position.z);
|
||||||
|
|
||||||
|
if (!hasTriggered) return;
|
||||||
|
|
||||||
|
const proximity = 1 - Math.max(0, Math.min(1, globalDistance.current / 32));
|
||||||
|
const jitterX = 1.0 + (Math.random() - 0.5) * 0.04 * proximity;
|
||||||
|
let jitterY = 1.0 + (Math.random() - 0.5) * 0.06 * proximity;
|
||||||
|
|
||||||
|
glitchCooldown.current -= delta;
|
||||||
|
if (glitchCooldown.current <= 0) {
|
||||||
|
if (Math.random() < 0.25 + proximity * 0.35) {
|
||||||
|
isGlitchSpiking.current = true;
|
||||||
|
glitchCooldown.current = 0.03 + Math.random() * 0.08;
|
||||||
|
} else {
|
||||||
|
isGlitchSpiking.current = false;
|
||||||
|
glitchCooldown.current = 0.08 + Math.random() * 0.4 * (1 - proximity * 0.7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGlitchSpiking.current) {
|
||||||
|
const spike = 0.15 + Math.random() * 0.35;
|
||||||
|
jitterY += Math.random() > 0.5 ? spike : -spike * 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
creature.scale.set(jitterX, jitterY, 1.0);
|
||||||
|
|
||||||
|
flickerCooldown.current -= delta;
|
||||||
|
if (flickerCooldown.current <= 0) {
|
||||||
|
if (creature.visible && Math.random() < 0.12 + proximity * 0.08) {
|
||||||
|
creature.visible = false;
|
||||||
|
flickerCooldown.current = 0.02 + Math.random() * 0.05;
|
||||||
|
} else {
|
||||||
|
creature.visible = true;
|
||||||
|
flickerCooldown.current = 0.05 + Math.random() * 0.3 * (1 - proximity * 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
texture.offset.set(
|
||||||
|
(Math.random() - 0.5) * 0.025 * proximity,
|
||||||
|
(Math.random() - 0.5) * 0.025 * proximity
|
||||||
|
);
|
||||||
|
|
||||||
|
if (proximity > 0.2) {
|
||||||
|
creature.position.x += (Math.random() - 0.5) * 0.12 * proximity;
|
||||||
|
creature.position.y += (Math.random() - 0.5) * 0.06 * proximity;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh
|
<mesh
|
||||||
ref={meshRef}
|
ref={meshRef}
|
||||||
visible={currentLoop >= 4}
|
visible={finaleTriggered}
|
||||||
>
|
>
|
||||||
<planeGeometry args={[3.0, 4.8]} />
|
<planeGeometry args={[3.0, 4.8]} />
|
||||||
<meshBasicMaterial
|
<meshStandardMaterial
|
||||||
map={texture}
|
map={texture}
|
||||||
transparent={true}
|
transparent={true}
|
||||||
depthWrite={false}
|
depthWrite={false}
|
||||||
side={THREE.DoubleSide}
|
side={THREE.DoubleSide}
|
||||||
|
onBeforeCompile={ShaderPatch}
|
||||||
|
emissive="#ffffff"
|
||||||
|
emissiveMap={texture}
|
||||||
|
emissiveIntensity={0.15}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{finaleTriggered && (
|
||||||
|
<PositionalAudio
|
||||||
|
url="fear/snd/riser.mp3"
|
||||||
|
ref={audioRef}
|
||||||
|
distance={25}
|
||||||
|
loop={false}
|
||||||
|
autoplay={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</mesh>
|
</mesh>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'VCR';
|
||||||
|
src: url('/fear/fonts/vcr.ttf') format('truetype');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finale-container {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
left: 0;
|
||||||
|
top: 0vh;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
align-content: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
/* filter: invert(100%); */
|
||||||
|
backdrop-filter: brightness(100%);
|
||||||
|
|
||||||
|
grid-auto-rows: 5vh;
|
||||||
|
/* grid-template-columns: 0; */
|
||||||
|
grid-template-rows: repeat(auto-fit, max-content);
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
will-change: filter;
|
||||||
|
animation: invertFlicker 0.07s infinite alternate;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes invertFlicker {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
43%,
|
||||||
|
45%,
|
||||||
|
88%,
|
||||||
|
92% {
|
||||||
|
filter: invert(0%) contrast(100%) brightness(100%);
|
||||||
|
backdrop-filter: brightness(100%) hue-rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
44%,
|
||||||
|
46%,
|
||||||
|
89%,
|
||||||
|
93%,
|
||||||
|
100% {
|
||||||
|
filter: invert(100%) contrast(300%) brightness(150%);
|
||||||
|
backdrop-filter: brightness(30%) hue-rotate(180deg) saturate(500%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.finale-text {
|
||||||
|
font-family: 'VCR', sans-serif;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
|
||||||
|
height: 0px;
|
||||||
|
width: 100%;
|
||||||
|
color: rgb(255, 255, 255);
|
||||||
|
font-size: 8vh;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.scanlines {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 900;
|
||||||
|
background: repeating-linear-gradient(rgba(0, 0, 0, 0) 0px,
|
||||||
|
rgba(0, 0, 0, 0) 2px,
|
||||||
|
rgba(0, 0, 0, 0.3) 2px,
|
||||||
|
rgba(0, 0, 0, 0.3) 4px);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { JSX, useEffect, useState } from "react"
|
||||||
|
import { fearState } from "../state"
|
||||||
|
|
||||||
|
import './finale-text.css';
|
||||||
|
|
||||||
|
const BLOCKS = [
|
||||||
|
"▀", "▂", "▃", "▄", "▅", "▆", "▇",
|
||||||
|
"█", "▉", "▊", "▋", "▌", "▍", "▎", "▏",
|
||||||
|
"▐", "░", "▒", "▓", "▔", "▕", "▖", "▗",
|
||||||
|
"▘", "▙", "▚", "▛", "▜", "▝", "▞", "▟"
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function FinaleText() {
|
||||||
|
const [wasCaught, setWasCaught] = useState(fearState.wasCaught);
|
||||||
|
const [elements, setElements] = useState<JSX.Element[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = fearState.subscribe(() => {
|
||||||
|
setWasCaught(fearState.wasCaught)
|
||||||
|
});
|
||||||
|
return () => unsubscribe();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!wasCaught)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (Math.random() > 0.9) return;
|
||||||
|
|
||||||
|
const baseText = "bwaaaaaaaaa";
|
||||||
|
const corrupted = baseText
|
||||||
|
.split("")
|
||||||
|
.map((char) => (Math.random() > 0.98 ? BLOCKS[Math.floor(Math.random() * BLOCKS.length)] : char))
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
setElements((prev) => [...prev.slice(-30),
|
||||||
|
<span className="finale-text" key={crypto.randomUUID()}>
|
||||||
|
{corrupted}
|
||||||
|
</span>
|
||||||
|
]);
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [wasCaught]);
|
||||||
|
|
||||||
|
if (!wasCaught) return null;
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<div className="finale-container">
|
||||||
|
{elements}
|
||||||
|
</div>
|
||||||
|
<div className="scanlines" />
|
||||||
|
</>)
|
||||||
|
}
|
||||||
@@ -1,50 +1,88 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { FEAR_SETTINGS, fearState } from "../state";
|
import { FEAR_SETTINGS, fearState } from "../state";
|
||||||
import { useTexture } from "@react-three/drei";
|
import { useTexture, PositionalAudio } from "@react-three/drei";
|
||||||
|
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { useFrame } from "@react-three/fiber";
|
import { useFrame } from "@react-three/fiber";
|
||||||
|
import { ShaderPatch } from "../shader-patch";
|
||||||
|
|
||||||
interface DoorProps {
|
interface DoorProps {
|
||||||
position: [number, number, number];
|
position: [number, number, number];
|
||||||
rotation: [number, number, number];
|
rotation: [number, number, number];
|
||||||
}
|
}
|
||||||
function Door({ position, rotation }: DoorProps) {
|
function Door({ position, rotation }: DoorProps) {
|
||||||
|
const [soundUrl, setSoundUrl] = useState<string | null>(null);
|
||||||
|
const currentSound = useRef<string | null>(null);
|
||||||
|
const steelTex = useTexture('fear/img/steel.png');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (Math.random() < 0.02) {
|
||||||
|
const chosenSound = Math.random() < 0.5 ? "fear/snd/knock1.mp3" : "fear/snd/knock2.mp3";
|
||||||
|
|
||||||
|
setSoundUrl(chosenSound);
|
||||||
|
currentSound.current = chosenSound;
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleAudioEnded = () => {
|
||||||
|
setSoundUrl(null);
|
||||||
|
currentSound.current = null;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group position={position} rotation={rotation}>
|
<group position={position} rotation={rotation}>
|
||||||
<mesh position={[0, 2, -0.14]}>
|
{/* frame */}
|
||||||
<boxGeometry args={[2.4, 4.0, 0.2]} />
|
<mesh position={[0, 2, -0.1]}>
|
||||||
<meshStandardMaterial color="#8a8585" roughness={0.8} metalness={0.2} />
|
<boxGeometry args={[2.4, 4.0, 0.2, 4, 4, 1]} />
|
||||||
|
<meshStandardMaterial map={steelTex} color="#8d8d8d" onBeforeCompile={ShaderPatch} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
<mesh position={[0, 1.95, -0.08]}>
|
{/* panel */}
|
||||||
<boxGeometry args={[2.1, 3.8, 0.1]} />
|
<mesh position={[0, 1.95, -0.0]}>
|
||||||
<meshStandardMaterial color="#4e4b4b" roughness={0.7} metalness={0.2} />
|
<boxGeometry args={[2.1, 3.8, 0.1, 4, 4, 1]} />
|
||||||
|
<meshStandardMaterial map={steelTex} color="#4e4a4a" onBeforeCompile={ShaderPatch} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
<mesh position={[0.9, 1.8, 0.08]}>
|
{/* handle */}
|
||||||
<boxGeometry args={[0.08, 0.08, 0.15]} />
|
<mesh position={[0.75, 1.8, .085]}>
|
||||||
<meshStandardMaterial color="#4e4b4b" roughness={0.4} metalness={0.2} />
|
<boxGeometry args={[0.3, 0.08, 0.1]} />
|
||||||
|
<meshStandardMaterial map={steelTex} color="#ffffff" onBeforeCompile={ShaderPatch} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
|
{soundUrl && (
|
||||||
|
<PositionalAudio
|
||||||
|
url={soundUrl}
|
||||||
|
distance={25}
|
||||||
|
loop={false}
|
||||||
|
autoplay={true}
|
||||||
|
onEnded={handleAudioEnded}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Hallway() {
|
export default function Hallway() {
|
||||||
const [width, setWidth] = useState(fearState.currentWidth);
|
const [width, setWidth] = useState(fearState.currentWidth);
|
||||||
const [floorTex, wallTex, rustTex] = useTexture([
|
const [floorTex, wallTex, rustWallTex, rustFloorTex] = useTexture([
|
||||||
'fear/img/concrete-floor.png',
|
'fear/img/concrete-floor.png',
|
||||||
'fear/img/concrete-wall.png',
|
'fear/img/concrete-wall.png',
|
||||||
|
'fear/img/rust.png',
|
||||||
'fear/img/rust.png'
|
'fear/img/rust.png'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
[floorTex, wallTex, rustTex].forEach((tex) => {
|
[floorTex, wallTex, rustWallTex, rustFloorTex].forEach((tex) => {
|
||||||
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
|
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
|
||||||
tex.minFilter = tex.magFilter = THREE.NearestFilter;
|
tex.minFilter = tex.magFilter = THREE.NearestFilter;
|
||||||
tex.colorSpace = THREE.SRGBColorSpace;
|
tex.colorSpace = THREE.SRGBColorSpace;
|
||||||
});
|
});
|
||||||
}, [floorTex, wallTex, rustTex]);
|
}, [floorTex, wallTex, rustWallTex, rustFloorTex]);
|
||||||
|
|
||||||
|
|
||||||
const segmentPool = [0, 1, 2, 3, 4];
|
const segmentPool = [0, 1, 2, 3, 4];
|
||||||
const segmentCount = segmentPool.length;
|
const segmentCount = segmentPool.length;
|
||||||
@@ -80,10 +118,6 @@ export default function Hallway() {
|
|||||||
useFrame((state, delta) => {
|
useFrame((state, delta) => {
|
||||||
const time = state.clock.elapsedTime;
|
const time = state.clock.elapsedTime;
|
||||||
|
|
||||||
fearState.update(delta);
|
|
||||||
if (fearState.isRustActive !== isRustActive)
|
|
||||||
setIsRustActive(fearState.isRustActive);
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
lights
|
lights
|
||||||
*/
|
*/
|
||||||
@@ -125,11 +159,13 @@ export default function Hallway() {
|
|||||||
const horizontalTexRepeat = width / FEAR_SETTINGS.HALLWAY_WIDTH;
|
const horizontalTexRepeat = width / FEAR_SETTINGS.HALLWAY_WIDTH;
|
||||||
floorTex.repeat.set(horizontalTexRepeat, 10);
|
floorTex.repeat.set(horizontalTexRepeat, 10);
|
||||||
wallTex.repeat.set(10, 1);
|
wallTex.repeat.set(10, 1);
|
||||||
rustTex.repeat.set(horizontalTexRepeat, 10);
|
rustWallTex.repeat.set(10, 1);
|
||||||
|
rustFloorTex.repeat.set(horizontalTexRepeat, 10);
|
||||||
|
|
||||||
floorTex.needsUpdate = true;
|
floorTex.needsUpdate = true;
|
||||||
wallTex.needsUpdate = true;
|
wallTex.needsUpdate = true;
|
||||||
rustTex.needsUpdate = true;
|
rustWallTex.needsUpdate = true;
|
||||||
|
rustFloorTex.needsUpdate = true;
|
||||||
|
|
||||||
let closestPoolIndex = 0;
|
let closestPoolIndex = 0;
|
||||||
let minDistance = Infinity;
|
let minDistance = Infinity;
|
||||||
@@ -140,7 +176,6 @@ export default function Hallway() {
|
|||||||
let segmentZIndex = poolIndex - Math.floor(segmentCount / 2) + playerSegmentZ;
|
let segmentZIndex = poolIndex - Math.floor(segmentCount / 2) + playerSegmentZ;
|
||||||
segGroup.position.z = segmentZIndex * length;
|
segGroup.position.z = segmentZIndex * length;
|
||||||
|
|
||||||
// Track which pool index is currently physically closest to the player's camera position
|
|
||||||
const distance = Math.abs(segGroup.position.z - state.camera.position.z);
|
const distance = Math.abs(segGroup.position.z - state.camera.position.z);
|
||||||
if (distance < minDistance) {
|
if (distance < minDistance) {
|
||||||
minDistance = distance;
|
minDistance = distance;
|
||||||
@@ -197,10 +232,10 @@ export default function Hallway() {
|
|||||||
/*
|
/*
|
||||||
materials
|
materials
|
||||||
*/
|
*/
|
||||||
const updateMaterials = (materials: THREE.MeshStandardMaterial[], defaultTex: THREE.Texture, activeColor: string, defaultColor: string, activeRough: number, defaultRough: number, activeMetal: number, defaultMetal: number) => {
|
const updateMaterials = (materials: THREE.MeshStandardMaterial[], defaultTex: THREE.Texture, targetRustTex: THREE.Texture, activeColor: string, defaultColor: string, activeRough: number, defaultRough: number, activeMetal: number, defaultMetal: number) => {
|
||||||
materials.forEach(mat => {
|
materials.forEach(mat => {
|
||||||
if (!mat) return;
|
if (!mat) return;
|
||||||
const targetTex = isRustActive ? rustTex : defaultTex;
|
const targetTex = isRustActive ? targetRustTex : defaultTex;
|
||||||
if (mat.map !== targetTex) {
|
if (mat.map !== targetTex) {
|
||||||
mat.map = targetTex;
|
mat.map = targetTex;
|
||||||
mat.needsUpdate = true;
|
mat.needsUpdate = true;
|
||||||
@@ -211,8 +246,8 @@ export default function Hallway() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
updateMaterials(wallMaterialsRef.current, wallTex, "#918a87", "#ffffff", 0.95, 0.7, 0.05, 0.1);
|
updateMaterials(wallMaterialsRef.current, wallTex, rustWallTex, "#c5c0be", "#ffffff", 0.95, 0.7, 0.05, 0.1);
|
||||||
updateMaterials(floorMaterialsRef.current, floorTex, "#8b827f", "#ffffff", 0.95, 0.8, 0.05, 0.2);
|
updateMaterials(floorMaterialsRef.current, floorTex, rustFloorTex, "#cabdb9", "#ffffff", 0.95, 0.8, 0.05, 0.2);
|
||||||
|
|
||||||
pipeMaterialsRef.current.forEach(mat => {
|
pipeMaterialsRef.current.forEach(mat => {
|
||||||
if (!mat) return;
|
if (!mat) return;
|
||||||
@@ -231,8 +266,6 @@ export default function Hallway() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ambientLight intensity={0.0225} />
|
|
||||||
|
|
||||||
{segmentPool.map((poolIndex) => (
|
{segmentPool.map((poolIndex) => (
|
||||||
<group
|
<group
|
||||||
key={poolIndex}
|
key={poolIndex}
|
||||||
@@ -255,6 +288,7 @@ export default function Hallway() {
|
|||||||
emissive="#a8a1a1"
|
emissive="#a8a1a1"
|
||||||
emissiveIntensity={0.8}
|
emissiveIntensity={0.8}
|
||||||
roughness={0.9}
|
roughness={0.9}
|
||||||
|
onBeforeCompile={ShaderPatch}
|
||||||
/>
|
/>
|
||||||
</mesh>
|
</mesh>
|
||||||
</group>
|
</group>
|
||||||
@@ -265,12 +299,11 @@ export default function Hallway() {
|
|||||||
rotation={[-Math.PI / 2, 0, 0]}
|
rotation={[-Math.PI / 2, 0, 0]}
|
||||||
position={[0, 0, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]}
|
position={[0, 0, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]}
|
||||||
>
|
>
|
||||||
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_WIDTH, FEAR_SETTINGS.HALLWAY_LENGTH]} />
|
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_WIDTH, FEAR_SETTINGS.HALLWAY_LENGTH, 4, 10]} />
|
||||||
<meshStandardMaterial
|
<meshStandardMaterial
|
||||||
ref={(el) => { if (el) floorMaterialsRef.current.push(el); }}
|
ref={(el) => { if (el) floorMaterialsRef.current.push(el); }}
|
||||||
map={floorTex}
|
map={floorTex}
|
||||||
roughness={0.8}
|
onBeforeCompile={ShaderPatch}
|
||||||
metalness={0.2}
|
|
||||||
/>
|
/>
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
@@ -280,24 +313,22 @@ export default function Hallway() {
|
|||||||
rotation={[Math.PI / 2, 0, 0]}
|
rotation={[Math.PI / 2, 0, 0]}
|
||||||
position={[0, FEAR_SETTINGS.HALLWAY_HEIGHT, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]}
|
position={[0, FEAR_SETTINGS.HALLWAY_HEIGHT, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]}
|
||||||
>
|
>
|
||||||
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_WIDTH, FEAR_SETTINGS.HALLWAY_LENGTH]} />
|
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_WIDTH, FEAR_SETTINGS.HALLWAY_LENGTH, 4, 10]} />
|
||||||
<meshStandardMaterial
|
<meshStandardMaterial
|
||||||
ref={(el) => { if (el) floorMaterialsRef.current.push(el); }}
|
ref={(el) => { if (el) floorMaterialsRef.current.push(el); }}
|
||||||
map={floorTex}
|
map={floorTex}
|
||||||
roughness={0.8}
|
onBeforeCompile={ShaderPatch}
|
||||||
metalness={0.2}
|
|
||||||
/>
|
/>
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* left wall */}
|
{/* left wall */}
|
||||||
<group name="left-wall-group">
|
<group name="left-wall-group">
|
||||||
<mesh rotation={[0, Math.PI / 2, 0]} position={[0, FEAR_SETTINGS.HALLWAY_HEIGHT / 2, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]}>
|
<mesh rotation={[0, Math.PI / 2, 0]} position={[0, FEAR_SETTINGS.HALLWAY_HEIGHT / 2, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]}>
|
||||||
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_LENGTH, FEAR_SETTINGS.HALLWAY_HEIGHT]} />
|
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_LENGTH, FEAR_SETTINGS.HALLWAY_HEIGHT, 10, 4]} />
|
||||||
<meshStandardMaterial
|
<meshStandardMaterial
|
||||||
ref={(el) => { if (el) wallMaterialsRef.current.push(el); }}
|
ref={(el) => { if (el) wallMaterialsRef.current.push(el); }}
|
||||||
map={wallTex}
|
map={wallTex}
|
||||||
roughness={0.7}
|
onBeforeCompile={ShaderPatch}
|
||||||
metalness={0.1}
|
|
||||||
/>
|
/>
|
||||||
</mesh>
|
</mesh>
|
||||||
{!isRustActive && (
|
{!isRustActive && (
|
||||||
@@ -311,12 +342,11 @@ export default function Hallway() {
|
|||||||
{/* right wall */}
|
{/* right wall */}
|
||||||
<group name="right-wall-group">
|
<group name="right-wall-group">
|
||||||
<mesh rotation={[0, -Math.PI / 2, 0]} position={[0, FEAR_SETTINGS.HALLWAY_HEIGHT / 2, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]}>
|
<mesh rotation={[0, -Math.PI / 2, 0]} position={[0, FEAR_SETTINGS.HALLWAY_HEIGHT / 2, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]}>
|
||||||
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_LENGTH, FEAR_SETTINGS.HALLWAY_HEIGHT]} />
|
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_LENGTH, FEAR_SETTINGS.HALLWAY_HEIGHT, 10, 4]} />
|
||||||
<meshStandardMaterial
|
<meshStandardMaterial
|
||||||
ref={(el) => { if (el) wallMaterialsRef.current.push(el); }}
|
ref={(el) => { if (el) wallMaterialsRef.current.push(el); }}
|
||||||
map={wallTex}
|
map={wallTex}
|
||||||
roughness={0.7}
|
onBeforeCompile={ShaderPatch}
|
||||||
metalness={0.1}
|
|
||||||
/>
|
/>
|
||||||
</mesh>
|
</mesh>
|
||||||
{!isRustActive && (
|
{!isRustActive && (
|
||||||
@@ -338,6 +368,7 @@ export default function Hallway() {
|
|||||||
color="#a5aca8"
|
color="#a5aca8"
|
||||||
roughness={0.0}
|
roughness={0.0}
|
||||||
metalness={0.4}
|
metalness={0.4}
|
||||||
|
onBeforeCompile={ShaderPatch}
|
||||||
/>
|
/>
|
||||||
</mesh>
|
</mesh>
|
||||||
))}
|
))}
|
||||||
@@ -357,6 +388,7 @@ export default function Hallway() {
|
|||||||
color="#a5aca8"
|
color="#a5aca8"
|
||||||
roughness={0.0}
|
roughness={0.0}
|
||||||
metalness={0.4}
|
metalness={0.4}
|
||||||
|
onBeforeCompile={ShaderPatch}
|
||||||
/>
|
/>
|
||||||
</mesh>
|
</mesh>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { useFrame, useThree } from "@react-three/fiber";
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import * as THREE from "three";
|
|
||||||
import { FEAR_SETTINGS, fearState } from "../state";
|
import { FEAR_SETTINGS, fearState } from "../state";
|
||||||
import { PointerLockControls } from "@react-three/drei";
|
import { PointerLockControls } from "@react-three/drei";
|
||||||
|
import * as THREE from "three";
|
||||||
|
|
||||||
const forward = new THREE.Vector3();
|
const forward = new THREE.Vector3();
|
||||||
const side = new THREE.Vector3();
|
const side = new THREE.Vector3();
|
||||||
const direction = new THREE.Vector3();
|
|
||||||
const viewDirection = new THREE.Vector3();
|
const viewDirection = new THREE.Vector3();
|
||||||
|
const targetDest = new THREE.Vector3();
|
||||||
|
|
||||||
|
const playerRoot = new THREE.Vector3(0, FEAR_SETTINGS.PLAYER_HEIGHT, 0);
|
||||||
|
const targetVelocity = new THREE.Vector3();
|
||||||
|
const currentVelocity = new THREE.Vector3();
|
||||||
|
|
||||||
function usePlayerControls() {
|
function usePlayerControls() {
|
||||||
const keys = useRef({ Forward: false, Backward: false, Left: false, Right: false });
|
const keys = useRef({ Forward: false, Backward: false, Left: false, Right: false });
|
||||||
@@ -45,63 +49,109 @@ export default function Player() {
|
|||||||
|
|
||||||
const flashlightRef = useRef<THREE.SpotLight>(null);
|
const flashlightRef = useRef<THREE.SpotLight>(null);
|
||||||
const movementCounter = useRef<number>(0);
|
const movementCounter = useRef<number>(0);
|
||||||
|
const bobIntensity = useRef<number>(0);
|
||||||
|
|
||||||
const confirmedSegment = useRef<number>(0);
|
const confirmedSegment = useRef<number>(0);
|
||||||
const hasTriggeredThisSegment = useRef<boolean>(false);
|
const hasTriggeredThisSegment = useRef<boolean>(false);
|
||||||
|
|
||||||
|
const footstepAudio = useRef<HTMLAudioElement[]>([]);
|
||||||
|
const hasStepped = useRef<boolean>(false);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
playerRoot.set(camera.position.x, FEAR_SETTINGS.PLAYER_HEIGHT, camera.position.z);
|
||||||
|
footstepAudio.current = Array.from({ length: 6 }, (_, i) => {
|
||||||
|
const audio = new Audio(`fear/snd/footstep${i + 1}.mp3`);
|
||||||
|
audio.volume = 0.4;
|
||||||
|
return audio;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const playRandomFootstep = () => {
|
||||||
|
if (footstepAudio.current.length === 0) return;
|
||||||
|
|
||||||
|
const randomIndex = Math.floor(Math.random() * footstepAudio.current.length);
|
||||||
|
const audio = footstepAudio.current[randomIndex];
|
||||||
|
|
||||||
|
audio.currentTime = 0;
|
||||||
|
audio.play().catch((err) => {
|
||||||
|
console.warn("Footstep playback blocked by browser autocomplete/interaction rules.", err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useFrame((state, delta) => {
|
useFrame((state, delta) => {
|
||||||
|
const dt = Math.min(delta, 0.1);
|
||||||
|
|
||||||
camera.getWorldDirection(forward);
|
camera.getWorldDirection(forward);
|
||||||
forward.y = 0;
|
forward.y = 0;
|
||||||
forward.normalize();
|
forward.normalize();
|
||||||
|
side.crossVectors(forward, THREE.Object3D.DEFAULT_UP).normalize();
|
||||||
side.crossVectors(forward, new THREE.Vector3(0, 1, 0)).normalize();
|
|
||||||
|
|
||||||
const moveForward = Number(controls.Forward) - Number(controls.Backward);
|
const moveForward = Number(controls.Forward) - Number(controls.Backward);
|
||||||
const moveSide = Number(controls.Right) - Number(controls.Left);
|
const moveSide = Number(controls.Right) - Number(controls.Left);
|
||||||
|
|
||||||
direction.set(0, 0, 0);
|
targetVelocity.set(0, 0, 0);
|
||||||
if (moveForward !== 0) direction.addScaledVector(forward, moveForward);
|
if (moveForward !== 0) targetVelocity.addScaledVector(forward, moveForward);
|
||||||
if (moveSide !== 0) direction.addScaledVector(side, moveSide);
|
if (moveSide !== 0) targetVelocity.addScaledVector(side, moveSide);
|
||||||
|
|
||||||
if (direction.lengthSq() > 0)
|
if (targetVelocity.lengthSq() > 0)
|
||||||
direction.normalize().multiplyScalar(FEAR_SETTINGS.PLAYER_SPEED * delta);
|
targetVelocity.normalize().multiplyScalar(FEAR_SETTINGS.PLAYER_SPEED);
|
||||||
|
|
||||||
camera.position.x += direction.x;
|
currentVelocity.lerp(targetVelocity, 10 * dt);
|
||||||
camera.position.z += direction.z;
|
|
||||||
|
|
||||||
const isMoving = controls.Forward || controls.Backward || controls.Left || controls.Right;
|
playerRoot.x += currentVelocity.x * dt;
|
||||||
if (isMoving) {
|
playerRoot.z += currentVelocity.z * dt;
|
||||||
movementCounter.current += delta * 10;
|
|
||||||
camera.position.y = FEAR_SETTINGS.PLAYER_HEIGHT + Math.sin(movementCounter.current) * 0.08;
|
|
||||||
camera.position.x += Math.cos(movementCounter.current / 2) * 0.006;
|
|
||||||
} else {
|
|
||||||
const breatheTime = state.clock.elapsedTime * 1.5;
|
|
||||||
const breatheY = FEAR_SETTINGS.PLAYER_HEIGHT + Math.sin(breatheTime) * 0.1;
|
|
||||||
camera.position.y = THREE.MathUtils.lerp(camera.position.y, breatheY, 4 * delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (flashlightRef.current) {
|
|
||||||
flashlightRef.current.position.copy(camera.position);
|
|
||||||
camera.getWorldDirection(viewDirection);
|
|
||||||
|
|
||||||
const targetDest = new THREE.Vector3()
|
|
||||||
.copy(camera.position)
|
|
||||||
.addScaledVector(viewDirection, 10);
|
|
||||||
|
|
||||||
flashlightRef.current.target.position.lerp(targetDest, 10 * delta);
|
|
||||||
flashlightRef.current.target.updateMatrixWorld();
|
|
||||||
|
|
||||||
flashlightRef.current.intensity = 5 + Math.sin(state.clock.elapsedTime * 30) * 0.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
const minX = -fearState.currentWidth / 2 + FEAR_SETTINGS.WALL_BUFFER;
|
const minX = -fearState.currentWidth / 2 + FEAR_SETTINGS.WALL_BUFFER;
|
||||||
const maxX = fearState.currentWidth / 2 - FEAR_SETTINGS.WALL_BUFFER;
|
const maxX = fearState.currentWidth / 2 - FEAR_SETTINGS.WALL_BUFFER;
|
||||||
|
playerRoot.x = THREE.MathUtils.clamp(playerRoot.x, minX, maxX);
|
||||||
|
|
||||||
if (camera.position.x < minX) camera.position.x = minX;
|
const isMoving = controls.Forward || controls.Backward || controls.Left || controls.Right;
|
||||||
if (camera.position.x > maxX) camera.position.x = maxX;
|
|
||||||
|
bobIntensity.current = THREE.MathUtils.lerp(bobIntensity.current, isMoving ? 1 : 0, 8 * dt);
|
||||||
|
|
||||||
|
if (isMoving)
|
||||||
|
movementCounter.current += dt * 12;
|
||||||
|
|
||||||
|
const sinWave = Math.sin(movementCounter.current);
|
||||||
|
const moveBobY = sinWave * 0.06 * bobIntensity.current;
|
||||||
|
const moveBobX = Math.cos(movementCounter.current / 2) * 0.04 * bobIntensity.current;
|
||||||
|
|
||||||
|
if (isMoving && sinWave < -0.9) {
|
||||||
|
if (!hasStepped.current) {
|
||||||
|
playRandomFootstep();
|
||||||
|
hasStepped.current = true;
|
||||||
|
}
|
||||||
|
} else if (sinWave > 0) {
|
||||||
|
hasStepped.current = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const breatheTime = state.clock.elapsedTime * 1.8;
|
||||||
|
const breatheBobY = Math.sin(breatheTime) * 0.03 * (1 - bobIntensity.current * 0.5);
|
||||||
|
|
||||||
|
camera.position.copy(playerRoot);
|
||||||
|
camera.position.y += moveBobY + breatheBobY;
|
||||||
|
camera.position.addScaledVector(side, moveBobX);
|
||||||
|
|
||||||
|
if (flashlightRef.current) {
|
||||||
|
flashlightRef.current.position.lerp(camera.position, 7 * dt);
|
||||||
|
camera.getWorldDirection(viewDirection);
|
||||||
|
|
||||||
|
targetDest
|
||||||
|
.copy(camera.position)
|
||||||
|
.addScaledVector(viewDirection, 10);
|
||||||
|
|
||||||
|
flashlightRef.current.target.position.lerp(targetDest, 12 * dt);
|
||||||
|
flashlightRef.current.target.updateMatrixWorld();
|
||||||
|
|
||||||
|
flashlightRef.current.intensity =
|
||||||
|
FEAR_SETTINGS.FLASHLIGHT_INTENSITY_BASE +
|
||||||
|
Math.sin(state.clock.elapsedTime * 30) * 0.15 * Math.cos(state.clock.elapsedTime * 3);
|
||||||
|
}
|
||||||
|
|
||||||
const length = FEAR_SETTINGS.HALLWAY_LENGTH;
|
const length = FEAR_SETTINGS.HALLWAY_LENGTH;
|
||||||
const absoluteZ = -camera.position.z;
|
const absoluteZ = -playerRoot.z;
|
||||||
const rawSegmentIndex = Math.floor(absoluteZ / length);
|
const rawSegmentIndex = Math.floor(absoluteZ / length);
|
||||||
const progressZ = ((absoluteZ % length) + length) % length / length;
|
const progressZ = ((absoluteZ % length) + length) % length / length;
|
||||||
|
|
||||||
@@ -120,8 +170,9 @@ export default function Player() {
|
|||||||
confirmedSegment.current = rawSegmentIndex;
|
confirmedSegment.current = rawSegmentIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rawSegmentIndex === confirmedSegment.current && progressZ > 0.35 && progressZ < 0.65)
|
if (rawSegmentIndex === confirmedSegment.current && progressZ > 0.35 && progressZ < 0.65) {
|
||||||
hasTriggeredThisSegment.current = false;
|
hasTriggeredThisSegment.current = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -129,12 +180,15 @@ export default function Player() {
|
|||||||
<PointerLockControls />
|
<PointerLockControls />
|
||||||
<spotLight
|
<spotLight
|
||||||
ref={flashlightRef}
|
ref={flashlightRef}
|
||||||
distance={22}
|
distance={25}
|
||||||
angle={0.35}
|
angle={0.35}
|
||||||
penumbra={0.7}
|
penumbra={0.8}
|
||||||
intensity={5}
|
intensity={0}
|
||||||
color="#fffaed"
|
color="#fffaed"
|
||||||
|
decay={2}
|
||||||
|
castShadow
|
||||||
|
shadow-bias={-0.001}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
export function ShaderPatch(shader: { vertexShader: string, fragmentShader: string, uniforms: Object }) {
|
||||||
|
shader.vertexShader = `
|
||||||
|
varying float vDepth;
|
||||||
|
#ifdef USE_MAP
|
||||||
|
varying vec2 vAffineUv;
|
||||||
|
#endif
|
||||||
|
${shader.vertexShader}
|
||||||
|
`;
|
||||||
|
|
||||||
|
shader.vertexShader = shader.vertexShader.replace(
|
||||||
|
`#include <project_vertex>`,
|
||||||
|
`
|
||||||
|
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
|
||||||
|
gl_Position = projectionMatrix * mvPosition;
|
||||||
|
|
||||||
|
float precisionModifier = 200.0;
|
||||||
|
gl_Position.xy /= gl_Position.w;
|
||||||
|
gl_Position.xy = floor(gl_Position.xy * precisionModifier) / precisionModifier;
|
||||||
|
gl_Position.xy *= gl_Position.w;
|
||||||
|
|
||||||
|
vDepth = gl_Position.w;
|
||||||
|
|
||||||
|
#ifdef USE_MAP
|
||||||
|
vAffineUv = vMapUv * gl_Position.w;
|
||||||
|
#endif
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
shader.fragmentShader = `
|
||||||
|
varying float vDepth;
|
||||||
|
#ifdef USE_MAP
|
||||||
|
varying vec2 vAffineUv;
|
||||||
|
#endif
|
||||||
|
${shader.fragmentShader}
|
||||||
|
`;
|
||||||
|
|
||||||
|
shader.fragmentShader = shader.fragmentShader.replace(
|
||||||
|
`#include <map_fragment>`,
|
||||||
|
`
|
||||||
|
#ifdef USE_MAP
|
||||||
|
vec2 flatAffineUV = vAffineUv / max(vDepth, 0.001);
|
||||||
|
|
||||||
|
vec2 warpDiff = flatAffineUV - vMapUv;
|
||||||
|
float warpDist = length(warpDiff);
|
||||||
|
|
||||||
|
float maxDistortion = 0.25;
|
||||||
|
|
||||||
|
float falloff = maxDistortion / (maxDistortion + warpDist);
|
||||||
|
|
||||||
|
vec2 finalUV = vMapUv + (warpDiff * falloff);
|
||||||
|
|
||||||
|
vec4 texelColor = texture2D( map, finalUV );
|
||||||
|
diffuseColor *= texelColor;
|
||||||
|
#endif
|
||||||
|
`
|
||||||
|
);
|
||||||
|
}
|
||||||
+28
-6
@@ -5,8 +5,18 @@ export const FEAR_SETTINGS = {
|
|||||||
HALLWAY_WIDTH: 6,
|
HALLWAY_WIDTH: 6,
|
||||||
HALLWAY_HEIGHT: 5,
|
HALLWAY_HEIGHT: 5,
|
||||||
PLAYER_HEIGHT: 3,
|
PLAYER_HEIGHT: 3,
|
||||||
PLAYER_SPEED: 6,
|
PLAYER_SPEED: 4,
|
||||||
|
FLASHLIGHT_INTENSITY_BASE: 8,
|
||||||
WALL_BUFFER: 0.6,
|
WALL_BUFFER: 0.6,
|
||||||
|
CREATURE_SPEED: 8,
|
||||||
|
|
||||||
|
EVENT_NARROW_LOOP_COUNT: 2,
|
||||||
|
EVENT_RUST_LOOP_COUNT: 4,
|
||||||
|
EVENT_FINALE_LOOP_COUNT: 5,
|
||||||
|
|
||||||
|
EVENT_FINALE_DURATION: 1,
|
||||||
|
|
||||||
|
TEST_MODE: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const listeners = new Set<() => void>();
|
const listeners = new Set<() => void>();
|
||||||
@@ -17,6 +27,7 @@ export const fearState = {
|
|||||||
isRustActive: false,
|
isRustActive: false,
|
||||||
finaleTriggered: false,
|
finaleTriggered: false,
|
||||||
wasCaught: false,
|
wasCaught: false,
|
||||||
|
finaleProgression: 0,
|
||||||
|
|
||||||
subscribe(listener: () => void) {
|
subscribe(listener: () => void) {
|
||||||
listeners.add(listener);
|
listeners.add(listener);
|
||||||
@@ -28,20 +39,31 @@ export const fearState = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
update(delta: number) {
|
update(delta: number) {
|
||||||
this.isRustActive = this.loopCount >= 3;
|
const targetWidth = this.loopCount >= FEAR_SETTINGS.EVENT_NARROW_LOOP_COUNT ? 2.5 : FEAR_SETTINGS.HALLWAY_WIDTH;
|
||||||
this.finaleTriggered = this.loopCount >= 4;
|
|
||||||
|
|
||||||
const targetWidth = this.loopCount >= 2 ? 2.5 : FEAR_SETTINGS.HALLWAY_WIDTH;
|
|
||||||
const newWidth = THREE.MathUtils.lerp(this.currentWidth, targetWidth, 2 * delta);
|
const newWidth = THREE.MathUtils.lerp(this.currentWidth, targetWidth, 2 * delta);
|
||||||
|
|
||||||
if (Math.abs(this.currentWidth - newWidth) > 0.001) {
|
if (Math.abs(this.currentWidth - newWidth) > 0.001) {
|
||||||
this.currentWidth = newWidth;
|
this.currentWidth = newWidth;
|
||||||
this.emit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.wasCaught) {
|
||||||
|
if (this.finaleProgression < FEAR_SETTINGS.EVENT_FINALE_DURATION) {
|
||||||
|
|
||||||
|
this.finaleProgression = Math.min(this.finaleProgression + delta, FEAR_SETTINGS.EVENT_FINALE_DURATION);
|
||||||
|
} else {
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit();
|
||||||
},
|
},
|
||||||
|
|
||||||
registerLoop(direction: 'forward' | 'backward') {
|
registerLoop(direction: 'forward' | 'backward') {
|
||||||
this.loopCount += 1;
|
this.loopCount += 1;
|
||||||
|
|
||||||
|
this.isRustActive = this.loopCount >= FEAR_SETTINGS.EVENT_RUST_LOOP_COUNT;
|
||||||
|
this.finaleTriggered = this.loopCount >= FEAR_SETTINGS.EVENT_FINALE_LOOP_COUNT;
|
||||||
|
|
||||||
this.emit();
|
this.emit();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
+17
-11
@@ -68,10 +68,10 @@ function Scene() {
|
|||||||
grassDryColor: '#495a17',
|
grassDryColor: '#495a17',
|
||||||
grassLushColor: '#255825',
|
grassLushColor: '#255825',
|
||||||
grassCount: { value: 8000, min: 1000, max: 30000, step: 500 },
|
grassCount: { value: 8000, min: 1000, max: 30000, step: 500 },
|
||||||
grassSize: { value: 0.65, min: 0.1, max: 2.0, step: 0.05 },
|
grassSize: { value: 0.85, min: 0.1, max: 2.0, step: 0.05 },
|
||||||
grassLOD: { value: 60, min: 10, max: 200, step: 5 },
|
grassLOD: { value: 60, min: 10, max: 200, step: 5 },
|
||||||
grassBlades: { value: 3, min: 1, max: 4, step: 1 },
|
grassBlades: { value: 3, min: 1, max: 5, step: 1 },
|
||||||
grassSegments: { value: 3, min: 1, max: 5, step: 1 },
|
grassSegments: { value: 4, min: 1, max: 5, step: 1 },
|
||||||
grassLODStart: { value: 0.15, min: 0.0, max: 0.9, step: 0.05 },
|
grassLODStart: { value: 0.15, min: 0.0, max: 0.9, step: 0.05 },
|
||||||
grassLODExponent: { value: 1.8, min: 0.5, max: 3.0, step: 0.1 },
|
grassLODExponent: { value: 1.8, min: 0.5, max: 3.0, step: 0.1 },
|
||||||
})
|
})
|
||||||
@@ -80,11 +80,17 @@ function Scene() {
|
|||||||
return (<>
|
return (<>
|
||||||
<Environment
|
<Environment
|
||||||
files={'niko/hdr/sky.hdr'}
|
files={'niko/hdr/sky.hdr'}
|
||||||
environmentIntensity={1}
|
environmentIntensity={0.85}
|
||||||
background
|
background
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<fogExp2 attach='fog' args={[0x9a9a9a, 0.01]} />
|
<fogExp2 attach='fog' args={[0xa3a5ba, 0.0125]} />
|
||||||
|
|
||||||
|
<ambientLight intensity={0.5} />
|
||||||
|
<directionalLight
|
||||||
|
position={[15, 25, 15]}
|
||||||
|
intensity={1}
|
||||||
|
/>
|
||||||
|
|
||||||
<Terrain
|
<Terrain
|
||||||
chunks={chunks}
|
chunks={chunks}
|
||||||
@@ -123,9 +129,9 @@ function PostProcessing() {
|
|||||||
<Vignette />
|
<Vignette />
|
||||||
<Noise opacity={0.05} />
|
<Noise opacity={0.05} />
|
||||||
<Bloom
|
<Bloom
|
||||||
intensity={2}
|
intensity={0.8}
|
||||||
luminanceThreshold={0.5}
|
luminanceThreshold={0.4}
|
||||||
luminanceSmoothing={0.1}
|
luminanceSmoothing={0.5}
|
||||||
/>
|
/>
|
||||||
<SMAA />
|
<SMAA />
|
||||||
<HueSaturation saturation={0.3} />
|
<HueSaturation saturation={0.3} />
|
||||||
@@ -145,11 +151,11 @@ export default function Seal() {
|
|||||||
<Canvas
|
<Canvas
|
||||||
shadows
|
shadows
|
||||||
camera={{ position: [0, 5, 15], fov: 50, far: 100 }}
|
camera={{ position: [0, 5, 15], fov: 50, far: 100 }}
|
||||||
gl={{ antialias: true }}
|
gl={{ antialias: false, powerPreference: "high-performance" }}
|
||||||
className='canvas'
|
className='canvas'
|
||||||
>
|
>
|
||||||
<AmbientSound url="niko/snd/wind.mp3" volume={0.4}/>
|
<AmbientSound url="niko/snd/wind.mp3" volume={0.4} />
|
||||||
<AmbientSound url="niko/snd/birds.mp3" volume={0.1}/>
|
<AmbientSound url="niko/snd/birds.mp3" volume={0.1} />
|
||||||
|
|
||||||
<Scene />
|
<Scene />
|
||||||
<PostProcessing />
|
<PostProcessing />
|
||||||
|
|||||||
@@ -7,24 +7,24 @@ import grassVert from './shaders/grass.vert';
|
|||||||
import grassFrag from './shaders/grass.frag';
|
import grassFrag from './shaders/grass.frag';
|
||||||
|
|
||||||
interface GrassProps {
|
interface GrassProps {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
size: number;
|
size: number;
|
||||||
count: number;
|
count: number;
|
||||||
grassSize: number;
|
grassSize: number;
|
||||||
scale: number;
|
scale: number;
|
||||||
hillScale: number;
|
hillScale: number;
|
||||||
hillHeight: number;
|
hillHeight: number;
|
||||||
detailScale: number;
|
detailScale: number;
|
||||||
detailHeight: number;
|
detailHeight: number;
|
||||||
noise2D: (x: number, y: number) => number;
|
noise2D: (x: number, y: number) => number;
|
||||||
grassLOD: number;
|
grassLOD: number;
|
||||||
dryColor: string;
|
dryColor: string;
|
||||||
lushColor: string;
|
lushColor: string;
|
||||||
grassBlades?: number;
|
grassBlades?: number;
|
||||||
grassSegments?: number;
|
grassSegments?: number;
|
||||||
grassLODStart?: number;
|
grassLODStart?: number;
|
||||||
grassLODExponent?: number;
|
grassLODExponent?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Grass({
|
export default function Grass({
|
||||||
@@ -173,24 +173,27 @@ export default function Grass({
|
|||||||
dummy.rotation.x = (Math.random() - 0.5) * 0.2;
|
dummy.rotation.x = (Math.random() - 0.5) * 0.2;
|
||||||
dummy.rotation.z = (Math.random() - 0.5) * 0.2;
|
dummy.rotation.z = (Math.random() - 0.5) * 0.2;
|
||||||
|
|
||||||
const baseScale = grassSize + Math.random() * grassSize * 0.5;
|
const noiseVal = noise2D(globalX * 0.02, globalZ * 0.02);
|
||||||
const heightMult = 0.5 + Math.random() * 1.0;
|
const t = (noiseVal + 1) / 2;
|
||||||
dummy.scale.set(baseScale, baseScale * heightMult, baseScale);
|
const randomInternal = (Math.random() - 0.5) * 0.2;
|
||||||
|
const finalT = Math.max(0, Math.min(1, t + randomInternal));
|
||||||
|
color.lerpColors(dryColorObj, lushColorObj, finalT);
|
||||||
|
meshRef.current.setColorAt(instanceIndex, color);
|
||||||
|
|
||||||
|
const heightNoise = noise2D(globalX * 0.08, globalZ * 0.08);
|
||||||
|
const macroHeight = (heightNoise + 1.0) * 0.5; // 0..1
|
||||||
|
const microNoise = noise2D(globalX * 0.3, globalZ * 0.3);
|
||||||
|
const microHeight = (microNoise + 1.0) * 0.25; // 0..0.5
|
||||||
|
const perBladeRandom = Math.random() * 0.4;
|
||||||
|
|
||||||
|
const grassWidth = grassSize * (0.7 + Math.random() * 0.5);
|
||||||
|
const grassHeight = grassSize * (0.4 + macroHeight * 0.8 + microHeight + perBladeRandom);
|
||||||
|
|
||||||
|
dummy.scale.set(grassWidth, grassHeight, grassWidth);
|
||||||
|
|
||||||
dummy.updateMatrix();
|
dummy.updateMatrix();
|
||||||
meshRef.current.setMatrixAt(instanceIndex, dummy.matrix);
|
meshRef.current.setMatrixAt(instanceIndex, dummy.matrix);
|
||||||
|
|
||||||
const noiseVal = noise2D(globalX * 0.02, globalZ * 0.02);
|
|
||||||
|
|
||||||
const t = (noiseVal + 1) / 2;
|
|
||||||
|
|
||||||
const randomInternal = (Math.random() - 0.5) * 0.2;
|
|
||||||
const finalT = Math.max(0, Math.min(1, t + randomInternal));
|
|
||||||
|
|
||||||
color.lerpColors(dryColorObj, lushColorObj, finalT);
|
|
||||||
|
|
||||||
meshRef.current.setColorAt(instanceIndex, color);
|
|
||||||
|
|
||||||
instanceIndex++;
|
instanceIndex++;
|
||||||
}
|
}
|
||||||
meshRef.current.count = instanceIndex;
|
meshRef.current.count = instanceIndex;
|
||||||
@@ -222,62 +225,61 @@ export default function Grass({
|
|||||||
shader.uniforms.uTime = { value: 0 };
|
shader.uniforms.uTime = { value: 0 };
|
||||||
|
|
||||||
shader.vertexShader = `
|
shader.vertexShader = `
|
||||||
uniform float uTime;
|
uniform float uTime;
|
||||||
varying vec2 vGrassUv;
|
varying vec2 vGrassUv;
|
||||||
|
varying vec3 vWorldPos;
|
||||||
float hash(vec2 p) {
|
|
||||||
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
|
float hash(vec2 p) {
|
||||||
}
|
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
|
||||||
|
}
|
||||||
float noise(vec2 p) {
|
|
||||||
vec2 i = floor(p);
|
float noise(vec2 p) {
|
||||||
vec2 f = fract(p);
|
vec2 i = floor(p);
|
||||||
f = f * f * (3.0 - 2.0 * f);
|
vec2 f = fract(p);
|
||||||
|
f = f * f * (3.0 - 2.0 * f);
|
||||||
float a = hash(i);
|
float a = hash(i);
|
||||||
float b = hash(i + vec2(1.0, 0.0));
|
float b = hash(i + vec2(1.0, 0.0));
|
||||||
float c = hash(i + vec2(0.0, 1.0));
|
float c = hash(i + vec2(0.0, 1.0));
|
||||||
float d = hash(i + vec2(1.0, 1.0));
|
float d = hash(i + vec2(1.0, 1.0));
|
||||||
|
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
||||||
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
}
|
||||||
}
|
|
||||||
|
float fbm(vec2 p) {
|
||||||
float fbm(vec2 p) {
|
float value = 0.0;
|
||||||
float value = 0.0;
|
float amplitude = 0.5;
|
||||||
float amplitude = 0.5;
|
float frequency = 1.0;
|
||||||
float frequency = 1.0;
|
for(int i = 0; i < 4; i++) {
|
||||||
|
value += amplitude * noise(p * frequency);
|
||||||
for(int i = 0; i < 4; i++) {
|
frequency *= 2.0;
|
||||||
value += amplitude * noise(p * frequency);
|
amplitude *= 0.5;
|
||||||
frequency *= 2.0;
|
}
|
||||||
amplitude *= 0.5;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
${shader.vertexShader}
|
||||||
}
|
`;
|
||||||
|
|
||||||
${shader.vertexShader}
|
|
||||||
`;
|
|
||||||
shader.vertexShader = shader.vertexShader.replace(
|
shader.vertexShader = shader.vertexShader.replace(
|
||||||
'#include <begin_vertex>',
|
'#include <begin_vertex>',
|
||||||
`
|
`
|
||||||
#include <begin_vertex>
|
#include <begin_vertex>
|
||||||
vGrassUv = uv;
|
${grassVert}
|
||||||
${grassVert}
|
`
|
||||||
`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
shader.fragmentShader = `
|
shader.fragmentShader = `
|
||||||
uniform float uTime;
|
uniform float uTime;
|
||||||
varying vec2 vGrassUv;
|
varying vec2 vGrassUv;
|
||||||
${shader.fragmentShader}
|
varying vec3 vWorldPos;
|
||||||
`;
|
${shader.fragmentShader}
|
||||||
|
`;
|
||||||
|
|
||||||
shader.fragmentShader = shader.fragmentShader.replace(
|
shader.fragmentShader = shader.fragmentShader.replace(
|
||||||
'#include <color_fragment>',
|
'#include <color_fragment>',
|
||||||
`
|
`
|
||||||
#include <color_fragment>
|
#include <color_fragment>
|
||||||
${grassFrag}
|
${grassFrag}
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (materialRef.current) {
|
if (materialRef.current) {
|
||||||
|
|||||||
@@ -12,13 +12,19 @@ const SealCube = forwardRef<Mesh>((props, ref) => {
|
|||||||
if (meshRef.current) {
|
if (meshRef.current) {
|
||||||
meshRef.current.rotation.x += delta * 0.5;
|
meshRef.current.rotation.x += delta * 0.5;
|
||||||
meshRef.current.rotation.y += delta * 0.5;
|
meshRef.current.rotation.y += delta * 0.5;
|
||||||
|
meshRef.current.position.y = 3 + Math.sin(state.clock.getElapsedTime() * 1) * 0.15;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh ref={meshRef} position={[0, 3, 0]} castShadow receiveShadow>
|
<mesh ref={meshRef} position={[0, 3, 0]} castShadow receiveShadow>
|
||||||
<boxGeometry args={[0.85, 0.85, 0.85]} />
|
<boxGeometry args={[0.85, 0.85, 0.85]} />
|
||||||
<meshBasicMaterial map={texture} depthWrite={true} />
|
<meshStandardMaterial
|
||||||
|
map={texture}
|
||||||
|
roughness={0.4}
|
||||||
|
metalness={0.1}
|
||||||
|
envMapIntensity={1.2}
|
||||||
|
/>
|
||||||
</mesh>
|
</mesh>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,31 @@
|
|||||||
float ao = smoothstep(0.0, 0.7, vGrassUv.y);
|
float ao = smoothstep(0.0, 0.7, vGrassUv.y);
|
||||||
ao = mix(0.05, 1.0, ao);
|
ao = mix(0.05, 1.0, pow(ao, 1.6));
|
||||||
|
|
||||||
vec3 rootColor = diffuseColor.rgb * 0.4;
|
vec3 rootColor = diffuseColor.rgb * 0.15;
|
||||||
vec3 tipColor = diffuseColor.rgb * 1.5;
|
vec3 midColor = diffuseColor.rgb;
|
||||||
|
vec3 tipColor = diffuseColor.rgb * 1.3 + vec3(0.06, 0.08, 0.0);
|
||||||
|
|
||||||
vec3 grassColor = mix(rootColor, tipColor, vGrassUv.y);
|
float heightParam = vGrassUv.y;
|
||||||
grassColor *= ao;
|
vec3 grassColor;
|
||||||
|
if (heightParam < 0.4) {
|
||||||
|
float t = smoothstep(0.0, 0.4, heightParam);
|
||||||
|
grassColor = mix(rootColor, midColor, t);
|
||||||
|
} else {
|
||||||
|
float t = smoothstep(0.4, 1.0, heightParam);
|
||||||
|
grassColor = mix(midColor, tipColor, t);
|
||||||
|
}
|
||||||
|
|
||||||
float translucency = pow(vGrassUv.y, 2.0) * 0.5;
|
vec3 viewDir = normalize(cameraPosition - vWorldPos);
|
||||||
grassColor += diffuseColor.rgb * translucency;
|
vec3 lightDir = normalize(vec3(15.0, 25.0, 15.0));
|
||||||
|
|
||||||
diffuseColor.rgb = grassColor;
|
float VdotL = max(0.0, dot(viewDir, -lightDir));
|
||||||
|
float sss = pow(VdotL, 3.0) * smoothstep(0.2, 0.9, vGrassUv.y);
|
||||||
|
|
||||||
|
vec3 sssColor = diffuseColor.rgb * vec3(0.6, 1.0, 0.15) * 1.8;
|
||||||
|
grassColor += sssColor * sss * 2.0;
|
||||||
|
|
||||||
|
float NdotV = 1.0 - max(0.0, dot(normalize(vNormal), viewDir));
|
||||||
|
float rim = pow(NdotV, 3.0) * smoothstep(0.3, 1.0, vGrassUv.y) * 0.15;
|
||||||
|
grassColor += vec3(0.3, 0.5, 0.1) * rim;
|
||||||
|
|
||||||
|
diffuseColor.rgb = grassColor * ao;
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
vGrassUv = uv;
|
||||||
|
|
||||||
vec4 worldPos = modelMatrix * instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
vec4 worldPos = modelMatrix * instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
float gx = worldPos.x;
|
float gx = worldPos.x;
|
||||||
float gz = worldPos.z;
|
float gz = worldPos.z;
|
||||||
@@ -8,7 +10,6 @@ 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;
|
||||||
@@ -20,19 +21,32 @@ float spring = sin(uTime * 2.0 + phase) * 0.06 + sin(uTime * 4.5 + phase * 1.5)
|
|||||||
float angleNoise = fbm(windSamplePos * 2.0 + uTime * 0.1) - 0.5;
|
float angleNoise = fbm(windSamplePos * 2.0 + uTime * 0.1) - 0.5;
|
||||||
vec2 windDir = normalize(mainWindDir + vec2(-mainWindDir.y, mainWindDir.x) * angleNoise * 0.4);
|
vec2 windDir = normalize(mainWindDir + vec2(-mainWindDir.y, mainWindDir.x) * angleNoise * 0.4);
|
||||||
|
|
||||||
float taperFactor = pow(uv.y, 4.0);
|
// taper (fade)
|
||||||
float taper = 1.0 - taperFactor * 0.6;
|
float taperFactor = uv.y * uv.y * uv.y;
|
||||||
|
float taper = 1.0 - taperFactor * 0.85;
|
||||||
transformed.x *= taper;
|
transformed.x *= taper;
|
||||||
transformed.z *= taper;
|
transformed.z *= taper;
|
||||||
|
|
||||||
|
// curve
|
||||||
float curveVal = fbm(vec2(gx, gz) * 0.5);
|
float curveVal = fbm(vec2(gx, gz) * 0.5);
|
||||||
float curveStrength = 2.0 + curveVal * 2.0;
|
float curveStrength = 1.5 + curveVal * 2.5;
|
||||||
float curveAmount = uv.y * uv.y * curveStrength;
|
float curveAmount = uv.y * uv.y * curveStrength;
|
||||||
vec2 curveDir = normalize(vec2(curveVal, fbm(vec2(gz, gx))) - 0.5);
|
vec2 curveDir = normalize(vec2(curveVal, fbm(vec2(gz, gx))) - 0.5);
|
||||||
transformed.x += curveAmount * curveDir.x * 0.5;
|
transformed.x += curveAmount * curveDir.x * 0.4;
|
||||||
transformed.z += curveAmount * curveDir.y * 0.5;
|
transformed.z += curveAmount * curveDir.y * 0.4;
|
||||||
|
|
||||||
|
// sway
|
||||||
float swayAmount = (totalWind + spring) * uv.y * uv.y;
|
float swayAmount = (totalWind + spring) * uv.y * uv.y;
|
||||||
transformed.x += swayAmount * windDir.x;
|
transformed.x += swayAmount * windDir.x;
|
||||||
transformed.z += swayAmount * windDir.y;
|
transformed.z += swayAmount * windDir.y;
|
||||||
transformed.y -= abs(swayAmount) * 0.2;
|
transformed.y -= abs(swayAmount) * 0.2;
|
||||||
|
|
||||||
|
// normal comp
|
||||||
|
vec2 totalBend = curveDir * curveAmount * 0.4 + windDir * swayAmount;
|
||||||
|
float bendMag = length(totalBend);
|
||||||
|
vec3 bentNormal = normalize(vec3(-totalBend.x * 0.5, 1.0, -totalBend.y * 0.5));
|
||||||
|
|
||||||
|
// normal mix
|
||||||
|
objectNormal = normalize(mix(vec3(0.0, 1.0, 0.0), bentNormal, uv.y));
|
||||||
|
|
||||||
|
vWorldPos = (modelMatrix * instanceMatrix * vec4(transformed, 1.0)).xyz;
|
||||||
Reference in New Issue
Block a user