update: instance map renderables

This commit is contained in:
Tom Boullay
2026-05-15 21:48:13 +02:00
parent 27951d13fd
commit 9dff245aab
7 changed files with 430 additions and 670 deletions
+7 -13
View File
@@ -17,24 +17,18 @@ import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
import { logger } from "@/utils/core/Logger";
import { loadMapSceneData } from "@/utils/map/loadMapSceneData";
import { logModelLoadError } from "@/utils/three/modelLoadLogger";
import { INSTANCED_MAP_EXCEPTIONS } from "@/world/vegetation/vegetationConfig";
import type { MapNode } from "@/types/editor/editor";
import type { OctreeReadyHandler } from "@/types/three/three";
const MODEL_RENDER_SCALE: [number, number, number] = [1, 1, 1];
interface LoadedMapNode {
node: MapNode;
modelUrl: string | null;
}
const MAP_STRUCTURE_NODE_NAMES = new Set(["Scene", "blocking"]);
const LITE_MAP_SKIPPED_NODE_NAMES = new Set([
"arbre",
"buisson",
"champdeble",
"champdesoja",
"champsdetournesol",
"sapin",
]);
interface ErrorBoundaryProps {
children: ReactNode;
fallback: ReactNode;
@@ -256,7 +250,7 @@ function liteMap(node: MapNode): boolean {
return false;
}
return !LITE_MAP_SKIPPED_NODE_NAMES.has(node.name);
return INSTANCED_MAP_EXCEPTIONS.has(node.name);
}
function MapNodeInstance({
@@ -294,12 +288,12 @@ function ModelInstance({
modelUrl: string;
onLoaded: () => void;
}): React.JSX.Element {
const { position, rotation, scale } = node;
const { position, rotation } = node;
const { scene } = useLoggedGLTF(modelUrl, {
scope: "GameMap.ModelInstance",
position,
rotation,
scale,
scale: MODEL_RENDER_SCALE,
});
const sceneInstance = useClonedObject(scene);
@@ -318,7 +312,7 @@ function ModelInstance({
object={sceneInstance}
position={position}
rotation={rotation}
scale={scale}
scale={MODEL_RENDER_SCALE}
/>
);
}
+8 -4
View File
@@ -14,10 +14,12 @@ interface InstancedVegetationProps {
interface MeshData {
geometry: THREE.BufferGeometry;
material: THREE.Material | THREE.Material[];
localMatrix: THREE.Matrix4;
}
function extractMeshes(scene: THREE.Group): MeshData[] {
const meshes: MeshData[] = [];
scene.updateMatrixWorld(true);
scene.traverse((child) => {
if (child instanceof THREE.Mesh) {
@@ -26,6 +28,7 @@ function extractMeshes(scene: THREE.Group): MeshData[] {
material: Array.isArray(child.material)
? child.material.map((m) => m.clone())
: child.material.clone(),
localMatrix: child.matrixWorld.clone(),
});
}
});
@@ -40,7 +43,7 @@ function createInstanceMatrices(
const position = new THREE.Vector3();
const rotation = new THREE.Euler();
const quaternion = new THREE.Quaternion();
const scale = new THREE.Vector3();
const scale = new THREE.Vector3(1, 1, 1);
for (const instance of instances) {
const matrix = new THREE.Matrix4();
@@ -48,8 +51,6 @@ function createInstanceMatrices(
position.set(...instance.position);
rotation.set(...instance.rotation);
quaternion.setFromEuler(rotation);
scale.set(...instance.scale);
matrix.compose(position, quaternion, scale);
matrices.push(matrix);
}
@@ -83,7 +84,10 @@ export function InstancedVegetation({
for (let i = 0; i < matrices.length; i++) {
const matrix = matrices[i];
if (matrix) {
instancedMesh.setMatrixAt(i, matrix);
instancedMesh.setMatrixAt(
i,
new THREE.Matrix4().multiplyMatrices(matrix, meshData.localMatrix),
);
}
}
+8 -18
View File
@@ -1,10 +1,6 @@
import { Suspense } from "react";
import { InstancedVegetation } from "@/world/vegetation/InstancedVegetation";
import { useVegetationData } from "@/world/vegetation/useVegetationData";
import {
VEGETATION_TYPES,
type VegetationType,
} from "@/world/vegetation/vegetationConfig";
export function VegetationSystem(): React.JSX.Element | null {
const { data, isLoading } = useVegetationData();
@@ -13,26 +9,20 @@ export function VegetationSystem(): React.JSX.Element | null {
return null;
}
const enabledTypes = Object.entries(VEGETATION_TYPES).filter(
([, config]) => config.enabled,
);
return (
<group name="vegetation-system">
{enabledTypes.map(([type, config]) => {
const instances = data.get(type as VegetationType);
if (!instances || instances.length === 0) {
<group name="instanced-map-system">
{[...data.entries()].map(([modelName, entry]) => {
if (entry.instances.length === 0) {
return null;
}
return (
<Suspense key={type} fallback={null}>
<Suspense key={modelName} fallback={null}>
<InstancedVegetation
modelPath={config.modelPath}
instances={instances}
castShadow={config.castShadow}
receiveShadow={config.receiveShadow}
modelPath={entry.modelPath}
instances={entry.instances}
castShadow={true}
receiveShadow={true}
/>
</Suspense>
);
+29 -30
View File
@@ -1,11 +1,8 @@
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 {
VEGETATION_TYPES,
type VegetationType,
} from "@/world/vegetation/vegetationConfig";
import { loadMapSceneData } from "@/utils/map/loadMapSceneData";
import { INSTANCED_MAP_EXCEPTIONS } from "@/world/vegetation/vegetationConfig";
export interface VegetationInstance {
position: Vector3Tuple;
@@ -13,7 +10,12 @@ export interface VegetationInstance {
scale: Vector3Tuple;
}
export type VegetationData = Map<VegetationType, VegetationInstance[]>;
export interface InstancedMapEntry {
modelPath: string;
instances: VegetationInstance[];
}
export type VegetationData = Map<string, InstancedMapEntry>;
function mapNodeToInstance(node: MapNode): VegetationInstance {
return {
@@ -23,20 +25,28 @@ function mapNodeToInstance(node: MapNode): VegetationInstance {
};
}
function extractVegetationData(mapNodes: MapNode[]): VegetationData {
function extractVegetationData(
mapNodes: MapNode[],
models: Map<string, string>,
): VegetationData {
const data: VegetationData = new Map();
for (const [type, config] of Object.entries(VEGETATION_TYPES)) {
if (!config.enabled) continue;
for (const node of mapNodes) {
if (node.type !== "Object3D") continue;
if (INSTANCED_MAP_EXCEPTIONS.has(node.name)) continue;
const instances = mapNodes
.filter(
(node) => node.name === config.mapName && node.type === "Object3D",
)
.map(mapNodeToInstance);
const modelPath = models.get(node.name);
if (!modelPath) continue;
if (instances.length > 0) {
data.set(type as VegetationType, instances);
const entry = data.get(node.name);
if (entry) {
entry.instances.push(mapNodeToInstance(node));
} else {
data.set(node.name, {
modelPath,
instances: [mapNodeToInstance(node)],
});
}
}
@@ -54,21 +64,10 @@ export function useVegetationData(): {
let cancelled = false;
async function load() {
const cachedNodes = getMapNodes();
const sceneData = await loadMapSceneData();
if (cachedNodes) {
if (!cancelled) {
setData(extractVegetationData(cachedNodes));
setIsLoading(false);
}
return;
}
await loadMapSceneData();
const nodes = getMapNodes();
if (!cancelled && nodes) {
setData(extractVegetationData(nodes));
if (!cancelled && sceneData) {
setData(extractVegetationData(sceneData.mapNodes, sceneData.models));
setIsLoading(false);
}
}
+8 -59
View File
@@ -4,62 +4,11 @@ export const VEGETATION_LOD = {
windFadeEnd: 70,
};
export const VEGETATION_TYPES = {
buissons: {
mapName: "buisson",
modelPath: "/models/buisson/model.gltf",
castShadow: true,
receiveShadow: true,
enabled: true,
windEnabled: false,
windIntensity: 1.2,
},
sapin: {
mapName: "sapin",
modelPath: "/models/sapin/model.gltf",
castShadow: true,
receiveShadow: true,
enabled: true,
windEnabled: false,
windIntensity: 0.6,
},
arbre: {
mapName: "arbre",
modelPath: "/models/arbre/model.gltf",
castShadow: true,
receiveShadow: true,
enabled: true,
windEnabled: false,
windIntensity: 0.8,
},
champdeble: {
mapName: "champdeble",
modelPath: "/models/champdeble/model.gltf",
castShadow: true,
receiveShadow: true,
enabled: true,
windEnabled: false,
windIntensity: 1.0,
},
champdesoja: {
mapName: "champdesoja",
modelPath: "/models/champdesoja/model.gltf",
castShadow: true,
receiveShadow: true,
enabled: true,
windEnabled: false,
windIntensity: 1.0,
},
champsdetournesol: {
mapName: "champsdetournesol",
modelPath: "/models/champsdetournesol/model.gltf",
castShadow: true,
receiveShadow: true,
enabled: true,
windEnabled: false,
windIntensity: 0.9,
},
} as const;
export type VegetationType = keyof typeof VEGETATION_TYPES;
export type VegetationConfig = (typeof VEGETATION_TYPES)[VegetationType];
export const INSTANCED_MAP_EXCEPTIONS = new Set([
"Scene",
"blocking",
"terrain",
"ecole",
"generateur",
"lafabrik",
]);