perf(map): snap assets to terrain
This commit is contained in:
+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