perf: stream water shader by distance
This commit is contained in:
@@ -32,6 +32,13 @@ export const WATER_SHADER_CONFIG = {
|
|||||||
deepOpacity: 0.45,
|
deepOpacity: 0.45,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const WATER_STREAMING_CONFIG = {
|
||||||
|
enabled: true,
|
||||||
|
loadDistance: 40,
|
||||||
|
unloadDistance: 35,
|
||||||
|
updateInterval: 250,
|
||||||
|
};
|
||||||
|
|
||||||
export const WATER_SURFACES: WaterSurfaceConfig[] = [
|
export const WATER_SURFACES: WaterSurfaceConfig[] = [
|
||||||
{
|
{
|
||||||
position: [40, TERRAIN_WATER_HEIGHT, -102],
|
position: [40, TERRAIN_WATER_HEIGHT, -102],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useMemo, useRef } from "react";
|
import { useMemo, useRef } from "react";
|
||||||
import * as THREE from "three";
|
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 { getWindVector } from "@/data/world/windConfig";
|
||||||
import { WATER_SHADER_CONFIG } from "@/data/world/waterConfig";
|
import { WATER_SHADER_CONFIG } from "@/data/world/waterConfig";
|
||||||
import type { WaterSurfaceConfig } from "@/data/world/waterConfig";
|
import type { WaterSurfaceConfig } from "@/data/world/waterConfig";
|
||||||
@@ -16,6 +17,7 @@ export function WaterSurface({
|
|||||||
rotation,
|
rotation,
|
||||||
size,
|
size,
|
||||||
}: WaterSurfaceConfig): React.JSX.Element {
|
}: WaterSurfaceConfig): React.JSX.Element {
|
||||||
|
const scene = useThree((state) => state.scene);
|
||||||
const materialRef = useRef<THREE.ShaderMaterial>(null);
|
const materialRef = useRef<THREE.ShaderMaterial>(null);
|
||||||
const wind = useWind();
|
const wind = useWind();
|
||||||
const uniforms = useMemo(
|
const uniforms = useMemo(
|
||||||
@@ -41,6 +43,10 @@ export function WaterSurface({
|
|||||||
},
|
},
|
||||||
uOpacity: { value: WATER_SHADER_CONFIG.opacity },
|
uOpacity: { value: WATER_SHADER_CONFIG.opacity },
|
||||||
uDeepOpacity: { value: WATER_SHADER_CONFIG.deepOpacity },
|
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 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 (uTime) uTime.value = clock.getElapsedTime();
|
||||||
if (uFlowX) uFlowX.value = WATER_SHADER_CONFIG.flowX + windVector.x;
|
if (uFlowX) uFlowX.value = WATER_SHADER_CONFIG.flowX + windVector.x;
|
||||||
@@ -59,6 +74,15 @@ export function WaterSurface({
|
|||||||
if (uNoiseScale) {
|
if (uNoiseScale) {
|
||||||
uNoiseScale.value = WATER_SHADER_CONFIG.noiseScale * wind.noiseScale;
|
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 (
|
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";
|
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 {
|
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) {
|
if (!WATER_SHADER_CONFIG.enabled) {
|
||||||
return null;
|
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 (
|
return (
|
||||||
<>
|
getDistanceToWaterSurface(
|
||||||
{WATER_SURFACES.map((surface, index) => (
|
surface,
|
||||||
|
camera.position.x,
|
||||||
|
camera.position.z,
|
||||||
|
) <= WATER_STREAMING_CONFIG.loadDistance
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group name="water-system">
|
||||||
|
{visibleSurfaces.map(({ index, surface }) => (
|
||||||
<WaterSurface key={index} {...surface} />
|
<WaterSurface key={index} {...surface} />
|
||||||
))}
|
))}
|
||||||
</>
|
</group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
export const WATER_VERTEX_SHADER = /* glsl */ `
|
export const WATER_VERTEX_SHADER = /* glsl */ `
|
||||||
varying vec2 vUv;
|
varying vec2 vUv;
|
||||||
|
varying vec3 vWorldPosition;
|
||||||
varying vec2 vWorldPos;
|
varying vec2 vWorldPos;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vUv = uv;
|
vUv = uv;
|
||||||
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
|
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
|
||||||
|
vWorldPosition = worldPosition.xyz;
|
||||||
vWorldPos = worldPosition.xz;
|
vWorldPos = worldPosition.xz;
|
||||||
gl_Position = projectionMatrix * viewMatrix * worldPosition;
|
gl_Position = projectionMatrix * viewMatrix * worldPosition;
|
||||||
}
|
}
|
||||||
@@ -30,8 +32,13 @@ export const WATER_FRAGMENT_SHADER = /* glsl */ `
|
|||||||
uniform vec3 uHighlight;
|
uniform vec3 uHighlight;
|
||||||
uniform float uOpacity;
|
uniform float uOpacity;
|
||||||
uniform float uDeepOpacity;
|
uniform float uDeepOpacity;
|
||||||
|
uniform float uFogEnabled;
|
||||||
|
uniform float uFogNear;
|
||||||
|
uniform float uFogFar;
|
||||||
|
uniform vec3 uFogColor;
|
||||||
|
|
||||||
varying vec2 vUv;
|
varying vec2 vUv;
|
||||||
|
varying vec3 vWorldPosition;
|
||||||
varying vec2 vWorldPos;
|
varying vec2 vWorldPos;
|
||||||
|
|
||||||
float roundedBoxMask(vec2 uv, float radius, float softness) {
|
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;
|
float alpha = mix(uDeepOpacity, 1.0, ramp) * uOpacity;
|
||||||
alpha *= roundedBoxMask(vUv, uBorderRadius, uBorderSoftness);
|
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) {
|
if (alpha < 0.01) {
|
||||||
discard;
|
discard;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user