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 * as THREE from "three";
|
||||
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 type { Vector3Tuple } from "@/types/three/three";
|
||||
import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene";
|
||||
|
||||
const ECOLE_MODEL_PATH = "/models/ecole/model.gltf";
|
||||
|
||||
interface EcoleModelProps {
|
||||
interface MergedStaticMapModelProps {
|
||||
modelPath: string;
|
||||
position: Vector3Tuple;
|
||||
rotation: Vector3Tuple;
|
||||
scale: Vector3Tuple;
|
||||
@@ -117,21 +118,26 @@ function createMergedMeshes(scene: THREE.Group): MergedMeshData[] {
|
||||
.filter((meshData): meshData is MergedMeshData => meshData !== null);
|
||||
}
|
||||
|
||||
export function EcoleModel({
|
||||
export function MergedStaticMapModel({
|
||||
modelPath,
|
||||
position,
|
||||
rotation,
|
||||
scale,
|
||||
castShadow = true,
|
||||
receiveShadow = true,
|
||||
onLoaded,
|
||||
}: EcoleModelProps): React.JSX.Element {
|
||||
const { scene } = useGLTF(ECOLE_MODEL_PATH);
|
||||
}: MergedStaticMapModelProps): React.JSX.Element {
|
||||
const { scene } = useGLTF(modelPath);
|
||||
const maxAnisotropy = useThree((state) =>
|
||||
state.gl.capabilities.getMaxAnisotropy(),
|
||||
);
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const group = groupRef.current;
|
||||
if (!group) return;
|
||||
|
||||
optimizeGLTFSceneTextures(scene, maxAnisotropy);
|
||||
const mergedMeshes = createMergedMeshes(scene);
|
||||
const meshes = mergedMeshes.map((meshData) => {
|
||||
const mesh = new THREE.Mesh(meshData.geometry, meshData.material);
|
||||
@@ -153,7 +159,7 @@ export function EcoleModel({
|
||||
disposeMaterial(mesh.material);
|
||||
}
|
||||
};
|
||||
}, [castShadow, onLoaded, receiveShadow, scene]);
|
||||
}, [castShadow, maxAnisotropy, modelPath, onLoaded, receiveShadow, scene]);
|
||||
|
||||
return (
|
||||
<group
|
||||
@@ -164,5 +170,3 @@ export function EcoleModel({
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
useGLTF.preload(ECOLE_MODEL_PATH);
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import * as THREE from "three";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import type { Vector3Tuple } from "@/types/three/three";
|
||||
import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene";
|
||||
|
||||
const TERRAIN_MODEL_PATH = "/models/terrain/model.gltf";
|
||||
const TERRAIN_DEFAULT_POSITION: Vector3Tuple = [0, 0, 0];
|
||||
@@ -39,12 +41,16 @@ export function TerrainModel({
|
||||
const internalRef = useRef<THREE.Group>(null);
|
||||
const ref = groupRef ?? internalRef;
|
||||
const { scene } = useGLTF(TERRAIN_MODEL_PATH);
|
||||
const maxAnisotropy = useThree((state) =>
|
||||
state.gl.capabilities.getMaxAnisotropy(),
|
||||
);
|
||||
|
||||
const terrainModel = useMemo(() => {
|
||||
optimizeGLTFSceneTextures(scene, maxAnisotropy);
|
||||
const model = scene.clone(true);
|
||||
applyTerrainMaterialSettings(model, receiveShadow);
|
||||
return model;
|
||||
}, [scene, receiveShadow]);
|
||||
}, [maxAnisotropy, scene, receiveShadow]);
|
||||
|
||||
useEffect(() => {
|
||||
onLoaded?.();
|
||||
|
||||
@@ -2,16 +2,16 @@ import { TERRAIN_COLORS } from "@/data/world/terrainConfig";
|
||||
|
||||
export const FOG_CONFIG = {
|
||||
enabled: true,
|
||||
color: "#c8dbbe",
|
||||
near: 34,
|
||||
far: 58,
|
||||
color: "#eef3f5",
|
||||
near: 38,
|
||||
far: 45,
|
||||
};
|
||||
|
||||
export const CHUNK_CONFIG = {
|
||||
enabled: true,
|
||||
chunkSize: 35,
|
||||
loadRadius: 45,
|
||||
unloadRadius: 58,
|
||||
unloadRadius: 45,
|
||||
updateInterval: 350,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import {
|
||||
logModelLoadSuccess,
|
||||
type ModelLoadLogContext,
|
||||
} from "@/utils/three/modelLoadLogger";
|
||||
import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene";
|
||||
|
||||
export function useLoggedGLTF(
|
||||
modelPath: string,
|
||||
context: Omit<ModelLoadLogContext, "modelPath">,
|
||||
) {
|
||||
const gltf = useGLTF(modelPath);
|
||||
const maxAnisotropy = useThree((state) =>
|
||||
state.gl.capabilities.getMaxAnisotropy(),
|
||||
);
|
||||
const hasLoggedRef = useRef(false);
|
||||
const { position, rotation, scale, scope } = context;
|
||||
|
||||
useEffect(() => {
|
||||
optimizeGLTFSceneTextures(gltf.scene, maxAnisotropy);
|
||||
}, [gltf.scene, maxAnisotropy]);
|
||||
|
||||
useEffect(() => {
|
||||
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 { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||
import {
|
||||
normalizeMapScale,
|
||||
useTerrainSnappedPosition,
|
||||
} from "@/hooks/three/useTerrainHeight";
|
||||
import { TerrainModel } from "@/components/three/world/TerrainModel";
|
||||
import {
|
||||
isMapModelVisible,
|
||||
@@ -33,7 +37,7 @@ interface LoadedMapNode {
|
||||
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([
|
||||
"arbre",
|
||||
"buisson",
|
||||
@@ -333,11 +337,13 @@ function ModelInstance({
|
||||
onLoaded: () => void;
|
||||
}): React.JSX.Element {
|
||||
const { position, rotation, scale } = node;
|
||||
const snappedPosition = useTerrainSnappedPosition(position);
|
||||
const normalizedScale = normalizeMapScale(scale);
|
||||
const { scene } = useLoggedGLTF(modelUrl, {
|
||||
scope: "GameMap.ModelInstance",
|
||||
position,
|
||||
position: snappedPosition,
|
||||
rotation,
|
||||
scale,
|
||||
scale: normalizedScale,
|
||||
});
|
||||
const sceneInstance = useClonedObject(scene);
|
||||
|
||||
@@ -354,18 +360,19 @@ function ModelInstance({
|
||||
return (
|
||||
<primitive
|
||||
object={sceneInstance}
|
||||
position={position}
|
||||
position={snappedPosition}
|
||||
rotation={rotation}
|
||||
scale={scale}
|
||||
scale={normalizedScale}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FallbackMapNode({ node }: { node: MapNode }): React.JSX.Element {
|
||||
const { position, rotation, scale } = node;
|
||||
const normalizedScale = normalizeMapScale(scale);
|
||||
|
||||
return (
|
||||
<mesh position={position} rotation={rotation} scale={scale}>
|
||||
<mesh position={position} rotation={rotation} scale={normalizedScale}>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<meshStandardMaterial color="#64748b" wireframe />
|
||||
</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";
|
||||
|
||||
interface GeneratedMapNodeInstanceProps {
|
||||
@@ -10,12 +17,48 @@ export function GeneratedMapNodeInstance({
|
||||
node,
|
||||
onLoaded,
|
||||
}: GeneratedMapNodeInstanceProps): React.JSX.Element | null {
|
||||
const position = useTerrainSnappedPosition(node.position);
|
||||
const scale = normalizeMapScale(node.scale);
|
||||
|
||||
if (node.name === "ecole") {
|
||||
return (
|
||||
<EcoleModel
|
||||
position={node.position}
|
||||
position={position}
|
||||
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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
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 { useGLTF } from "@react-three/drei";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
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";
|
||||
|
||||
interface InstancedMapAssetProps {
|
||||
@@ -153,21 +159,40 @@ export function InstancedMapAsset({
|
||||
receiveShadow,
|
||||
}: InstancedMapAssetProps): React.JSX.Element | null {
|
||||
const { scene } = useGLTF(modelPath);
|
||||
const terrainHeight = useTerrainHeightSampler();
|
||||
const maxAnisotropy = useThree((state) =>
|
||||
state.gl.capabilities.getMaxAnisotropy(),
|
||||
);
|
||||
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(() => {
|
||||
const group = groupRef.current;
|
||||
if (!group || instances.length === 0) return;
|
||||
if (!group || groundedInstances.length === 0) return;
|
||||
|
||||
optimizeGLTFSceneTextures(scene, maxAnisotropy);
|
||||
const meshDataList = extractMeshes(scene);
|
||||
const instancedMeshes = meshDataList.map((meshData, index) => {
|
||||
const instancedMesh = new THREE.InstancedMesh(
|
||||
meshData.geometry,
|
||||
meshData.material,
|
||||
instances.length,
|
||||
groundedInstances.length,
|
||||
);
|
||||
|
||||
setInstanceMatrices(instancedMesh, instances);
|
||||
setInstanceMatrices(instancedMesh, groundedInstances);
|
||||
instancedMesh.castShadow = castShadow;
|
||||
instancedMesh.receiveShadow = receiveShadow;
|
||||
instancedMesh.name = `instanced-map-asset-${index}`;
|
||||
@@ -187,7 +212,7 @@ export function InstancedMapAsset({
|
||||
disposeInstancedMapMesh(mesh);
|
||||
}
|
||||
};
|
||||
}, [castShadow, instances, receiveShadow, scene]);
|
||||
}, [castShadow, groundedInstances, maxAnisotropy, receiveShadow, scene]);
|
||||
|
||||
if (instances.length === 0) {
|
||||
return null;
|
||||
|
||||
@@ -1,25 +1,4 @@
|
||||
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: {
|
||||
mapName: "boiteauxlettres",
|
||||
modelPath: "/models/boiteauxlettres/model.gltf",
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import * as THREE from "three";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
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";
|
||||
|
||||
interface InstancedVegetationProps {
|
||||
modelPath: string;
|
||||
instances: VegetationInstance[];
|
||||
scaleMultiplier: number;
|
||||
castShadow: boolean;
|
||||
receiveShadow: boolean;
|
||||
}
|
||||
@@ -70,12 +74,17 @@ function extractMeshes(scene: THREE.Group): MeshData[] {
|
||||
|
||||
function createInstanceMatrices(
|
||||
instances: VegetationInstance[],
|
||||
scaleMultiplier: number,
|
||||
): THREE.Matrix4[] {
|
||||
const matrices: THREE.Matrix4[] = [];
|
||||
const position = new THREE.Vector3();
|
||||
const rotation = new THREE.Euler();
|
||||
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) {
|
||||
const matrix = new THREE.Matrix4();
|
||||
@@ -93,16 +102,36 @@ function createInstanceMatrices(
|
||||
export function InstancedVegetation({
|
||||
modelPath,
|
||||
instances,
|
||||
scaleMultiplier,
|
||||
castShadow,
|
||||
receiveShadow,
|
||||
}: InstancedVegetationProps): React.JSX.Element | null {
|
||||
const { scene } = useGLTF(modelPath);
|
||||
const terrainHeight = useTerrainHeightSampler();
|
||||
const maxAnisotropy = useThree((state) =>
|
||||
state.gl.capabilities.getMaxAnisotropy(),
|
||||
);
|
||||
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(
|
||||
() => createInstanceMatrices(instances),
|
||||
[instances],
|
||||
() => createInstanceMatrices(groundedInstances, scaleMultiplier),
|
||||
[groundedInstances, scaleMultiplier],
|
||||
);
|
||||
|
||||
const instancedMeshes = useMemo(() => {
|
||||
@@ -110,7 +139,7 @@ export function InstancedVegetation({
|
||||
const instancedMesh = new THREE.InstancedMesh(
|
||||
meshData.geometry,
|
||||
meshData.material,
|
||||
instances.length,
|
||||
groundedInstances.length,
|
||||
);
|
||||
|
||||
for (let i = 0; i < matrices.length; i++) {
|
||||
@@ -129,7 +158,13 @@ export function InstancedVegetation({
|
||||
|
||||
return instancedMesh;
|
||||
});
|
||||
}, [meshDataList, matrices, instances.length, castShadow, receiveShadow]);
|
||||
}, [
|
||||
meshDataList,
|
||||
matrices,
|
||||
groundedInstances.length,
|
||||
castShadow,
|
||||
receiveShadow,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const group = groupRef.current;
|
||||
@@ -162,7 +197,7 @@ export function InstancedVegetation({
|
||||
};
|
||||
}, [meshDataList]);
|
||||
|
||||
if (instances.length === 0) {
|
||||
if (groundedInstances.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ interface VegetationChunk {
|
||||
key: string;
|
||||
type: VegetationType;
|
||||
modelPath: string;
|
||||
scaleMultiplier: number;
|
||||
castShadow: boolean;
|
||||
receiveShadow: boolean;
|
||||
centerX: number;
|
||||
@@ -66,6 +67,7 @@ function createVegetationChunks(
|
||||
key: `${type}:${chunkKey}`,
|
||||
type,
|
||||
modelPath: config.modelPath,
|
||||
scaleMultiplier: config.scaleMultiplier,
|
||||
castShadow: config.castShadow,
|
||||
receiveShadow: config.receiveShadow,
|
||||
centerX: center.x / chunkInstances.length,
|
||||
@@ -103,6 +105,10 @@ export function VegetationSystem(): React.JSX.Element | null {
|
||||
});
|
||||
}, [data, groups, models]);
|
||||
|
||||
const visibleChunks = streamingEnabled
|
||||
? chunks.filter((chunk) => activeChunkKeys.has(chunk.key))
|
||||
: chunks;
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (!streamingEnabled) return;
|
||||
|
||||
@@ -143,10 +149,6 @@ export function VegetationSystem(): React.JSX.Element | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
const visibleChunks = streamingEnabled
|
||||
? chunks.filter((chunk) => activeChunkKeys.has(chunk.key))
|
||||
: chunks;
|
||||
|
||||
return (
|
||||
<group name="vegetation-system">
|
||||
{visibleChunks.map((chunk) => (
|
||||
@@ -154,6 +156,7 @@ export function VegetationSystem(): React.JSX.Element | null {
|
||||
<InstancedVegetation
|
||||
modelPath={chunk.modelPath}
|
||||
instances={chunk.instances}
|
||||
scaleMultiplier={chunk.scaleMultiplier}
|
||||
castShadow={chunk.castShadow}
|
||||
receiveShadow={chunk.receiveShadow}
|
||||
/>
|
||||
|
||||
@@ -8,6 +8,7 @@ export const VEGETATION_TYPES = {
|
||||
buissons: {
|
||||
mapName: "buisson",
|
||||
modelPath: "/models/buisson/model.gltf",
|
||||
scaleMultiplier: 2,
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
enabled: true,
|
||||
@@ -15,6 +16,7 @@ export const VEGETATION_TYPES = {
|
||||
sapin: {
|
||||
mapName: "sapin",
|
||||
modelPath: "/models/sapin/model.gltf",
|
||||
scaleMultiplier: 2,
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
enabled: true,
|
||||
@@ -22,6 +24,7 @@ export const VEGETATION_TYPES = {
|
||||
arbre: {
|
||||
mapName: "arbre",
|
||||
modelPath: "/models/arbre/model.gltf",
|
||||
scaleMultiplier: 1,
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
enabled: true,
|
||||
@@ -29,6 +32,7 @@ export const VEGETATION_TYPES = {
|
||||
champdeble: {
|
||||
mapName: "champdeble",
|
||||
modelPath: "/models/champdeble/model.gltf",
|
||||
scaleMultiplier: 1,
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
enabled: true,
|
||||
@@ -36,6 +40,7 @@ export const VEGETATION_TYPES = {
|
||||
champdesoja: {
|
||||
mapName: "champdesoja",
|
||||
modelPath: "/models/champdesoja/model.gltf",
|
||||
scaleMultiplier: 1,
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
enabled: true,
|
||||
@@ -43,6 +48,7 @@ export const VEGETATION_TYPES = {
|
||||
champsdetournesol: {
|
||||
mapName: "champsdetournesol",
|
||||
modelPath: "/models/champsdetournesol/model.gltf",
|
||||
scaleMultiplier: 1,
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
enabled: true,
|
||||
|
||||
Reference in New Issue
Block a user