4 Commits

Author SHA1 Message Date
Tom Boullay f035195b56 Merge branch 'feat/map-environment' of https://git.fabrik.mathieu-chavanel.fr/math-pixel/La-Fabrik into feat/map-environment
🔍 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
2026-05-24 22:07:15 +02:00
Tom Boullay b8e5c4d1a9 upadte: gain some fps 2026-05-24 22:06:59 +02:00
Tom Boullay 6957b9e4f0 update: merge instanced map geometry 2026-05-15 23:29:07 +02:00
Tom Boullay 9dff245aab update: instance map renderables 2026-05-15 21:48:13 +02:00
6 changed files with 564 additions and 728 deletions
+401 -620
View File
File diff suppressed because it is too large Load Diff
+64 -8
View File
@@ -18,6 +18,7 @@ const IDENTITY_NODE = {
rotation: [0, 0, 0], rotation: [0, 0, 0],
scale: [1, 1, 1], scale: [1, 1, 1],
}; };
const MAX_MESH_Y_PLACEMENT_OFFSET = 2;
function cloneNode(node) { function cloneNode(node) {
return { return {
@@ -63,15 +64,35 @@ function getOrCreateModelGroup(parent, modelName) {
function createRenderableObject(objectNode, meshNode) { function createRenderableObject(objectNode, meshNode) {
const mappedMesh = mapMeshNode(meshNode); const mappedMesh = mapMeshNode(meshNode);
const renderableNode = cloneNode(objectNode ?? meshNode);
if (objectNode && meshNode) {
const yOffset = Math.abs(objectNode.position[1] - meshNode.position[1]);
if (yOffset <= MAX_MESH_Y_PLACEMENT_OFFSET) {
renderableNode.position = [
objectNode.position[0],
meshNode.position[1],
objectNode.position[2],
];
}
}
return { return {
...cloneNode(objectNode ?? meshNode), ...renderableNode,
name: mappedMesh.name, name: mappedMesh.name,
type: "Object3D", type: "Object3D",
children: [mappedMesh], children: [mappedMesh],
}; };
} }
function createRenderableContainer(objectNode, meshNodes) {
return {
...cloneNode(objectNode),
type: "Object3D",
children: meshNodes.map(mapMeshNode),
};
}
function addRenderable(parent, objectNode, meshNode) { function addRenderable(parent, objectNode, meshNode) {
const renderable = createRenderableObject(objectNode, meshNode); const renderable = createRenderableObject(objectNode, meshNode);
getOrCreateModelGroup(parent, renderable.name).children.push(renderable); getOrCreateModelGroup(parent, renderable.name).children.push(renderable);
@@ -95,6 +116,36 @@ function addObjectsByRange(rawData, parent, start, end, allowedNames) {
} }
} }
function addBuildingsByRange(rawData, parent, start, end) {
for (let i = start; i <= end; i++) {
const node = rawData[i];
if (node?.type !== "Object3D" || node.name !== "immeuble1") continue;
const meshNodes = [];
for (let childIndex = i + 1; childIndex <= end; childIndex++) {
const childNode = rawData[childIndex];
if (childNode?.type === "Object3D") {
if (BUILDING_CHILD_OBJECT_NAMES.has(childNode.name)) continue;
break;
}
if (
childNode?.type === "Mesh" &&
BUILDING_MESH_NAMES.has(childNode.name)
) {
meshNodes.push(childNode);
}
}
if (meshNodes.length > 0) {
getOrCreateModelGroup(parent, node.name).children.push(
createRenderableContainer(node, meshNodes),
);
}
}
}
function getNearestGroup(groups, node) { function getNearestGroup(groups, node) {
const [x, , z] = node.position; const [x, , z] = node.position;
@@ -116,6 +167,9 @@ function createResidenceZones(rawData, residence) {
return zone; return zone;
}); });
addBuildingsByRange(rawData, zones[0], 831, 873);
addBuildingsByRange(rawData, zones[1], 875, 891);
addBuildingsByRange(rawData, zones[2], 893, 942);
addObjectsByRange(rawData, zones[0], 831, 873, RESIDENCE_MESH_NAMES); addObjectsByRange(rawData, zones[0], 831, 873, RESIDENCE_MESH_NAMES);
addObjectsByRange(rawData, zones[1], 875, 891, RESIDENCE_MESH_NAMES); addObjectsByRange(rawData, zones[1], 875, 891, RESIDENCE_MESH_NAMES);
addObjectsByRange(rawData, zones[2], 893, 942, RESIDENCE_MESH_NAMES); addObjectsByRange(rawData, zones[2], 893, 942, RESIDENCE_MESH_NAMES);
@@ -144,7 +198,13 @@ const CHAMP_MESH_NAMES = new Set([
"champsdetournesol", "champsdetournesol",
]); ]);
const FERME_MESH_NAMES = new Set(["buissons", "buisson", "fermeverticale"]); const FERME_MESH_NAMES = new Set(["buissons", "buisson", "fermeverticale"]);
const RESIDENCE_MESH_NAMES = new Set(["immeuble_1", "immeuble_2", "maison1"]); const RESIDENCE_MESH_NAMES = new Set(["maison1"]);
const BUILDING_CHILD_OBJECT_NAMES = new Set([
"immeuble",
"immeuble_1",
"immeuble_2",
]);
const BUILDING_MESH_NAMES = new Set(["immeuble_1", "immeuble_2"]);
const ENERGIE_MESH_NAMES = new Set([ const ENERGIE_MESH_NAMES = new Set([
"pyloneelectrique", "pyloneelectrique",
"eoliennes", "eoliennes",
@@ -161,12 +221,7 @@ const DIRECTION_MESH_NAMES = new Set([
"panneaudirresidences2", "panneaudirresidences2",
"panneauxquartier", "panneauxquartier",
]); ]);
const LAFABRIK_MESH_NAMES = new Set([ const LAFABRIK_MESH_NAMES = new Set(["lafabrik", "maison1"]);
"lafabrik",
"immeuble_1",
"immeuble_2",
"maison1",
]);
function transformMap() { function transformMap() {
console.log("Reading map_raw.json..."); console.log("Reading map_raw.json...");
const rawData = JSON.parse(fs.readFileSync(INPUT_PATH, "utf-8")); const rawData = JSON.parse(fs.readFileSync(INPUT_PATH, "utf-8"));
@@ -225,6 +280,7 @@ function transformMap() {
addObjectsByRange(rawData, ferme, 4595, 4799, FERME_MESH_NAMES); addObjectsByRange(rawData, ferme, 4595, 4799, FERME_MESH_NAMES);
addObjectsByRange(rawData, vegetation, 4750, 4797, VEGETATION_MESH_NAMES); addObjectsByRange(rawData, vegetation, 4750, 4797, VEGETATION_MESH_NAMES);
addObjectsByRange(rawData, energie, 4801, 4872, ENERGIE_MESH_NAMES); addObjectsByRange(rawData, energie, 4801, 4872, ENERGIE_MESH_NAMES);
addBuildingsByRange(rawData, lafabrik, 4874, 4894);
addObjectsByRange(rawData, lafabrik, 4874, 4894, LAFABRIK_MESH_NAMES); addObjectsByRange(rawData, lafabrik, 4874, 4894, LAFABRIK_MESH_NAMES);
addObjectsByRange(rawData, direction, 4896, 4897, DIRECTION_MESH_NAMES); addObjectsByRange(rawData, direction, 4896, 4897, DIRECTION_MESH_NAMES);
addObjectsByRange(rawData, vegetation, 4898, 4997, VEGETATION_MESH_NAMES); addObjectsByRange(rawData, vegetation, 4898, 4997, VEGETATION_MESH_NAMES);
+9
View File
@@ -31,3 +31,12 @@ declare module "three/addons/math/Octree.js" {
capsuleIntersect(capsule: Capsule): CapsuleIntersectResult | false; capsuleIntersect(capsule: Capsule): CapsuleIntersectResult | false;
} }
} }
declare module "three/addons/utils/BufferGeometryUtils.js" {
import { BufferGeometry } from "three";
export function mergeGeometries(
geometries: BufferGeometry[],
useGroups?: boolean,
): BufferGeometry | null;
}
+46 -12
View File
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useRef } from "react"; 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 { mergeGeometries } from "three/addons/utils/BufferGeometryUtils.js";
import type { VegetationInstance } from "@/world/vegetation/useVegetationData"; import type { VegetationInstance } from "@/world/vegetation/useVegetationData";
import { disposeInstancedMesh } from "@/utils/three/dispose"; import { disposeInstancedMesh } from "@/utils/three/dispose";
@@ -13,24 +14,59 @@ interface InstancedVegetationProps {
interface MeshData { interface MeshData {
geometry: THREE.BufferGeometry; geometry: THREE.BufferGeometry;
material: THREE.Material | THREE.Material[]; material: THREE.Material;
} }
function extractMeshes(scene: THREE.Group): MeshData[] { function extractMeshes(scene: THREE.Group): MeshData[] {
const meshes: MeshData[] = []; const meshesByMaterial = new Map<
string,
{ geometries: THREE.BufferGeometry[]; material: THREE.Material }
>();
scene.updateMatrixWorld(true);
scene.traverse((child) => { scene.traverse((child) => {
if (child instanceof THREE.Mesh) { if (!(child instanceof THREE.Mesh)) return;
meshes.push({
geometry: child.geometry.clone(), const material = Array.isArray(child.material)
material: Array.isArray(child.material) ? child.material[0]
? child.material.map((m) => m.clone()) : child.material;
: child.material.clone(), if (!material) return;
const geometry = child.geometry.clone();
geometry.applyMatrix4(child.matrixWorld);
const existing = meshesByMaterial.get(material.uuid);
if (existing) {
existing.geometries.push(geometry);
} else {
meshesByMaterial.set(material.uuid, {
geometries: [geometry],
material: material.clone(),
}); });
} }
}); });
return meshes; return [...meshesByMaterial.values()]
.map(({ geometries, material }) => {
const mergedGeometry = mergeGeometries(geometries, false);
for (const geometry of geometries) {
if (geometry !== mergedGeometry) {
geometry.dispose();
}
}
if (!mergedGeometry) {
material.dispose();
return null;
}
return {
geometry: mergedGeometry,
material,
};
})
.filter((meshData): meshData is MeshData => meshData !== null);
} }
function createInstanceMatrices( function createInstanceMatrices(
@@ -40,7 +76,7 @@ function createInstanceMatrices(
const position = new THREE.Vector3(); const position = new THREE.Vector3();
const rotation = new THREE.Euler(); const rotation = new THREE.Euler();
const quaternion = new THREE.Quaternion(); const quaternion = new THREE.Quaternion();
const scale = new THREE.Vector3(); const scale = new THREE.Vector3(1, 1, 1);
for (const instance of instances) { for (const instance of instances) {
const matrix = new THREE.Matrix4(); const matrix = new THREE.Matrix4();
@@ -48,8 +84,6 @@ function createInstanceMatrices(
position.set(...instance.position); position.set(...instance.position);
rotation.set(...instance.rotation); rotation.set(...instance.rotation);
quaternion.setFromEuler(rotation); quaternion.setFromEuler(rotation);
scale.set(...instance.scale);
matrix.compose(position, quaternion, scale); matrix.compose(position, quaternion, scale);
matrices.push(matrix); matrices.push(matrix);
} }
+29 -30
View File
@@ -1,11 +1,8 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import type { MapNode } from "@/types/editor/editor"; import type { MapNode } from "@/types/editor/editor";
import type { Vector3Tuple } from "@/types/three/three"; import type { Vector3Tuple } from "@/types/three/three";
import { getMapNodes, loadMapSceneData } from "@/utils/map/loadMapSceneData"; import { loadMapSceneData } from "@/utils/map/loadMapSceneData";
import { import { INSTANCED_MAP_EXCEPTIONS } from "@/world/vegetation/vegetationConfig";
VEGETATION_TYPES,
type VegetationType,
} from "@/world/vegetation/vegetationConfig";
export interface VegetationInstance { export interface VegetationInstance {
position: Vector3Tuple; position: Vector3Tuple;
@@ -13,7 +10,12 @@ export interface VegetationInstance {
scale: Vector3Tuple; 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 { function mapNodeToInstance(node: MapNode): VegetationInstance {
return { 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(); const data: VegetationData = new Map();
for (const [type, config] of Object.entries(VEGETATION_TYPES)) { for (const node of mapNodes) {
if (!config.enabled) continue; if (node.type !== "Object3D") continue;
if (INSTANCED_MAP_EXCEPTIONS.has(node.name)) continue;
const instances = mapNodes const modelPath = models.get(node.name);
.filter( if (!modelPath) continue;
(node) => node.name === config.mapName && node.type === "Object3D",
)
.map(mapNodeToInstance);
if (instances.length > 0) { const entry = data.get(node.name);
data.set(type as VegetationType, instances);
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; let cancelled = false;
async function load() { async function load() {
const cachedNodes = getMapNodes(); const sceneData = await loadMapSceneData();
if (cachedNodes) { if (!cancelled && sceneData) {
if (!cancelled) { setData(extractVegetationData(sceneData.mapNodes, sceneData.models));
setData(extractVegetationData(cachedNodes));
setIsLoading(false);
}
return;
}
await loadMapSceneData();
const nodes = getMapNodes();
if (!cancelled && nodes) {
setData(extractVegetationData(nodes));
setIsLoading(false); setIsLoading(false);
} }
} }
+15 -58
View File
@@ -4,62 +4,19 @@ export const VEGETATION_LOD = {
windFadeEnd: 70, windFadeEnd: 70,
}; };
export const VEGETATION_TYPES = { export const INSTANCED_MAP_EXCEPTIONS = new Set([
buissons: { "Scene",
mapName: "buisson", "blocking",
modelPath: "/models/buisson/model.gltf", "terrain",
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 const INSTANCED_MAP_CHUNK_SIZE = 45;
export type VegetationConfig = (typeof VEGETATION_TYPES)[VegetationType];
export const INSTANCED_MAP_NO_SHADOW_NAMES = new Set([
"arbre",
"sapin",
"buisson",
"champdeble",
"champdesoja",
"champsdetournesol",
]);