Memory Leak With New Scenes

Hi, I am using react with threejs and not using react-fiber-three for some reason. I put threejs render code inside useEffect. I render 6 scenes and since they can change depending on the user’s data dynamically, i put them in state variables using useState hook. It seems whenever new data comes in and i change state variables using something, even dispose the objects before doing it, the old variables are still living inside GPU. Is it ok to store scenes inside state variables or is there any way to clear renderer without deleting or modifying scenes? If there is a way to clear renderer without touching scenes it will be best for our application. Since we are updating scenes everytime user do something (btw the upcoming data when user does someting is fetched from backend so we don’t store the new data somewhere till response comes in from backend)

That should not be the case. If you traverse through your scene and call dispose() on any geometry, material, texture and render targets, the respective GPU data should be deallocated. Assuming you are no longer perform a render with the scene. If so, GPU entities like buffers or shader programs would be created from scratch by the engine.

I guess you have to demonstrate the memory leak with a (minimal) live example so it’s possible to investigate the issue. Considering you are correctly use dispose(), the memory leak probably happens in your JavaScript code (such that the GC can’t do its work).

1 Like

Hello @Mugen87 thanks for your reply. Yea i try to dispose all the materials, textures and geometries. Here is the whole code related to threejs:

const [styleOuterScene, setStyleOuterScene] = useState(getInitialScene());
  const [styleUpperScene, setStyleUpperScene] = useState(getInitialScene());
  const [styleLowerScene, setStyleLowerScene] = useState(getInitialScene());
  const [styleBodyScene, setStyleBodyScene] = useState(getInitialScene());
  const [stressOuterScene, setStressOuterScene] = useState(getInitialScene());
  const [stressUpperScene, setStressUpperScene] = useState(getInitialScene());
  const [stressLowerScene, setStressLowerScene] = useState(getInitialScene());
  const [stressBodyScene, setStressBodyScene] = useState(getInitialScene());

 
  function getInitialScene(isStyleView) {
    const scene = new THREE.Scene();
    return scene;
  }

  

  function deleteGarment(garment_type) {
    if (currentPossibleGarmentTypes.includes(garment_type)) {
      setLocalStorage(garment_type, '')
      const filtered = currentPossibleGarmentTypes.filter(function (
        value,
        index,
        arr
      ) {
        return value != garment_type
      })
      setCurrentPossibleGarmentTypes(filtered)
      //setDataFetched(false)
    }
  }

  const loadGarments = (scene, data, isStyleView, key) => {
    var texture_keys = ["map", "normalMap", "roughnessMap", "displacementMap", "aoMap", "metalnessMap"]
    var backend_texture_keys = ["baseColor", "normal", "roughness", "displacement", "occlusion", "metallic"]
    var material_keys = {};
    var material = "";
    if (data.hasOwnProperty(key)) {
      loader.load(
        // resource URL
        isStyleView || data[key + '_name'].indexOf('_exported') === -1
          ? 'https://gorjhkeordksair.cloudfront.net/' + data[key + '_name']
          : 'https://gorjhkeordksair.cloudfront.net/' +
          data[key + '_name'].slice(0, -13) +
          '_exported_stress.obj',
        // called when resource is loaded
        function (object) {
          console.log(
            'texture:',
            'https://gorjhkeordksair.cloudfront.net/' +
            data[key]['baseColor'].split('/').pop()
          )
          if (isStyleView) {

            for (let texture_id = 0; texture_id < texture_keys.length; texture_id++) {
              let backend_key = backend_texture_keys[texture_id];
              let texture_key = texture_keys[texture_id];


              if (data[key].hasOwnProperty(backend_key)) {
                if (data[key][backend_key] != "NO") {
                  let pbr_texture = textureLoader.load(
                    'https://gorjhkeordksair.cloudfront.net/' +
                    data[key][backend_key].split('/').pop()
                  )
                  material_keys[texture_key] = pbr_texture;
                }

              }
            }


            material = new THREE.MeshStandardMaterial(material_keys)
          } else {
            let colorTexture = textureLoader.load(
              'https://gorjhkeordksair.cloudfront.net/stressTexture.jpg'
            )
            material = new THREE.MeshStandardMaterial({
              map: colorTexture,
              opacity: 1.0,
              side: THREE.FrontSide
            })
          }


          object.traverse(function (node) {
            if (node.isMesh) {
              node.material = material
              //console.log("renderOrders.key:",renderOrders[key],renderOrders["upper"],renderOrders["lower"])
              //node.onBeforeRender = function( renderer ) { renderer.clearDepth(); };
            }
          })
          //object.renderOrder=renderOrders[key]
          //object.onBeforeRender = function( renderer ) { renderer.clearDepth(); };
          scene.add(object)
        },
        // called when loading is in progresses
        function (xhr) {
          console.log(
            'Garments:' + (xhr.loaded / xhr.total) * 100 + '% loaded'
          )
        },
        // called when loading has errors
        function (error) {
          console.log(error)
          console.log('An error happened')
        }
      )
    }

    return scene
  }

  const loadHair = (scene, data) => {
    if (data.hasOwnProperty('hair')) {
      // load a resource
      loader.load(
        // resource URL
        'https://gorjhkeordksair.cloudfront.net/' + data['hair'],
        // called when resource is loaded
        function (object) {
          let colorTexture = textureLoader.load(
            'https://gorjhkeordksair.cloudfront.net/' + data['hair_baseColor']
          )
          let normalTexture = textureLoader.load(
            'https://gorjhkeordksair.cloudfront.net/' + data['hair_normal']
          )
          let material = new THREE.MeshStandardMaterial({
            map: colorTexture,
            normalMap: normalTexture,
            opacity: 0.5
          })
          //material.depthTest=false;

          object.traverse(function (node) {
            if (node.isMesh) {
              node.material = material
              console.log('hair.name:', node.name)
              //node.onBeforeRender = function( renderer ) { renderer.clearDepth(); };
            }
          })
          //object. rder=1;
          //object.onBeforeRender = function( renderer ) { renderer.clearDepth(); };
          scene.add(object)
        },
        // called when loading is in progresses
        function (xhr) {
          console.log((xhr.loaded / xhr.total) * 100 + '% loaded')
        },
        // called when loading has errors
        function (error) {
          console.log('An error happened' + error)
        }
      )
    }
    return scene
  }

  const loadBody = (scene, data) => {

    if (data.hasOwnProperty('body_path')) {
      // load a resource
      console.log("data['body_path']:" + data['body_path'])
      loader.load(
        // resource URL
        'https://gorjhkeordksair.cloudfront.net/' + data['body_path'],
        // called when resource is loaded
        function (object) {
          let colorTexture = textureLoader.load(
            'https://gorjhkeordksair.cloudfront.net/' + data['texture']
          )
          let material = new THREE.MeshStandardMaterial({ map: colorTexture })
          //material.depthTest=false;

          object.traverse(function (node) {
            if (node.isMesh) {
              node.material = material
              console.log('node.name:', node.name)
              //node.onBeforeRender = function( renderer ) { renderer.clearDepth(); };
            }
          })
          //object.renderOrder=1;
          //object.onBeforeRender = function( renderer ) { renderer.clearDepth(); };
          scene.add(object)
        },
        // called when loading is in progresses
        function (xhr) {
          console.log((xhr.loaded / xhr.total) * 100 + '% loaded')
        },
        // called when loading has errors
        function (error) {
          console.log('An error happened' + error)
        }
      )
    }

    return scene
  }

  const addLights = (scene) => {
    const skyColor = 0xffffff // light blue
    const groundColor = 0xffffff // brownish orange
    var intensity = 1.0
    const hemisphereLight = new THREE.HemisphereLight(
      skyColor,
      groundColor,
      intensity
    )
    scene.add(hemisphereLight)

    const color = 0xffffff
    intensity = 0.1
    const directionalLight = new THREE.DirectionalLight(color, intensity)
    directionalLight.position.set(0, 10, 5)
    scene.add(directionalLight)

    const reversDirectionalLight = new THREE.DirectionalLight(color, intensity)
    reversDirectionalLight.position.set(0, 10, -5)
    scene.add(reversDirectionalLight)

    return scene
  }

  function clearThree(obj){
    while(obj.children.length > 0){ 
      clearThree(obj.children[0]);
      obj.remove(obj.children[0]);
    }
    if(obj.geometry) obj.geometry.dispose();
  
    if(obj.material){ 
      //in case of map, bumpMap, normalMap, envMap ...
      Object.keys(obj.material).forEach(prop => {
        if(!obj.material[prop])
          return;
        if(obj.material[prop] !== null && typeof obj.material[prop].dispose === 'function')                                  
          obj.material[prop].dispose();                                                      
      })
      obj.material.dispose();
    }
  }   

  function clearThree(obj){
    while(obj.children.length > 0){ 
      clearThree(obj.children[0]);
      obj.remove(obj.children[0]);
    }
    if(obj.geometry) obj.geometry.dispose();
  
    if(obj.material){ 
      //in case of map, bumpMap, normalMap, envMap ...
      Object.keys(obj.material).forEach(prop => {
        if(!obj.material[prop])
          return;
        if(obj.material[prop] !== null && typeof obj.material[prop].dispose === 'function')                                  
          obj.material[prop].dispose();                                                      
      })
      obj.material.dispose();
    }
  } 
  


    useEffect(
      () => {
        console.log("vt-canvas-style vt-canvas-stylevt-canvas-stylevt-canvas-stylevt-canvas-style")

        const vtCanvas = document.getElementById("vt-canvas-style");

        let tempOuterScene = new THREE.Scene();
        let tempUpperScene = new THREE.Scene();
        let tempBodyScene = new THREE.Scene();
        let tempLowerScene = new THREE.Scene();

        let camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10);
        let renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setPixelRatio(window.devicePixelRatio)
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setClearColor(new THREE.Color(0.95, 0.95, 0.95), 1);
        
        const controls = new OrbitControls(camera, renderer.domElement);
        controls.minDistance = 0.3;
        controls.maxDistance = 5;
        controls.enableDamping = true;
        controls.dampingFactor = 0.05;
        controls.minPolarAngle = Math.PI/2; // radians
        controls.maxPolarAngle = Math.PI/2; // radians
        //style_renderer.dispose();
        //stress_renderer.dispose();
        if (vtCanvas.childNodes.length > 0){
          //renderer.dispose();
          console.log("vtCanvas.childNodes.length:",vtCanvas.childNodes.length);
          vtCanvas.removeChild(vtCanvas.childNodes[0])
        }
        vtCanvas.appendChild(renderer.domElement);
        console.log("################### SCENE IS ENTERED");
        if (!styleDataFetched) {
          clearThree(styleBodyScene);
          clearThree(styleUpperScene);
          clearThree(styleLowerScene);
          clearThree(styleOuterScene);
          setStyleBodyScene(undefined);
          setStyleUpperScene(undefined);
          setStyleLowerScene(undefined);
          setStyleOuterScene(undefined);

          //renderer.dispose()
 
           

          tempOuterScene = loadGarments(tempOuterScene, subData, true, "outer");
          tempOuterScene = addLights(tempOuterScene);

          tempUpperScene = loadGarments(tempUpperScene, subData, true, "upper");
          tempUpperScene = addLights(tempUpperScene);

          tempBodyScene = loadBody(tempBodyScene, subData);
          tempBodyScene = loadHair(tempBodyScene, subData);
          tempBodyScene = addLights(tempBodyScene);

          tempLowerScene = loadGarments(tempLowerScene, subData, true, "lower");
          tempLowerScene = addLights(tempLowerScene);

          setStyleOuterScene(tempOuterScene);
          setStyleUpperScene(tempUpperScene);
          setStyleLowerScene(tempLowerScene);
          setStyleBodyScene(tempBodyScene);

          setStyleDataFetched(true)
          console.log("################### DATA FETCHED")

        }


        const garment_scenes = {
          "outer": styleOuterScene,
          "upper": styleUpperScene,
          "lower": styleLowerScene
        };


        let scenes = [styleBodyScene];

        for (let i = 0; i < currentPossibleGarmentTypes.length; i++) {
          scenes.push(garment_scenes[currentPossibleGarmentTypes[i]]);
        }



        console.log("scenes:", scenes);
        console.log("currentPossibleGarmentTypes:", currentPossibleGarmentTypes);
        camera.position.z = 2;

        controls.target.set(0, 0, 0);
        
        var animate = function () {
          controls.update()
          requestAnimationFrame(animate);
          //renderer.render( scene1, camera );
          renderer.render(scenes[0], camera);

          for (let i = 1; i < scenes.length; i++) {
            renderer.autoClear = false;
            renderer.clearDepth();
            renderer.render(scenes[i], camera);
          }

          renderer.autoClear = true;

        };
        animate();
        // === THREE.JS EXAMPLE CODE END ===
      }
      , [styleDataFetched,currentPossibleGarmentTypes]);

    const cleanMaterial = material => {
      console.log('dispose material!')
      material.dispose()

      // dispose textures
      for (const key of Object.keys(material)) {
        const value = material[key]
        if (value && typeof value === 'object' && 'minFilter' in value) {
          console.log('dispose texture!')
          value.dispose()
        }
      }
    }


    

    useEffect(
      () => {
        
        console.log("vt-canvas-stress vt-canvas-stress vt-canvas-stressvt-canvas-stressvt-canvas-stress")
        const vtCanvas = document.getElementById("vt-canvas-stress");

        let tempOuterScene = new THREE.Scene();
        let tempUpperScene = new THREE.Scene();
        let tempBodyScene = new THREE.Scene();
        let tempLowerScene = new THREE.Scene();

        let camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10);
        let renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setPixelRatio(window.devicePixelRatio)
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setClearColor(new THREE.Color(0.95, 0.95, 0.95), 1);
        const controls = new OrbitControls(camera, renderer.domElement);
        controls.minDistance = 0.3;
        controls.maxDistance = 5;
        controls.enableDamping = true;
        controls.dampingFactor = 0.05;
        controls.minPolarAngle = Math.PI/2; // radians
        controls.maxPolarAngle = Math.PI/2; // radians
        //style_renderer.dispose();
        //stress_renderer.dispose();
        
        if (vtCanvas.childNodes.length > 0){
          renderer.dispose();
          console.log("vtCanvas.childNodes.length:",vtCanvas.childNodes.length);
          vtCanvas.removeChild(vtCanvas.childNodes[0])
        }
        vtCanvas.appendChild(renderer.domElement);
        
        console.log("################### SCENE IS ENTERED");
        if (!stressDataFetched) {

          clearThree(stressBodyScene);
          clearThree(stressUpperScene);
          clearThree(stressLowerScene);
          clearThree(stressOuterScene);

          setStressBodyScene(undefined);
          setStressUpperScene(undefined);
          setStressLowerScene(undefined);
          setStressOuterScene(undefined);

          console.log('dispose renderer!')
          renderer.dispose()



          tempOuterScene = loadGarments(tempOuterScene, subData, false, "outer");
          tempOuterScene = addLights(tempOuterScene);

          tempUpperScene = loadGarments(tempUpperScene, subData, false, "upper");
          tempUpperScene = addLights(tempUpperScene);

          tempBodyScene = loadBody(tempBodyScene, subData);
          tempBodyScene = loadHair(tempBodyScene, subData);
          tempBodyScene = addLights(tempBodyScene)
          tempLowerScene = loadGarments(tempLowerScene, subData, false, "lower");
          tempLowerScene = addLights(tempLowerScene);

          setStressOuterScene(tempOuterScene);
          setStressUpperScene(tempUpperScene);
          setStressLowerScene(tempLowerScene);
          setStressBodyScene(tempBodyScene);

          setStressDataFetched(true)
          console.log("################### DATA FETCHED")

        }


        const garment_scenes = {
          "outer": stressOuterScene,
          "upper": stressUpperScene,
          "lower": stressLowerScene
        };


        let scenes = [stressBodyScene];

        for (let i = 0; i < currentPossibleGarmentTypes.length; i++) {
          scenes.push(garment_scenes[currentPossibleGarmentTypes[i]]);
        }



        console.log("scenes:", scenes);
        console.log("currentPossibleGarmentTypes:", currentPossibleGarmentTypes);
        camera.position.z = 2;

        controls.target.set(0, 0, 0);
        var animate = function () {
          controls.update()
          requestAnimationFrame(animate);
          //renderer.render( scene1, camera );
          renderer.render(scenes[0], camera);

          for (let i = 1; i < scenes.length; i++) {
            renderer.autoClear = false;
            renderer.clearDepth();
            renderer.render(scenes[i], camera);
          }

          renderer.autoClear = true;

        };
        animate();
        // === THREE.JS EXAMPLE CODE END ===
      }
      , [stressDataFetched,currentPossibleGarmentTypes]);
    console.log("bottom of scene %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%");
    

