Three.js dispose things so hard

I have created a scene, then added many GLTF models, each model has children, recursively.

How to dispose the scene, and all the models, and models’ children models?

I tried this:

this.#scene.traverse(e => {
  if (e instanceof THREE.Mesh) {
    e.geometry.dispose()
    e.material.dispose()
  }
})

// 1. models not removed, why?
this.#scene.children.forEach(e => this.#scene.remove(e))

// 2. models removed, but memory don't reduce.
this.rootEntity?.group?.removeFromParent()

memory don’t reduce at all.

Do I need to call removeFromParent() in every model?

removeFromParent only sets the parent property of your Object3d to null.
You can console.log to verify this.
This is not the same as disposing or deleting.

Read this page
How to dispose of objects

1 Like

I have created function to dispose all children from parent. may that can help you

function removeObjWithChildren(obj) {
    if (obj.children.length > 0) {
        for (var x = obj.children.length - 1; x >= 0; x--) {
            removeObjWithChildren(obj.children[x])
        }
    }
    if (obj.isMesh) {
        obj.geometry.dispose();
        obj.material.dispose();
    }
    if (obj.parent) {
        obj.parent.remove(obj)

    }
}
1 Like

Shouldn’t the OP’s code do much the same thing?

Note, though, that this won’t dispose of any textures… so you might want to do something like loop over material properties and call dispose if it exists (this is a vague suggestion maybe not the best approach to begin with, and of course if there’s any sharing of these resources that changes things a lot).

1 Like

Also, don’t forget the case where the .material property can also be an array of materials which can have multiple textures :smile:

1 Like

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)

3 Likes

Three.js Cleanup helps me.

2 Likes

Potential issue with this is if you use a non-standard material. I think I’d rather reflect on the properties, like in the Cleanup linked, rather than have a hard-coded list of property names from standard material:

// We also have to check if any uniforms reference textures or arrays of textures
      if (resource.uniforms) {
        for (const value of Object.values(resource.uniforms)) {
          if (value) {
            const uniformValue = value.value;
            if (uniformValue instanceof THREE.Texture ||
                Array.isArray(uniformValue)) {
              this.track(uniformValue);
            }
          }
        }
1 Like

Thanks for pointing that out, somehow it completely went over my head, I guess cause I wasn’t using ShaderMaterials at the time, and yes, iterating over the Object.values is better than using a hard-coded list, another great suggestion, so thanks again.

Here is the updated code, also removed the disposeMediaElement method for the sake of simplicity.

import { Object3D, BufferGeometry, Material, Texture, ShaderMaterial } from "three"

/**
 * 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 deepDispose(
  object: Object3D | BufferGeometry | Material | Texture
) {
  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, dispose);
  };

  if (object instanceof BufferGeometry || object instanceof Texture)
    return dispose(object);

  if (object instanceof Material)
    return traverseMaterialsTextures(object, dispose, dispose);

  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 = (mat: Material) => {
    if (materialCallback) materialCallback(mat);

    if(!textureCallback) return;

    Object.values(mat)
      .filter((value) => value instanceof Texture)
      .forEach((texture) => textureCallback(texture)
    );

    if((mat as ShaderMaterial).uniforms)
      Object.values((mat as ShaderMaterial).uniforms)
        .filter(({ value }) => value instanceof Texture)
        .forEach(({ value }) => textureCallback(value))
  };

  if (Array.isArray(material)) {
    material.forEach((mat) => traverseMaterial(mat));
  } else traverseMaterial(material);
}

export { deepDispose }
1 Like

try this dispose function

It’s the kind of thing it’s very easy to not think about until some months later when you have some obscure bug you’re trying to track down, that you didn’t spot until you suddenly need your code to run for days on end in an exhibition or something…

2 Likes

i totally agree with u xinaes,

Hi, I have tried your code to dispose my scene, but the memory didn’t reduce, did I use the code in the wrong way?

dispose() {
  console.log(this.#renderer?.info, 'info')
  try {
    // this.#resTracker.dispose()
    
    // HERE
    hardDispose(this.#scene)

    // OR
    // deepDispose(this.#scene)
    
    this.#scene.clear()
    this.#renderer!.dispose()
    this.#renderer!.forceContextLoss()
    if (this.#animationFrameId) {
      cancelAnimationFrame(this.#animationFrameId)
    }
    const gl = this.#renderer?.domElement.getContext('webgl')
    gl?.getExtension('WEBGL_lose_context')?.loseContext()
  } catch (error) {
    console.log(error)
  }
}

Try with the updated version, also double check if you are keeping any reference to the loaded GLTF Models elsewhere in your app, you need to remove any variable pointing to any removed object.

You said right, I removed all objects, it works!

this.rootEntity?.removeFromParentObject()
this.rootEntity = undefined
1 Like

Just delete all references.