Compare commits
7 Commits
28f7db172c
...
cdd919c010
| Author | SHA1 | Date | |
|---|---|---|---|
| cdd919c010 | |||
| cea2856fd0 | |||
| d245d6b460 | |||
| dba7aec6fa | |||
| d376d0ba6b | |||
| 242a3dcd37 | |||
| 225ac828df |
+39952
-35030
File diff suppressed because it is too large
Load Diff
+35051
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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();
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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) {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user