feat(world): add map lod graphics presets
This commit is contained in:
@@ -2,11 +2,11 @@ import type { ReactNode } from "react";
|
||||
import {
|
||||
Component,
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useCallback,
|
||||
} from "react";
|
||||
import * as THREE from "three";
|
||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from "@/managers/stores/useMapPerformanceStore";
|
||||
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||
import { useRepairMissionAnchorStore } from "@/managers/stores/useRepairMissionAnchorStore";
|
||||
import { useMapLodModelPath } from "@/hooks/world/useMapLodModelPath";
|
||||
import { GameMapCollision } from "@/world/GameMapCollision";
|
||||
import { GeneratedMapNodeInstance } from "@/world/map-generated/GeneratedMapNodeInstance";
|
||||
import { isGeneratedMapModelName } from "@/data/world/generatedMapModelConfig";
|
||||
@@ -362,6 +363,11 @@ function ModelInstance({
|
||||
const { position, rotation, scale } = node;
|
||||
const scaleMultiplier = getMapSingleModelScaleMultiplier(node.name);
|
||||
const baseScale = normalizeMapScale(scale);
|
||||
const activeModelUrl = useMapLodModelPath({
|
||||
modelName: node.name,
|
||||
modelPath: modelUrl,
|
||||
position: node.position,
|
||||
});
|
||||
const normalizedScale = useMemo(
|
||||
() =>
|
||||
[
|
||||
@@ -372,7 +378,7 @@ function ModelInstance({
|
||||
[baseScale, scaleMultiplier],
|
||||
);
|
||||
const terrainHeight = useTerrainHeightSampler();
|
||||
const { scene } = useLoggedGLTF(modelUrl, {
|
||||
const { scene } = useLoggedGLTF(activeModelUrl, {
|
||||
scope: "GameMap.ModelInstance",
|
||||
position,
|
||||
rotation,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
||||
import { useDebugStore } from "@/hooks/debug/useDebugStore";
|
||||
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
||||
import { useFogSettings } from "@/hooks/world/useFogSettings";
|
||||
import { useGraphicsPresetConfig } from "@/hooks/world/useGraphicsSettings";
|
||||
import { LIGHTING_STATE } from "@/world/lightingState";
|
||||
|
||||
const tempSunFogColor = new THREE.Color();
|
||||
@@ -23,11 +24,14 @@ export function FogSystem(): React.JSX.Element | null {
|
||||
const cameraMode = useCameraMode();
|
||||
const sceneMode = useSceneMode();
|
||||
const fog = useFogSettings();
|
||||
const graphicsPreset = useGraphicsPresetConfig();
|
||||
const fogEnabled = useDebugStore((debug) => debug.getFogEnabled());
|
||||
const scene = useThree((state) => state.scene);
|
||||
const fogColor = useMemo(() => getLightingFogColor(new THREE.Color()), []);
|
||||
const shouldShowFog =
|
||||
fogEnabled && sceneMode === "game" && cameraMode === "player";
|
||||
(fogEnabled || graphicsPreset.fogEnabled) &&
|
||||
sceneMode === "game" &&
|
||||
cameraMode === "player";
|
||||
|
||||
useFrame(() => {
|
||||
if (!scene.fog) return;
|
||||
|
||||
@@ -1,7 +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 { selectMapModelPathByDistance } from "@/data/world/mapLodConfig";
|
||||
import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
||||
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
||||
import {
|
||||
useGraphicsPreset,
|
||||
useGraphicsPresetConfig,
|
||||
} from "@/hooks/world/useGraphicsSettings";
|
||||
import { useVisibleWorldChunks } from "@/hooks/world/useVisibleWorldChunks";
|
||||
import {
|
||||
isMapModelVisible,
|
||||
@@ -16,6 +29,7 @@ import {
|
||||
} from "@/data/world/mapInstancingConfig";
|
||||
import { useMapInstancingData } from "@/hooks/world/useMapInstancingData";
|
||||
import type { MapAssetInstance } from "@/types/map/mapScene";
|
||||
import type { GraphicsPreset } from "@/data/world/graphicsConfig";
|
||||
import { createWorldInstanceChunks } from "@/utils/world/chunkInstances";
|
||||
|
||||
interface MapInstancingSystemProps {
|
||||
@@ -47,12 +61,88 @@ function createMapAssetChunks(
|
||||
});
|
||||
}
|
||||
|
||||
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 getNearestChunkInstanceDistance(
|
||||
chunk: MapAssetChunk,
|
||||
cameraX: number,
|
||||
cameraZ: number,
|
||||
): number {
|
||||
return chunk.instances.reduce((nearestDistance, instance) => {
|
||||
const distance = Math.hypot(
|
||||
instance.position[0] - cameraX,
|
||||
instance.position[2] - cameraZ,
|
||||
);
|
||||
|
||||
return Math.min(nearestDistance, distance);
|
||||
}, Number.POSITIVE_INFINITY);
|
||||
}
|
||||
|
||||
function useChunkModelPaths(
|
||||
chunks: readonly MapAssetChunk[],
|
||||
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 nextModelPaths = new Map<string, string>();
|
||||
|
||||
for (const chunk of chunks) {
|
||||
const distance = getNearestChunkInstanceDistance(chunk, cameraX, cameraZ);
|
||||
const modelPath = selectMapModelPathByDistance({
|
||||
distance,
|
||||
modelName: chunk.config.mapName,
|
||||
modelPath: chunk.config.modelPath,
|
||||
preset,
|
||||
});
|
||||
|
||||
nextModelPaths.set(chunk.key, modelPath);
|
||||
}
|
||||
|
||||
if (areChunkModelPathsEqual(nextModelPaths, modelPathsRef.current)) return;
|
||||
|
||||
modelPathsRef.current = nextModelPaths;
|
||||
setModelPaths(nextModelPaths);
|
||||
}, [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 MapInstancingSystem({
|
||||
onlyMapName = null,
|
||||
streaming = true,
|
||||
}: MapInstancingSystemProps): React.JSX.Element | null {
|
||||
const camera = useThree((state) => state.camera);
|
||||
const cameraMode = useCameraMode();
|
||||
const sceneMode = useSceneMode();
|
||||
const graphicsPreset = useGraphicsPreset();
|
||||
const graphicsPresetConfig = useGraphicsPresetConfig();
|
||||
const groups = useMapPerformanceStore((state) => state.groups);
|
||||
const models = useMapPerformanceStore((state) => state.models);
|
||||
const { data, isLoading } = useMapInstancingData();
|
||||
@@ -84,7 +174,29 @@ export function MapInstancingSystem({
|
||||
});
|
||||
}, [data, groups, models, onlyMapName]);
|
||||
|
||||
const visibleChunks = useVisibleWorldChunks(chunks, streamingEnabled);
|
||||
const visibleChunks = useVisibleWorldChunks(chunks, streamingEnabled, {
|
||||
loadRadius: graphicsPresetConfig.chunkLoadRadius,
|
||||
unloadRadius: graphicsPresetConfig.chunkUnloadRadius,
|
||||
});
|
||||
const chunkModelPaths = useChunkModelPaths(visibleChunks, graphicsPreset);
|
||||
const getChunkModelPath = useCallback(
|
||||
(chunk: MapAssetChunk): string => {
|
||||
const cachedModelPath = chunkModelPaths.get(chunk.key);
|
||||
if (cachedModelPath) return cachedModelPath;
|
||||
|
||||
return selectMapModelPathByDistance({
|
||||
distance: getNearestChunkInstanceDistance(
|
||||
chunk,
|
||||
camera.position.x,
|
||||
camera.position.z,
|
||||
),
|
||||
modelName: chunk.config.mapName,
|
||||
modelPath: chunk.config.modelPath,
|
||||
preset: graphicsPreset,
|
||||
});
|
||||
},
|
||||
[camera, chunkModelPaths, graphicsPreset],
|
||||
);
|
||||
|
||||
if (isLoading || !data) {
|
||||
return null;
|
||||
@@ -92,17 +204,21 @@ export function MapInstancingSystem({
|
||||
|
||||
return (
|
||||
<group name="map-instancing-system">
|
||||
{visibleChunks.map((chunk) => (
|
||||
<Suspense key={chunk.key} fallback={null}>
|
||||
<InstancedMapAsset
|
||||
modelPath={chunk.config.modelPath}
|
||||
instances={chunk.instances}
|
||||
scaleMultiplier={chunk.config.scaleMultiplier}
|
||||
castShadow={chunk.config.castShadow}
|
||||
receiveShadow={chunk.config.receiveShadow}
|
||||
/>
|
||||
</Suspense>
|
||||
))}
|
||||
{visibleChunks.map((chunk) => {
|
||||
const modelPath = getChunkModelPath(chunk);
|
||||
|
||||
return (
|
||||
<Suspense key={`${chunk.key}:${modelPath}`} fallback={null}>
|
||||
<InstancedMapAsset
|
||||
modelPath={modelPath}
|
||||
instances={chunk.instances}
|
||||
scaleMultiplier={chunk.config.scaleMultiplier}
|
||||
castShadow={chunk.config.castShadow}
|
||||
receiveShadow={chunk.config.receiveShadow}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
})}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Suspense, useMemo } from "react";
|
||||
import { CHUNK_CONFIG } from "@/data/world/chunkStreamingConfig";
|
||||
import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
||||
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
||||
import { useGraphicsPresetConfig } from "@/hooks/world/useGraphicsSettings";
|
||||
import { useVisibleWorldChunks } from "@/hooks/world/useVisibleWorldChunks";
|
||||
import {
|
||||
isMapModelVisible,
|
||||
@@ -65,6 +66,7 @@ export function VegetationSystem({
|
||||
}: VegetationSystemProps): React.JSX.Element | null {
|
||||
const cameraMode = useCameraMode();
|
||||
const sceneMode = useSceneMode();
|
||||
const graphicsPreset = useGraphicsPresetConfig();
|
||||
const groups = useMapPerformanceStore((state) => state.groups);
|
||||
const models = useMapPerformanceStore((state) => state.models);
|
||||
const { data, isLoading } = useVegetationData();
|
||||
@@ -92,7 +94,10 @@ export function VegetationSystem({
|
||||
});
|
||||
}, [data, groups, models, onlyMapName]);
|
||||
|
||||
const visibleChunks = useVisibleWorldChunks(chunks, streamingEnabled);
|
||||
const visibleChunks = useVisibleWorldChunks(chunks, streamingEnabled, {
|
||||
loadRadius: graphicsPreset.chunkLoadRadius,
|
||||
unloadRadius: graphicsPreset.chunkUnloadRadius,
|
||||
});
|
||||
|
||||
if (isLoading || !data) {
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user