Draco loader configuration options

Hi,

In my project, I’m loading 30+ models using the GLTFLoader and the DRACOLoader.

While using the DRACOLoader I noticed that setting the following option

const dracoLoader = new THREE.DRACOLoader(); 

dracoLoader.setDecoderConfig({ type: 'js' });

saves you from getting the error WebAssemply.Memory(): could not allocate memory. Without this option, some of the models do not even load. So the question if what does this option do? Are there any other options we need to set up as well to avoid errors?

What the option does is to switch the THREE.DRACOLoader from using WebAssembly to using plain JavaScript. Presumably this avoids letting WASM run out of memory, because there is no WASM used. But WASM is faster, and just disabling it might be sweeping another problem under the rug…

It’s important to only create a single THREE.DRACOLoader instance, no matter how many models you’re using. If you must create more than one loader, be sure to .dispose() it when you’re done loading. Otherwise you’ll have lots of idle Web Workers taking up memory, and this might be the cause of the OOM error.

Hi again and thanks for your help so far!

So, here is what I’m doing

let loader = new THREE.GLTFLoader(loadingManager);

const dracoLoader = new THREE.DRACOLoader();

dracoLoader.setDecoderPath( ‘js/vendor/draco/’ );

loader.setDRACOLoader( dracoLoader );

loader.load(

// load gltf model

);

dracoLoader.dispose()

This results in an OOM error as before. However, if I try this:

let loader = new THREE.GLTFLoader(loadingManager);

const dracoLoader = new THREE.DRACOLoader();

dracoLoader.setDecoderPath( ‘js/vendor/draco/’ );

dracoLoader.setDecoderConfig({ type: ‘js’ });

loader.setDRACOLoader( dracoLoader );

loader.load(

// load gltf model

);

dracoLoader.dispose()

Things work and I get no errors. Moreover, I don’t think that the .dispose() method does anything because I tried printing the Draco loader’s properties using the following code:

for (const property in dracoLoader)
{
alert(${property}: ${dracoLoader[property]});
}

dracoLoader.dispose();

for (const property in dracoLoader)
{
alert(${property}: ${dracoLoader[property]});
}

I expected that after using the .dispose() command, the for loop would not get executed but it did and gave me the same results as before. So, I’m either doing something wrong or I completely misunderstood what I have to do.

Thank you very much again!

From this code I still can’t tell if there’s a different loader for each model, or if the loaders are reused.

… assuming they’re not reused (ideally they should be), then calling .dispose() is strongly recommended. I wouldn’t expect it to affect enumerable properties in “for” loops in a meaningful way. But note that you’ll need to wait to dispose of it until after the model has loaded, not before. This means it should happen in the “load” callback.

Hi again,

Sorry about the late response but I’m working on this project on and off. So what I’m trying to do, is to save a project from crashing due to loading multiple GLTF models.

The initial problem, which is now fixed due to using dracoLoader.setDecoderConfig({ type: 'js' });, was that once I added many models to the scene, of the order of 30, the scene would load but some of the models (2 to 5), were missing from the scene. They would never load and the scene seemed as if the models disappeared. That problem is now fixed.

The new problem is that we want to be able to load up to say 80-90 GLTF models, each of which having a size of about ~1 Mb. We are able to load about 45 models with the current code, but then the browser crashes. The project has about 60 unique models, so in some of the scenes, we might be loading the same model multiple times. So a scene might contain for example 10 models of the kind A, 20 of the kind B, 30 of the kind C, and 20 of the kind D.

The way the program works right now is that we create an array called requestedObjects, which is filled with the User’s input. The user selects the models he wants and their position in the scene.

So the requestedObjects looks like this:

requestedObjects = [
    {
      'model': "modelA",
      'folder': "modelA_folder",
      'xPos3D': "142.00",
      'yPos3D': "163.00",
      'zPos': '0',
      'rotation': 90
    },
    {
      'model': "modelB",
      'folder': "modelB_folder",
      'xPos3D': '268',
      'yPos3D': "163.00",
      'zPos': '0',
      'rotation': 90
    }
]

Then we call this function:

