perf: stream water shader by distance

This commit is contained in:
Tom Boullay
2026-05-26 23:19:41 +02:00
parent d6d3d5b685
commit 0696ca2ae3
4 changed files with 137 additions and 6 deletions
+7
View File
@@ -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],
+26 -2
View File
@@ -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<THREE.ShaderMaterial>(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 (
+90 -4
View File
@@ -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<Set<number>>(
() => new Set(),
);
const updateActiveSurfaces = useCallback(() => {
const nextIndexes = new Set<number>();
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 (
<>
{WATER_SURFACES.map((surface, index) => (
getDistanceToWaterSurface(
surface,
camera.position.x,
camera.position.z,
) <= WATER_STREAMING_CONFIG.loadDistance
);
});
return (
<group name="water-system">
{visibleSurfaces.map(({ index, surface }) => (
<WaterSurface key={index} {...surface} />
))}
</>
</group>
);
}
+14
View File
@@ -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;
}