diff --git a/src/data/world/mapLodConfig.ts b/src/data/world/mapLodConfig.ts index 2bea581..b7860b9 100644 --- a/src/data/world/mapLodConfig.ts +++ b/src/data/world/mapLodConfig.ts @@ -14,6 +14,9 @@ export const MAP_LOD_MODEL_PATHS = { maison1: "/models/maison1-LOD/model.gltf", panneauaffichage: "/models/panneauaffichage-LOD/model.gltf", talkie: "/models/talkie-LOD/model.gltf", + arbre: "/models/arbre-LOD/model.glb", + buisson: "/models/buisson-LOD/model.glb", + sapin: "/models/sapin-LOD/model.glb", } as const satisfies Record; export function getMapLodModelPath(modelName: string): string | null { diff --git a/src/world/vegetation/VegetationSystem.tsx b/src/world/vegetation/VegetationSystem.tsx index c74a931..002e0f2 100644 --- a/src/world/vegetation/VegetationSystem.tsx +++ b/src/world/vegetation/VegetationSystem.tsx @@ -1,8 +1,20 @@ -import { Suspense, useMemo } from "react"; +import { + Suspense, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { useFrame, useThree } from "@react-three/fiber"; import { CHUNK_CONFIG } from "@/data/world/chunkStreamingConfig"; +import { selectMapModelPathByDistance } from "@/data/world/mapLodConfig"; import { useCameraMode } from "@/hooks/debug/useCameraMode"; import { useSceneMode } from "@/hooks/debug/useSceneMode"; -import { useGraphicsPresetConfig } from "@/hooks/world/useGraphicsSettings"; +import { + useGraphicsPreset, + useGraphicsPresetConfig, +} from "@/hooks/world/useGraphicsSettings"; import { useVisibleWorldChunks } from "@/hooks/world/useVisibleWorldChunks"; import { isMapModelVisible, @@ -18,6 +30,7 @@ import { } from "@/data/world/vegetationConfig"; import { isInsideLaFabrikFootprint } from "@/data/world/laFabrikConfig"; import { createWorldInstanceChunks } from "@/utils/world/chunkInstances"; +import type { GraphicsPreset } from "@/data/world/graphicsConfig"; interface VegetationSystemProps { onlyMapName?: string | null; @@ -70,12 +83,73 @@ function removeLaFabrikVegetation( }); } +function areChunkModelPathsEqual( + a: ReadonlyMap, + b: ReadonlyMap, +): boolean { + return ( + a.size === b.size && [...a].every(([key, value]) => b.get(key) === value) + ); +} + +function useVegetationChunkModelPaths( + chunks: readonly VegetationChunk[], + preset: GraphicsPreset, +): ReadonlyMap { + const camera = useThree((state) => state.camera); + const lastUpdateRef = useRef(-CHUNK_CONFIG.updateInterval); + const modelPathsRef = useRef>(new Map()); + const [modelPaths, setModelPaths] = useState>( + () => new Map(), + ); + + const updateModelPaths = useCallback(() => { + const cameraX = camera.position.x; + const cameraZ = camera.position.z; + const next = new Map(); + + for (const chunk of chunks) { + const distance = Math.hypot( + chunk.centerX - cameraX, + chunk.centerZ - cameraZ, + ); + const modelPath = selectMapModelPathByDistance({ + distance, + modelName: VEGETATION_TYPES[chunk.type].mapName, + modelPath: chunk.modelPath, + preset, + }); + next.set(chunk.key, modelPath); + } + + if (areChunkModelPathsEqual(next, modelPathsRef.current)) return; + + modelPathsRef.current = next; + setModelPaths(next); + }, [camera, chunks, preset]); + + useEffect(() => { + updateModelPaths(); + }, [updateModelPaths]); + + useFrame(({ clock }) => { + const now = clock.elapsedTime * 1000; + if (now - lastUpdateRef.current < CHUNK_CONFIG.updateInterval) return; + lastUpdateRef.current = now; + + updateModelPaths(); + }); + + return modelPaths; +} + export function VegetationSystem({ onlyMapName = null, streaming = true, }: VegetationSystemProps): React.JSX.Element | null { const cameraMode = useCameraMode(); const sceneMode = useSceneMode(); + const graphicsPresetKey = useGraphicsPreset(); const graphicsPreset = useGraphicsPresetConfig(); const groups = useMapPerformanceStore((state) => state.groups); const models = useMapPerformanceStore((state) => state.models); @@ -113,25 +187,33 @@ export function VegetationSystem({ unloadRadius: graphicsPreset.chunkUnloadRadius, }); + const chunkModelPaths = useVegetationChunkModelPaths( + visibleChunks, + graphicsPresetKey, + ); + if (isLoading || !data) { return null; } return ( - {visibleChunks.map((chunk) => ( - - - - ))} + {visibleChunks.map((chunk) => { + const modelPath = chunkModelPaths.get(chunk.key) ?? chunk.modelPath; + return ( + + + + ); + })} ); }