const renderObjects = function () {
  // render the objects from the array passed
  requestedObjects.map((item, i) => {
    console.log(`park width = ${parkWidth} & park height = ${parkHeight}`);

    createGLTF(i, item.model, item.folder, item.xPos3D, item.yPos3D, item.zPos, item.rotation);

    // getting dynamic position for the camera to see all items
    if (Number(item.xPos3D) > cameraX) {
      cameraX = Number(item.xPos3D)
    }
    if (Number(item.yPos3D) > cameraY) {
      cameraY = Number(item.yPos3D)
    }

    // once all the objects are ready run the initial set up functions
    if (i + 1 === requestedObjects.length) {

      // update the preloader text for total number of objects
      totalModels = requestedObjects.length
      //document.querySelector('#jsTotalLoad').textContent = totalModels

      container = document.querySelector('#scene-container');
      scene = new THREE.Scene();
      // change sky colour
      scene.background = new THREE.Color(0xffffff);

      // initial set up functions
      createCamera((cameraX - 50), (cameraY - 50));
      createControls();
      createLights();
      createRenderer();
      createWater();
      initSky();

      if (gridHelp) {
        displayFloorGrid();
      }

      // start the animation loop
      renderer.setAnimationLoop(() => {
        update();
        render();
      });
    }
  })
}

The functions initSky(), createWater(), etc add effects to the scene and I don’t think they are causing an issue. I think the most relevant function is the one where the GLTF loading takes place:

// the function to create a new 3D object
function createGLTF(i, model, folder, xPos, yPos, zPos, rotation) {

  const loadingManager = new THREE.LoadingManager( () => {
	
		const loadingScreen = document.getElementById( 'loading-screen' );
		loadingScreen.classList.add( 'fade-out' );
		
		// optional: remove loader from DOM via event listener
		loadingScreen.addEventListener( 'transitionend', onTransitionEnd );
	}
  );

   loadingManager.onProgress = function ( url, itemsLoaded, itemsTotal ) {

 };

  let loader = new THREE.GLTFLoader(loadingManager);
  
  const dracoLoader = new THREE.DRACOLoader(loadingManager); 
  
  dracoLoader.setDecoderPath( 'js/vendor/draco/' ); 

  dracoLoader.setDecoderConfig({ type: 'js' });

  loader.setDRACOLoader( dracoLoader );

  loader.load(
    // resource URL
    `model_lib/${folder}/${model}.gltf`,

    // called when resource is loaded
    function (object) {

      const aabb = new THREE.Box3().setFromObject(object.scene);
      const centerHere = aabb.getCenter(new THREE.Vector3());

      object.scene.position.x += (object.scene.position.x - centerHere.x);
      //object.position.y += ( object.position.y - centerHere.y );
      object.scene.position.z += (object.scene.position.z - centerHere.z);

      if (gridHelp) {
        var boxHelp = new THREE.BoxHelper(object.scene, 0x000000);
        object.scene.add(boxHelp);
      }

      // this little helper allows the park items to be centered in view
      const xPosCenter = xPos - (parkWidth / 2)
      const yPosCenter = yPos - (parkHeight / 2)

      // note zPos & yPos are switched in 2D to 3D exchange
      object.scene.position.set(xPosCenter, 0, yPosCenter);
      const rotationRadian = -(Number(rotation) * Math.PI / 180).toString();
      object.scene.rotation.set(0, rotationRadian, 0);

      scene.add(object.scene);
   
    },

    // called when loading is in progresses
    function (xhr) {
    
      let contentLength = xhr.loaded;

      // if an object is 100% loaded
      if ((xhr.loaded / contentLength * 100) === 100) {
      modelsLoaded++

        // if all models are loaded
        if (totalModels === modelsLoaded) {
          setTimeout(function () {
            
          }, 1500)
        }
      }
    }
    ,
    // called when loading has errors
    function (error) {
      console.log('An error happened');
    }
  );
}

Finally the objects are rendered:

function render() {
  const time = performance.now() * 0.001;

  water.material.uniforms['time'].value += 1.0 / 60.0;

  renderer.render(scene, camera);
}

So the two main functions that do the job are createGLTF and renderObjects . I don’t understand why the system would crash. Even in the extreme case of using 100 models (of 1 Mb each), the scene shouldn’t crash I think.

One issue for the crashing might be the dispose() of the DracoLoader you mentioned. Another issue might be that I think that we are not reusing models that are already loaded. Is there a way to check that a certain model is already in the scene, clone or copy the existing model, and create a clone in which we set up new parameters for its position? Would that save memory?

I’m sorry about the long response, but I’m trying to kind of save this project without knowing much about three.js.

