perf: stream water shader by distance
This commit is contained in:
@@ -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],
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
getDistanceToWaterSurface(
|
||||
surface,
|
||||
camera.position.x,
|
||||
camera.position.z,
|
||||
) <= WATER_STREAMING_CONFIG.loadDistance
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{WATER_SURFACES.map((surface, index) => (
|
||||
<group name="water-system">
|
||||
{visibleSurfaces.map(({ index, surface }) => (
|
||||
<WaterSurface key={index} {...surface} />
|
||||
))}
|
||||
</>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user