feat(environment): apply shared wind to vegetation

This commit is contained in:
Tom Boullay
2026-05-28 01:28:16 +02:00
parent e45fdbf97d
commit 1b9ac5c996
3 changed files with 148 additions and 2 deletions
+139 -2
View File
@@ -1,9 +1,10 @@
import { useEffect, useMemo, useRef } from "react";
import * as THREE from "three";
import { useGLTF } from "@react-three/drei";
import { useThree } from "@react-three/fiber";
import { useFrame, useThree } from "@react-three/fiber";
import { mergeGeometries } from "three/addons/utils/BufferGeometryUtils.js";
import { useTerrainHeightSampler } from "@/hooks/three/useTerrainHeight";
import { useWind } from "@/hooks/world/useWind";
import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene";
import type { VegetationInstance } from "@/world/vegetation/useVegetationData";
@@ -13,6 +14,7 @@ interface InstancedVegetationProps {
scaleMultiplier: number;
castShadow: boolean;
receiveShadow: boolean;
windStrength: number;
}
interface MeshData {
@@ -20,6 +22,111 @@ interface MeshData {
material: THREE.Material;
}
type WindShaderMaterial = THREE.Material & {
userData: THREE.Material["userData"] & {
windUniforms?: VegetationWindUniforms;
};
};
interface VegetationWindUniforms {
time: { value: number };
direction: { value: number };
speed: { value: number };
strength: { value: number };
noiseScale: { value: number };
}
function updateVegetationWindUniforms(
uniforms: VegetationWindUniforms,
elapsedTime: number,
direction: number,
speed: number,
strength: number,
noiseScale: number,
): void {
uniforms.time.value = elapsedTime;
uniforms.direction.value = direction;
uniforms.speed.value = speed;
uniforms.strength.value = strength;
uniforms.noiseScale.value = noiseScale;
}
function addWindWeightAttribute(geometry: THREE.BufferGeometry): void {
geometry.computeBoundingBox();
const position = geometry.getAttribute("position");
const bounds = geometry.boundingBox;
if (!position || !bounds) return;
const height = Math.max(bounds.max.y - bounds.min.y, 0.0001);
const weights = new Float32Array(position.count);
for (let index = 0; index < position.count; index++) {
const y = position.getY(index);
const normalizedHeight = THREE.MathUtils.clamp(
(y - bounds.min.y) / height,
0,
1,
);
weights[index] = normalizedHeight * normalizedHeight;
}
geometry.setAttribute("aWindWeight", new THREE.BufferAttribute(weights, 1));
}
function applyVegetationWindMaterial(
material: THREE.Material,
): WindShaderMaterial {
const windMaterial = material as WindShaderMaterial;
const windUniforms: VegetationWindUniforms = {
time: { value: 0 },
direction: { value: 0 },
speed: { value: 0 },
strength: { value: 0 },
noiseScale: { value: 1 },
};
windMaterial.userData.windUniforms = windUniforms;
windMaterial.onBeforeCompile = (shader) => {
shader.uniforms.uVegetationWindTime = windUniforms.time;
shader.uniforms.uVegetationWindDirection = windUniforms.direction;
shader.uniforms.uVegetationWindSpeed = windUniforms.speed;
shader.uniforms.uVegetationWindStrength = windUniforms.strength;
shader.uniforms.uVegetationWindNoiseScale = windUniforms.noiseScale;
shader.vertexShader = shader.vertexShader
.replace(
"#include <common>",
`#include <common>
attribute float aWindWeight;
uniform float uVegetationWindTime;
uniform float uVegetationWindDirection;
uniform float uVegetationWindSpeed;
uniform float uVegetationWindStrength;
uniform float uVegetationWindNoiseScale;`,
)
.replace(
"#include <begin_vertex>",
`#include <begin_vertex>
#ifdef USE_INSTANCING
vec2 instanceOffset = instanceMatrix[3].xz;
#else
vec2 instanceOffset = vec2(0.0);
#endif
vec2 windDirection = vec2(cos(uVegetationWindDirection), sin(uVegetationWindDirection));
float windPhase = dot(instanceOffset + position.xz, windDirection) * uVegetationWindNoiseScale;
float windWave = sin(windPhase + uVegetationWindTime * uVegetationWindSpeed);
float windGust = sin(windPhase * 0.37 + uVegetationWindTime * uVegetationWindSpeed * 0.63);
float windOffset = (windWave * 0.65 + windGust * 0.35) * uVegetationWindStrength * aWindWeight;
transformed.xz += windDirection * windOffset;`,
);
};
windMaterial.customProgramCacheKey = () => "vegetation-wind-v1";
return windMaterial;
}
function extractMeshes(scene: THREE.Group): MeshData[] {
const meshesByMaterial = new Map<
string,
@@ -64,9 +171,11 @@ function extractMeshes(scene: THREE.Group): MeshData[] {
return null;
}
addWindWeightAttribute(mergedGeometry);
return {
geometry: mergedGeometry,
material,
material: applyVegetationWindMaterial(material),
};
})
.filter((meshData): meshData is MeshData => meshData !== null);
@@ -121,13 +230,16 @@ export function InstancedVegetation({
scaleMultiplier,
castShadow,
receiveShadow,
windStrength,
}: InstancedVegetationProps): React.JSX.Element | null {
const { scene } = useGLTF(modelPath);
const wind = useWind();
const terrainHeight = useTerrainHeightSampler();
const maxAnisotropy = useThree((state) =>
state.gl.capabilities.getMaxAnisotropy(),
);
const groupRef = useRef<THREE.Group>(null);
const windUniformsRef = useRef<VegetationWindUniforms[]>([]);
const meshDataList = useMemo(() => {
optimizeGLTFSceneTextures(scene, maxAnisotropy);
@@ -204,7 +316,19 @@ export function InstancedVegetation({
}, [instancedMeshes]);
useEffect(() => {
windUniformsRef.current = meshDataList
.map(
(meshData) =>
(meshData.material as WindShaderMaterial).userData.windUniforms,
)
.filter(
(uniforms): uniforms is VegetationWindUniforms =>
uniforms !== undefined,
);
return () => {
windUniformsRef.current = [];
for (const meshData of meshDataList) {
meshData.geometry.dispose();
if (Array.isArray(meshData.material)) {
@@ -218,6 +342,19 @@ export function InstancedVegetation({
};
}, [meshDataList]);
useFrame(({ clock }) => {
for (const windUniforms of windUniformsRef.current) {
updateVegetationWindUniforms(
windUniforms,
clock.elapsedTime,
wind.direction,
wind.speed,
wind.strength * windStrength,
wind.noiseScale,
);
}
});
if (groundedInstances.length === 0) {
return null;
}
@@ -24,6 +24,7 @@ interface VegetationChunk {
scaleMultiplier: number;
castShadow: boolean;
receiveShadow: boolean;
windStrength: number;
centerX: number;
centerZ: number;
instances: VegetationInstance[];
@@ -70,6 +71,7 @@ function createVegetationChunks(
scaleMultiplier: config.scaleMultiplier,
castShadow: config.castShadow,
receiveShadow: config.receiveShadow,
windStrength: config.windStrength,
centerX: center.x / chunkInstances.length,
centerZ: center.z / chunkInstances.length,
instances: chunkInstances,
@@ -174,6 +176,7 @@ export function VegetationSystem(): React.JSX.Element | null {
scaleMultiplier={chunk.scaleMultiplier}
castShadow={chunk.castShadow}
receiveShadow={chunk.receiveShadow}
windStrength={chunk.windStrength}
/>
</Suspense>
))}
+6
View File
@@ -11,6 +11,7 @@ export const VEGETATION_TYPES = {
scaleMultiplier: 2,
castShadow: true,
receiveShadow: true,
windStrength: 0.08,
enabled: true,
},
sapin: {
@@ -19,6 +20,7 @@ export const VEGETATION_TYPES = {
scaleMultiplier: 5,
castShadow: true,
receiveShadow: true,
windStrength: 0.04,
enabled: true,
},
arbre: {
@@ -27,6 +29,7 @@ export const VEGETATION_TYPES = {
scaleMultiplier: 1,
castShadow: true,
receiveShadow: true,
windStrength: 0.06,
enabled: true,
},
champdeble: {
@@ -35,6 +38,7 @@ export const VEGETATION_TYPES = {
scaleMultiplier: 1,
castShadow: true,
receiveShadow: true,
windStrength: 0.18,
enabled: true,
},
champdesoja: {
@@ -43,6 +47,7 @@ export const VEGETATION_TYPES = {
scaleMultiplier: 1,
castShadow: true,
receiveShadow: true,
windStrength: 0.16,
enabled: true,
},
champsdetournesol: {
@@ -51,6 +56,7 @@ export const VEGETATION_TYPES = {
scaleMultiplier: 1,
castShadow: true,
receiveShadow: true,
windStrength: 0.14,
enabled: true,
},
} as const;