Feat/map-environment #6

Merged
math-pixel merged 116 commits from feat/map-environment into develop 2026-05-29 00:00:51 +00:00
4 changed files with 210 additions and 51 deletions
Showing only changes of commit e4857135b1 - Show all commits
+6 -6
View File
@@ -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;
+24 -10
View File
@@ -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 ? (
<SkyModel
fallbackColor={GAME_SCENE_FALLBACK_BACKGROUND_COLOR}
fallbackModelPath={GAME_SCENE_FALLBACK_SKY_MODEL_PATH}
fallbackScale={GAME_SCENE_FALLBACK_SKY_MODEL_SCALE}
modelPath={GAME_SCENE_SKY_MODEL_PATH}
scale={GAME_SCENE_SKY_MODEL_SCALE}
/>
) : (
<color attach="background" args={[GAME_SCENE_FALLBACK_BACKGROUND_COLOR]} />
return (
<>
{FOG_CONFIG.enabled ? (
<fog
attach="fog"
args={[FOG_CONFIG.color, FOG_CONFIG.near, FOG_CONFIG.far]}
/>
) : null}
{showSky ? (
<SkyModel
fallbackColor={GAME_SCENE_FALLBACK_BACKGROUND_COLOR}
fallbackModelPath={GAME_SCENE_FALLBACK_SKY_MODEL_PATH}
fallbackScale={GAME_SCENE_FALLBACK_SKY_MODEL_SCALE}
modelPath={GAME_SCENE_SKY_MODEL_PATH}
scale={GAME_SCENE_SKY_MODEL_SCALE}
/>
) : (
<color
attach="background"
args={[GAME_SCENE_FALLBACK_BACKGROUND_COLOR]}
/>
)}
</>
);
}
+133 -24
View File
@@ -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<string, VegetationInstance[]>();
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<Set<string>>(
() => 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<string>();
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 (
<group name="vegetation-system">
{enabledTypes.map(([type, config]) => {
const instances = data.get(type as VegetationType);
if (!instances || instances.length === 0) {
return null;
}
return (
<Suspense key={type} fallback={null}>
<InstancedVegetation
modelPath={config.modelPath}
instances={instances}
castShadow={config.castShadow}
receiveShadow={config.receiveShadow}
/>
</Suspense>
);
})}
{visibleChunks.map((chunk) => (
<Suspense key={chunk.key} fallback={null}>
<InstancedVegetation
modelPath={chunk.modelPath}
instances={chunk.instances}
castShadow={chunk.castShadow}
receiveShadow={chunk.receiveShadow}
/>
</Suspense>
))}
</group>
);
}
+47 -11
View File
@@ -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",
]);