I think the first thing I would try here would be to move the creation of the loaders outside of the createGLTF function block, so you’re using the same loaders each time:

  const loader = new THREE.GLTFLoader(loadingManager);
  const dracoLoader = new THREE.DRACOLoader(loadingManager); 
  dracoLoader.setDecoderPath( 'js/vendor/draco/' ); 
  loader.setDRACOLoader( dracoLoader );

  function createGLTF (...) {
    // ...
    loader.load( ... );
  }

Caching the loaded objects and reusing them is also a good idea – could be as simple as a map of URLs to loaded objects:

if (cache[url]) {
  return cache[url].clone(); // use SkeletonUtils.clone(...) if these are skinned.
}

// ...

cache[url] = gltf.scene;

Hi! I think this got off topic again (sorry) but thank you very much for catching that I was declaring the loaders inside createGLTF (....), which resulted in instantiating the same things over and over again. Things got slightly improved now.

I tried to use cache, but I still can’t get it to work. I never used it before. So following the instructions here, I declared:

THREE.Cache.enabled = true;

on top of my js file, next to the place where I declare the loaders:

THREE.Cache.enabled = true;
const loader = new THREE.GLTFLoader(loadingManager);
const dracoLoader = new THREE.DRACOLoader(loadingManager); 
dracoLoader.setDecoderPath( 'js/vendor/draco/' ); 
loader.setDRACOLoader( dracoLoader );

  function createGLTF (...) {
    // ...
    loader.load( ... );
  }

I then tried to do the following:

function createGLTF(...) {

....

let url = `model_lib/${folder}/${model}.gltf`;

  if (THREE.Cache.get(model) !==null){
    alert("FOUND");
    scene.add(THREE.Cache.get(model));
  }
  else{
   
  loader.load(
    // resource URL
    //`model_lib/${folder}/${model}.gltf`,
    url,

    // called when resource is loaded
    function (object) {

      THREE.Cache.add(model, object.scene);

      const aabb = new THREE.Box3().setFromObject(object.scene);
      const centerHere = aabb.getCenter(new THREE.Vector3());

      object.scene.position.x += (object.scene.position.x - centerHere.x);
      object.scene.position.z += (object.scene.position.z - centerHere.z);

      if (gridHelp) {
        var boxHelp = new THREE.BoxHelper(object.scene, 0x000000);
        object.scene.add(boxHelp);
      }

      // this little helper allows the park items to be centered in view
      const xPosCenter = xPos - (parkWidth / 2)
      const yPosCenter = yPos - (parkHeight / 2)

      // note zPos & yPos are switched in 2D to 3D exchange
      object.scene.position.set(xPosCenter, 0, yPosCenter);
      const rotationRadian = -(Number(rotation) * Math.PI / 180).toString();
      object.scene.rotation.set(0, rotationRadian, 0);

      scene.add(object.scene);
    },

    // called when loading is in progresses
    function (xhr) {
      let contentLength = xhr.loaded;

      modelsLoaded++
      
        if (totalModels === modelsLoaded) {
          setTimeout(function () {
         
          }, 1500)
        }

      }
    }
    ,
    // called when loading has errors
    function (error) {
      console.log('An error happened');
    }

  );

}

What happens is that I get the alert FOUND on my browser and then the error:

Uncaught TypeError: Cannot read properties of undefined (reading 'add')
    at createGLTF (app-dev.js:743)
    at app-dev.js:985
    at Array.map (<anonymous>)
    at renderObjects (app-dev.js:979)
    at startThreeJS (app-dev.js:1091)
    at app-dev.js:1095

I tried to read through this and also this but didn’t find a resolution yet.

The three.js documentation is not very enlightening either. So what am I missing?

Thanks again for going through the trouble!

Update: My previous implementation would load about 45 models and crash. Now I can load 55 models without the browser crashing, however, once the scene loads, my whole computer freezes and I need to shut it down and reboot.

I wouldn’t recommend using THREE.Cache for this – it’s intended for network request caching, and what you really want to cache is the fully-parsed result of the loading operation.

Instead, you can manually store loader results with their URLs and the key. Before making each request, check if a result for that URL has already been returned. A cache can be as simple as just an empty JS object:

const cache = {};

...

if (cache[url]) {
  onLoad(cache[url].clone());
} else {
  loader.load(url, (gltf) => {
    cache[url] = gltf.scene;
    onLoad(gltf.scene);
  });
}

function onLoad(model) {
  ...
}

Hi @donmccurdy,

