This may be an overkill, but I made this a while ago for an undergoing project. It basically dispose of all nested geometry
, material
/array of material
, and texture
, including removing and pausing any image/video/stream elements.
It is typescript
, if you are using javascript
you may need to do some clean up.
import { Object3D, BufferGeometry, Material, Texture } from "three"
const MATERIAL_TEXTURE_MAPPING = [
"map",
"clearcoatMap",
"clearcoatRoughnessMap",
"clearcoatNormalMap",
"matcap",
"alphaMap",
"lightMap",
"aoMap",
"bumpMap",
"normalMap",
"displacementMap",
"roughnessMap",
"metalnessMap",
"emissiveMap",
"specularMap",
"envMap",
"gradientMap",
];
/**
* Dispose of all Object3D`s nested Geometries, Materials and Textures
*
* @param object Object3D, BufferGeometry, Material or Texture
* @param disposeMedia If set to true will dispose of the texture image or video element, default false
*/
function hardDispose(
object: Object3D | BufferGeometry | Material | Texture,
disposeMedia: boolean = false
) {
const dispose = (object: BufferGeometry | Material | Texture) =>
object.dispose();
;
const disposeObject = (object: any) => {
if (object.geometry) dispose(object.geometry);
if (object.material)
traverseMaterialsTextures(object.material, dispose, disposeTexture);
};
const disposeTexture = !disposeMedia
? dispose
: (texture: Texture) => {
dispose(texture);
if (texture.image) disposeMediaElement(texture.image, true);
};
if (object instanceof BufferGeometry || object instanceof Texture)
return dispose(object);
if (object instanceof Material)
return traverseMaterialsTextures(object, dispose, disposeTexture);
disposeObject(object);
if (object.traverse) object.traverse((obj) => disposeObject(obj));
}
/**
* Traverse material or array of materials and all nested textures
* executing there respective callback
*
* @param material Three js Material or array of material
* @param materialCallback Material callback
* @param textureCallback Texture callback
*/
function traverseMaterialsTextures(
material: Material | Material[],
materialCallback?: (material: any) => void,
textureCallback?: (texture: any) => void
) {
const traverseMaterial = (subMat: any) => {
if (textureCallback)
MATERIAL_TEXTURE_MAPPING.forEach((texture) => {
if (subMat[texture]) textureCallback(subMat[texture]);
});
if (materialCallback) materialCallback(subMat);
};
if (Array.isArray(material) && material.length) {
material.forEach((subMat) => traverseMaterial(subMat));
} else traverseMaterial(material);
}
/**
* Dispose of ImageBitmap, clean and remove HTMLElement, revoke ObjectUrl source if any,
* pause and clear video and audio elements, stop all MediaStream tracks if any.
*
* @param media AssetElement
* @param force Force remove the HTMLElement if implemented elsewhere in the dom
*/
export function disposeMediaElement(
media:
| HTMLCanvasElement
| HTMLImageElement
| HTMLVideoElement
| HTMLAudioElement
| ImageBitmap,
force: boolean = false
) {
if (media instanceof ImageBitmap) return media.close();
// If the element is implemented elsewhere in the dom, set "force" to true to remove it
if (media.parentElement && !force) {
if (media instanceof HTMLMediaElement) media.pause();
return;
}
if (media instanceof HTMLImageElement || media instanceof HTMLCanvasElement) return media.remove();
media.pause();
if (media.srcObject) {
// Stop all MediaStream tracks (User camera, microphone ...)
if (media.srcObject instanceof MediaStream)
media.srcObject.getTracks().forEach((track) => track.stop());
media.srcObject = null;
}
if (media.src) {
if (media.src.startsWith("blob:")) URL.revokeObjectURL(media.src);
media.src = "";
}
media.remove();
}
One last note, removing every nested object is irrelevant, you only need to remove the root object, don’t keep any references to it, or any to its nested children or components and the garbage collector will automatically dispose of all of them. (console.logging an object will keep a reference to the said object)