feat(vegetation): wire arbre/sapin/buisson into LOD system

Register the three new vegetation LOD models in MAP_LOD_MODEL_PATHS and
extend VegetationSystem with per-chunk distance-based LOD selection
(mirroring MapInstancingSystem). Chunk model paths are re-evaluated on
the existing CHUNK_CONFIG.updateInterval cadence and Suspense keys
include the resolved path so a swap unmounts the previous instanced mesh
cleanly.
This commit is contained in:
Tom Boullay
2026-06-02 14:17:19 +02:00
parent 0b801888f0
commit 627c8d4eb9
2 changed files with 100 additions and 15 deletions
+3
View File
@@ -14,6 +14,9 @@ export const MAP_LOD_MODEL_PATHS = {
maison1: "/models/maison1-LOD/model.gltf", maison1: "/models/maison1-LOD/model.gltf",
panneauaffichage: "/models/panneauaffichage-LOD/model.gltf", panneauaffichage: "/models/panneauaffichage-LOD/model.gltf",
talkie: "/models/talkie-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<string, string>; } as const satisfies Record<string, string>;
export function getMapLodModelPath(modelName: string): string | null { export function getMapLodModelPath(modelName: string): string | null {
+88 -6
View File
@@ -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 { CHUNK_CONFIG } from "@/data/world/chunkStreamingConfig";
import { selectMapModelPathByDistance } from "@/data/world/mapLodConfig";
import { useCameraMode } from "@/hooks/debug/useCameraMode"; import { useCameraMode } from "@/hooks/debug/useCameraMode";
import { useSceneMode } from "@/hooks/debug/useSceneMode"; 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 { useVisibleWorldChunks } from "@/hooks/world/useVisibleWorldChunks";
import { import {
isMapModelVisible, isMapModelVisible,
@@ -18,6 +30,7 @@ import {
} from "@/data/world/vegetationConfig"; } from "@/data/world/vegetationConfig";
import { isInsideLaFabrikFootprint } from "@/data/world/laFabrikConfig"; import { isInsideLaFabrikFootprint } from "@/data/world/laFabrikConfig";
import { createWorldInstanceChunks } from "@/utils/world/chunkInstances"; import { createWorldInstanceChunks } from "@/utils/world/chunkInstances";
import type { GraphicsPreset } from "@/data/world/graphicsConfig";
interface VegetationSystemProps { interface VegetationSystemProps {
onlyMapName?: string | null; onlyMapName?: string | null;
@@ -70,12 +83,73 @@ function removeLaFabrikVegetation(
}); });
} }
function areChunkModelPathsEqual(
a: ReadonlyMap<string, string>,
b: ReadonlyMap<string, string>,
): boolean {
return (
a.size === b.size && [...a].every(([key, value]) => b.get(key) === value)
);
}
function useVegetationChunkModelPaths(
chunks: readonly VegetationChunk[],
preset: GraphicsPreset,
): ReadonlyMap<string, string> {
const camera = useThree((state) => state.camera);
const lastUpdateRef = useRef(-CHUNK_CONFIG.updateInterval);
const modelPathsRef = useRef<Map<string, string>>(new Map());
const [modelPaths, setModelPaths] = useState<ReadonlyMap<string, string>>(
() => new Map(),
);
const updateModelPaths = useCallback(() => {
const cameraX = camera.position.x;
const cameraZ = camera.position.z;
const next = new Map<string, string>();
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({ export function VegetationSystem({
onlyMapName = null, onlyMapName = null,
streaming = true, streaming = true,
}: VegetationSystemProps): React.JSX.Element | null { }: VegetationSystemProps): React.JSX.Element | null {
const cameraMode = useCameraMode(); const cameraMode = useCameraMode();
const sceneMode = useSceneMode(); const sceneMode = useSceneMode();
const graphicsPresetKey = useGraphicsPreset();
const graphicsPreset = useGraphicsPresetConfig(); const graphicsPreset = useGraphicsPresetConfig();
const groups = useMapPerformanceStore((state) => state.groups); const groups = useMapPerformanceStore((state) => state.groups);
const models = useMapPerformanceStore((state) => state.models); const models = useMapPerformanceStore((state) => state.models);
@@ -113,16 +187,23 @@ export function VegetationSystem({
unloadRadius: graphicsPreset.chunkUnloadRadius, unloadRadius: graphicsPreset.chunkUnloadRadius,
}); });
const chunkModelPaths = useVegetationChunkModelPaths(
visibleChunks,
graphicsPresetKey,
);
if (isLoading || !data) { if (isLoading || !data) {
return null; return null;
} }
return ( return (
<group name="vegetation-system"> <group name="vegetation-system">
{visibleChunks.map((chunk) => ( {visibleChunks.map((chunk) => {
<Suspense key={chunk.key} fallback={null}> const modelPath = chunkModelPaths.get(chunk.key) ?? chunk.modelPath;
return (
<Suspense key={`${chunk.key}:${modelPath}`} fallback={null}>
<InstancedVegetation <InstancedVegetation
modelPath={chunk.modelPath} modelPath={modelPath}
instances={chunk.instances} instances={chunk.instances}
scaleMultiplier={chunk.scaleMultiplier} scaleMultiplier={chunk.scaleMultiplier}
castShadow={chunk.castShadow} castShadow={chunk.castShadow}
@@ -131,7 +212,8 @@ export function VegetationSystem({
rotationOffset={chunk.rotationOffset} rotationOffset={chunk.rotationOffset}
/> />
</Suspense> </Suspense>
))} );
})}
</group> </group>
); );
} }