feat(environment): apply shared wind to vegetation
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
import { useEffect, useMemo, useRef } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { useGLTF } from "@react-three/drei";
|
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 { mergeGeometries } from "three/addons/utils/BufferGeometryUtils.js";
|
||||||
import { useTerrainHeightSampler } from "@/hooks/three/useTerrainHeight";
|
import { useTerrainHeightSampler } from "@/hooks/three/useTerrainHeight";
|
||||||
|
import { useWind } from "@/hooks/world/useWind";
|
||||||
import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene";
|
import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene";
|
||||||
import type { VegetationInstance } from "@/world/vegetation/useVegetationData";
|
import type { VegetationInstance } from "@/world/vegetation/useVegetationData";
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ interface InstancedVegetationProps {
|
|||||||
scaleMultiplier: number;
|
scaleMultiplier: number;
|
||||||
castShadow: boolean;
|
castShadow: boolean;
|
||||||
receiveShadow: boolean;
|
receiveShadow: boolean;
|
||||||
|
windStrength: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MeshData {
|
interface MeshData {
|
||||||
@@ -20,6 +22,111 @@ interface MeshData {
|
|||||||
material: THREE.Material;
|
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[] {
|
function extractMeshes(scene: THREE.Group): MeshData[] {
|
||||||
const meshesByMaterial = new Map<
|
const meshesByMaterial = new Map<
|
||||||
string,
|
string,
|
||||||
@@ -64,9 +171,11 @@ function extractMeshes(scene: THREE.Group): MeshData[] {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addWindWeightAttribute(mergedGeometry);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
geometry: mergedGeometry,
|
geometry: mergedGeometry,
|
||||||
material,
|
material: applyVegetationWindMaterial(material),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((meshData): meshData is MeshData => meshData !== null);
|
.filter((meshData): meshData is MeshData => meshData !== null);
|
||||||
@@ -121,13 +230,16 @@ export function InstancedVegetation({
|
|||||||
scaleMultiplier,
|
scaleMultiplier,
|
||||||
castShadow,
|
castShadow,
|
||||||
receiveShadow,
|
receiveShadow,
|
||||||
|
windStrength,
|
||||||
}: InstancedVegetationProps): React.JSX.Element | null {
|
}: InstancedVegetationProps): React.JSX.Element | null {
|
||||||
const { scene } = useGLTF(modelPath);
|
const { scene } = useGLTF(modelPath);
|
||||||
|
const wind = useWind();
|
||||||
const terrainHeight = useTerrainHeightSampler();
|
const terrainHeight = useTerrainHeightSampler();
|
||||||
const maxAnisotropy = useThree((state) =>
|
const maxAnisotropy = useThree((state) =>
|
||||||
state.gl.capabilities.getMaxAnisotropy(),
|
state.gl.capabilities.getMaxAnisotropy(),
|
||||||
);
|
);
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
|
const windUniformsRef = useRef<VegetationWindUniforms[]>([]);
|
||||||
|
|
||||||
const meshDataList = useMemo(() => {
|
const meshDataList = useMemo(() => {
|
||||||
optimizeGLTFSceneTextures(scene, maxAnisotropy);
|
optimizeGLTFSceneTextures(scene, maxAnisotropy);
|
||||||
@@ -204,7 +316,19 @@ export function InstancedVegetation({
|
|||||||
}, [instancedMeshes]);
|
}, [instancedMeshes]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
windUniformsRef.current = meshDataList
|
||||||
|
.map(
|
||||||
|
(meshData) =>
|
||||||
|
(meshData.material as WindShaderMaterial).userData.windUniforms,
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
(uniforms): uniforms is VegetationWindUniforms =>
|
||||||
|
uniforms !== undefined,
|
||||||
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
windUniformsRef.current = [];
|
||||||
|
|
||||||
for (const meshData of meshDataList) {
|
for (const meshData of meshDataList) {
|
||||||
meshData.geometry.dispose();
|
meshData.geometry.dispose();
|
||||||
if (Array.isArray(meshData.material)) {
|
if (Array.isArray(meshData.material)) {
|
||||||
@@ -218,6 +342,19 @@ export function InstancedVegetation({
|
|||||||
};
|
};
|
||||||
}, [meshDataList]);
|
}, [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) {
|
if (groundedInstances.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ interface VegetationChunk {
|
|||||||
scaleMultiplier: number;
|
scaleMultiplier: number;
|
||||||
castShadow: boolean;
|
castShadow: boolean;
|
||||||
receiveShadow: boolean;
|
receiveShadow: boolean;
|
||||||
|
windStrength: number;
|
||||||
centerX: number;
|
centerX: number;
|
||||||
centerZ: number;
|
centerZ: number;
|
||||||
instances: VegetationInstance[];
|
instances: VegetationInstance[];
|
||||||
@@ -70,6 +71,7 @@ function createVegetationChunks(
|
|||||||
scaleMultiplier: config.scaleMultiplier,
|
scaleMultiplier: config.scaleMultiplier,
|
||||||
castShadow: config.castShadow,
|
castShadow: config.castShadow,
|
||||||
receiveShadow: config.receiveShadow,
|
receiveShadow: config.receiveShadow,
|
||||||
|
windStrength: config.windStrength,
|
||||||
centerX: center.x / chunkInstances.length,
|
centerX: center.x / chunkInstances.length,
|
||||||
centerZ: center.z / chunkInstances.length,
|
centerZ: center.z / chunkInstances.length,
|
||||||
instances: chunkInstances,
|
instances: chunkInstances,
|
||||||
@@ -174,6 +176,7 @@ export function VegetationSystem(): React.JSX.Element | null {
|
|||||||
scaleMultiplier={chunk.scaleMultiplier}
|
scaleMultiplier={chunk.scaleMultiplier}
|
||||||
castShadow={chunk.castShadow}
|
castShadow={chunk.castShadow}
|
||||||
receiveShadow={chunk.receiveShadow}
|
receiveShadow={chunk.receiveShadow}
|
||||||
|
windStrength={chunk.windStrength}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const VEGETATION_TYPES = {
|
|||||||
scaleMultiplier: 2,
|
scaleMultiplier: 2,
|
||||||
castShadow: true,
|
castShadow: true,
|
||||||
receiveShadow: true,
|
receiveShadow: true,
|
||||||
|
windStrength: 0.08,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
sapin: {
|
sapin: {
|
||||||
@@ -19,6 +20,7 @@ export const VEGETATION_TYPES = {
|
|||||||
scaleMultiplier: 5,
|
scaleMultiplier: 5,
|
||||||
castShadow: true,
|
castShadow: true,
|
||||||
receiveShadow: true,
|
receiveShadow: true,
|
||||||
|
windStrength: 0.04,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
arbre: {
|
arbre: {
|
||||||
@@ -27,6 +29,7 @@ export const VEGETATION_TYPES = {
|
|||||||
scaleMultiplier: 1,
|
scaleMultiplier: 1,
|
||||||
castShadow: true,
|
castShadow: true,
|
||||||
receiveShadow: true,
|
receiveShadow: true,
|
||||||
|
windStrength: 0.06,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
champdeble: {
|
champdeble: {
|
||||||
@@ -35,6 +38,7 @@ export const VEGETATION_TYPES = {
|
|||||||
scaleMultiplier: 1,
|
scaleMultiplier: 1,
|
||||||
castShadow: true,
|
castShadow: true,
|
||||||
receiveShadow: true,
|
receiveShadow: true,
|
||||||
|
windStrength: 0.18,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
champdesoja: {
|
champdesoja: {
|
||||||
@@ -43,6 +47,7 @@ export const VEGETATION_TYPES = {
|
|||||||
scaleMultiplier: 1,
|
scaleMultiplier: 1,
|
||||||
castShadow: true,
|
castShadow: true,
|
||||||
receiveShadow: true,
|
receiveShadow: true,
|
||||||
|
windStrength: 0.16,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
champsdetournesol: {
|
champsdetournesol: {
|
||||||
@@ -51,6 +56,7 @@ export const VEGETATION_TYPES = {
|
|||||||
scaleMultiplier: 1,
|
scaleMultiplier: 1,
|
||||||
castShadow: true,
|
castShadow: true,
|
||||||
receiveShadow: true,
|
receiveShadow: true,
|
||||||
|
windStrength: 0.14,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
Reference in New Issue
Block a user