7 Commits

Author SHA1 Message Date
Tom Boullay cdd919c010 update: clean map hierarchy and mesh model names
🔍 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-14 17:25:39 +02:00
Tom Boullay cea2856fd0 update: models 2026-05-14 17:17:54 +02:00
Tom Boullay d245d6b460 Update vegetationConfig.ts 2026-05-14 16:13:34 +02:00
Tom Boullay dba7aec6fa fix: dupplicate buisson and ecole 2026-05-14 16:13:32 +02:00
Tom Boullay d376d0ba6b update: add new models 2026-05-14 16:13:16 +02:00
Tom Boullay 242a3dcd37 refactor: remove vegetation instance limit (will be handled by chunks later) 2026-05-14 00:26:42 +02:00
Tom Boullay 225ac828df feat: enable all vegetation types and remove debug logs 2026-05-14 00:23:18 +02:00
76 changed files with 75340 additions and 35073 deletions
+39952 -35030
View File
File diff suppressed because it is too large Load Diff
+35051
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+124
View File
@@ -0,0 +1,124 @@
const fs = require("fs");
const path = require("path");
const INPUT_PATH = path.join(__dirname, "../public/map_raw.json");
const OUTPUT_PATH = path.join(__dirname, "../public/map.json");
const MESH_NAME_MAPPINGS = {
boitesauxlettres: "boiteauxlettres",
pyloneelectrique: "pylone",
eoliennes: "eolienne",
immeuble_1: "immeuble1",
buissons: "buisson",
panneauxquartier: "panneauaffichage",
};
const REMOVED_NODE_NAMES = new Set(["ROOT", "mc"]);
function cloneNode(node) {
return {
name: node.name,
type: node.type,
position: node.position,
rotation: node.rotation,
scale: node.scale,
};
}
function mapMeshName(node) {
if (node.type !== "Mesh") {
return cloneNode(node);
}
return {
...cloneNode(node),
name: MESH_NAME_MAPPINGS[node.name] ?? node.name,
};
}
function createGroup(node, children = []) {
return {
...cloneNode(node),
name: node.name === "Neutre" ? "blocking" : node.name,
children,
};
}
function transformMap() {
console.log("Reading map_raw.json...");
const rawData = JSON.parse(fs.readFileSync(INPUT_PATH, "utf-8"));
console.log(`Found ${rawData.length} nodes in raw file`);
let removedCount = 0;
let renamedCount = 0;
const sceneRaw = rawData.find(
(node) => node.name === "Scene" && node.type === "Object3D",
);
const terrainRaw = rawData.find(
(node) => node.name === "terrain" && node.type === "Object3D",
);
const blockingRaw = rawData.find(
(node) => node.name === "Neutre" && node.type === "Object3D",
);
if (!sceneRaw || !terrainRaw || !blockingRaw) {
throw new Error("Missing required Scene, terrain, or Neutre node");
}
const scene = createGroup(sceneRaw);
const terrain = createGroup(terrainRaw);
const blocking = createGroup(blockingRaw);
let currentGroup = null;
for (const rawNode of rawData) {
if (REMOVED_NODE_NAMES.has(rawNode.name)) {
removedCount++;
continue;
}
if (rawNode.name === "Scene" || rawNode.name === "Neutre") {
continue;
}
if (rawNode.name === "terrain" && rawNode.type === "Object3D") {
currentGroup = terrain;
continue;
}
if (rawNode.type === "Object3D") {
currentGroup = createGroup(rawNode);
blocking.children.push(currentGroup);
continue;
}
const mappedNode = mapMeshName(rawNode);
if (mappedNode.name !== rawNode.name) {
renamedCount++;
}
if (rawNode.name === "terrain" && currentGroup === terrain) {
terrain.children.push(mappedNode);
continue;
}
if (currentGroup) {
currentGroup.children.push(mappedNode);
continue;
}
blocking.children.push(mappedNode);
}
scene.children = [terrain, blocking];
console.log(`\nTransformation complete:`);
console.log(` - Removed ${removedCount} mc/ROOT nodes`);
console.log(` - Renamed ${renamedCount} mesh nodes`);
console.log(` - Output: hierarchical Scene root`);
fs.writeFileSync(OUTPUT_PATH, JSON.stringify(scene, null, 2));
console.log(`\nWritten to ${OUTPUT_PATH}`);
}
transformMap();
-10
View File
@@ -3,7 +3,6 @@ import type { RefObject } from "react";
import type { Object3D } from "three";
import { Octree } from "three/addons/math/Octree.js";
import type { OctreeReadyHandler } from "@/types/three/three";
import { logger } from "@/utils/core/Logger";
export function useOctreeGraphNode(
graphNodeRef: RefObject<Object3D | null>,
@@ -18,25 +17,16 @@ export function useOctreeGraphNode(
}, [rebuildKey]);
useEffect(() => {
logger.debug("useOctreeGraphNode", "Check", {
enabled,
octreeBuilt: octreeBuilt.current,
hasGraphNode: !!graphNodeRef.current,
rebuildKey,
});
if (!enabled) return;
const graphNode = graphNodeRef.current;
if (!enabled || octreeBuilt.current || !graphNode) return;
octreeBuilt.current = true;
logger.info("useOctreeGraphNode", "Building octree from graph node");
graphNode.updateMatrixWorld(true);
const octree = new Octree();
octree.fromGraphNode(graphNode);
logger.info("useOctreeGraphNode", "Octree built, calling onOctreeReady");
onOctreeReady(octree);
}, [enabled, graphNodeRef, onOctreeReady, rebuildKey]);
}
-4
View File
@@ -2,7 +2,6 @@ import { useCallback, useEffect, useState } from "react";
import type { Octree } from "three/addons/math/Octree.js";
import type { SceneMode } from "@/types/debug/debug";
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
import { logger } from "@/utils/core/Logger";
interface UseWorldSceneLoadingOptions {
onLoadingStateChange?: SceneLoadingChangeHandler | undefined;
@@ -32,12 +31,10 @@ export function useWorldSceneLoading({
(sceneMode === "physics" && octree !== null);
const handleGameMapLoaded = useCallback(() => {
logger.info("WorldSceneLoading", "GameMap loaded");
setGameMapLoaded(true);
}, []);
const handleGameStageLoaded = useCallback(() => {
logger.info("WorldSceneLoading", "GameStage loaded");
setGameStageLoaded(true);
onLoadingStateChange?.({
currentStep: "Initialisation gameplay",
@@ -48,7 +45,6 @@ export function useWorldSceneLoading({
const handleOctreeReady = useCallback(
(nextOctree: Octree) => {
logger.info("WorldSceneLoading", "Octree ready");
setOctree(nextOctree);
onLoadingStateChange?.({
currentStep: "Collision prête",
+4
View File
@@ -8,6 +8,10 @@ export interface MapNode {
scale: Vector3Tuple;
}
export interface HierarchicalMapNode extends MapNode {
children?: HierarchicalMapNode[];
}
export interface SceneData {
mapNodes: MapNode[];
models: Map<string, string>;
+37 -1
View File
@@ -5,6 +5,7 @@ const MAP_JSON_PATH = "/map.json";
const MODEL_FILE_NAMES = ["model.glb", "model.gltf"];
const HTML_CONTENT_TYPE = "text/html";
const MAP_STRUCTURE_NODE_NAMES = new Set(["Scene", "blocking"]);
const POSITION_PRECISION = 3;
type ModelEntry = [modelName: string, modelUrl: string];
let cachedSceneData: SceneData | null = null;
@@ -45,7 +46,42 @@ async function loadMapSceneDataInternal(): Promise<SceneData | null> {
const mapPayload: unknown = await response.json();
const mapNodes = parseMapNodes(mapPayload);
return createSceneData(mapNodes);
const deduplicatedNodes = deduplicateMapNodes(mapNodes);
return createSceneData(deduplicatedNodes);
}
function createPositionKey(node: MapNode): string {
const [x, y, z] = node.position;
const px = x.toFixed(POSITION_PRECISION);
const py = y.toFixed(POSITION_PRECISION);
const pz = z.toFixed(POSITION_PRECISION);
return `${node.name}:${px},${py},${pz}`;
}
function deduplicateMapNodes(nodes: MapNode[]): MapNode[] {
const seen = new Set<string>();
const result: MapNode[] = [];
const sortedNodes = [...nodes].sort((a, b) => {
if (a.type === "Object3D" && b.type !== "Object3D") return -1;
if (a.type !== "Object3D" && b.type === "Object3D") return 1;
return 0;
});
for (const node of sortedNodes) {
if (MAP_STRUCTURE_NODE_NAMES.has(node.name)) {
result.push(node);
continue;
}
const key = createPositionKey(node);
if (!seen.has(key)) {
seen.add(key);
result.push(node);
}
}
return result;
}
async function createSceneData(mapNodes: MapNode[]): Promise<SceneData> {
+38 -5
View File
@@ -1,4 +1,4 @@
import type { MapNode } from "../../types/editor/editor";
import type { HierarchicalMapNode, MapNode } from "../../types/editor/editor";
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
@@ -26,10 +26,43 @@ function isMapNode(value: unknown): value is MapNode {
);
}
export function parseMapNodes(value: unknown): MapNode[] {
if (!Array.isArray(value) || !value.every(isMapNode)) {
throw new Error("Invalid map node data");
function isHierarchicalMapNode(value: unknown): value is HierarchicalMapNode {
if (!isMapNode(value)) {
return false;
}
return value;
if (!("children" in value)) {
return true;
}
return (
value.children === undefined ||
(Array.isArray(value.children) &&
value.children.every(isHierarchicalMapNode))
);
}
function flattenMapNode(node: HierarchicalMapNode): MapNode[] {
const mapNode: MapNode = {
name: node.name,
type: node.type,
position: node.position,
rotation: node.rotation,
scale: node.scale,
};
const childNodes = node.children?.flatMap(flattenMapNode) ?? [];
return [mapNode, ...childNodes];
}
export function parseMapNodes(value: unknown): MapNode[] {
if (Array.isArray(value) && value.every(isHierarchicalMapNode)) {
return value.flatMap(flattenMapNode);
}
if (isHierarchicalMapNode(value)) {
return flattenMapNode(value);
}
throw new Error("Invalid map node data");
}
+1 -1
View File
@@ -20,7 +20,7 @@ function disposeMaterial(material: THREE.Material): void {
material.dispose();
for (const key of Object.keys(material)) {
const value = (material as Record<string, unknown>)[key];
const value = (material as unknown as Record<string, unknown>)[key];
if (value instanceof THREE.Texture) {
value.dispose();
}
+1 -1
View File
@@ -28,7 +28,7 @@ interface LoadedMapNode {
const MAP_STRUCTURE_NODE_NAMES = new Set(["Scene", "blocking"]);
const LITE_MAP_SKIPPED_NODE_NAMES = new Set([
"arbre",
"buissons",
"buisson",
"champdeble",
"champdesoja",
"champsdetournesol",
-10
View File
@@ -14,7 +14,6 @@ import { useOctreeGraphNode } from "@/hooks/three/useOctreeGraphNode";
import type { MapNode } from "@/types/editor/editor";
import type { OctreeReadyHandler } from "@/types/three/three";
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
import { logger } from "@/utils/core/Logger";
import { logModelLoadError } from "@/utils/three/modelLoadLogger";
export interface GameMapCollisionNode {
@@ -109,14 +108,6 @@ export function GameMapCollision({
const collisionReady =
mapReady && settledCollisionNodeCount >= collisionNodes.length;
logger.debug("GameMapCollision", "State", {
mapReady,
collisionNodesCount: collisionNodes.length,
settledCollisionNodeCount,
collisionReady,
buildOctree,
});
const notifyLoaded = useCallback(() => {
if (loadedNotifiedRef.current) return;
@@ -133,7 +124,6 @@ export function GameMapCollision({
const handleOctreeReady = useCallback<OctreeReadyHandler>(
(octree) => {
logger.info("GameMapCollision", "Octree built, calling onOctreeReady");
onLoadingStateChange?.({
currentStep: "Collision prête",
progress: 0.92,
+4 -1
View File
@@ -81,7 +81,10 @@ export function InstancedVegetation({
);
for (let i = 0; i < matrices.length; i++) {
instancedMesh.setMatrixAt(i, matrices[i]);
const matrix = matrices[i];
if (matrix) {
instancedMesh.setMatrixAt(i, matrix);
}
}
instancedMesh.instanceMatrix.needsUpdate = true;
+3 -3
View File
@@ -3,7 +3,6 @@ import type { MapNode } from "@/types/editor/editor";
import type { Vector3Tuple } from "@/types/three/three";
import { getMapNodes, loadMapSceneData } from "@/utils/map/loadMapSceneData";
import {
VEGETATION_MAX_INSTANCES,
VEGETATION_TYPES,
type VegetationType,
} from "@/world/vegetation/vegetationConfig";
@@ -31,8 +30,9 @@ function extractVegetationData(mapNodes: MapNode[]): VegetationData {
if (!config.enabled) continue;
const instances = mapNodes
.filter((node) => node.name === config.mapName)
.slice(0, VEGETATION_MAX_INSTANCES)
.filter(
(node) => node.name === config.mapName && node.type === "Object3D",
)
.map(mapNodeToInstance);
if (instances.length > 0) {
+5 -7
View File
@@ -4,15 +4,13 @@ export const VEGETATION_LOD = {
windFadeEnd: 70,
};
export const VEGETATION_MAX_INSTANCES = 500;
export const VEGETATION_TYPES = {
buissons: {
mapName: "buissons",
mapName: "buisson",
modelPath: "/models/buisson/model.gltf",
castShadow: true,
receiveShadow: true,
enabled: false,
enabled: true,
windEnabled: false,
windIntensity: 1.2,
},
@@ -39,7 +37,7 @@ export const VEGETATION_TYPES = {
modelPath: "/models/champdeble/model.gltf",
castShadow: true,
receiveShadow: true,
enabled: false,
enabled: true,
windEnabled: false,
windIntensity: 1.0,
},
@@ -48,7 +46,7 @@ export const VEGETATION_TYPES = {
modelPath: "/models/champdesoja/model.gltf",
castShadow: true,
receiveShadow: true,
enabled: false,
enabled: true,
windEnabled: false,
windIntensity: 1.0,
},
@@ -57,7 +55,7 @@ export const VEGETATION_TYPES = {
modelPath: "/models/champsdetournesol/model.gltf",
castShadow: true,
receiveShadow: true,
enabled: false,
enabled: true,
windEnabled: false,
windIntensity: 0.9,
},