Correctly remove mesh from scene and dispose material and geometry

Let’s say I create thousands of meshes like:
pseudo code

for 10000 IDs in array{
    var cellgeom = createCellGeom( hex[ID] );
    var cellmaterial = createCellMaterial( color[ID] );
    var cellmesh = new THREE.Mesh( cellgeom, cellmaterial );
    cellmesh.name = ID;
    somearray.push(cellmesh.uuid);
    scene.add(cellmesh);
    };

Now, I have an somearray of uuids and at some point in time I want to remove these meshes, geometries and materials from the scene. Do I have to .dispose() the geometry and material first and then remove the mesh from the scene?

I came up with the below function, but disposing geometry and material like this does not work, i.e. they are still in memory.

function removeCells( ) {
    somearray.map( ( i ) => {
        scene.getObjectByProperty( 'uuid', i ).geometry.dispose( );
        scene.getObjectByProperty( 'uuid', i ).material.dispose( );
        scene.remove( scene.getObjectByProperty( 'uuid', i ) );
	} );
};

What would be the correct procedure?

Execute this code once and not three times in a row. This will save some overhead. Do it like so:

const object = scene.getObjectByProperty( 'uuid', i );

object.geometry.dispose();
object.material.dispose();
scene.remove( object );

The order of the last three statements does actually not matter.

Does the memory leak disappear if you execute the following code after removing the cells?

renderer.renderLists.dispose();
6 Likes

Fantastic, this keeps my memory clean at about 100MB. Thanks a lot!

1 Like

There is actually an issue for this problem since three.js keeps references to objects in THREE.WebGLRenderLists even if they are already disposed. Such a behavior is obviously not correct.

2 Likes

Thanks for the info, I was not aware of this bug.

Hi Guys – I have been away from threejs for many months with regular 3D gigs… So do I understand this correct, that three is not currently disposing of stuff (mesh, materials, maps) correctly in the newer versions? I have a couple configurators I’ve made that used R86. These are full PBR MeshStandardMaterial with dozens and sometimes hundreds of instances of things.They have always seemed to do a great job disposing everything and had them crash-free, even on ipad3. I will be needing to update these apps with new products soon-ish, and if I go with latest threejs, will I be running into memory problems that weren’t there in R86?..

There is already a PR that fixes the issue but it was not merged yet: https://github.com/mrdoob/three.js/pull/12464

I think it’s best when you upvote it at github :wink:

THANK YOU, this just provided 20fps gain on my game!!!

Edit: On a serious not this is a huge problem in ThreeJS. M


y goodness it has taken so much time to learn ThreeJS and all the little quirks that it has, but daaaamn man, look at the results in my MMO!

UPDATE: At first it wasn’t working super well, but then I realized I had other arrays that were holding data from the objects that would be deleted. Updated my code and now I’m running at nearly 60fps on my GARBAGE computer, my iPhone looks like pure butter at 60fps at all times. I cannot believe it.

Ehhh still holding objects, if I count how many there are… it gets crazy, see below

Ah, now I can see I am storing data about objects in an array (engineTrails) which I don’t clear the array on, so not a ThreeJS problem at this time! Just need to clean up the code I built FROM ThreeJS to do animation stuff with.

Will leave this here for someone else in the future (possibly even me…)

if(trail.material.opacity <= -1.00){
						scene.remove(trail);
						disposeHierarchy(trail);
						window.engineTrails = window.engineTrails.slice(0,trail_index).concat(window.engineTrails.slice(trail_index+1));
					}

Can you please tell me how you compute three_object_counter?
I need to count the number of three objects.
Is there a direct three js API call to do so?

Yeah this was a real trick!

So what I ended up doing was for each type of ‘object’ I have, Starships, Asteroids, Planets, etc – I would group them in their own individual array.

starships_array.push(three_object);

Then in my render() function, I did foreach starships_array as starship.

At the beginning of the render() loop I just created an index called three_object_counter = 0;

Now for each starship I increase three_object_counter += 1;

But I also repeat this for any other type of object I created like asteroids, planets, etc.

In this way, I now have the COMPLETE total of objects in memory, which I can Three.GetByName() and move their sprites around and do whatever.

Happy to chat about techniques :slight_smile: Cheers

Oh, and I also ended up using this function to make sure things are deleted properly, can’t remember where I found it but it’s epic.

Now with proper object cleaning and proper sized materials (1024x1024) my game is running hella-smooth 60FPS+

// DISPOSE NODE FUNCTION
// What's worse, having your game hit 0fps because it keeps all the materials in memory, or having it run 30+ and being a little less 'optimized'?
// Well we're gonna just make it work by clearing memory from ThreeJS, which it doesn't do automatically
// WHY? Well because in larger applications we actually want to cache materials and such, they're expensive to load (apparently)
// But this function will DESTROY stuff forever, which serves the purpose for now

