Feat/map-environment #6
@@ -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()),
|
||||||
|
}));
|
||||||
@@ -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
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
Reference in New Issue
Block a user