The pipeline is like this

1.user press button
2. frontend sends request
3. backend responses data
4. frontend fetches textures and obj files from cdn
5. objloader and mtlloader loads files
6. scenes are disposed
7. scene states updated with useState
8. useEffect runs and renderer renders new scenes
(btw there are 6 scenes there are 2 useEffects change 2 dom elements. One dom element has 3 scenes so totally 6 scenes are rendered with 2 canvases)

I basically create the new scene by calling a function then put the newly created scene into state variable. Is it possible that the temp scene variable has a reference so that scene is not totally deleted. I also see that after 4gb of memory when new request comes in it first decreases then increases to 4gb. But till 4gb it decreases a little bit then increases too much. Another case is i also need to change the render order of scenes and also determine if a scene is gonna be rendered or not. Is there any way to do this without deleting all the scenes and create again. The example is like this. There are 4 scenes and initially all are being rendered. But i want to change the render order of the scenes with a button like normally the order is

renderer.render(scene1);
renderer.render(scene2);
renderer.render(scene3);
renderer.render(scene4);

and i wanto modify it to

renderer.render(scene1);
renderer.render(scene2);
renderer.render(scene4);
renderer.render(scene3);

and even do this

renderer.render(scene1);
renderer.render(scene2);
renderer.render(scene3);

where i disable the rendering of one of the scenes

react executes useeffect twice in dev move, that means you already have two threejs and two running loops. without a cleanup phase everything will just accumulate, webgl resources aren’t garbage collected so easily, you need to dispose it manually and force the old context to shut down.

in general it makes little sense to use threejs in react like that, this is a black box, you have zero integration with react. use GitHub - pmndrs/react-three-fiber: 🇨🇭 A React renderer for Three.js this a small renderer, just like react-dom is a renderer for the dom, it is not an abstraction or binding. it integrates threejs into react, including memory management. 90% of that code block above goes away as a bonus.

What about the MeshDepthMaterial used by the shadow map renderer? How is it disposed? I’ve tried everything I know, and a pair of boots…

I think you are right here. When the renderer creates internal, unique depth or distance materials for shadow map rendering under certain conditions, these material are never removed. Even if you call dispose() on the actual material. I’ll try to fix this with a PR:

1 Like

Cool. I haven’t seen the PR yet… other fish to fry. Many thanks.