// Note: Execute not with disposeNode, but with disposeHierarchy
function disposeNode (node)
{
    if (node instanceof THREE.Mesh)
    {
        if (node.geometry)
        {
            node.geometry.dispose ();
        }

        if (node.material)
        {
            if (node.material instanceof THREE.MeshFaceMaterial)
            {
                $.each (node.material.materials, function (idx, mtrl)
                {
                    if (mtrl.map)               mtrl.map.dispose ();
                    if (mtrl.lightMap)          mtrl.lightMap.dispose ();
                    if (mtrl.bumpMap)           mtrl.bumpMap.dispose ();
                    if (mtrl.normalMap)         mtrl.normalMap.dispose ();
                    if (mtrl.specularMap)       mtrl.specularMap.dispose ();
                    if (mtrl.envMap)            mtrl.envMap.dispose ();
                    if (mtrl.alphaMap)          mtrl.alphaMap.dispose();
                    if (mtrl.aoMap)             mtrl.aoMap.dispose();
                    if (mtrl.displacementMap)   mtrl.displacementMap.dispose();
                    if (mtrl.emissiveMap)       mtrl.emissiveMap.dispose();
                    if (mtrl.gradientMap)       mtrl.gradientMap.dispose();
                    if (mtrl.metalnessMap)      mtrl.metalnessMap.dispose();
                    if (mtrl.roughnessMap)      mtrl.roughnessMap.dispose();

                    mtrl.dispose ();    // disposes any programs associated with the material
                });
            }
            else
            {
                if (node.material.map)              node.material.map.dispose ();
                if (node.material.lightMap)         node.material.lightMap.dispose ();
                if (node.material.bumpMap)          node.material.bumpMap.dispose ();
                if (node.material.normalMap)        node.material.normalMap.dispose ();
                if (node.material.specularMap)      node.material.specularMap.dispose ();
                if (node.material.envMap)           node.material.envMap.dispose ();
                if (node.material.alphaMap)         node.material.alphaMap.dispose();
                if (node.material.aoMap)            node.material.aoMap.dispose();
                if (node.material.displacementMap)  node.material.displacementMap.dispose();
                if (node.material.emissiveMap)      node.material.emissiveMap.dispose();
                if (node.material.gradientMap)      node.material.gradientMap.dispose();
                if (node.material.metalnessMap)     node.material.metalnessMap.dispose();
                if (node.material.roughnessMap)     node.material.roughnessMap.dispose();

                node.material.dispose ();   // disposes any programs associated with the material
            }
        }
    }
}   // disposeNode
function disposeHierarchy (node, callback)
{
    for (var i = node.children.length - 1; i >= 0; i--)
    {
        var child = node.children[i];
        disposeHierarchy (child, callback);
        callback (child);
    }
}
2 Likes

This is insane. I wonder if you could just crawl the hierarchy recursively, depth-first, and find everything with a dispose function then call it.

I wonder if this would be non-performant

function HierarchyDisposal (showLogging = false) {
  const bag = []
  const ignoreKeys = [
    '_super',
    'parent'
  ]

  const dispose = (obj, ondone) => {
    const id = obj.uuid
    const name = obj.name || id || obj
    if (bag.includes(id)) return
    if (showLogging) console.log('calling dispose on', name)

    for (var key in obj) {
      const childObj = obj[key]
      if (
        obj.hasOwnProperty(key) &&
        !ignoreKeys.includes(key) && 
        childObj && 
        typeof childObj === 'object'
      ) dispose(childObj)
    }
    if (typeof obj.dispose === 'function' && obj.uuid) {
      if (showLogging) console.log('disposing of', name)
      obj.dispose()
      bag.push(id)
    }
    if (typeof ondone === 'function') ondone(bag)
  }
  
  return dispose
}

// usage
const trash = HierarchyDisposal(true)

trash([some Object3D])

I have some questions regarding the implementation:

  • In your comments I see Execute not with disposeNode, but with disposeHierarchy.
    Is disposeNode called at all (indirectly?) or could it be completely removed?
    I don’t see it being called anywhere…

  • The disposeHierarchy signature has a callback but in the actual call disposeHierarchy(trail); I don’t any function passed for the callback.

  • when you get to the final leaf where do you actually dispose things?

Thanks

1 Like

This all being said, some times I do have some leftover sprites left on my screen that don’t get deleted from the Canvas, but are removed from the array.

Let me consider your suggestions and get back to you on it.

PS you can see my demo at:

https://mmodev.solarwarheroes.com/
Login with:
demo / demo

Just seeing this. Try setting the invisibility on your sprites before you delete them. This is funny because I did nearly the same thing. I use a custom approach to tagging all my geometries at time of creation and then marking them for cleanup if they aren’t needed anymore. I used a similar crawler for the dispose() at the end of my session. But I went about this slightly differently. I wanted to know how many triangles I was rendering when I had different layers of instancedMesh(s) visible/invisible. I am instancing but there is still a limit on the number of triangles you can render even if you manage to find ways to reduce vertice counts and merge everything into one, or a few, ‘draw.calls’. In order to know a rough estimate of this limit (per layer) I had to count the triangles in my visible layers. I ran each of my geometries through a similar method of having them increment a counter, but I did this per layer, since the user can control the number of instances visible in the sceneGraph. Once I had them all, and was counting a fairly accurate number of the triangles, I also had a collection of all that needed to be disposed.

At the end of the day its how many triangles the GPU you are rendering on can handle at 60FPS. Knowing the different GPUs limits is the 1st problem. Knowing if you are reaching that limit is the 2nd. I do get some nice metrics per layer of instances though…

Real Nice metrics.

1 Like

Are you using a Chrome extension to get those metrics?

No extensions.