refactor: tighten terrain and sky model resource ownership
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
This commit is contained in:
@@ -143,6 +143,7 @@ WS ws://localhost:8000/ws
|
|||||||
| `docs/technical/hand-tracking.md` | Webcam, backend/browser MediaPipe, glove, and gesture flow |
|
| `docs/technical/hand-tracking.md` | Webcam, backend/browser MediaPipe, glove, and gesture flow |
|
||||||
| `docs/technical/zustand.md` | Game, settings, and subtitle stores |
|
| `docs/technical/zustand.md` | Game, settings, and subtitle stores |
|
||||||
| `docs/technical/three-debugging.md` | DevTools workflow for stepping into Three.js internals |
|
| `docs/technical/three-debugging.md` | DevTools workflow for stepping into Three.js internals |
|
||||||
|
| `docs/technical/map-performance.md` | Map draw-call bottlenecks and optimization notes |
|
||||||
| `docs/technical/editor.md` | Editor implementation details |
|
| `docs/technical/editor.md` | Editor implementation details |
|
||||||
| `docs/technical/animation.md` | Animated, explodable, and reusable 3D model components |
|
| `docs/technical/animation.md` | Animated, explodable, and reusable 3D model components |
|
||||||
| `docs/user/features.md` | Implemented feature inventory |
|
| `docs/user/features.md` | Implemented feature inventory |
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Map Performance Notes
|
||||||
|
|
||||||
|
This document tracks the current map-rendering performance pass.
|
||||||
|
|
||||||
|
## Current Runtime Path
|
||||||
|
|
||||||
|
- `public/map.json` is the source of map transforms.
|
||||||
|
- `src/world/GameMap.tsx` renders regular visual map nodes.
|
||||||
|
- `src/world/vegetation/VegetationSystem.tsx` already instances dense vegetation.
|
||||||
|
- `src/world/map-instancing/MapInstancingSystem.tsx` instances selected repeated static map assets.
|
||||||
|
- `src/world/GameMapCollision.tsx` keeps terrain collision separate for the player octree.
|
||||||
|
|
||||||
|
## Main Bottlenecks Found
|
||||||
|
|
||||||
|
The most important signal is draw calls, not only triangle count.
|
||||||
|
|
||||||
|
| Model | Instances | Meshes / primitives | Notes |
|
||||||
|
| ---------------- | --------: | ------------------: | ---------------------------------------------------------------- |
|
||||||
|
| `generateur` | 3 | 3152 | Worst draw-call offender. Needs asset-side mesh merging. |
|
||||||
|
| `lafabrik` | 4 | 56 | Moderate draw calls, heavy 2048 texture set. |
|
||||||
|
| `ecole` | 1 | 107 | One material but many primitives; should be merged. |
|
||||||
|
| `fermeverticale` | 3 | 1 | Geometry is fine; textures are large for the visible complexity. |
|
||||||
|
|
||||||
|
`generateur` is especially expensive because three visible instances can multiply thousands of primitives into thousands of draw calls. Instancing reduces repeated instance cost, but the source asset still needs a cleaner export.
|
||||||
|
|
||||||
|
## Current Code-Side Optimization
|
||||||
|
|
||||||
|
Repeated static assets are configured in:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
src/world/map-instancing/mapInstancingConfig.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Those names are excluded from the regular `GameMap` clone path, then rendered by `MapInstancingSystem` with `THREE.InstancedMesh`.
|
||||||
|
|
||||||
|
This keeps the existing map authoring format while reducing repeated draw calls for selected assets.
|
||||||
|
|
||||||
|
## Asset-Side Follow-Up
|
||||||
|
|
||||||
|
Design/export should prioritize:
|
||||||
|
|
||||||
|
1. Merge `generateur` meshes from 3152 primitives to a small number of material groups.
|
||||||
|
2. Reduce `lafabrik` texture count and downscale flat/low-detail maps.
|
||||||
|
3. Merge `ecole` primitives because it uses a single material.
|
||||||
|
4. Prefer runtime `.glb` or compressed runtime textures when the pipeline supports it.
|
||||||
|
|
||||||
|
## Safety Rules
|
||||||
|
|
||||||
|
- Do not instance `terrain` for player collision without validating `Octree.fromGraphNode` support.
|
||||||
|
- Do not replace repair-game models with optimized map models unless repair node names are preserved.
|
||||||
|
- Dispose only GPU resources created locally. Do not dispose textures or geometries owned by `useGLTF`'s cache.
|
||||||
@@ -3,7 +3,6 @@ import { useGLTF } from "@react-three/drei";
|
|||||||
import { Component, useEffect, useMemo, useRef, type ReactNode } from "react";
|
import { Component, useEffect, useMemo, useRef, type ReactNode } from "react";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||||
import { disposeObject3D } from "@/utils/three/dispose";
|
|
||||||
|
|
||||||
interface SkyModelProps {
|
interface SkyModelProps {
|
||||||
modelPath: string;
|
modelPath: string;
|
||||||
@@ -28,6 +27,7 @@ interface SkyModelErrorBoundaryState {
|
|||||||
|
|
||||||
const SKY_MODEL_SCALE = 1;
|
const SKY_MODEL_SCALE = 1;
|
||||||
const SKY_MODEL_RENDER_ORDER = -1000;
|
const SKY_MODEL_RENDER_ORDER = -1000;
|
||||||
|
const SKYBOX_MODEL_PATH = "/models/skybox/skybox.glb";
|
||||||
const LEGACY_SKY_MODEL_PATH = "/models/sky/model.glb";
|
const LEGACY_SKY_MODEL_PATH = "/models/sky/model.glb";
|
||||||
|
|
||||||
class SkyModelErrorBoundary extends Component<
|
class SkyModelErrorBoundary extends Component<
|
||||||
@@ -83,7 +83,7 @@ function SkyModelContent({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
disposeObject3D(model);
|
disposeSkyModelMaterials(model);
|
||||||
};
|
};
|
||||||
}, [model]);
|
}, [model]);
|
||||||
|
|
||||||
@@ -129,5 +129,20 @@ function createSkyMaterial<T extends THREE.Material>(material: T): T {
|
|||||||
return skyMaterial as T;
|
return skyMaterial as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
useGLTF.preload("/models/skybox/model.gltf");
|
function disposeSkyModelMaterials(model: THREE.Object3D): void {
|
||||||
|
model.traverse((object) => {
|
||||||
|
if (!(object instanceof THREE.Mesh)) return;
|
||||||
|
|
||||||
|
if (Array.isArray(object.material)) {
|
||||||
|
for (const material of object.material) {
|
||||||
|
material.dispose();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
object.material.dispose();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useGLTF.preload(SKYBOX_MODEL_PATH);
|
||||||
useGLTF.preload(LEGACY_SKY_MODEL_PATH);
|
useGLTF.preload(LEGACY_SKY_MODEL_PATH);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useEffect, useMemo, useRef } from "react";
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { useGLTF } from "@react-three/drei";
|
import { useGLTF } from "@react-three/drei";
|
||||||
import type { Vector3Tuple } from "@/types/three/three";
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
import { disposeObject3D } from "@/utils/three/dispose";
|
|
||||||
|
|
||||||
const TERRAIN_MODEL_PATH = "/models/terrain/model.gltf";
|
const TERRAIN_MODEL_PATH = "/models/terrain/model.gltf";
|
||||||
const TERRAIN_DEFAULT_POSITION: Vector3Tuple = [0, 0, 0];
|
const TERRAIN_DEFAULT_POSITION: Vector3Tuple = [0, 0, 0];
|
||||||
@@ -47,12 +46,6 @@ export function TerrainModel({
|
|||||||
return model;
|
return model;
|
||||||
}, [scene, receiveShadow]);
|
}, [scene, receiveShadow]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
disposeObject3D(terrainModel);
|
|
||||||
};
|
|
||||||
}, [terrainModel]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onLoaded?.();
|
onLoaded?.();
|
||||||
}, [onLoaded]);
|
}, [onLoaded]);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export const GAME_SCENE_SKY_MODEL_PATH = "/models/skybox/model.gltf";
|
export const GAME_SCENE_SKY_MODEL_PATH = "/models/skybox/skybox.glb";
|
||||||
export const GAME_SCENE_FALLBACK_SKY_MODEL_PATH = "/models/sky/model.glb";
|
export const GAME_SCENE_FALLBACK_SKY_MODEL_PATH = "/models/sky/model.glb";
|
||||||
export const GAME_SCENE_SKY_MODEL_SCALE = 100;
|
export const GAME_SCENE_SKY_MODEL_SCALE = 100;
|
||||||
export const GAME_SCENE_FALLBACK_SKY_MODEL_SCALE = 1;
|
export const GAME_SCENE_FALLBACK_SKY_MODEL_SCALE = 1;
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ 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 { GameMapCollision } from "@/world/GameMapCollision";
|
import { GameMapCollision } from "@/world/GameMapCollision";
|
||||||
|
import { MapInstancingSystem } from "@/world/map-instancing/MapInstancingSystem";
|
||||||
|
import { isInstancedMapNodeName } from "@/world/map-instancing/mapInstancingConfig";
|
||||||
import { VegetationSystem } from "@/world/vegetation/VegetationSystem";
|
import { VegetationSystem } from "@/world/vegetation/VegetationSystem";
|
||||||
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
|
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
|
||||||
import { logger } from "@/utils/core/Logger";
|
import { logger } from "@/utils/core/Logger";
|
||||||
@@ -225,6 +227,7 @@ export function GameMap({
|
|||||||
</ModelErrorBoundary>
|
</ModelErrorBoundary>
|
||||||
))}
|
))}
|
||||||
</group>
|
</group>
|
||||||
|
<MapInstancingSystem />
|
||||||
<VegetationSystem />
|
<VegetationSystem />
|
||||||
<TerrainModel />
|
<TerrainModel />
|
||||||
<GameMapCollision
|
<GameMapCollision
|
||||||
@@ -256,7 +259,10 @@ function liteMap(node: MapNode): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return !LITE_MAP_SKIPPED_NODE_NAMES.has(node.name);
|
return (
|
||||||
|
!LITE_MAP_SKIPPED_NODE_NAMES.has(node.name) &&
|
||||||
|
!isInstancedMapNodeName(node.name)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MapNodeInstance({
|
function MapNodeInstance({
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { useGLTF } from "@react-three/drei";
|
||||||
|
import type { MapAssetInstance } from "@/world/map-instancing/useMapInstancingData";
|
||||||
|
|
||||||
|
interface InstancedMapAssetProps {
|
||||||
|
modelPath: string;
|
||||||
|
instances: MapAssetInstance[];
|
||||||
|
castShadow: boolean;
|
||||||
|
receiveShadow: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MeshData {
|
||||||
|
geometry: THREE.BufferGeometry;
|
||||||
|
material: THREE.Material | THREE.Material[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneMaterial(
|
||||||
|
material: THREE.Material | THREE.Material[],
|
||||||
|
): THREE.Material | THREE.Material[] {
|
||||||
|
return Array.isArray(material)
|
||||||
|
? material.map((item) => item.clone())
|
||||||
|
: material.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
function disposeMaterialOnly(
|
||||||
|
material: THREE.Material | THREE.Material[],
|
||||||
|
): void {
|
||||||
|
if (Array.isArray(material)) {
|
||||||
|
for (const item of material) {
|
||||||
|
item.dispose();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
material.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
function disposeInstancedMapMesh(mesh: THREE.InstancedMesh): void {
|
||||||
|
mesh.geometry.dispose();
|
||||||
|
disposeMaterialOnly(mesh.material);
|
||||||
|
mesh.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractMeshes(scene: THREE.Group): MeshData[] {
|
||||||
|
const meshes: MeshData[] = [];
|
||||||
|
|
||||||
|
scene.updateMatrixWorld(true);
|
||||||
|
scene.traverse((child) => {
|
||||||
|
if (!(child instanceof THREE.Mesh)) return;
|
||||||
|
|
||||||
|
const geometry = child.geometry.clone();
|
||||||
|
geometry.applyMatrix4(child.matrixWorld);
|
||||||
|
|
||||||
|
meshes.push({
|
||||||
|
geometry,
|
||||||
|
material: cloneMaterial(child.material),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return meshes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setInstanceMatrices(
|
||||||
|
instancedMesh: THREE.InstancedMesh,
|
||||||
|
instances: MapAssetInstance[],
|
||||||
|
): void {
|
||||||
|
const position = new THREE.Vector3();
|
||||||
|
const rotation = new THREE.Euler();
|
||||||
|
const quaternion = new THREE.Quaternion();
|
||||||
|
const scale = new THREE.Vector3();
|
||||||
|
const matrix = new THREE.Matrix4();
|
||||||
|
|
||||||
|
for (let i = 0; i < instances.length; i++) {
|
||||||
|
const instance = instances[i];
|
||||||
|
if (!instance) continue;
|
||||||
|
|
||||||
|
position.set(...instance.position);
|
||||||
|
rotation.set(...instance.rotation);
|
||||||
|
quaternion.setFromEuler(rotation);
|
||||||
|
scale.set(...instance.scale);
|
||||||
|
matrix.compose(position, quaternion, scale);
|
||||||
|
instancedMesh.setMatrixAt(i, matrix);
|
||||||
|
}
|
||||||
|
|
||||||
|
instancedMesh.instanceMatrix.needsUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InstancedMapAsset({
|
||||||
|
modelPath,
|
||||||
|
instances,
|
||||||
|
castShadow,
|
||||||
|
receiveShadow,
|
||||||
|
}: InstancedMapAssetProps): React.JSX.Element | null {
|
||||||
|
const { scene } = useGLTF(modelPath);
|
||||||
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const group = groupRef.current;
|
||||||
|
if (!group || instances.length === 0) return;
|
||||||
|
|
||||||
|
const meshDataList = extractMeshes(scene);
|
||||||
|
const instancedMeshes = meshDataList.map((meshData, index) => {
|
||||||
|
const instancedMesh = new THREE.InstancedMesh(
|
||||||
|
meshData.geometry,
|
||||||
|
meshData.material,
|
||||||
|
instances.length,
|
||||||
|
);
|
||||||
|
|
||||||
|
setInstanceMatrices(instancedMesh, instances);
|
||||||
|
instancedMesh.castShadow = castShadow;
|
||||||
|
instancedMesh.receiveShadow = receiveShadow;
|
||||||
|
instancedMesh.name = `instanced-map-asset-${index}`;
|
||||||
|
instancedMesh.frustumCulled = true;
|
||||||
|
instancedMesh.computeBoundingSphere();
|
||||||
|
|
||||||
|
return instancedMesh;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const mesh of instancedMeshes) {
|
||||||
|
group.add(mesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
for (const mesh of instancedMeshes) {
|
||||||
|
group.remove(mesh);
|
||||||
|
disposeInstancedMapMesh(mesh);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [castShadow, instances, receiveShadow, scene]);
|
||||||
|
|
||||||
|
if (instances.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <group ref={groupRef} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
import { InstancedMapAsset } from "@/world/map-instancing/InstancedMapAsset";
|
||||||
|
import {
|
||||||
|
MAP_INSTANCING_ASSETS,
|
||||||
|
type MapInstancingAssetType,
|
||||||
|
} from "@/world/map-instancing/mapInstancingConfig";
|
||||||
|
import { useMapInstancingData } from "@/world/map-instancing/useMapInstancingData";
|
||||||
|
|
||||||
|
export function MapInstancingSystem(): React.JSX.Element | null {
|
||||||
|
const { data, isLoading } = useMapInstancingData();
|
||||||
|
|
||||||
|
if (isLoading || !data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabledAssets = Object.entries(MAP_INSTANCING_ASSETS).filter(
|
||||||
|
([, config]) => config.enabled,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group name="map-instancing-system">
|
||||||
|
{enabledAssets.map(([type, config]) => {
|
||||||
|
const instances = data.get(type as MapInstancingAssetType);
|
||||||
|
|
||||||
|
if (!instances || instances.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense key={type} fallback={null}>
|
||||||
|
<InstancedMapAsset
|
||||||
|
modelPath={config.modelPath}
|
||||||
|
instances={instances}
|
||||||
|
castShadow={config.castShadow}
|
||||||
|
receiveShadow={config.receiveShadow}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
export const MAP_INSTANCING_ASSETS = {
|
||||||
|
generateur: {
|
||||||
|
mapName: "generateur",
|
||||||
|
modelPath: "/models/generateur/model.gltf",
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: true,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
lafabrik: {
|
||||||
|
mapName: "lafabrik",
|
||||||
|
modelPath: "/models/lafabrik/model.gltf",
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: true,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
fermeverticale: {
|
||||||
|
mapName: "fermeverticale",
|
||||||
|
modelPath: "/models/fermeverticale/model.gltf",
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: true,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
boiteauxlettres: {
|
||||||
|
mapName: "boiteauxlettres",
|
||||||
|
modelPath: "/models/boiteauxlettres/model.gltf",
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: true,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
pylone: {
|
||||||
|
mapName: "pylone",
|
||||||
|
modelPath: "/models/pylone/model.gltf",
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: true,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
immeuble1: {
|
||||||
|
mapName: "immeuble1",
|
||||||
|
modelPath: "/models/immeuble1/model.gltf",
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: true,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
maison1: {
|
||||||
|
mapName: "maison1",
|
||||||
|
modelPath: "/models/maison1/model.gltf",
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: true,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
eolienne: {
|
||||||
|
mapName: "eolienne",
|
||||||
|
modelPath: "/models/eolienne/model.gltf",
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: true,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
parcebike: {
|
||||||
|
mapName: "parcebike",
|
||||||
|
modelPath: "/models/parcebike/model.gltf",
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: true,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type MapInstancingAssetType = keyof typeof MAP_INSTANCING_ASSETS;
|
||||||
|
|
||||||
|
export type MapInstancingAssetConfig =
|
||||||
|
(typeof MAP_INSTANCING_ASSETS)[MapInstancingAssetType];
|
||||||
|
|
||||||
|
export const MAP_INSTANCED_NODE_NAMES: ReadonlySet<string> = new Set(
|
||||||
|
Object.values(MAP_INSTANCING_ASSETS)
|
||||||
|
.filter((config) => config.enabled)
|
||||||
|
.map((config) => config.mapName),
|
||||||
|
);
|
||||||
|
|
||||||
|
export function isInstancedMapNodeName(name: string): boolean {
|
||||||
|
return MAP_INSTANCED_NODE_NAMES.has(name);
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import type { MapNode } from "@/types/editor/editor";
|
||||||
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
|
import { getMapNodes, loadMapSceneData } from "@/utils/map/loadMapSceneData";
|
||||||
|
import {
|
||||||
|
MAP_INSTANCING_ASSETS,
|
||||||
|
type MapInstancingAssetType,
|
||||||
|
} from "@/world/map-instancing/mapInstancingConfig";
|
||||||
|
|
||||||
|
export interface MapAssetInstance {
|
||||||
|
position: Vector3Tuple;
|
||||||
|
rotation: Vector3Tuple;
|
||||||
|
scale: Vector3Tuple;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MapInstancingData = Map<MapInstancingAssetType, MapAssetInstance[]>;
|
||||||
|
|
||||||
|
function mapNodeToInstance(node: MapNode): MapAssetInstance {
|
||||||
|
return {
|
||||||
|
position: node.position,
|
||||||
|
rotation: node.rotation,
|
||||||
|
scale: node.scale,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractMapInstancingData(mapNodes: MapNode[]): MapInstancingData {
|
||||||
|
const data: MapInstancingData = new Map();
|
||||||
|
|
||||||
|
for (const [type, config] of Object.entries(MAP_INSTANCING_ASSETS)) {
|
||||||
|
if (!config.enabled) continue;
|
||||||
|
|
||||||
|
const instances = mapNodes
|
||||||
|
.filter(
|
||||||
|
(node) => node.name === config.mapName && node.type === "Object3D",
|
||||||
|
)
|
||||||
|
.map(mapNodeToInstance);
|
||||||
|
|
||||||
|
if (instances.length > 0) {
|
||||||
|
data.set(type as MapInstancingAssetType, instances);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMapInstancingData(): {
|
||||||
|
data: MapInstancingData | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
} {
|
||||||
|
const [data, setData] = useState<MapInstancingData | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
const cachedNodes = getMapNodes();
|
||||||
|
|
||||||
|
if (cachedNodes) {
|
||||||
|
if (!cancelled) {
|
||||||
|
setData(extractMapInstancingData(cachedNodes));
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadMapSceneData();
|
||||||
|
const nodes = getMapNodes();
|
||||||
|
|
||||||
|
if (!cancelled && nodes) {
|
||||||
|
setData(extractMapInstancingData(nodes));
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
load();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { data, isLoading };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user