Compare commits
4 Commits
a2fc417be6
...
f2cecfbbc9
| Author | SHA1 | Date | |
|---|---|---|---|
| f2cecfbbc9 | |||
| 1b9ac5c996 | |||
| e45fdbf97d | |||
| 08b01715c0 |
@@ -56,12 +56,19 @@ export function HomePage(): React.JSX.Element {
|
|||||||
({ gl }: { gl: THREE.WebGLRenderer }) => {
|
({ gl }: { gl: THREE.WebGLRenderer }) => {
|
||||||
const canvas = gl.domElement;
|
const canvas = gl.domElement;
|
||||||
|
|
||||||
|
gl.shadowMap.enabled = true;
|
||||||
|
gl.shadowMap.type = THREE.PCFShadowMap;
|
||||||
|
gl.shadowMap.autoUpdate = true;
|
||||||
|
|
||||||
const handleContextLost = (event: Event) => {
|
const handleContextLost = (event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
logger.error("WebGL", "Context lost - GPU resources exhausted");
|
logger.error("WebGL", "Context lost - GPU resources exhausted");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleContextRestored = () => {
|
const handleContextRestored = () => {
|
||||||
|
gl.shadowMap.enabled = true;
|
||||||
|
gl.shadowMap.type = THREE.PCFShadowMap;
|
||||||
|
gl.shadowMap.autoUpdate = true;
|
||||||
logger.info("WebGL", "Context restored");
|
logger.info("WebGL", "Context restored");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+17
-7
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useFrame } from "@react-three/fiber";
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
import type { AmbientLight, DirectionalLight } from "three";
|
import type { AmbientLight, DirectionalLight, Object3D } from "three";
|
||||||
import {
|
import {
|
||||||
AMBIENT_INTENSITY_MAX,
|
AMBIENT_INTENSITY_MAX,
|
||||||
AMBIENT_INTENSITY_MIN,
|
AMBIENT_INTENSITY_MIN,
|
||||||
@@ -22,17 +22,22 @@ import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
|||||||
import { LIGHTING_STATE } from "@/world/lightingState";
|
import { LIGHTING_STATE } from "@/world/lightingState";
|
||||||
|
|
||||||
const SHADOW_MAP_SIZE = 2048;
|
const SHADOW_MAP_SIZE = 2048;
|
||||||
const SHADOW_CAMERA_SIZE = 170;
|
const SHADOW_CAMERA_SIZE = 95;
|
||||||
const SHADOW_CAMERA_NEAR = 0.5;
|
const SHADOW_CAMERA_NEAR = 0.5;
|
||||||
const SHADOW_CAMERA_FAR = 300;
|
const SHADOW_CAMERA_FAR = 300;
|
||||||
|
|
||||||
export function Lighting(): React.JSX.Element {
|
export function Lighting(): React.JSX.Element {
|
||||||
|
const camera = useThree((state) => state.camera);
|
||||||
const ambient = useRef<AmbientLight>(null);
|
const ambient = useRef<AmbientLight>(null);
|
||||||
const sun = useRef<DirectionalLight>(null);
|
const sun = useRef<DirectionalLight>(null);
|
||||||
|
const sunTarget = useRef<Object3D>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!sun.current) return;
|
if (!sun.current || !sunTarget.current) return;
|
||||||
|
|
||||||
|
sun.current.target = sunTarget.current;
|
||||||
|
sun.current.shadow.autoUpdate = true;
|
||||||
|
sun.current.shadow.needsUpdate = true;
|
||||||
sun.current.shadow.mapSize.width = SHADOW_MAP_SIZE;
|
sun.current.shadow.mapSize.width = SHADOW_MAP_SIZE;
|
||||||
sun.current.shadow.mapSize.height = SHADOW_MAP_SIZE;
|
sun.current.shadow.mapSize.height = SHADOW_MAP_SIZE;
|
||||||
sun.current.shadow.camera.left = -SHADOW_CAMERA_SIZE;
|
sun.current.shadow.camera.left = -SHADOW_CAMERA_SIZE;
|
||||||
@@ -82,14 +87,18 @@ export function Lighting(): React.JSX.Element {
|
|||||||
ambient.current.intensity = LIGHTING_STATE.ambientIntensity;
|
ambient.current.intensity = LIGHTING_STATE.ambientIntensity;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sun.current) {
|
if (sun.current && sunTarget.current) {
|
||||||
|
sunTarget.current.position.set(camera.position.x, 0, camera.position.z);
|
||||||
|
sunTarget.current.updateMatrixWorld();
|
||||||
sun.current.position.set(
|
sun.current.position.set(
|
||||||
LIGHTING_STATE.sunX,
|
camera.position.x + LIGHTING_STATE.sunX,
|
||||||
LIGHTING_STATE.sunY,
|
LIGHTING_STATE.sunY,
|
||||||
LIGHTING_STATE.sunZ,
|
camera.position.z + LIGHTING_STATE.sunZ,
|
||||||
);
|
);
|
||||||
sun.current.color.set(LIGHTING_STATE.sunColor);
|
sun.current.color.set(LIGHTING_STATE.sunColor);
|
||||||
sun.current.intensity = LIGHTING_STATE.sunIntensity;
|
sun.current.intensity = LIGHTING_STATE.sunIntensity;
|
||||||
|
sun.current.updateMatrixWorld();
|
||||||
|
sun.current.shadow.needsUpdate = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -111,6 +120,7 @@ export function Lighting(): React.JSX.Element {
|
|||||||
color={LIGHTING_STATE.sunColor}
|
color={LIGHTING_STATE.sunColor}
|
||||||
castShadow
|
castShadow
|
||||||
/>
|
/>
|
||||||
|
<object3D ref={sunTarget} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { Suspense } from "react";
|
import { Suspense, useCallback, useMemo, useRef, useState } from "react";
|
||||||
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
|
import { CHUNK_CONFIG } from "@/data/world/fogConfig";
|
||||||
|
import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
||||||
|
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
||||||
import {
|
import {
|
||||||
isMapModelVisible,
|
isMapModelVisible,
|
||||||
useMapPerformanceStore,
|
useMapPerformanceStore,
|
||||||
@@ -6,44 +10,174 @@ import {
|
|||||||
import { InstancedMapAsset } from "@/world/map-instancing/InstancedMapAsset";
|
import { InstancedMapAsset } from "@/world/map-instancing/InstancedMapAsset";
|
||||||
import {
|
import {
|
||||||
MAP_INSTANCING_ASSETS,
|
MAP_INSTANCING_ASSETS,
|
||||||
|
type MapInstancingAssetConfig,
|
||||||
type MapInstancingAssetType,
|
type MapInstancingAssetType,
|
||||||
} from "@/world/map-instancing/mapInstancingConfig";
|
} from "@/world/map-instancing/mapInstancingConfig";
|
||||||
import { useMapInstancingData } from "@/world/map-instancing/useMapInstancingData";
|
import {
|
||||||
|
type MapAssetInstance,
|
||||||
|
useMapInstancingData,
|
||||||
|
} from "@/world/map-instancing/useMapInstancingData";
|
||||||
|
|
||||||
|
interface MapAssetChunk {
|
||||||
|
key: string;
|
||||||
|
config: MapInstancingAssetConfig;
|
||||||
|
centerX: number;
|
||||||
|
centerZ: number;
|
||||||
|
instances: MapAssetInstance[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChunkKey(instance: MapAssetInstance): string {
|
||||||
|
const [x, , z] = instance.position;
|
||||||
|
const chunkX = Math.floor(x / CHUNK_CONFIG.chunkSize);
|
||||||
|
const chunkZ = Math.floor(z / CHUNK_CONFIG.chunkSize);
|
||||||
|
return `${chunkX}:${chunkZ}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapAssetChunks(
|
||||||
|
type: MapInstancingAssetType,
|
||||||
|
config: MapInstancingAssetConfig,
|
||||||
|
instances: MapAssetInstance[],
|
||||||
|
): MapAssetChunk[] {
|
||||||
|
const chunks = new Map<string, MapAssetInstance[]>();
|
||||||
|
|
||||||
|
for (const instance of instances) {
|
||||||
|
const key = getChunkKey(instance);
|
||||||
|
const chunk = chunks.get(key);
|
||||||
|
|
||||||
|
if (chunk) {
|
||||||
|
chunk.push(instance);
|
||||||
|
} else {
|
||||||
|
chunks.set(key, [instance]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...chunks.entries()].map(([chunkKey, chunkInstances]) => {
|
||||||
|
const center = chunkInstances.reduce(
|
||||||
|
(sum, instance) => {
|
||||||
|
sum.x += instance.position[0];
|
||||||
|
sum.z += instance.position[2];
|
||||||
|
return sum;
|
||||||
|
},
|
||||||
|
{ x: 0, z: 0 },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: `${type}:${chunkKey}`,
|
||||||
|
config,
|
||||||
|
centerX: center.x / chunkInstances.length,
|
||||||
|
centerZ: center.z / chunkInstances.length,
|
||||||
|
instances: chunkInstances,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function MapInstancingSystem(): React.JSX.Element | null {
|
export function MapInstancingSystem(): React.JSX.Element | null {
|
||||||
|
const camera = useThree((state) => state.camera);
|
||||||
|
const cameraMode = useCameraMode();
|
||||||
|
const sceneMode = useSceneMode();
|
||||||
const groups = useMapPerformanceStore((state) => state.groups);
|
const groups = useMapPerformanceStore((state) => state.groups);
|
||||||
const models = useMapPerformanceStore((state) => state.models);
|
const models = useMapPerformanceStore((state) => state.models);
|
||||||
const { data, isLoading } = useMapInstancingData();
|
const { data, isLoading } = useMapInstancingData();
|
||||||
|
const lastUpdateRef = useRef(-CHUNK_CONFIG.updateInterval);
|
||||||
|
const [activeChunkKeys, setActiveChunkKeys] = useState<Set<string>>(
|
||||||
|
() => new Set(),
|
||||||
|
);
|
||||||
|
const streamingEnabled =
|
||||||
|
CHUNK_CONFIG.enabled && sceneMode === "game" && cameraMode === "player";
|
||||||
|
|
||||||
|
const chunks = useMemo(() => {
|
||||||
|
if (!data) return [];
|
||||||
|
|
||||||
|
return Object.entries(MAP_INSTANCING_ASSETS).flatMap(([type, config]) => {
|
||||||
|
if (
|
||||||
|
!config.enabled ||
|
||||||
|
!isMapModelVisible(config.mapName, { groups, models })
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const instances = data.get(type as MapInstancingAssetType);
|
||||||
|
if (!instances || instances.length === 0) return [];
|
||||||
|
|
||||||
|
return createMapAssetChunks(
|
||||||
|
type as MapInstancingAssetType,
|
||||||
|
config,
|
||||||
|
instances,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [data, groups, models]);
|
||||||
|
|
||||||
|
const visibleChunks = streamingEnabled
|
||||||
|
? chunks.filter((chunk) => {
|
||||||
|
if (activeChunkKeys.size > 0) {
|
||||||
|
return activeChunkKeys.has(chunk.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
Math.hypot(
|
||||||
|
chunk.centerX - camera.position.x,
|
||||||
|
chunk.centerZ - camera.position.z,
|
||||||
|
) <= CHUNK_CONFIG.loadRadius
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: chunks;
|
||||||
|
|
||||||
|
const updateActiveChunks = useCallback(() => {
|
||||||
|
const nextKeys = new Set<string>();
|
||||||
|
const cameraX = camera.position.x;
|
||||||
|
const cameraZ = camera.position.z;
|
||||||
|
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
const distance = Math.hypot(
|
||||||
|
chunk.centerX - cameraX,
|
||||||
|
chunk.centerZ - cameraZ,
|
||||||
|
);
|
||||||
|
const wasActive = activeChunkKeys.has(chunk.key);
|
||||||
|
const radius = wasActive
|
||||||
|
? CHUNK_CONFIG.unloadRadius
|
||||||
|
: CHUNK_CONFIG.loadRadius;
|
||||||
|
|
||||||
|
if (distance <= radius) {
|
||||||
|
nextKeys.add(chunk.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
nextKeys.size === activeChunkKeys.size &&
|
||||||
|
[...nextKeys].every((key) => activeChunkKeys.has(key))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveChunkKeys(nextKeys);
|
||||||
|
}, [activeChunkKeys, camera, chunks]);
|
||||||
|
|
||||||
|
useFrame(({ clock }) => {
|
||||||
|
if (!streamingEnabled) return;
|
||||||
|
|
||||||
|
const now = clock.elapsedTime * 1000;
|
||||||
|
if (now - lastUpdateRef.current < CHUNK_CONFIG.updateInterval) return;
|
||||||
|
lastUpdateRef.current = now;
|
||||||
|
|
||||||
|
updateActiveChunks();
|
||||||
|
});
|
||||||
|
|
||||||
if (isLoading || !data) {
|
if (isLoading || !data) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const enabledAssets = Object.entries(MAP_INSTANCING_ASSETS).filter(
|
|
||||||
([, config]) =>
|
|
||||||
config.enabled && isMapModelVisible(config.mapName, { groups, models }),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group name="map-instancing-system">
|
<group name="map-instancing-system">
|
||||||
{enabledAssets.map(([type, config]) => {
|
{visibleChunks.map((chunk) => (
|
||||||
const instances = data.get(type as MapInstancingAssetType);
|
<Suspense key={chunk.key} fallback={null}>
|
||||||
|
|
||||||
if (!instances || instances.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Suspense key={type} fallback={null}>
|
|
||||||
<InstancedMapAsset
|
<InstancedMapAsset
|
||||||
modelPath={config.modelPath}
|
modelPath={chunk.config.modelPath}
|
||||||
instances={instances}
|
instances={chunk.instances}
|
||||||
castShadow={config.castShadow}
|
castShadow={chunk.config.castShadow}
|
||||||
receiveShadow={config.receiveShadow}
|
receiveShadow={chunk.config.receiveShadow}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
))}
|
||||||
})}
|
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,34 @@ export const MAP_INSTANCING_ASSETS = {
|
|||||||
receiveShadow: true,
|
receiveShadow: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
|
panneauaffichage: {
|
||||||
|
mapName: "panneauaffichage",
|
||||||
|
modelPath: "/models/panneauaffichage/model.gltf",
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: true,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
panneauclassique: {
|
||||||
|
mapName: "panneauclassique",
|
||||||
|
modelPath: "/models/panneauclassique/model.gltf",
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: true,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
panneaufleche: {
|
||||||
|
mapName: "panneaufleche",
|
||||||
|
modelPath: "/models/panneaufleche/model.gltf",
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: true,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
panneausolaire: {
|
||||||
|
mapName: "panneausolaire",
|
||||||
|
modelPath: "/models/panneausolaire/model.gltf",
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: true,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type MapInstancingAssetType = keyof typeof MAP_INSTANCING_ASSETS;
|
export type MapInstancingAssetType = keyof typeof MAP_INSTANCING_ASSETS;
|
||||||
|
|||||||
@@ -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