Feat/map-environment #6

Merged
math-pixel merged 116 commits from feat/map-environment into develop 2026-05-29 00:00:51 +00:00
7 changed files with 258 additions and 9 deletions
Showing only changes of commit e4ee2d768b - Show all commits
+58
View File
@@ -0,0 +1,58 @@
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
import {
MAP_PERFORMANCE_GROUP_NAMES,
MAP_PERFORMANCE_MODEL_NAMES,
useMapPerformanceStore,
} from "@/managers/stores/useMapPerformanceStore";
function toLabel(value: string): string {
return value
.split(/[-_\s]+/)
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(" ");
}
export function useMapPerformanceDebug(): void {
useDebugFolder("Performance / Map", (folder) => {
const {
groups,
models,
setGroupVisible,
setModelVisible,
resetVisibility,
} = useMapPerformanceStore.getState();
const controls = {
...groups,
...models,
reset: () => {
resetVisibility();
for (const key of [
...MAP_PERFORMANCE_GROUP_NAMES,
...MAP_PERFORMANCE_MODEL_NAMES,
]) {
controls[key] = true;
}
folder.controllersRecursive().forEach((controller) => {
controller.updateDisplay();
});
},
};
for (const group of MAP_PERFORMANCE_GROUP_NAMES) {
folder
.add(controls, group)
.name(toLabel(group))
.onChange((visible: boolean) => setGroupVisible(group, visible));
}
for (const model of MAP_PERFORMANCE_MODEL_NAMES) {
folder
.add(controls, model)
.name(toLabel(model))
.onChange((visible: boolean) => setModelVisible(model, visible));
}
folder.add(controls, "reset").name("Reset visibility");
});
}
@@ -0,0 +1,145 @@
import { create } from "zustand";
export type MapPerformanceGroupName =
| "vegetation"
| "crops"
| "trees"
| "buildings"
| "landmarks"
| "props"
| "terrain"
| "sky";
export type MapPerformanceModelName =
| "buisson"
| "arbre"
| "sapin"
| "champdeble"
| "champdesoja"
| "champsdetournesol"
| "ecole"
| "generateur"
| "fermeverticale"
| "lafabrik"
| "immeuble1"
| "eolienne"
| "pylone"
| "boiteauxlettres"
| "maison1"
| "parcebike"
| "terrain"
| "sky";
export interface MapPerformanceVisibility {
groups: Record<MapPerformanceGroupName, boolean>;
models: Record<MapPerformanceModelName, boolean>;
}
interface MapPerformanceActions {
setGroupVisible: (group: MapPerformanceGroupName, visible: boolean) => void;
setModelVisible: (model: MapPerformanceModelName, visible: boolean) => void;
resetVisibility: () => void;
}
type MapPerformanceStore = MapPerformanceVisibility & MapPerformanceActions;
export const MAP_PERFORMANCE_GROUP_NAMES: readonly MapPerformanceGroupName[] = [
"vegetation",
"crops",
"trees",
"buildings",
"landmarks",
"props",
"terrain",
"sky",
];
export const MAP_PERFORMANCE_MODEL_NAMES: readonly MapPerformanceModelName[] = [
"buisson",
"arbre",
"sapin",
"champdeble",
"champdesoja",
"champsdetournesol",
"ecole",
"generateur",
"fermeverticale",
"lafabrik",
"immeuble1",
"eolienne",
"pylone",
"boiteauxlettres",
"maison1",
"parcebike",
"terrain",
"sky",
];
const MODEL_GROUPS: Record<
MapPerformanceModelName,
readonly MapPerformanceGroupName[]
> = {
buisson: ["vegetation"],
arbre: ["vegetation", "trees"],
sapin: ["vegetation", "trees"],
champdeble: ["vegetation", "crops"],
champdesoja: ["vegetation", "crops"],
champsdetournesol: ["vegetation", "crops"],
ecole: ["buildings", "landmarks"],
generateur: ["landmarks"],
fermeverticale: ["buildings", "landmarks"],
lafabrik: ["buildings", "landmarks"],
immeuble1: ["buildings"],
eolienne: ["props"],
pylone: ["props"],
boiteauxlettres: ["props"],
maison1: ["buildings"],
parcebike: ["props"],
terrain: ["terrain"],
sky: ["sky"],
};
function createVisibleRecord<T extends string>(
keys: readonly T[],
): Record<T, boolean> {
return Object.fromEntries(keys.map((key) => [key, true])) as Record<
T,
boolean
>;
}
function createDefaultVisibility(): MapPerformanceVisibility {
return {
groups: createVisibleRecord(MAP_PERFORMANCE_GROUP_NAMES),
models: createVisibleRecord(MAP_PERFORMANCE_MODEL_NAMES),
};
}
export function isMapPerformanceModelName(
name: string,
): name is MapPerformanceModelName {
return MAP_PERFORMANCE_MODEL_NAMES.includes(name as MapPerformanceModelName);
}
export function isMapModelVisible(
name: string,
visibility: MapPerformanceVisibility,
): boolean {
if (!isMapPerformanceModelName(name)) return true;
if (!visibility.models[name]) return false;
return MODEL_GROUPS[name].every((group) => visibility.groups[group]);
}
export const useMapPerformanceStore = create<MapPerformanceStore>()((set) => ({
...createDefaultVisibility(),
setGroupVisible: (group, visible) =>
set((state) => ({
groups: { ...state.groups, [group]: visible },
})),
setModelVisible: (model, visible) =>
set((state) => ({
models: { ...state.models, [model]: visible },
})),
resetVisibility: () => set(createDefaultVisibility()),
}));
+10 -1
View File
@@ -7,10 +7,17 @@ import {
PHYSICS_SCENE_BACKGROUND_COLOR, PHYSICS_SCENE_BACKGROUND_COLOR,
} from "@/data/world/environmentConfig"; } from "@/data/world/environmentConfig";
import { useSceneMode } from "@/hooks/debug/useSceneMode"; import { useSceneMode } from "@/hooks/debug/useSceneMode";
import {
isMapModelVisible,
useMapPerformanceStore,
} from "@/managers/stores/useMapPerformanceStore";
import { SkyModel } from "@/components/three/world/SkyModel"; import { SkyModel } from "@/components/three/world/SkyModel";
export function Environment(): React.JSX.Element { export function Environment(): React.JSX.Element {
const sceneMode = useSceneMode(); const sceneMode = useSceneMode();
const groups = useMapPerformanceStore((state) => state.groups);
const models = useMapPerformanceStore((state) => state.models);
const showSky = isMapModelVisible("sky", { groups, models });
if (sceneMode === "physics") { if (sceneMode === "physics") {
return ( return (
@@ -18,7 +25,7 @@ export function Environment(): React.JSX.Element {
); );
} }
return ( return showSky ? (
<SkyModel <SkyModel
fallbackColor={GAME_SCENE_FALLBACK_BACKGROUND_COLOR} fallbackColor={GAME_SCENE_FALLBACK_BACKGROUND_COLOR}
fallbackModelPath={GAME_SCENE_FALLBACK_SKY_MODEL_PATH} fallbackModelPath={GAME_SCENE_FALLBACK_SKY_MODEL_PATH}
@@ -26,5 +33,7 @@ export function Environment(): React.JSX.Element {
modelPath={GAME_SCENE_SKY_MODEL_PATH} modelPath={GAME_SCENE_SKY_MODEL_PATH}
scale={GAME_SCENE_SKY_MODEL_SCALE} scale={GAME_SCENE_SKY_MODEL_SCALE}
/> />
) : (
<color attach="background" args={[GAME_SCENE_FALLBACK_BACKGROUND_COLOR]} />
); );
} }
+26 -6
View File
@@ -11,6 +11,10 @@ import * as THREE from "three";
import { useClonedObject } from "@/hooks/three/useClonedObject"; import { useClonedObject } from "@/hooks/three/useClonedObject";
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF"; import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
import { TerrainModel } from "@/components/three/world/TerrainModel"; import { TerrainModel } from "@/components/three/world/TerrainModel";
import {
isMapModelVisible,
useMapPerformanceStore,
} from "@/managers/stores/useMapPerformanceStore";
import { GameMapCollision } from "@/world/GameMapCollision"; import { GameMapCollision } from "@/world/GameMapCollision";
import { GeneratedMapNodeInstance } from "@/world/map-generated/GeneratedMapNodeInstance"; import { GeneratedMapNodeInstance } from "@/world/map-generated/GeneratedMapNodeInstance";
import { isGeneratedMapModelName } from "@/world/map-generated/generatedMapModelConfig"; import { isGeneratedMapModelName } from "@/world/map-generated/generatedMapModelConfig";
@@ -101,6 +105,8 @@ export function GameMap({
onOctreeReady, onOctreeReady,
}: GameMapProps): React.JSX.Element { }: GameMapProps): React.JSX.Element {
const settledMapNodesRef = useRef(new Set<number>()); const settledMapNodesRef = useRef(new Set<number>());
const groups = useMapPerformanceStore((state) => state.groups);
const models = useMapPerformanceStore((state) => state.models);
const [mapNodes, setMapNodes] = useState<LoadedMapNode[]>([]); const [mapNodes, setMapNodes] = useState<LoadedMapNode[]>([]);
const [mapLoaded, setMapLoaded] = useState(false); const [mapLoaded, setMapLoaded] = useState(false);
const [settledMapNodeCount, setSettledMapNodeCount] = useState(0); const [settledMapNodeCount, setSettledMapNodeCount] = useState(0);
@@ -221,17 +227,23 @@ export function GameMap({
node={mapNode.node} node={mapNode.node}
onSettled={() => handleMapNodeSettled(index)} onSettled={() => handleMapNodeSettled(index)}
> >
<MapNodeInstance {isMapModelVisible(mapNode.node.name, { groups, models }) ? (
node={mapNode.node} <MapNodeInstance
modelUrl={mapNode.modelUrl} node={mapNode.node}
onSettled={() => handleMapNodeSettled(index)} modelUrl={mapNode.modelUrl}
/> onSettled={() => handleMapNodeSettled(index)}
/>
) : (
<HiddenMapNode onSettled={() => handleMapNodeSettled(index)} />
)}
</ModelErrorBoundary> </ModelErrorBoundary>
))} ))}
</group> </group>
<MapInstancingSystem /> <MapInstancingSystem />
<VegetationSystem /> <VegetationSystem />
<TerrainModel /> {isMapModelVisible("terrain", { groups, models }) ? (
<TerrainModel />
) : null}
<GameMapCollision <GameMapCollision
buildOctree={buildOctree} buildOctree={buildOctree}
mapReady={mapReady} mapReady={mapReady}
@@ -244,6 +256,14 @@ export function GameMap({
); );
} }
function HiddenMapNode({ onSettled }: { onSettled: () => void }): null {
useEffect(() => {
onSettled();
}, [onSettled]);
return null;
}
/** /**
* Temporary development-only map reducer. * Temporary development-only map reducer.
* *
+3
View File
@@ -5,6 +5,7 @@ import {
PLAYER_SPAWN_POSITION_PHYSICS, PLAYER_SPAWN_POSITION_PHYSICS,
} from "@/data/player/playerConfig"; } from "@/data/player/playerConfig";
import { useCameraMode } from "@/hooks/debug/useCameraMode"; import { useCameraMode } from "@/hooks/debug/useCameraMode";
import { useMapPerformanceDebug } from "@/hooks/debug/useMapPerformanceDebug";
import { useSceneMode } from "@/hooks/debug/useSceneMode"; import { useSceneMode } from "@/hooks/debug/useSceneMode";
import { useHandTrackingSnapshot } from "@/hooks/handTracking/useHandTrackingSnapshot"; import { useHandTrackingSnapshot } from "@/hooks/handTracking/useHandTrackingSnapshot";
import { useWorldSceneLoading } from "@/hooks/world/useWorldSceneLoading"; import { useWorldSceneLoading } from "@/hooks/world/useWorldSceneLoading";
@@ -35,6 +36,8 @@ interface WorldProps {
} }
export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element { export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
useMapPerformanceDebug();
const cameraMode = useCameraMode(); const cameraMode = useCameraMode();
const sceneMode = useSceneMode(); const sceneMode = useSceneMode();
const mainState = useGameStore((state) => state.mainState); const mainState = useGameStore((state) => state.mainState);
@@ -1,4 +1,8 @@
import { Suspense } from "react"; import { Suspense } from "react";
import {
isMapModelVisible,
useMapPerformanceStore,
} from "@/managers/stores/useMapPerformanceStore";
import { InstancedMapAsset } from "@/world/map-instancing/InstancedMapAsset"; import { InstancedMapAsset } from "@/world/map-instancing/InstancedMapAsset";
import { import {
MAP_INSTANCING_ASSETS, MAP_INSTANCING_ASSETS,
@@ -7,6 +11,8 @@ import {
import { useMapInstancingData } from "@/world/map-instancing/useMapInstancingData"; import { useMapInstancingData } from "@/world/map-instancing/useMapInstancingData";
export function MapInstancingSystem(): React.JSX.Element | null { export function MapInstancingSystem(): React.JSX.Element | null {
const groups = useMapPerformanceStore((state) => state.groups);
const models = useMapPerformanceStore((state) => state.models);
const { data, isLoading } = useMapInstancingData(); const { data, isLoading } = useMapInstancingData();
if (isLoading || !data) { if (isLoading || !data) {
@@ -14,7 +20,8 @@ export function MapInstancingSystem(): React.JSX.Element | null {
} }
const enabledAssets = Object.entries(MAP_INSTANCING_ASSETS).filter( const enabledAssets = Object.entries(MAP_INSTANCING_ASSETS).filter(
([, config]) => config.enabled, ([, config]) =>
config.enabled && isMapModelVisible(config.mapName, { groups, models }),
); );
return ( return (
+8 -1
View File
@@ -1,4 +1,8 @@
import { Suspense } from "react"; import { Suspense } from "react";
import {
isMapModelVisible,
useMapPerformanceStore,
} from "@/managers/stores/useMapPerformanceStore";
import { InstancedVegetation } from "@/world/vegetation/InstancedVegetation"; import { InstancedVegetation } from "@/world/vegetation/InstancedVegetation";
import { useVegetationData } from "@/world/vegetation/useVegetationData"; import { useVegetationData } from "@/world/vegetation/useVegetationData";
import { import {
@@ -7,6 +11,8 @@ import {
} from "@/world/vegetation/vegetationConfig"; } from "@/world/vegetation/vegetationConfig";
export function VegetationSystem(): React.JSX.Element | null { export function VegetationSystem(): React.JSX.Element | null {
const groups = useMapPerformanceStore((state) => state.groups);
const models = useMapPerformanceStore((state) => state.models);
const { data, isLoading } = useVegetationData(); const { data, isLoading } = useVegetationData();
if (isLoading || !data) { if (isLoading || !data) {
@@ -14,7 +20,8 @@ export function VegetationSystem(): React.JSX.Element | null {
} }
const enabledTypes = Object.entries(VEGETATION_TYPES).filter( const enabledTypes = Object.entries(VEGETATION_TYPES).filter(
([, config]) => config.enabled, ([, config]) =>
config.enabled && isMapModelVisible(config.mapName, { groups, models }),
); );
return ( return (