diff --git a/src/world/vegetation/InstancedVegetation.tsx b/src/world/vegetation/InstancedVegetation.tsx index c17078e..3c25985 100644 --- a/src/world/vegetation/InstancedVegetation.tsx +++ b/src/world/vegetation/InstancedVegetation.tsx @@ -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 ", + `#include + attribute float aWindWeight; + uniform float uVegetationWindTime; + uniform float uVegetationWindDirection; + uniform float uVegetationWindSpeed; + uniform float uVegetationWindStrength; + uniform float uVegetationWindNoiseScale;`, + ) + .replace( + "#include ", + `#include + #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(null); + const windUniformsRef = useRef([]); 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; } diff --git a/src/world/vegetation/VegetationSystem.tsx b/src/world/vegetation/VegetationSystem.tsx index dfc9539..f342bd4 100644 --- a/src/world/vegetation/VegetationSystem.tsx +++ b/src/world/vegetation/VegetationSystem.tsx @@ -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} /> ))} diff --git a/src/world/vegetation/vegetationConfig.ts b/src/world/vegetation/vegetationConfig.ts index 19d8ce8..712f262 100644 --- a/src/world/vegetation/vegetationConfig.ts +++ b/src/world/vegetation/vegetationConfig.ts @@ -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;