We continue to clean up memory with three.js

Introduction



I recently wrote about my experience of cleaning up memory in an application using three.js . Let me remind you that the goal was to redraw several scenes with loading gltf models.



Since then, I have carried out a number of experiments and I consider it necessary to supplement what I said earlier with this small article. Here are some points that helped me improve the performance of the application.



Main part



Studying various examples of garbage collection on three.js I was interested in the approach proposed on threejsfundamentals.org . However, after implementing the proposed configuration and wrapping all materials and geometry in this.track (), it turned out that the load on the GPU continues to grow when loading new scenes. Moreover, the proposed example does not work correctly with EffectComposer and other classes for post-processing, since track () cannot be used in these classes.



The solution with the addition of ResourceTracker to all the classes used is not attractive, for obvious reasons, so I decided to supplement the method for cleaning the mentioned class. Here are some of the techniques that have been used:



Reception 1. Rough



Add renderer.info after the cleanup method. We remove resources from the application one by one in order to understand which of them make up the load and are hidden in textures or materials. This is not a way to solve problems, but just a way to debug that someone might not know about.



Reception 2. Long



Having opened the code of the class used (for example, AfterimagePass, which can be found on the three.js github ), we look at where the resources are created that we need to clean up in order to maintain the number of geometries and materials within the required framework.



this.textureComp = new WebGLRenderTarget( window.innerWidth, window.innerHeight, { ... }


That's what you need. According to the documentation, WebGLRenderTarget has a dispose function to clean up memory. We get something like:



class Scene {
//...
    postprocessing_init(){ //   
        this.afterimagePass = new AfterimagePass(0);
        this.composer.addPass(this.afterimagePass);
    }
//...
}
//...

class ResourceTracker {
//...
    dispose() {
    //...
    sceneObject.afterimagePass.WebGLRenderTarget.dispose();
    //...
    }
}


Reception 3



Works, but the cleanup code gets bloated in this case. Let's try to use the approach familiar to us from the previous article. Let me remind you that in it we implemented the disposeNode (node) method, in which the resource was searched for what can be cleaned up. disposeNode () might look something like this:



disposeNode(node) {
            node.parent = undefined;
            if (node.geometry) {
                node.geometry.dispose();
            }
            let material = node.material;
            if (material) {
                if (material.map) {
                    material.map.dispose();
                }
                if (material.lightMap) {
                    material.lightMap.dispose();
                }
                if (material.bumpMap) {
                    material.bumpMap.dispose();
                }
                if (material.normalMap) {
                    material.normalMap.dispose();
                }
                if (material.specularMap) {
                    material.specularMap.dispose();
                }
                if (material.envMap) {
                    material.envMap.dispose();
                }
                material.dispose();
            }
        } else if (node.constructor.name === "Object3D") {
            node.parent.remove(node);
            node.parent = undefined;
        }
    }


Great, now let's take all the additional classes we used and add to our ResourceTracker:



dispose() {
    for (let key in sceneObject.afterimagePass) {
        this.disposeNode(sceneObject.afterimagePass[key]);
    }
    for (let key in sceneObject.bloomPass) {
        this.disposeNode(sceneObject.bloomPass[key]);
    }
    for (let key in sceneObject.composer) {
        this.disposeNode(sceneObject.composer[key]);
    }
}


Outcome



As a result of all these actions, I significantly increased the FPS and reduced the GPU load in my application. I may have used the ResourceTracker incorrectly, but it wouldnโ€™t help with additional classes anyway. I havenโ€™t seen anywhere that iterating over EffectComposer through our disposeNode (node) affects the number of textures in memory (but this is the case). This issue should be considered separately.



For comparison, the previous version will remain at the old address , while the new one can be viewed separately .



The project is in some form on the github .



I would be glad to hear your experience with similar projects and discuss the details!



All Articles