perf(map): snap assets to terrain
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
import { useGLTF } from "@react-three/drei";
|
||||||
|
import { MergedStaticMapModel } from "@/components/three/world/MergedStaticMapModel";
|
||||||
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
|
|
||||||
|
const ECOLE_MODEL_PATH = "/models/ecole/model.gltf";
|
||||||
|
|
||||||
|
interface EcoleModelProps {
|
||||||
|
position: Vector3Tuple;
|
||||||
|
rotation: Vector3Tuple;
|
||||||
|
scale: Vector3Tuple;
|
||||||
|
castShadow?: boolean;
|
||||||
|
receiveShadow?: boolean;
|
||||||
|
onLoaded?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EcoleModel(props: EcoleModelProps): React.JSX.Element {
|
||||||
|
return <MergedStaticMapModel modelPath={ECOLE_MODEL_PATH} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
useGLTF.preload(ECOLE_MODEL_PATH);
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { useGLTF } from "@react-three/drei";
|
||||||
|
import { MergedStaticMapModel } from "@/components/three/world/MergedStaticMapModel";
|
||||||
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
|
|
||||||
|
const FERME_VERTICALE_MODEL_PATH = "/models/fermeverticale/model.gltf";
|
||||||
|
|
||||||
|
interface FermeVerticaleModelProps {
|
||||||
|
position: Vector3Tuple;
|
||||||
|
rotation: Vector3Tuple;
|
||||||
|
scale: Vector3Tuple;
|
||||||
|
castShadow?: boolean;
|
||||||
|
receiveShadow?: boolean;
|
||||||
|
onLoaded?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FermeVerticaleModel(
|
||||||
|
props: FermeVerticaleModelProps,
|
||||||
|
): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<MergedStaticMapModel modelPath={FERME_VERTICALE_MODEL_PATH} {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
useGLTF.preload(FERME_VERTICALE_MODEL_PATH);
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { useGLTF } from "@react-three/drei";
|
||||||
|
import { MergedStaticMapModel } from "@/components/three/world/MergedStaticMapModel";
|
||||||
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
|
|
||||||
|
const GENERATEUR_MODEL_PATH = "/models/generateur/model.gltf";
|
||||||
|
|
||||||
|
interface GenerateurModelProps {
|
||||||
|
position: Vector3Tuple;
|
||||||
|
rotation: Vector3Tuple;
|
||||||
|
scale: Vector3Tuple;
|
||||||
|
castShadow?: boolean;
|
||||||
|
receiveShadow?: boolean;
|
||||||
|
onLoaded?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GenerateurModel(
|
||||||
|
props: GenerateurModelProps,
|
||||||
|
): React.JSX.Element {
|
||||||
|
return <MergedStaticMapModel modelPath={GENERATEUR_MODEL_PATH} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
useGLTF.preload(GENERATEUR_MODEL_PATH);
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { useGLTF } from "@react-three/drei";
|
||||||
|
import { MergedStaticMapModel } from "@/components/three/world/MergedStaticMapModel";
|
||||||
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
|
|
||||||
|
const LAFABRIK_MODEL_PATH = "/models/lafabrik/model.gltf";
|
||||||
|
|
||||||
|
interface LafabrikModelProps {
|
||||||
|
position: Vector3Tuple;
|
||||||
|
rotation: Vector3Tuple;
|
||||||
|
scale: Vector3Tuple;
|
||||||
|
castShadow?: boolean;
|
||||||
|
receiveShadow?: boolean;
|
||||||
|
onLoaded?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LafabrikModel(props: LafabrikModelProps): React.JSX.Element {
|
||||||
|
return <MergedStaticMapModel modelPath={LAFABRIK_MODEL_PATH} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
useGLTF.preload(LAFABRIK_MODEL_PATH);
|
||||||
+14
-10
@@ -1,12 +1,13 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import * as THREE from "three";
|
|
||||||
import { useGLTF } from "@react-three/drei";
|
import { useGLTF } from "@react-three/drei";
|
||||||
|
import { useThree } from "@react-three/fiber";
|
||||||
|
import * as THREE from "three";
|
||||||
import { mergeGeometries } from "three/addons/utils/BufferGeometryUtils.js";
|
import { mergeGeometries } from "three/addons/utils/BufferGeometryUtils.js";
|
||||||
import type { Vector3Tuple } from "@/types/three/three";
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
|
import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene";
|
||||||
|
|
||||||
const ECOLE_MODEL_PATH = "/models/ecole/model.gltf";
|
interface MergedStaticMapModelProps {
|
||||||
|
modelPath: string;
|
||||||
interface EcoleModelProps {
|
|
||||||
position: Vector3Tuple;
|
position: Vector3Tuple;
|
||||||
rotation: Vector3Tuple;
|
rotation: Vector3Tuple;
|
||||||
scale: Vector3Tuple;
|
scale: Vector3Tuple;
|
||||||
@@ -117,21 +118,26 @@ function createMergedMeshes(scene: THREE.Group): MergedMeshData[] {
|
|||||||
.filter((meshData): meshData is MergedMeshData => meshData !== null);
|
.filter((meshData): meshData is MergedMeshData => meshData !== null);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EcoleModel({
|
export function MergedStaticMapModel({
|
||||||
|
modelPath,
|
||||||
position,
|
position,
|
||||||
rotation,
|
rotation,
|
||||||
scale,
|
scale,
|
||||||
castShadow = true,
|
castShadow = true,
|
||||||
receiveShadow = true,
|
receiveShadow = true,
|
||||||
onLoaded,
|
onLoaded,
|
||||||
}: EcoleModelProps): React.JSX.Element {
|
}: MergedStaticMapModelProps): React.JSX.Element {
|
||||||
const { scene } = useGLTF(ECOLE_MODEL_PATH);
|
const { scene } = useGLTF(modelPath);
|
||||||
|
const maxAnisotropy = useThree((state) =>
|
||||||
|
state.gl.capabilities.getMaxAnisotropy(),
|
||||||
|
);
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const group = groupRef.current;
|
const group = groupRef.current;
|
||||||
if (!group) return;
|
if (!group) return;
|
||||||
|
|
||||||
|
optimizeGLTFSceneTextures(scene, maxAnisotropy);
|
||||||
const mergedMeshes = createMergedMeshes(scene);
|
const mergedMeshes = createMergedMeshes(scene);
|
||||||
const meshes = mergedMeshes.map((meshData) => {
|
const meshes = mergedMeshes.map((meshData) => {
|
||||||
const mesh = new THREE.Mesh(meshData.geometry, meshData.material);
|
const mesh = new THREE.Mesh(meshData.geometry, meshData.material);
|
||||||
@@ -153,7 +159,7 @@ export function EcoleModel({
|
|||||||
disposeMaterial(mesh.material);
|
disposeMaterial(mesh.material);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [castShadow, onLoaded, receiveShadow, scene]);
|
}, [castShadow, maxAnisotropy, modelPath, onLoaded, receiveShadow, scene]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group
|
<group
|
||||||
@@ -164,5 +170,3 @@ export function EcoleModel({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
useGLTF.preload(ECOLE_MODEL_PATH);
|
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
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 { useThree } from "@react-three/fiber";
|
||||||
import type { Vector3Tuple } from "@/types/three/three";
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
|
import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene";
|
||||||
|
|
||||||
const TERRAIN_MODEL_PATH = "/models/terrain/model.gltf";
|
const TERRAIN_MODEL_PATH = "/models/terrain/model.gltf";
|
||||||
const TERRAIN_DEFAULT_POSITION: Vector3Tuple = [0, 0, 0];
|
const TERRAIN_DEFAULT_POSITION: Vector3Tuple = [0, 0, 0];
|
||||||
@@ -39,12 +41,16 @@ export function TerrainModel({
|
|||||||
const internalRef = useRef<THREE.Group>(null);
|
const internalRef = useRef<THREE.Group>(null);
|
||||||
const ref = groupRef ?? internalRef;
|
const ref = groupRef ?? internalRef;
|
||||||
const { scene } = useGLTF(TERRAIN_MODEL_PATH);
|
const { scene } = useGLTF(TERRAIN_MODEL_PATH);
|
||||||
|
const maxAnisotropy = useThree((state) =>
|
||||||
|
state.gl.capabilities.getMaxAnisotropy(),
|
||||||
|
);
|
||||||
|
|
||||||
const terrainModel = useMemo(() => {
|
const terrainModel = useMemo(() => {
|
||||||
|
optimizeGLTFSceneTextures(scene, maxAnisotropy);
|
||||||
const model = scene.clone(true);
|
const model = scene.clone(true);
|
||||||
applyTerrainMaterialSettings(model, receiveShadow);
|
applyTerrainMaterialSettings(model, receiveShadow);
|
||||||
return model;
|
return model;
|
||||||
}, [scene, receiveShadow]);
|
}, [maxAnisotropy, scene, receiveShadow]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onLoaded?.();
|
onLoaded?.();
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ import { TERRAIN_COLORS } from "@/data/world/terrainConfig";
|
|||||||
|
|
||||||
export const FOG_CONFIG = {
|
export const FOG_CONFIG = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
color: "#c8dbbe",
|
color: "#eef3f5",
|
||||||
near: 34,
|
near: 38,
|
||||||
far: 58,
|
far: 45,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CHUNK_CONFIG = {
|
export const CHUNK_CONFIG = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
chunkSize: 35,
|
chunkSize: 35,
|
||||||
loadRadius: 45,
|
loadRadius: 45,
|
||||||
unloadRadius: 58,
|
unloadRadius: 45,
|
||||||
updateInterval: 350,
|
updateInterval: 350,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,27 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useGLTF } from "@react-three/drei";
|
import { useGLTF } from "@react-three/drei";
|
||||||
|
import { useThree } from "@react-three/fiber";
|
||||||
import {
|
import {
|
||||||
logModelLoadSuccess,
|
logModelLoadSuccess,
|
||||||
type ModelLoadLogContext,
|
type ModelLoadLogContext,
|
||||||
} from "@/utils/three/modelLoadLogger";
|
} from "@/utils/three/modelLoadLogger";
|
||||||
|
import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene";
|
||||||
|
|
||||||
export function useLoggedGLTF(
|
export function useLoggedGLTF(
|
||||||
modelPath: string,
|
modelPath: string,
|
||||||
context: Omit<ModelLoadLogContext, "modelPath">,
|
context: Omit<ModelLoadLogContext, "modelPath">,
|
||||||
) {
|
) {
|
||||||
const gltf = useGLTF(modelPath);
|
const gltf = useGLTF(modelPath);
|
||||||
|
const maxAnisotropy = useThree((state) =>
|
||||||
|
state.gl.capabilities.getMaxAnisotropy(),
|
||||||
|
);
|
||||||
const hasLoggedRef = useRef(false);
|
const hasLoggedRef = useRef(false);
|
||||||
const { position, rotation, scale, scope } = context;
|
const { position, rotation, scale, scope } = context;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
optimizeGLTFSceneTextures(gltf.scene, maxAnisotropy);
|
||||||
|
}, [gltf.scene, maxAnisotropy]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasLoggedRef.current) return;
|
if (hasLoggedRef.current) return;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { useGLTF } from "@react-three/drei";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
|
|
||||||
|
const TERRAIN_MODEL_PATH = "/models/terrain/model.gltf";
|
||||||
|
const RAYCAST_Y = 500;
|
||||||
|
const RAYCAST_FAR = 1000;
|
||||||
|
const DOWN = new THREE.Vector3(0, -1, 0);
|
||||||
|
|
||||||
|
interface TerrainHeightSampler {
|
||||||
|
getHeight: (x: number, z: number) => number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTerrainHeightSampler(
|
||||||
|
scene: THREE.Object3D,
|
||||||
|
): TerrainHeightSampler {
|
||||||
|
const meshes: THREE.Mesh[] = [];
|
||||||
|
const raycaster = new THREE.Raycaster(
|
||||||
|
new THREE.Vector3(),
|
||||||
|
DOWN,
|
||||||
|
0,
|
||||||
|
RAYCAST_FAR,
|
||||||
|
);
|
||||||
|
|
||||||
|
scene.updateMatrixWorld(true);
|
||||||
|
scene.traverse((child) => {
|
||||||
|
if (child instanceof THREE.Mesh) {
|
||||||
|
meshes.push(child);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
getHeight: (x, z) => {
|
||||||
|
raycaster.set(new THREE.Vector3(x, RAYCAST_Y, z), DOWN);
|
||||||
|
const hit = raycaster.intersectObjects(meshes, false)[0];
|
||||||
|
return hit?.point.y ?? null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTerrainHeightSampler(): TerrainHeightSampler {
|
||||||
|
const { scene } = useGLTF(TERRAIN_MODEL_PATH);
|
||||||
|
|
||||||
|
return useMemo(() => createTerrainHeightSampler(scene), [scene]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTerrainSnappedPosition(
|
||||||
|
position: Vector3Tuple,
|
||||||
|
): Vector3Tuple {
|
||||||
|
const terrainHeight = useTerrainHeightSampler();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const [x, y, z] = position;
|
||||||
|
const height = terrainHeight.getHeight(x, z);
|
||||||
|
return [x, height ?? y, z];
|
||||||
|
}, [position, terrainHeight]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeMapScale(scale: Vector3Tuple): Vector3Tuple {
|
||||||
|
const [x, y, z] = scale;
|
||||||
|
const isUniform = Math.abs(x - y) < 0.001 && Math.abs(x - z) < 0.001;
|
||||||
|
return isUniform ? scale : [x, x, x];
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import * as THREE from "three";
|
||||||
|
|
||||||
|
const TEXTURE_KEYS = [
|
||||||
|
"map",
|
||||||
|
"alphaMap",
|
||||||
|
"aoMap",
|
||||||
|
"bumpMap",
|
||||||
|
"displacementMap",
|
||||||
|
"emissiveMap",
|
||||||
|
"envMap",
|
||||||
|
"lightMap",
|
||||||
|
"metalnessMap",
|
||||||
|
"normalMap",
|
||||||
|
"roughnessMap",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
type TextureKey = (typeof TEXTURE_KEYS)[number];
|
||||||
|
type TexturedMaterial = THREE.Material &
|
||||||
|
Partial<Record<TextureKey, THREE.Texture>>;
|
||||||
|
|
||||||
|
const optimizedTextures = new WeakSet<THREE.Texture>();
|
||||||
|
|
||||||
|
function optimizeTexture(texture: THREE.Texture, maxAnisotropy: number): void {
|
||||||
|
if (optimizedTextures.has(texture)) return;
|
||||||
|
|
||||||
|
optimizedTextures.add(texture);
|
||||||
|
texture.anisotropy = Math.min(4, Math.max(1, maxAnisotropy));
|
||||||
|
|
||||||
|
if (!(texture instanceof THREE.CompressedTexture)) {
|
||||||
|
texture.generateMipmaps = true;
|
||||||
|
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
||||||
|
texture.magFilter = THREE.LinearFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
texture.needsUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function optimizeMaterialTextures(
|
||||||
|
material: THREE.Material,
|
||||||
|
maxAnisotropy: number,
|
||||||
|
): void {
|
||||||
|
const texturedMaterial = material as TexturedMaterial;
|
||||||
|
|
||||||
|
for (const key of TEXTURE_KEYS) {
|
||||||
|
const texture = texturedMaterial[key];
|
||||||
|
if (texture instanceof THREE.Texture) {
|
||||||
|
optimizeTexture(texture, maxAnisotropy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function optimizeGLTFSceneTextures(
|
||||||
|
scene: THREE.Object3D,
|
||||||
|
maxAnisotropy: number,
|
||||||
|
): void {
|
||||||
|
scene.traverse((child) => {
|
||||||
|
if (!(child instanceof THREE.Mesh)) return;
|
||||||
|
|
||||||
|
if (Array.isArray(child.material)) {
|
||||||
|
for (const material of child.material) {
|
||||||
|
optimizeMaterialTextures(material, maxAnisotropy);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
optimizeMaterialTextures(child.material, maxAnisotropy);
|
||||||
|
});
|
||||||
|
}
|
||||||
+13
-6
@@ -10,6 +10,10 @@ import {
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||||
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||||
|
import {
|
||||||
|
normalizeMapScale,
|
||||||
|
useTerrainSnappedPosition,
|
||||||
|
} from "@/hooks/three/useTerrainHeight";
|
||||||
import { TerrainModel } from "@/components/three/world/TerrainModel";
|
import { TerrainModel } from "@/components/three/world/TerrainModel";
|
||||||
import {
|
import {
|
||||||
isMapModelVisible,
|
isMapModelVisible,
|
||||||
@@ -33,7 +37,7 @@ interface LoadedMapNode {
|
|||||||
modelUrl: string | null;
|
modelUrl: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAP_STRUCTURE_NODE_NAMES = new Set(["Scene", "blocking"]);
|
const MAP_STRUCTURE_NODE_NAMES = new Set(["Scene", "blocking", "terrain"]);
|
||||||
const LITE_MAP_SKIPPED_NODE_NAMES = new Set([
|
const LITE_MAP_SKIPPED_NODE_NAMES = new Set([
|
||||||
"arbre",
|
"arbre",
|
||||||
"buisson",
|
"buisson",
|
||||||
@@ -333,11 +337,13 @@ function ModelInstance({
|
|||||||
onLoaded: () => void;
|
onLoaded: () => void;
|
||||||
}): React.JSX.Element {
|
}): React.JSX.Element {
|
||||||
const { position, rotation, scale } = node;
|
const { position, rotation, scale } = node;
|
||||||
|
const snappedPosition = useTerrainSnappedPosition(position);
|
||||||
|
const normalizedScale = normalizeMapScale(scale);
|
||||||
const { scene } = useLoggedGLTF(modelUrl, {
|
const { scene } = useLoggedGLTF(modelUrl, {
|
||||||
scope: "GameMap.ModelInstance",
|
scope: "GameMap.ModelInstance",
|
||||||
position,
|
position: snappedPosition,
|
||||||
rotation,
|
rotation,
|
||||||
scale,
|
scale: normalizedScale,
|
||||||
});
|
});
|
||||||
const sceneInstance = useClonedObject(scene);
|
const sceneInstance = useClonedObject(scene);
|
||||||
|
|
||||||
@@ -354,18 +360,19 @@ function ModelInstance({
|
|||||||
return (
|
return (
|
||||||
<primitive
|
<primitive
|
||||||
object={sceneInstance}
|
object={sceneInstance}
|
||||||
position={position}
|
position={snappedPosition}
|
||||||
rotation={rotation}
|
rotation={rotation}
|
||||||
scale={scale}
|
scale={normalizedScale}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FallbackMapNode({ node }: { node: MapNode }): React.JSX.Element {
|
function FallbackMapNode({ node }: { node: MapNode }): React.JSX.Element {
|
||||||
const { position, rotation, scale } = node;
|
const { position, rotation, scale } = node;
|
||||||
|
const normalizedScale = normalizeMapScale(scale);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh position={position} rotation={rotation} scale={scale}>
|
<mesh position={position} rotation={rotation} scale={normalizedScale}>
|
||||||
<boxGeometry args={[1, 1, 1]} />
|
<boxGeometry args={[1, 1, 1]} />
|
||||||
<meshStandardMaterial color="#64748b" wireframe />
|
<meshStandardMaterial color="#64748b" wireframe />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { EcoleModel } from "@/components/three/models/generated/EcoleModel";
|
import { EcoleModel } from "@/components/three/world/EcoleModel";
|
||||||
|
import { FermeVerticaleModel } from "@/components/three/world/FermeVerticaleModel";
|
||||||
|
import { GenerateurModel } from "@/components/three/world/GenerateurModel";
|
||||||
|
import { LafabrikModel } from "@/components/three/world/LafabrikModel";
|
||||||
|
import {
|
||||||
|
normalizeMapScale,
|
||||||
|
useTerrainSnappedPosition,
|
||||||
|
} from "@/hooks/three/useTerrainHeight";
|
||||||
import type { MapNode } from "@/types/editor/editor";
|
import type { MapNode } from "@/types/editor/editor";
|
||||||
|
|
||||||
interface GeneratedMapNodeInstanceProps {
|
interface GeneratedMapNodeInstanceProps {
|
||||||
@@ -10,12 +17,48 @@ export function GeneratedMapNodeInstance({
|
|||||||
node,
|
node,
|
||||||
onLoaded,
|
onLoaded,
|
||||||
}: GeneratedMapNodeInstanceProps): React.JSX.Element | null {
|
}: GeneratedMapNodeInstanceProps): React.JSX.Element | null {
|
||||||
|
const position = useTerrainSnappedPosition(node.position);
|
||||||
|
const scale = normalizeMapScale(node.scale);
|
||||||
|
|
||||||
if (node.name === "ecole") {
|
if (node.name === "ecole") {
|
||||||
return (
|
return (
|
||||||
<EcoleModel
|
<EcoleModel
|
||||||
position={node.position}
|
position={position}
|
||||||
rotation={node.rotation}
|
rotation={node.rotation}
|
||||||
scale={node.scale}
|
scale={scale}
|
||||||
|
onLoaded={onLoaded}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.name === "fermeverticale") {
|
||||||
|
return (
|
||||||
|
<FermeVerticaleModel
|
||||||
|
position={position}
|
||||||
|
rotation={node.rotation}
|
||||||
|
scale={scale}
|
||||||
|
onLoaded={onLoaded}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.name === "generateur") {
|
||||||
|
return (
|
||||||
|
<GenerateurModel
|
||||||
|
position={position}
|
||||||
|
rotation={node.rotation}
|
||||||
|
scale={scale}
|
||||||
|
onLoaded={onLoaded}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.name === "lafabrik") {
|
||||||
|
return (
|
||||||
|
<LafabrikModel
|
||||||
|
position={position}
|
||||||
|
rotation={node.rotation}
|
||||||
|
scale={scale}
|
||||||
onLoaded={onLoaded}
|
onLoaded={onLoaded}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
const GENERATED_MAP_MODEL_NAMES = new Set(["ecole"]);
|
const GENERATED_MAP_MODEL_NAMES = new Set([
|
||||||
|
"ecole",
|
||||||
|
"fermeverticale",
|
||||||
|
"generateur",
|
||||||
|
"lafabrik",
|
||||||
|
]);
|
||||||
|
|
||||||
export function isGeneratedMapModelName(name: string): boolean {
|
export function isGeneratedMapModelName(name: string): boolean {
|
||||||
return GENERATED_MAP_MODEL_NAMES.has(name);
|
return GENERATED_MAP_MODEL_NAMES.has(name);
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { useEffect, 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 { useThree } from "@react-three/fiber";
|
||||||
import { mergeGeometries } from "three/addons/utils/BufferGeometryUtils.js";
|
import { mergeGeometries } from "three/addons/utils/BufferGeometryUtils.js";
|
||||||
|
import {
|
||||||
|
normalizeMapScale,
|
||||||
|
useTerrainHeightSampler,
|
||||||
|
} from "@/hooks/three/useTerrainHeight";
|
||||||
|
import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene";
|
||||||
import type { MapAssetInstance } from "@/world/map-instancing/useMapInstancingData";
|
import type { MapAssetInstance } from "@/world/map-instancing/useMapInstancingData";
|
||||||
|
|
||||||
interface InstancedMapAssetProps {
|
interface InstancedMapAssetProps {
|
||||||
@@ -153,21 +159,40 @@ export function InstancedMapAsset({
|
|||||||
receiveShadow,
|
receiveShadow,
|
||||||
}: InstancedMapAssetProps): React.JSX.Element | null {
|
}: InstancedMapAssetProps): React.JSX.Element | null {
|
||||||
const { scene } = useGLTF(modelPath);
|
const { scene } = useGLTF(modelPath);
|
||||||
|
const terrainHeight = useTerrainHeightSampler();
|
||||||
|
const maxAnisotropy = useThree((state) =>
|
||||||
|
state.gl.capabilities.getMaxAnisotropy(),
|
||||||
|
);
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
|
const groundedInstances = useMemo(
|
||||||
|
() =>
|
||||||
|
instances.map((instance) => {
|
||||||
|
const [x, y, z] = instance.position;
|
||||||
|
const height = terrainHeight.getHeight(x, z);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...instance,
|
||||||
|
position: [x, height ?? y, z] as MapAssetInstance["position"],
|
||||||
|
scale: normalizeMapScale(instance.scale),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
[instances, terrainHeight],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const group = groupRef.current;
|
const group = groupRef.current;
|
||||||
if (!group || instances.length === 0) return;
|
if (!group || groundedInstances.length === 0) return;
|
||||||
|
|
||||||
|
optimizeGLTFSceneTextures(scene, maxAnisotropy);
|
||||||
const meshDataList = extractMeshes(scene);
|
const meshDataList = extractMeshes(scene);
|
||||||
const instancedMeshes = meshDataList.map((meshData, index) => {
|
const instancedMeshes = meshDataList.map((meshData, index) => {
|
||||||
const instancedMesh = new THREE.InstancedMesh(
|
const instancedMesh = new THREE.InstancedMesh(
|
||||||
meshData.geometry,
|
meshData.geometry,
|
||||||
meshData.material,
|
meshData.material,
|
||||||
instances.length,
|
groundedInstances.length,
|
||||||
);
|
);
|
||||||
|
|
||||||
setInstanceMatrices(instancedMesh, instances);
|
setInstanceMatrices(instancedMesh, groundedInstances);
|
||||||
instancedMesh.castShadow = castShadow;
|
instancedMesh.castShadow = castShadow;
|
||||||
instancedMesh.receiveShadow = receiveShadow;
|
instancedMesh.receiveShadow = receiveShadow;
|
||||||
instancedMesh.name = `instanced-map-asset-${index}`;
|
instancedMesh.name = `instanced-map-asset-${index}`;
|
||||||
@@ -187,7 +212,7 @@ export function InstancedMapAsset({
|
|||||||
disposeInstancedMapMesh(mesh);
|
disposeInstancedMapMesh(mesh);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [castShadow, instances, receiveShadow, scene]);
|
}, [castShadow, groundedInstances, maxAnisotropy, receiveShadow, scene]);
|
||||||
|
|
||||||
if (instances.length === 0) {
|
if (instances.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,25 +1,4 @@
|
|||||||
export const MAP_INSTANCING_ASSETS = {
|
export const MAP_INSTANCING_ASSETS = {
|
||||||
generateur: {
|
|
||||||
mapName: "generateur",
|
|
||||||
modelPath: "/models/generateur/model.gltf",
|
|
||||||
castShadow: true,
|
|
||||||
receiveShadow: true,
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
lafabrik: {
|
|
||||||
mapName: "lafabrik",
|
|
||||||
modelPath: "/models/lafabrik/model.gltf",
|
|
||||||
castShadow: true,
|
|
||||||
receiveShadow: true,
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
fermeverticale: {
|
|
||||||
mapName: "fermeverticale",
|
|
||||||
modelPath: "/models/fermeverticale/model.gltf",
|
|
||||||
castShadow: true,
|
|
||||||
receiveShadow: true,
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
boiteauxlettres: {
|
boiteauxlettres: {
|
||||||
mapName: "boiteauxlettres",
|
mapName: "boiteauxlettres",
|
||||||
modelPath: "/models/boiteauxlettres/model.gltf",
|
modelPath: "/models/boiteauxlettres/model.gltf",
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
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 { useThree } from "@react-three/fiber";
|
||||||
import { mergeGeometries } from "three/addons/utils/BufferGeometryUtils.js";
|
import { mergeGeometries } from "three/addons/utils/BufferGeometryUtils.js";
|
||||||
|
import { useTerrainHeightSampler } from "@/hooks/three/useTerrainHeight";
|
||||||
|
import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene";
|
||||||
import type { VegetationInstance } from "@/world/vegetation/useVegetationData";
|
import type { VegetationInstance } from "@/world/vegetation/useVegetationData";
|
||||||
|
|
||||||
interface InstancedVegetationProps {
|
interface InstancedVegetationProps {
|
||||||
modelPath: string;
|
modelPath: string;
|
||||||
instances: VegetationInstance[];
|
instances: VegetationInstance[];
|
||||||
|
scaleMultiplier: number;
|
||||||
castShadow: boolean;
|
castShadow: boolean;
|
||||||
receiveShadow: boolean;
|
receiveShadow: boolean;
|
||||||
}
|
}
|
||||||
@@ -70,12 +74,17 @@ function extractMeshes(scene: THREE.Group): MeshData[] {
|
|||||||
|
|
||||||
function createInstanceMatrices(
|
function createInstanceMatrices(
|
||||||
instances: VegetationInstance[],
|
instances: VegetationInstance[],
|
||||||
|
scaleMultiplier: number,
|
||||||
): THREE.Matrix4[] {
|
): THREE.Matrix4[] {
|
||||||
const matrices: THREE.Matrix4[] = [];
|
const matrices: THREE.Matrix4[] = [];
|
||||||
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(1, 1, 1);
|
const scale = new THREE.Vector3(
|
||||||
|
scaleMultiplier,
|
||||||
|
scaleMultiplier,
|
||||||
|
scaleMultiplier,
|
||||||
|
);
|
||||||
|
|
||||||
for (const instance of instances) {
|
for (const instance of instances) {
|
||||||
const matrix = new THREE.Matrix4();
|
const matrix = new THREE.Matrix4();
|
||||||
@@ -93,16 +102,36 @@ function createInstanceMatrices(
|
|||||||
export function InstancedVegetation({
|
export function InstancedVegetation({
|
||||||
modelPath,
|
modelPath,
|
||||||
instances,
|
instances,
|
||||||
|
scaleMultiplier,
|
||||||
castShadow,
|
castShadow,
|
||||||
receiveShadow,
|
receiveShadow,
|
||||||
}: InstancedVegetationProps): React.JSX.Element | null {
|
}: InstancedVegetationProps): React.JSX.Element | null {
|
||||||
const { scene } = useGLTF(modelPath);
|
const { scene } = useGLTF(modelPath);
|
||||||
|
const terrainHeight = useTerrainHeightSampler();
|
||||||
|
const maxAnisotropy = useThree((state) =>
|
||||||
|
state.gl.capabilities.getMaxAnisotropy(),
|
||||||
|
);
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
|
|
||||||
const meshDataList = useMemo(() => extractMeshes(scene), [scene]);
|
const meshDataList = useMemo(() => {
|
||||||
|
optimizeGLTFSceneTextures(scene, maxAnisotropy);
|
||||||
|
return extractMeshes(scene);
|
||||||
|
}, [maxAnisotropy, scene]);
|
||||||
|
const groundedInstances = useMemo(
|
||||||
|
() =>
|
||||||
|
instances.map((instance) => {
|
||||||
|
const [x, y, z] = instance.position;
|
||||||
|
const height = terrainHeight.getHeight(x, z);
|
||||||
|
return {
|
||||||
|
...instance,
|
||||||
|
position: [x, height ?? y, z] as VegetationInstance["position"],
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
[instances, terrainHeight],
|
||||||
|
);
|
||||||
const matrices = useMemo(
|
const matrices = useMemo(
|
||||||
() => createInstanceMatrices(instances),
|
() => createInstanceMatrices(groundedInstances, scaleMultiplier),
|
||||||
[instances],
|
[groundedInstances, scaleMultiplier],
|
||||||
);
|
);
|
||||||
|
|
||||||
const instancedMeshes = useMemo(() => {
|
const instancedMeshes = useMemo(() => {
|
||||||
@@ -110,7 +139,7 @@ export function InstancedVegetation({
|
|||||||
const instancedMesh = new THREE.InstancedMesh(
|
const instancedMesh = new THREE.InstancedMesh(
|
||||||
meshData.geometry,
|
meshData.geometry,
|
||||||
meshData.material,
|
meshData.material,
|
||||||
instances.length,
|
groundedInstances.length,
|
||||||
);
|
);
|
||||||
|
|
||||||
for (let i = 0; i < matrices.length; i++) {
|
for (let i = 0; i < matrices.length; i++) {
|
||||||
@@ -129,7 +158,13 @@ export function InstancedVegetation({
|
|||||||
|
|
||||||
return instancedMesh;
|
return instancedMesh;
|
||||||
});
|
});
|
||||||
}, [meshDataList, matrices, instances.length, castShadow, receiveShadow]);
|
}, [
|
||||||
|
meshDataList,
|
||||||
|
matrices,
|
||||||
|
groundedInstances.length,
|
||||||
|
castShadow,
|
||||||
|
receiveShadow,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const group = groupRef.current;
|
const group = groupRef.current;
|
||||||
@@ -162,7 +197,7 @@ export function InstancedVegetation({
|
|||||||
};
|
};
|
||||||
}, [meshDataList]);
|
}, [meshDataList]);
|
||||||
|
|
||||||
if (instances.length === 0) {
|
if (groundedInstances.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ interface VegetationChunk {
|
|||||||
key: string;
|
key: string;
|
||||||
type: VegetationType;
|
type: VegetationType;
|
||||||
modelPath: string;
|
modelPath: string;
|
||||||
|
scaleMultiplier: number;
|
||||||
castShadow: boolean;
|
castShadow: boolean;
|
||||||
receiveShadow: boolean;
|
receiveShadow: boolean;
|
||||||
centerX: number;
|
centerX: number;
|
||||||
@@ -66,6 +67,7 @@ function createVegetationChunks(
|
|||||||
key: `${type}:${chunkKey}`,
|
key: `${type}:${chunkKey}`,
|
||||||
type,
|
type,
|
||||||
modelPath: config.modelPath,
|
modelPath: config.modelPath,
|
||||||
|
scaleMultiplier: config.scaleMultiplier,
|
||||||
castShadow: config.castShadow,
|
castShadow: config.castShadow,
|
||||||
receiveShadow: config.receiveShadow,
|
receiveShadow: config.receiveShadow,
|
||||||
centerX: center.x / chunkInstances.length,
|
centerX: center.x / chunkInstances.length,
|
||||||
@@ -103,6 +105,10 @@ export function VegetationSystem(): React.JSX.Element | null {
|
|||||||
});
|
});
|
||||||
}, [data, groups, models]);
|
}, [data, groups, models]);
|
||||||
|
|
||||||
|
const visibleChunks = streamingEnabled
|
||||||
|
? chunks.filter((chunk) => activeChunkKeys.has(chunk.key))
|
||||||
|
: chunks;
|
||||||
|
|
||||||
useFrame(({ clock }) => {
|
useFrame(({ clock }) => {
|
||||||
if (!streamingEnabled) return;
|
if (!streamingEnabled) return;
|
||||||
|
|
||||||
@@ -143,10 +149,6 @@ export function VegetationSystem(): React.JSX.Element | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const visibleChunks = streamingEnabled
|
|
||||||
? chunks.filter((chunk) => activeChunkKeys.has(chunk.key))
|
|
||||||
: chunks;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group name="vegetation-system">
|
<group name="vegetation-system">
|
||||||
{visibleChunks.map((chunk) => (
|
{visibleChunks.map((chunk) => (
|
||||||
@@ -154,6 +156,7 @@ export function VegetationSystem(): React.JSX.Element | null {
|
|||||||
<InstancedVegetation
|
<InstancedVegetation
|
||||||
modelPath={chunk.modelPath}
|
modelPath={chunk.modelPath}
|
||||||
instances={chunk.instances}
|
instances={chunk.instances}
|
||||||
|
scaleMultiplier={chunk.scaleMultiplier}
|
||||||
castShadow={chunk.castShadow}
|
castShadow={chunk.castShadow}
|
||||||
receiveShadow={chunk.receiveShadow}
|
receiveShadow={chunk.receiveShadow}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const VEGETATION_TYPES = {
|
|||||||
buissons: {
|
buissons: {
|
||||||
mapName: "buisson",
|
mapName: "buisson",
|
||||||
modelPath: "/models/buisson/model.gltf",
|
modelPath: "/models/buisson/model.gltf",
|
||||||
|
scaleMultiplier: 2,
|
||||||
castShadow: true,
|
castShadow: true,
|
||||||
receiveShadow: true,
|
receiveShadow: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -15,6 +16,7 @@ export const VEGETATION_TYPES = {
|
|||||||
sapin: {
|
sapin: {
|
||||||
mapName: "sapin",
|
mapName: "sapin",
|
||||||
modelPath: "/models/sapin/model.gltf",
|
modelPath: "/models/sapin/model.gltf",
|
||||||
|
scaleMultiplier: 2,
|
||||||
castShadow: true,
|
castShadow: true,
|
||||||
receiveShadow: true,
|
receiveShadow: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -22,6 +24,7 @@ export const VEGETATION_TYPES = {
|
|||||||
arbre: {
|
arbre: {
|
||||||
mapName: "arbre",
|
mapName: "arbre",
|
||||||
modelPath: "/models/arbre/model.gltf",
|
modelPath: "/models/arbre/model.gltf",
|
||||||
|
scaleMultiplier: 1,
|
||||||
castShadow: true,
|
castShadow: true,
|
||||||
receiveShadow: true,
|
receiveShadow: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -29,6 +32,7 @@ export const VEGETATION_TYPES = {
|
|||||||
champdeble: {
|
champdeble: {
|
||||||
mapName: "champdeble",
|
mapName: "champdeble",
|
||||||
modelPath: "/models/champdeble/model.gltf",
|
modelPath: "/models/champdeble/model.gltf",
|
||||||
|
scaleMultiplier: 1,
|
||||||
castShadow: true,
|
castShadow: true,
|
||||||
receiveShadow: true,
|
receiveShadow: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -36,6 +40,7 @@ export const VEGETATION_TYPES = {
|
|||||||
champdesoja: {
|
champdesoja: {
|
||||||
mapName: "champdesoja",
|
mapName: "champdesoja",
|
||||||
modelPath: "/models/champdesoja/model.gltf",
|
modelPath: "/models/champdesoja/model.gltf",
|
||||||
|
scaleMultiplier: 1,
|
||||||
castShadow: true,
|
castShadow: true,
|
||||||
receiveShadow: true,
|
receiveShadow: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -43,6 +48,7 @@ export const VEGETATION_TYPES = {
|
|||||||
champsdetournesol: {
|
champsdetournesol: {
|
||||||
mapName: "champsdetournesol",
|
mapName: "champsdetournesol",
|
||||||
modelPath: "/models/champsdetournesol/model.gltf",
|
modelPath: "/models/champsdetournesol/model.gltf",
|
||||||
|
scaleMultiplier: 1,
|
||||||
castShadow: true,
|
castShadow: true,
|
||||||
receiveShadow: true,
|
receiveShadow: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user