mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-07 10:31:46 +00:00
Three paths in the 3D stage dropped objects without releasing what they held on the GPU: - Scene.clear() (New Scene) assigned `scene.children = []`, which disposed nothing and left each child parented to the scene. Every New Scene leaked all geometries, materials, textures and splat buffers of the previous one. - SceneUtils.deleteObject disposed geometries and materials but not the textures they reference (Material.dispose() does not free its maps), so deleting a textured GLB leaked every map. - _create_skybox replaced scene.background without disposing the previous cube texture. Add disposeObject3D(), which frees geometries, materials and their textures (deduplicated), skeleton bone textures and Spark splat buffers, and use it in clear() and deleteObject. clear() now uses Object3D.clear() so children are actually unparented. The old deleteObject also checked a custom `.texture` field that nothing in the codebase sets; that branch is dropped. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
35 lines
1.4 KiB
TypeScript
35 lines
1.4 KiB
TypeScript
import * as THREE from "three";
|
|
import { SplatMesh } from "@sparkjsdev/spark";
|
|
|
|
// Free the GPU resources held by an object tree that is leaving the scene
|
|
// for good: geometries, materials, the textures those materials reference
|
|
// (Material.dispose() does not free its maps), skeleton bone textures and
|
|
// Spark splat buffers. Call it after the tree has been removed.
|
|
//
|
|
// Geometry, material and texture disposal is safe even if one of them is
|
|
// shared with an object that stays — three.js re-uploads it the next time
|
|
// it renders. Splat buffers are not re-creatable, but every SplatMesh owns
|
|
// its own (scene.ts builds one per load), and undo re-loads deleted
|
|
// objects from their snapshot rather than reusing the instance.
|
|
export function disposeObject3D(root: THREE.Object3D): void {
|
|
const materials = new Set<THREE.Material>();
|
|
root.traverse((child) => {
|
|
if (child instanceof SplatMesh) child.dispose();
|
|
if (child instanceof THREE.SkinnedMesh) child.skeleton?.dispose();
|
|
|
|
const { geometry, material } = child as Partial<THREE.Mesh>;
|
|
geometry?.dispose();
|
|
const childMaterials = Array.isArray(material) ? material : [material];
|
|
for (const childMaterial of childMaterials) {
|
|
if (childMaterial) materials.add(childMaterial);
|
|
}
|
|
});
|
|
|
|
for (const material of materials) {
|
|
for (const value of Object.values(material)) {
|
|
if (value instanceof THREE.Texture) value.dispose();
|
|
}
|
|
material.dispose();
|
|
}
|
|
}
|