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:
@@ -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 {
|
||||||
|
|||||||
@@ -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,25 +187,33 @@ 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;
|
||||||
<InstancedVegetation
|
return (
|
||||||
modelPath={chunk.modelPath}
|
<Suspense key={`${chunk.key}:${modelPath}`} fallback={null}>
|
||||||
instances={chunk.instances}
|
<InstancedVegetation
|
||||||
scaleMultiplier={chunk.scaleMultiplier}
|
modelPath={modelPath}
|
||||||
castShadow={chunk.castShadow}
|
instances={chunk.instances}
|
||||||
receiveShadow={chunk.receiveShadow}
|
scaleMultiplier={chunk.scaleMultiplier}
|
||||||
windStrength={chunk.windStrength}
|
castShadow={chunk.castShadow}
|
||||||
rotationOffset={chunk.rotationOffset}
|
receiveShadow={chunk.receiveShadow}
|
||||||
/>
|
windStrength={chunk.windStrength}
|
||||||
</Suspense>
|
rotationOffset={chunk.rotationOffset}
|
||||||
))}
|
/>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user