Compare commits
4 Commits
4e6582b543
...
f035195b56
| Author | SHA1 | Date | |
|---|---|---|---|
| f035195b56 | |||
| b8e5c4d1a9 | |||
| 6957b9e4f0 | |||
| 9dff245aab |
+401
-620
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||||
|
|||||||
Vendored
+9
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
]);
|
||||||
|
|||||||
Reference in New Issue
Block a user