Sorry for taking so long to respond. I found the solution before you posted, but my solution is very similar in spirit so thank you anyway :slight_smile:
I did realize that THREE.Cache does something else so thank you for pointing it out. Moreover, I understand your solution as well.

What I ended up doing was going back to my array:

requestedObjects = [
    {
      'model': "modelA",
      'folder': "modelA_folder",
      'xPos3D': "142.00",
      'yPos3D': "163.00",
      'zPos': '0',
      'rotation': 90
    },
    {
      'model': "modelB",
      'folder': "modelB_folder",
      'xPos3D': '268',
      'yPos3D': "163.00",
      'zPos': '0',
      'rotation': 90
    },
    {
      'model': "modelA",
      'folder': "modelA_folder",
      'xPos3D': "341.00",
      'yPos3D': '162',
      'zPos': '0',
      'rotation': 0
    }
  ]

and processing it to filter items that are unique using the function:

let filtered = requestedObjects.filter(function (el) {
  if (!this[el.model]) {
    this[el.model] = true;
    return true;
  }
  return false;
}, Object.create(null));

Then I go back to where I load my items and use the filtered array instead of requestedObjects to load only unique items. Then inside my GLTFLoader, I do a for loop to clone the already loaded unique item:

 let model3d = new THREE.Object3D();

  loader.load(
    // resource URL
    url,
    // called when resource is loaded
    function (object) {
      // model3d holds the object so we can reuse it below

      model3d.add(object.scene);

      const aabb = new THREE.Box3().setFromObject(model3d);
      const centerHere = aabb.getCenter(new THREE.Vector3());

      model3d.position.x += (model3d.position.x - centerHere.x);
      model3d.position.z += (model3d.position.z - centerHere.z);

      if (gridHelp) {
        var boxHelp = new THREE.BoxHelper(model3d, 0x000000);
        model3d.add(boxHelp);
      }

      // this little helper allows the park items to be centered in view
      const xPosCenter = xPos - (parkWidth / 2)
      const yPosCenter = yPos - (parkHeight / 2)

      // note zPos & yPos are switched in 2D to 3D exchange
      model3d.position.set(xPosCenter, 0, yPosCenter);
      const rotationRadian = -(Number(rotation) * Math.PI / 180).toString();
      model3d.rotation.set(0, rotationRadian, 0);

      scene.add(model3d);

      // extraModels holds an array with all the information needed to clone a loaded model
      // as many times as it appears in a scene

      let extraModels = requestedObjects.filter(obj => obj.model === model && obj.xPos3D !== xPos);

      extraModels.map((item, i) => {

        let newModel = model3d.clone();

        const aabb = new THREE.Box3().setFromObject(newModel);
        const centerHere = aabb.getCenter(new THREE.Vector3());

        newModel.position.x += (newModel.position.x - centerHere.x);
        newModel.position.z += (newModel.position.z - centerHere.z);

        if (gridHelp) {
          var boxHelp = new THREE.BoxHelper(newModel, 0x000000);
          newModel.add(boxHelp);
        }

        // this little helper allows the park items to be centered in view
        const xPosCenter = item.xPos - (parkWidth / 2)
        const yPosCenter = item.yPos - (parkHeight / 2)

        // note zPos & yPos are switched in 2D to 3D exchange
        newModel.position.set(xPosCenter, 0, yPosCenter);
        const rotationRadian = -(Number(rotation) * Math.PI / 180).toString();
        newModel.rotation.set(0, rotationRadian, 0);

        scene.add(newModel);
      });
})

And it works like a charm :slight_smile:

I can now load up to 100 models very easily and very fast. The scene is very responsive, the lagging is gone, etc. The thing is that out of these 100 models (for example), there are 10 unique items so the scene works perfectly and everything (in the previous implementation the browser would crash at 45 models), and lag at about 35 (most of which were repeated items).

I have one final question though. What if I want to load 45 models, and all of them are unique. The problem of a laggy screen is still there in that case (again 45 pieces with only 10 of them being unique is perfect). The question is, what else can I try to make even the extreme scenario of having 45 (or even 60, 70) unique models both load as fast as possible and work fine? Are there any other tricks?

The number of models doesn’t mean all that much — what you’ll want to be counting is the number of: (1) meshes (roughly an indicator of draw calls), (2) materials, (3) vertices, and (4) textures and texture resolution.

A single model could contain 1 draw call, or 1000+ draw calls, so it’s hard to gauge by just the number of GLB models. Ideally you want to aim for <100 draw calls, or <1000 at the most, using InstancedMesh if necessary.

