Files
La-Fabrik/src/world/vegetation/InstancedVegetation.tsx
T
2026-05-15 23:29:07 +02:00

172 lines
4.4 KiB
TypeScript

import { useEffect, useMemo, useRef } from "react";
import * as THREE from "three";
import { useGLTF } from "@react-three/drei";
import { mergeGeometries } from "three/addons/utils/BufferGeometryUtils.js";
import type { VegetationInstance } from "@/world/vegetation/useVegetationData";
import { disposeInstancedMesh } from "@/utils/three/dispose";
interface InstancedVegetationProps {
modelPath: string;
instances: VegetationInstance[];
castShadow: boolean;
receiveShadow: boolean;
}
interface MeshData {
geometry: THREE.BufferGeometry;
material: THREE.Material;
}
function extractMeshes(scene: THREE.Group): MeshData[] {
const meshesByMaterial = new Map<
string,
{ geometries: THREE.BufferGeometry[]; material: THREE.Material }
>();
scene.updateMatrixWorld(true);
scene.traverse((child) => {
if (!(child instanceof THREE.Mesh)) return;
const material = Array.isArray(child.material)
? child.material[0]
: child.material;
if (!material) return;
const geometry = child.geometry.clone();
geometry.applyMatrix4(child.matrixWorld);
const existing = meshesByMaterial.get(material.uuid);
if (existing) {
existing.geometries.push(geometry);
} else {
meshesByMaterial.set(material.uuid, {
geometries: [geometry],
material: material.clone(),
});
}
});
return [...meshesByMaterial.values()]
.map(({ geometries, material }) => {
const mergedGeometry = mergeGeometries(geometries, false);
for (const geometry of geometries) {
if (geometry !== mergedGeometry) {
geometry.dispose();
}
}
if (!mergedGeometry) {
material.dispose();
return null;
}
return {
geometry: mergedGeometry,
material,
};
})
.filter((meshData): meshData is MeshData => meshData !== null);
}
function createInstanceMatrices(
instances: VegetationInstance[],
): 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);
for (const instance of instances) {
const matrix = new THREE.Matrix4();
position.set(...instance.position);
rotation.set(...instance.rotation);
quaternion.setFromEuler(rotation);
matrix.compose(position, quaternion, scale);
matrices.push(matrix);
}
return matrices;
}
export function InstancedVegetation({
modelPath,
instances,
castShadow,
receiveShadow,
}: InstancedVegetationProps): React.JSX.Element | null {
const { scene } = useGLTF(modelPath);
const groupRef = useRef<THREE.Group>(null);
const meshDataList = useMemo(() => extractMeshes(scene), [scene]);
const matrices = useMemo(
() => createInstanceMatrices(instances),
[instances],
);
const instancedMeshes = useMemo(() => {
return meshDataList.map((meshData, index) => {
const instancedMesh = new THREE.InstancedMesh(
meshData.geometry,
meshData.material,
instances.length,
);
for (let i = 0; i < matrices.length; i++) {
const matrix = matrices[i];
if (matrix) {
instancedMesh.setMatrixAt(i, matrix);
}
}
instancedMesh.instanceMatrix.needsUpdate = true;
instancedMesh.castShadow = castShadow;
instancedMesh.receiveShadow = receiveShadow;
instancedMesh.name = `instanced-mesh-${index}`;
instancedMesh.frustumCulled = true;
instancedMesh.computeBoundingSphere();
return instancedMesh;
});
}, [meshDataList, matrices, instances.length, castShadow, receiveShadow]);
useEffect(() => {
const group = groupRef.current;
if (!group) return;
for (const mesh of instancedMeshes) {
group.add(mesh);
}
return () => {
for (const mesh of instancedMeshes) {
group.remove(mesh);
disposeInstancedMesh(mesh);
}
};
}, [instancedMeshes]);
useEffect(() => {
return () => {
for (const meshData of meshDataList) {
meshData.geometry.dispose();
if (Array.isArray(meshData.material)) {
for (const mat of meshData.material) {
mat.dispose();
}
} else {
meshData.material.dispose();
}
}
};
}, [meshDataList]);
if (instances.length === 0) {
return null;
}
return <group ref={groupRef} />;
}