diff --git a/src/world/vegetation/InstancedVegetation.tsx b/src/world/vegetation/InstancedVegetation.tsx new file mode 100644 index 0000000..af968c7 --- /dev/null +++ b/src/world/vegetation/InstancedVegetation.tsx @@ -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(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 ; +} diff --git a/src/world/vegetation/VegetationSystem.tsx b/src/world/vegetation/VegetationSystem.tsx new file mode 100644 index 0000000..f5bb962 --- /dev/null +++ b/src/world/vegetation/VegetationSystem.tsx @@ -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 ( + + {enabledTypes.map(([type, config]) => { + const instances = data.get(type as VegetationType); + + if (!instances || instances.length === 0) { + return null; + } + + return ( + + + + ); + })} + + ); +} diff --git a/src/world/vegetation/useVegetationData.ts b/src/world/vegetation/useVegetationData.ts new file mode 100644 index 0000000..3a81441 --- /dev/null +++ b/src/world/vegetation/useVegetationData.ts @@ -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; + +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(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 }; +} diff --git a/src/world/vegetation/vegetationConfig.ts b/src/world/vegetation/vegetationConfig.ts new file mode 100644 index 0000000..52011e8 --- /dev/null +++ b/src/world/vegetation/vegetationConfig.ts @@ -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];