Similarly – it doesn’t take very many 4K textures to use up GPU memory or VRAM, meaning your page will perform slowly or crash. So low-res textures or fewer textures are better.

Thank you! I came into the three.js world as a scientist who can code so I’m still learning how to use three.js and JavaScript in general. I’ll talk to the 3D artist who prepares the models about this. But in general, we used to use OBJ models but after my suggestion, we switched to GLTF models, and the artist basically converted everything into GLTF using blender. He also used the Draco compression, also after my suggestion. In general, we would like to make as many improvements to the site’s performance as possible, so any ideas are welcome. I personally don’t know much about meshes and textures yet, though I get a general idea. Thanks again!

P.S: Do you still think I need to dispose of the DracoLoader somewhere in the program?

1 Like

It’ll free up a bit of memory (and 1-4 Web Workers) to dispose of it, so if you know you’re done using the loader then disposing is a good idea, but not strictly necessary.

Hi again,

Saving a bit more memory is welcomed however I still don’t understand how to dispose of the DracoLoader. Since I’m using a loading manager, am I disposing the dracoloader in onLoad part of the loading manager, or the onLoad part of the GLTFLoader? Or somewhere else completely. Here is the code:

const loadingManager = new THREE.LoadingManager();

let loader = new THREE.GLTFLoader(loadingManager);

const dracoLoader = new THREE.DRACOLoader(loadingManager);

dracoLoader.setDecoderPath('js/vendor/draco/');

dracoLoader.setDecoderConfig({ type: 'js' });

loader.setDRACOLoader(dracoLoader);

// the function to create a new 3D object
function createGLTF(i, model, folder, xPos, yPos, zPos, rotation) {

  loadingManager.onLoad = function () {
    const loadingScreen = document.getElementById('loading-screen');
    loadingScreen.classList.add('fade-out');

    // optional: remove loader from DOM via event listener
    loadingScreen.addEventListener('transitionend', onTransitionEnd);

   // Do I dispose here?
   
    dracoLoader.dispose();

  };

  let model3d = new THREE.Object3D();

  let url = `model_lib/${folder}/${model}.gltf`;

  loader.load(
    // resource URL
    url,
    // called when resource is loaded
    function (object) {
      model3d.add(object.scene);
      scene.add(model3d);

      let extraModels = requestedObjects.filter(obj => obj.model === model && obj.xPos3D !== xPos);

      extraModels.map((item, i) => {
        let newModel = model3d.clone();
        scene.add(newModel);
      });

  // Or do I dispose here?
   
    dracoLoader.dispose();

},
    // called when loading is in progresses
    function (xhr) {
    }
    ,
    // called when loading has errors
    function (error) {
    }
  );
}

I indicated the two places where it appears logical to dispose of the DracoLoader. But I might be wrong. Because when I use alert the DracoLoader is still there, whenever I dispose of it inside the code. Thanks again!

Knowing when “no more models are ever going to be loaded” is very application-specific, I don’t think I can tell you the answer to that, but it’s not in the onLoad function because that will run every time any model is loaded… if you’re not sure then I think it’s OK just to skip it!

Actually, this is what I have been trying to figure out. By printing random statements using alert, I figured out that the last function that runs before you get your scene is the onLoad part of the loading manager. I use the loading manager to run a nice loading screen that runs as long as the models are loading and disappears the moment the last model is loaded. So, to me, it makes the most sense to dispose the DracoLoader here:

loadingManager.onLoad = function () {
    const loadingScreen = document.getElementById('loading-screen');
    loadingScreen.classList.add('fade-out');

    // optional: remove loader from DOM via event listener
    loadingScreen.addEventListener('transitionend', onTransitionEnd);
   
    dracoLoader.dispose();

  };

I’m not seeing noticeable changes though. What am I expecting to see? Is there a way to use the web dev tools to see the changes caused by disposing of the Dracoloader?

So, to me, it makes the most sense to dispose the DracoLoader here…

As long as you’re loading all resources in up front (i.e. not lazy-loading more GLBs later) then that’s fine. :+1:

When you call .dispose() it will shut down the Web Workers it was using. In Chrome Developer Tools you can see any active Web Workers with a “gear” icon:

Screen Shot 2021-10-25 at 1.41.47 PM

1 Like

Thank you so much for your help. Our project is now saved :slight_smile: