feat: stream vegetation chunks near player
This commit is contained in:
@@ -3,16 +3,16 @@ import { TERRAIN_COLORS } from "@/data/world/terrainConfig";
|
|||||||
export const FOG_CONFIG = {
|
export const FOG_CONFIG = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
color: "#c8dbbe",
|
color: "#c8dbbe",
|
||||||
near: 50,
|
near: 22,
|
||||||
far: 70,
|
far: 38,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CHUNK_CONFIG = {
|
export const CHUNK_CONFIG = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
chunkSize: 40,
|
chunkSize: 30,
|
||||||
loadRadius: 70,
|
loadRadius: 30,
|
||||||
unloadRadius: 80,
|
unloadRadius: 40,
|
||||||
updateInterval: 500,
|
updateInterval: 350,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GROUND_PLANE_COLOR = TERRAIN_COLORS.grass1.hex;
|
export const GROUND_PLANE_COLOR = TERRAIN_COLORS.grass1.hex;
|
||||||
|
|||||||
+24
-10
@@ -6,6 +6,7 @@ import {
|
|||||||
GAME_SCENE_SKY_MODEL_SCALE,
|
GAME_SCENE_SKY_MODEL_SCALE,
|
||||||
PHYSICS_SCENE_BACKGROUND_COLOR,
|
PHYSICS_SCENE_BACKGROUND_COLOR,
|
||||||
} from "@/data/world/environmentConfig";
|
} from "@/data/world/environmentConfig";
|
||||||
|
import { FOG_CONFIG } from "@/data/world/fogConfig";
|
||||||
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
||||||
import {
|
import {
|
||||||
isMapModelVisible,
|
isMapModelVisible,
|
||||||
@@ -25,15 +26,28 @@ export function Environment(): React.JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return showSky ? (
|
return (
|
||||||
<SkyModel
|
<>
|
||||||
fallbackColor={GAME_SCENE_FALLBACK_BACKGROUND_COLOR}
|
{FOG_CONFIG.enabled ? (
|
||||||
fallbackModelPath={GAME_SCENE_FALLBACK_SKY_MODEL_PATH}
|
<fog
|
||||||
fallbackScale={GAME_SCENE_FALLBACK_SKY_MODEL_SCALE}
|
attach="fog"
|
||||||
modelPath={GAME_SCENE_SKY_MODEL_PATH}
|
args={[FOG_CONFIG.color, FOG_CONFIG.near, FOG_CONFIG.far]}
|
||||||
scale={GAME_SCENE_SKY_MODEL_SCALE}
|
/>
|
||||||
/>
|
) : null}
|
||||||
) : (
|
{showSky ? (
|
||||||
<color attach="background" args={[GAME_SCENE_FALLBACK_BACKGROUND_COLOR]} />
|
<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]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
import {
|
||||||
isMapModelVisible,
|
isMapModelVisible,
|
||||||
useMapPerformanceStore,
|
useMapPerformanceStore,
|
||||||
} from "@/managers/stores/useMapPerformanceStore";
|
} from "@/managers/stores/useMapPerformanceStore";
|
||||||
import { InstancedVegetation } from "@/world/vegetation/InstancedVegetation";
|
import { InstancedVegetation } from "@/world/vegetation/InstancedVegetation";
|
||||||
import { useVegetationData } from "@/world/vegetation/useVegetationData";
|
import {
|
||||||
|
type VegetationInstance,
|
||||||
|
useVegetationData,
|
||||||
|
} from "@/world/vegetation/useVegetationData";
|
||||||
import {
|
import {
|
||||||
VEGETATION_TYPES,
|
VEGETATION_TYPES,
|
||||||
type VegetationType,
|
type VegetationType,
|
||||||
} from "@/world/vegetation/vegetationConfig";
|
} 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 {
|
export function VegetationSystem(): React.JSX.Element | null {
|
||||||
|
const camera = useThree((state) => state.camera);
|
||||||
const groups = useMapPerformanceStore((state) => state.groups);
|
const groups = useMapPerformanceStore((state) => state.groups);
|
||||||
const models = useMapPerformanceStore((state) => state.models);
|
const models = useMapPerformanceStore((state) => state.models);
|
||||||
const { data, isLoading } = useVegetationData();
|
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) {
|
if (isLoading || !data) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const enabledTypes = Object.entries(VEGETATION_TYPES).filter(
|
const visibleChunks = CHUNK_CONFIG.enabled
|
||||||
([, config]) =>
|
? chunks.filter((chunk) => activeChunkKeys.has(chunk.key))
|
||||||
config.enabled && isMapModelVisible(config.mapName, { groups, models }),
|
: chunks;
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group name="vegetation-system">
|
<group name="vegetation-system">
|
||||||
{enabledTypes.map(([type, config]) => {
|
{visibleChunks.map((chunk) => (
|
||||||
const instances = data.get(type as VegetationType);
|
<Suspense key={chunk.key} fallback={null}>
|
||||||
|
<InstancedVegetation
|
||||||
if (!instances || instances.length === 0) {
|
modelPath={chunk.modelPath}
|
||||||
return null;
|
instances={chunk.instances}
|
||||||
}
|
castShadow={chunk.castShadow}
|
||||||
|
receiveShadow={chunk.receiveShadow}
|
||||||
return (
|
/>
|
||||||
<Suspense key={type} fallback={null}>
|
</Suspense>
|
||||||
<InstancedVegetation
|
))}
|
||||||
modelPath={config.modelPath}
|
|
||||||
instances={instances}
|
|
||||||
castShadow={config.castShadow}
|
|
||||||
receiveShadow={config.receiveShadow}
|
|
||||||
/>
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,19 +4,55 @@ export const VEGETATION_LOD = {
|
|||||||
windFadeEnd: 70,
|
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([
|
export const INSTANCED_MAP_EXCEPTIONS = new Set([
|
||||||
"Scene",
|
"Scene",
|
||||||
"blocking",
|
"blocking",
|
||||||
"terrain",
|
"terrain",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const INSTANCED_MAP_CHUNK_SIZE = 45;
|
|
||||||
|
|
||||||
export const INSTANCED_MAP_NO_SHADOW_NAMES = new Set([
|
|
||||||
"arbre",
|
|
||||||
"sapin",
|
|
||||||
"buisson",
|
|
||||||
"champdeble",
|
|
||||||
"champdesoja",
|
|
||||||
"champsdetournesol",
|
|
||||||
]);
|
|
||||||
|
|||||||
Reference in New Issue
Block a user