feat: add VegetationSystem with InstancedMesh rendering
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import * as THREE from "three";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
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 | THREE.Material[];
|
||||
}
|
||||
|
||||
function extractMeshes(scene: THREE.Group): MeshData[] {
|
||||
const meshes: MeshData[] = [];
|
||||
|
||||
scene.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
meshes.push({
|
||||
geometry: child.geometry.clone(),
|
||||
material: Array.isArray(child.material)
|
||||
? child.material.map((m) => m.clone())
|
||||
: child.material.clone(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return meshes;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
for (const instance of instances) {
|
||||
const matrix = new THREE.Matrix4();
|
||||
|
||||
position.set(...instance.position);
|
||||
rotation.set(...instance.rotation);
|
||||
quaternion.setFromEuler(rotation);
|
||||
scale.set(...instance.scale);
|
||||
|
||||
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++) {
|
||||
instancedMesh.setMatrixAt(i, matrices[i]);
|
||||
}
|
||||
|
||||
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} />;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Suspense } from "react";
|
||||
import { InstancedVegetation } from "@/world/vegetation/InstancedVegetation";
|
||||
import { useVegetationData } from "@/world/vegetation/useVegetationData";
|
||||
import {
|
||||
VEGETATION_TYPES,
|
||||
type VegetationType,
|
||||
} from "@/world/vegetation/vegetationConfig";
|
||||
|
||||
export function VegetationSystem(): React.JSX.Element | null {
|
||||
const { data, isLoading } = useVegetationData();
|
||||
|
||||
if (isLoading || !data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const enabledTypes = Object.entries(VEGETATION_TYPES).filter(
|
||||
([, config]) => config.enabled,
|
||||
);
|
||||
|
||||
return (
|
||||
<group name="vegetation-system">
|
||||
{enabledTypes.map(([type, config]) => {
|
||||
const instances = data.get(type as VegetationType);
|
||||
|
||||
if (!instances || instances.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense key={type} fallback={null}>
|
||||
<InstancedVegetation
|
||||
modelPath={config.modelPath}
|
||||
instances={instances}
|
||||
castShadow={config.castShadow}
|
||||
receiveShadow={config.receiveShadow}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
})}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { MapNode } from "@/types/editor/editor";
|
||||
import type { Vector3Tuple } from "@/types/three/three";
|
||||
import { getMapNodes, loadMapSceneData } from "@/utils/map/loadMapSceneData";
|
||||
import {
|
||||
VEGETATION_MAX_INSTANCES,
|
||||
VEGETATION_TYPES,
|
||||
type VegetationType,
|
||||
} from "@/world/vegetation/vegetationConfig";
|
||||
|
||||
export interface VegetationInstance {
|
||||
position: Vector3Tuple;
|
||||
rotation: Vector3Tuple;
|
||||
scale: Vector3Tuple;
|
||||
}
|
||||
|
||||
export type VegetationData = Map<VegetationType, VegetationInstance[]>;
|
||||
|
||||
function mapNodeToInstance(node: MapNode): VegetationInstance {
|
||||
return {
|
||||
position: node.position,
|
||||
rotation: node.rotation,
|
||||
scale: node.scale,
|
||||
};
|
||||
}
|
||||
|
||||
function extractVegetationData(mapNodes: MapNode[]): VegetationData {
|
||||
const data: VegetationData = new Map();
|
||||
|
||||
for (const [type, config] of Object.entries(VEGETATION_TYPES)) {
|
||||
if (!config.enabled) continue;
|
||||
|
||||
const instances = mapNodes
|
||||
.filter((node) => node.name === config.mapName)
|
||||
.slice(0, VEGETATION_MAX_INSTANCES)
|
||||
.map(mapNodeToInstance);
|
||||
|
||||
if (instances.length > 0) {
|
||||
data.set(type as VegetationType, instances);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export function useVegetationData(): {
|
||||
data: VegetationData | null;
|
||||
isLoading: boolean;
|
||||
} {
|
||||
const [data, setData] = useState<VegetationData | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
async function load() {
|
||||
const cachedNodes = getMapNodes();
|
||||
|
||||
if (cachedNodes) {
|
||||
if (!cancelled) {
|
||||
setData(extractVegetationData(cachedNodes));
|
||||
setIsLoading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await loadMapSceneData();
|
||||
const nodes = getMapNodes();
|
||||
|
||||
if (!cancelled && nodes) {
|
||||
setData(extractVegetationData(nodes));
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { data, isLoading };
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
export const VEGETATION_LOD = {
|
||||
windAnimationRadius: 70,
|
||||
windFadeStart: 50,
|
||||
windFadeEnd: 70,
|
||||
};
|
||||
|
||||
export const VEGETATION_MAX_INSTANCES = 500;
|
||||
|
||||
export const VEGETATION_TYPES = {
|
||||
buissons: {
|
||||
mapName: "buissons",
|
||||
modelPath: "/models/buisson/model.gltf",
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
enabled: false,
|
||||
windEnabled: false,
|
||||
windIntensity: 1.2,
|
||||
},
|
||||
sapin: {
|
||||
mapName: "sapin",
|
||||
modelPath: "/models/sapin/model.gltf",
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
enabled: true,
|
||||
windEnabled: false,
|
||||
windIntensity: 0.6,
|
||||
},
|
||||
arbre: {
|
||||
mapName: "arbre",
|
||||
modelPath: "/models/arbre/model.gltf",
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
enabled: true,
|
||||
windEnabled: false,
|
||||
windIntensity: 0.8,
|
||||
},
|
||||
champdeble: {
|
||||
mapName: "champdeble",
|
||||
modelPath: "/models/champdeble/model.gltf",
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
enabled: false,
|
||||
windEnabled: false,
|
||||
windIntensity: 1.0,
|
||||
},
|
||||
champdesoja: {
|
||||
mapName: "champdesoja",
|
||||
modelPath: "/models/champdesoja/model.gltf",
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
enabled: false,
|
||||
windEnabled: false,
|
||||
windIntensity: 1.0,
|
||||
},
|
||||
champsdetournesol: {
|
||||
mapName: "champsdetournesol",
|
||||
modelPath: "/models/champsdetournesol/model.gltf",
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
enabled: false,
|
||||
windEnabled: false,
|
||||
windIntensity: 0.9,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type VegetationType = keyof typeof VEGETATION_TYPES;
|
||||
export type VegetationConfig = (typeof VEGETATION_TYPES)[VegetationType];
|
||||
Reference in New Issue
Block a user