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",
-]);