diff --git a/src/data/world/waterConfig.ts b/src/data/world/waterConfig.ts index 46a76f8..5f94972 100644 --- a/src/data/world/waterConfig.ts +++ b/src/data/world/waterConfig.ts @@ -32,6 +32,13 @@ export const WATER_SHADER_CONFIG = { deepOpacity: 0.45, }; +export const WATER_STREAMING_CONFIG = { + enabled: true, + loadDistance: 40, + unloadDistance: 35, + updateInterval: 250, +}; + export const WATER_SURFACES: WaterSurfaceConfig[] = [ { position: [40, TERRAIN_WATER_HEIGHT, -102], diff --git a/src/world/water/WaterSurface.tsx b/src/world/water/WaterSurface.tsx index c78cec5..a96079d 100644 --- a/src/world/water/WaterSurface.tsx +++ b/src/world/water/WaterSurface.tsx @@ -1,6 +1,7 @@ import { useMemo, useRef } from "react"; import * as THREE from "three"; -import { useFrame } from "@react-three/fiber"; +import { useFrame, useThree } from "@react-three/fiber"; +import { FOG_CONFIG } from "@/data/world/fogConfig"; import { getWindVector } from "@/data/world/windConfig"; import { WATER_SHADER_CONFIG } from "@/data/world/waterConfig"; import type { WaterSurfaceConfig } from "@/data/world/waterConfig"; @@ -16,6 +17,7 @@ export function WaterSurface({ rotation, size, }: WaterSurfaceConfig): React.JSX.Element { + const scene = useThree((state) => state.scene); const materialRef = useRef(null); const wind = useWind(); const uniforms = useMemo( @@ -41,6 +43,10 @@ export function WaterSurface({ }, uOpacity: { value: WATER_SHADER_CONFIG.opacity }, uDeepOpacity: { value: WATER_SHADER_CONFIG.deepOpacity }, + uFogEnabled: { value: 0 }, + uFogNear: { value: FOG_CONFIG.near }, + uFogFar: { value: FOG_CONFIG.far }, + uFogColor: { value: new THREE.Color(FOG_CONFIG.color) }, }), [], ); @@ -51,7 +57,16 @@ export function WaterSurface({ const windVector = getWindVector(wind); - const { uFlowX, uFlowZ, uNoiseScale, uTime } = material.uniforms; + const { + uFlowX, + uFlowZ, + uFogColor, + uFogEnabled, + uFogFar, + uFogNear, + uNoiseScale, + uTime, + } = material.uniforms; if (uTime) uTime.value = clock.getElapsedTime(); if (uFlowX) uFlowX.value = WATER_SHADER_CONFIG.flowX + windVector.x; @@ -59,6 +74,15 @@ export function WaterSurface({ if (uNoiseScale) { uNoiseScale.value = WATER_SHADER_CONFIG.noiseScale * wind.noiseScale; } + + if (scene.fog instanceof THREE.Fog) { + if (uFogEnabled) uFogEnabled.value = 1; + if (uFogNear) uFogNear.value = scene.fog.near; + if (uFogFar) uFogFar.value = scene.fog.far; + if (uFogColor) uFogColor.value.copy(scene.fog.color); + } else if (uFogEnabled) { + uFogEnabled.value = 0; + } }); return ( diff --git a/src/world/water/WaterSystem.tsx b/src/world/water/WaterSystem.tsx index 07195a3..740381d 100644 --- a/src/world/water/WaterSystem.tsx +++ b/src/world/water/WaterSystem.tsx @@ -1,16 +1,102 @@ -import { WATER_SHADER_CONFIG, WATER_SURFACES } from "@/data/world/waterConfig"; +import { useCallback, useRef, useState } from "react"; +import { useFrame, useThree } from "@react-three/fiber"; +import { + WATER_SHADER_CONFIG, + WATER_STREAMING_CONFIG, + WATER_SURFACES, +} from "@/data/world/waterConfig"; +import type { WaterSurfaceConfig } from "@/data/world/waterConfig"; import { WaterSurface } from "@/world/water/WaterSurface"; +function getDistanceToWaterSurface( + surface: WaterSurfaceConfig, + x: number, + z: number, +): number { + const halfWidth = surface.size[0] / 2; + const halfDepth = surface.size[1] / 2; + const distanceX = Math.max(Math.abs(x - surface.position[0]) - halfWidth, 0); + const distanceZ = Math.max(Math.abs(z - surface.position[2]) - halfDepth, 0); + + return Math.hypot(distanceX, distanceZ); +} + export function WaterSystem(): React.JSX.Element | null { + const camera = useThree((state) => state.camera); + const lastUpdateRef = useRef(-WATER_STREAMING_CONFIG.updateInterval); + const [activeSurfaceIndexes, setActiveSurfaceIndexes] = useState>( + () => new Set(), + ); + + const updateActiveSurfaces = useCallback(() => { + const nextIndexes = new Set(); + const cameraX = camera.position.x; + const cameraZ = camera.position.z; + + WATER_SURFACES.forEach((surface, index) => { + const distance = getDistanceToWaterSurface(surface, cameraX, cameraZ); + const wasActive = activeSurfaceIndexes.has(index); + const radius = wasActive + ? WATER_STREAMING_CONFIG.unloadDistance + : WATER_STREAMING_CONFIG.loadDistance; + + if (distance <= radius) { + nextIndexes.add(index); + } + }); + + if ( + nextIndexes.size === activeSurfaceIndexes.size && + [...nextIndexes].every((index) => activeSurfaceIndexes.has(index)) + ) { + return; + } + + setActiveSurfaceIndexes(nextIndexes); + }, [activeSurfaceIndexes, camera]); + + useFrame(({ clock }) => { + if (!WATER_STREAMING_CONFIG.enabled) return; + + const now = clock.elapsedTime * 1000; + if (now - lastUpdateRef.current < WATER_STREAMING_CONFIG.updateInterval) { + return; + } + + lastUpdateRef.current = now; + updateActiveSurfaces(); + }); + if (!WATER_SHADER_CONFIG.enabled) { return null; } + const visibleSurfaces = WATER_SURFACES.map((surface, index) => ({ + index, + surface, + })).filter(({ index, surface }) => { + if (!WATER_STREAMING_CONFIG.enabled) { + return true; + } + + if (activeSurfaceIndexes.size > 0) { + return activeSurfaceIndexes.has(index); + } + + return ( + getDistanceToWaterSurface( + surface, + camera.position.x, + camera.position.z, + ) <= WATER_STREAMING_CONFIG.loadDistance + ); + }); + return ( - <> - {WATER_SURFACES.map((surface, index) => ( + + {visibleSurfaces.map(({ index, surface }) => ( ))} - + ); } diff --git a/src/world/water/waterShaders.ts b/src/world/water/waterShaders.ts index 890fdd7..d31cebc 100644 --- a/src/world/water/waterShaders.ts +++ b/src/world/water/waterShaders.ts @@ -1,10 +1,12 @@ export const WATER_VERTEX_SHADER = /* glsl */ ` varying vec2 vUv; + varying vec3 vWorldPosition; varying vec2 vWorldPos; void main() { vUv = uv; vec4 worldPosition = modelMatrix * vec4(position, 1.0); + vWorldPosition = worldPosition.xyz; vWorldPos = worldPosition.xz; gl_Position = projectionMatrix * viewMatrix * worldPosition; } @@ -30,8 +32,13 @@ export const WATER_FRAGMENT_SHADER = /* glsl */ ` uniform vec3 uHighlight; uniform float uOpacity; uniform float uDeepOpacity; + uniform float uFogEnabled; + uniform float uFogNear; + uniform float uFogFar; + uniform vec3 uFogColor; varying vec2 vUv; + varying vec3 vWorldPosition; varying vec2 vWorldPos; float roundedBoxMask(vec2 uv, float radius, float softness) { @@ -142,6 +149,13 @@ export const WATER_FRAGMENT_SHADER = /* glsl */ ` float alpha = mix(uDeepOpacity, 1.0, ramp) * uOpacity; alpha *= roundedBoxMask(vUv, uBorderRadius, uBorderSoftness); + if (uFogEnabled > 0.5) { + float fogDistance = distance(cameraPosition, vWorldPosition); + float fogFactor = smoothstep(uFogNear, uFogFar, fogDistance); + color = mix(color, uFogColor, fogFactor); + alpha *= 1.0 - fogFactor; + } + if (alpha < 0.01) { discard; }