import type { Material, Mesh, Object3D, Skeleton, SkinnedMesh, Texture, } from "three"; export interface Object3DDisposalOptions { readonly geometries?: boolean; readonly materials?: boolean; readonly textures?: boolean; readonly skeletons?: boolean; } export interface Object3DDisposalCounts { readonly geometries: number; readonly materials: number; readonly textures: number; readonly skeletons: number; } function materialTextures(material: Material): readonly Texture[] { const textures = new Set(); for (const value of Object.values(material)) { if (value && typeof value === "object" && "isTexture" in value) { textures.add(value as Texture); } } const uniforms = (material as Material & { readonly uniforms?: Readonly>; }).uniforms; for (const uniform of Object.values(uniforms ?? {})) { const values = Array.isArray(uniform.value) ? uniform.value : [uniform.value]; for (const value of values) { if (value && typeof value === "object" && "isTexture" in value) { textures.add(value as Texture); } } } return [...textures]; } /** Disposes shared Three.js GPU resources exactly once per object graph. */ export function disposeObject3DResources( root: Object3D, options: Object3DDisposalOptions = {}, ): Object3DDisposalCounts { const disposeGeometries = options.geometries ?? true; const disposeMaterials = options.materials ?? true; const disposeTextures = options.textures ?? true; const disposeSkeletons = options.skeletons ?? true; const geometries = new Set(); const materials = new Set(); const textures = new Set(); const skeletons = new Set(); root.traverse((node) => { const mesh = node as Mesh; if (mesh.isMesh && disposeGeometries) geometries.add(mesh.geometry); if (mesh.isMesh && (disposeMaterials || disposeTextures)) { const meshMaterials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]; for (const material of meshMaterials) { if (disposeMaterials) materials.add(material); if (disposeTextures) { for (const texture of materialTextures(material)) textures.add(texture); } } } const skinnedMesh = node as SkinnedMesh; if (skinnedMesh.isSkinnedMesh && disposeSkeletons) skeletons.add(skinnedMesh.skeleton); }); for (const skeleton of skeletons) skeleton.dispose(); for (const material of materials) material.dispose(); for (const geometry of geometries) geometry.dispose(); for (const texture of textures) texture.dispose(); return { geometries: geometries.size, materials: materials.size, textures: textures.size, skeletons: skeletons.size, }; }