perf(map): snap assets to terrain

This commit is contained in:
Tom Boullay
2026-05-25 00:51:03 +02:00
parent 50fa94b3ad
commit d17738eaf1
18 changed files with 402 additions and 62 deletions
+42 -7
View File
@@ -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;
}
+7 -4
View File
@@ -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}
/>
+6
View File
@@ -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,