diff --git a/src/data/world/fogConfig.ts b/src/data/world/fogConfig.ts index 3365f80..16ce426 100644 --- a/src/data/world/fogConfig.ts +++ b/src/data/world/fogConfig.ts @@ -3,16 +3,16 @@ import { TERRAIN_COLORS } from "@/data/world/terrainConfig"; export const FOG_CONFIG = { enabled: true, color: "#c8dbbe", - near: 50, - far: 70, + near: 22, + far: 38, }; export const CHUNK_CONFIG = { enabled: true, - chunkSize: 40, - loadRadius: 70, - unloadRadius: 80, - updateInterval: 500, + chunkSize: 30, + loadRadius: 30, + unloadRadius: 40, + updateInterval: 350, }; export const GROUND_PLANE_COLOR = TERRAIN_COLORS.grass1.hex; diff --git a/src/world/Environment.tsx b/src/world/Environment.tsx index d1ffeb1..ee20441 100644 --- a/src/world/Environment.tsx +++ b/src/world/Environment.tsx @@ -6,6 +6,7 @@ import { GAME_SCENE_SKY_MODEL_SCALE, PHYSICS_SCENE_BACKGROUND_COLOR, } from "@/data/world/environmentConfig"; +import { FOG_CONFIG } from "@/data/world/fogConfig"; import { useSceneMode } from "@/hooks/debug/useSceneMode"; import { isMapModelVisible, @@ -25,15 +26,28 @@ export function Environment(): React.JSX.Element { ); } - return showSky ? ( - - ) : ( - + return ( + <> + {FOG_CONFIG.enabled ? ( + + ) : null} + {showSky ? ( + + ) : ( + + )} + ); } diff --git a/src/world/vegetation/VegetationSystem.tsx b/src/world/vegetation/VegetationSystem.tsx index ebe158c..9d0120a 100644 --- a/src/world/vegetation/VegetationSystem.tsx +++ b/src/world/vegetation/VegetationSystem.tsx @@ -1,49 +1,158 @@ -import { Suspense } from "react"; +import { Suspense, useMemo, useRef, useState } from "react"; +import { useFrame, useThree } from "@react-three/fiber"; +import { CHUNK_CONFIG } from "@/data/world/fogConfig"; import { isMapModelVisible, useMapPerformanceStore, } from "@/managers/stores/useMapPerformanceStore"; import { InstancedVegetation } from "@/world/vegetation/InstancedVegetation"; -import { useVegetationData } from "@/world/vegetation/useVegetationData"; +import { + type VegetationInstance, + useVegetationData, +} from "@/world/vegetation/useVegetationData"; import { VEGETATION_TYPES, type VegetationType, } from "@/world/vegetation/vegetationConfig"; +interface VegetationChunk { + key: string; + type: VegetationType; + modelPath: string; + castShadow: boolean; + receiveShadow: boolean; + centerX: number; + centerZ: number; + instances: VegetationInstance[]; +} + +function getChunkKey(instance: VegetationInstance): 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 createVegetationChunks( + type: VegetationType, + instances: VegetationInstance[], +): VegetationChunk[] { + const config = VEGETATION_TYPES[type]; + const chunks = new Map(); + + 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}`, + type, + modelPath: config.modelPath, + castShadow: config.castShadow, + receiveShadow: config.receiveShadow, + centerX: center.x / chunkInstances.length, + centerZ: center.z / chunkInstances.length, + instances: chunkInstances, + }; + }); +} + export function VegetationSystem(): React.JSX.Element | null { + const camera = useThree((state) => state.camera); const groups = useMapPerformanceStore((state) => state.groups); const models = useMapPerformanceStore((state) => state.models); const { data, isLoading } = useVegetationData(); + const lastUpdateRef = useRef(-CHUNK_CONFIG.updateInterval); + const [activeChunkKeys, setActiveChunkKeys] = useState>( + () => new Set(), + ); + + const chunks = useMemo(() => { + if (!data) return []; + + return Object.entries(VEGETATION_TYPES).flatMap(([type, config]) => { + if (!config.enabled) return []; + if (!isMapModelVisible(config.mapName, { groups, models })) return []; + + const entry = data.get(config.mapName); + if (!entry || entry.instances.length === 0) return []; + + return createVegetationChunks(type as VegetationType, entry.instances); + }); + }, [data, groups, models]); + + useFrame(({ clock }) => { + if (!CHUNK_CONFIG.enabled) return; + + const now = clock.elapsedTime * 1000; + if (now - lastUpdateRef.current < CHUNK_CONFIG.updateInterval) return; + lastUpdateRef.current = now; + + const nextKeys = new Set(); + 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); + }); if (isLoading || !data) { return null; } - const enabledTypes = Object.entries(VEGETATION_TYPES).filter( - ([, config]) => - config.enabled && isMapModelVisible(config.mapName, { groups, models }), - ); + const visibleChunks = CHUNK_CONFIG.enabled + ? chunks.filter((chunk) => activeChunkKeys.has(chunk.key)) + : chunks; return ( - {enabledTypes.map(([type, config]) => { - const instances = data.get(type as VegetationType); - - if (!instances || instances.length === 0) { - return null; - } - - return ( - - - - ); - })} + {visibleChunks.map((chunk) => ( + + + + ))} ); } diff --git a/src/world/vegetation/vegetationConfig.ts b/src/world/vegetation/vegetationConfig.ts index 73e7200..b279fea 100644 --- a/src/world/vegetation/vegetationConfig.ts +++ b/src/world/vegetation/vegetationConfig.ts @@ -4,19 +4,55 @@ export const VEGETATION_LOD = { windFadeEnd: 70, }; +export const VEGETATION_TYPES = { + buissons: { + mapName: "buisson", + modelPath: "/models/buisson/model.gltf", + castShadow: true, + receiveShadow: true, + enabled: true, + }, + sapin: { + mapName: "sapin", + modelPath: "/models/sapin/model.gltf", + castShadow: true, + receiveShadow: true, + enabled: true, + }, + arbre: { + mapName: "arbre", + modelPath: "/models/arbre/model.gltf", + castShadow: true, + receiveShadow: true, + enabled: true, + }, + champdeble: { + mapName: "champdeble", + modelPath: "/models/champdeble/model.gltf", + castShadow: true, + receiveShadow: true, + enabled: true, + }, + champdesoja: { + mapName: "champdesoja", + modelPath: "/models/champdesoja/model.gltf", + castShadow: true, + receiveShadow: true, + enabled: true, + }, + champsdetournesol: { + mapName: "champsdetournesol", + modelPath: "/models/champsdetournesol/model.gltf", + castShadow: true, + receiveShadow: true, + enabled: true, + }, +} as const; + +export type VegetationType = keyof typeof VEGETATION_TYPES; + export const INSTANCED_MAP_EXCEPTIONS = new Set([ "Scene", "blocking", "terrain", ]); - -export const INSTANCED_MAP_CHUNK_SIZE = 45; - -export const INSTANCED_MAP_NO_SHADOW_NAMES = new Set([ - "arbre", - "sapin", - "buisson", - "champdeble", - "champdesoja", - "champsdetournesol", -]);