GLTFLoader OOP Pattern

What is best workflow for GLTFLoader() . when loading 3 or 4 models which will be used repeatedly. Is there a design pattern where the GLTFLoader is wrapped in some sort of Factory pattern and only loads requested objects which haven’t been loaded yet, otherwise returns existing already loaded objects?

const loader = new Loader()

const cache = {}

function getAsset( url ) {
  if(!cache[url]) cache[url] = loader.load(url)
  return cache[url]
}

Something along those lines?

The async loading may be a problem. I like the texture that creates an instance of Texture and then populates it. You can pass it around without loading it and it will work fine. gltfLoader loads Scene , so maybe you can return an instance of another Scene then replace the children of it with the loaded one. Still from the cache you take the one you created.

So something like:

const loader = new Loader()

const cache = {}

function getAsset( url ) {
  if(!cache[url]) {
    cache[url] = new Scene()
    loader.load(url).then(scene=> cache[url].children = scene.children)
  }
  return cache[url]
}

You can of course then elaborate from there:

class MyFactory {
 constructor(){
  this.cache={}
 }
 getAsset(url){ ... }
}

class LoadableGLTF extends THREE.Scene {
  constructor(url, factory){
    super()
    doSomething( factory.getAsset(url) )
  }

1 Like

Different ways to do this, not sure there’s any one best way. Wrapping GLTFLoader as @pailhead shows above is probably the most customizable option. There’s also some WIP (https://github.com/mrdoob/three.js/pull/14779, and https://github.com/mrdoob/three.js/pull/14492) that would help you put everything you need into a single glTF asset, then lazily load parts of that one by one. If your different models should share dependencies (materials, textures, …) then that might be a good option.

1 Like

Thanks pailhead,
In this example code, in a scenario: if( !cache[url] ) returns false, the code will return a blank scene. right?

Does anyone know of a working example of this? Efficient factory pattern for GLTFLoader returning objects from a previously loaded cache?

The snippet above will return an initially empty scene, and that Scene will have content added to it as the content loads. If you want to wait for the content to be fully loaded before appending anything, you could use a callback, promise, or async/await. Most applications only need very simple caching, and even a factory/OOP pattern feels like overkill if you’re only dealing with 3-4 models. I would suggest trying something simple and letting us know if you have trouble.

var models = {};

function loadModel ( url ) {

  if ( models[ url ] ) {
    return models[ url ].then( ( o ) => o.clone() );
  }

  return models[ url ] = new Promise( ( resolve, reject ) => {
    loader.load( url, function ( gltf ) {
      resolve( gltf.scene );
    }, undefined, reject );
  } );

}

Also note that if you’re going to have the same model in the scene multiple places at the same time, you should probably clone it.

EDIT: Updated to handle multiple requests during initial load.

2 Likes

I would love to just have a magicalDelivery class which abstracts the loading / cloning / etc. and efficiently returns loaded/cloned objects from cache, ready to be added to a scene. Like this:

magicalDelivery.getObject( "model_identifier" ,  function( model_or_clone  ){
      scene.add( model_or_clone )
} ); 

MagicalDelivery returns complete 3D model, with materials applied, efficiently loaded or cloned if called repeatedly, async proof and simplified, repeated calls for same “model_identifier” return clone from cache.

Does something like this exist? Is there a working example of something close?

Thanks, donmccurdy. These are all great tips , but I imagine something like this must exist already instead of requiring low-level implementation?

The 11 lines of code above do everything you’re asking, and they do it efficiently — I think you’re overestimating the amount of low-level magic required to do this. :slight_smile:

Thanks donmccurdy, I’m checking these exciting 11 lines. That is working code? Materials and everything for GLTFLoader? loader is an instance of something?

GLTFLoader handles materials itself, yes. To create the loader you’d just do:

var loader = new THREE.GLTFLoader();

See GLTFLoader docs, you might do extra things here if you plan to use animation or compression.

I would expect it to “just work” with usage like this:

var scene = new THREE.Scene();

// Loads model from scratch.
loadModel('car.glb').then( ( car ) => scene.add( car ) );

// Uses a clone of the car loaded above, without requesting new resources.
loadModel('car.glb').then( ( car ) => scene.add( car ) );
1 Like

This is correct. Let’s put it this way, when you make your call:

magicalDelivery
.getObject( 
  "model_identifier" ,  
  model_or_clone => scene.add( model_or_clone )
)

What do you want to happen in the 1483 seconds it may take to download a very large file over a slow connection? A magical solution is only magical for your use case.

donmccurdy
This. Straight. Up. WORKS !! :smiley:

donmccurdy THANKS, you da man!

1 Like

What a marvelous solution. So tiny, so effective.

Note you probably don’t want to do a .clone if your loader returns Geometry or Texture, specifically for GLTF cloning a Scene makes sense.

pailhead , wait ? What ? So that snippet is inefficient? How to modify to make it better?

No i meant, it’s not a generic enough of a solution to cover all the loaders. It depends on the use case. For example, if you’re using the JSONLoader it loads Geometry.

If your cache returns this geometry, you wouldn’t want to call clone inside of it every time as you would run out of memory.

for ( let i = 0 ; i < 1000 ; i ++ ) meshes.push(new Mesh(cache.getGeometry(url))

you would want to get the same instance every time, not a clone of geometry since the “cloning” is already done by making 1000 Meshes.

Ie.

getAsset(url){
return cache[url].clone()
}

vs

getAsset(url)
return cache[url]
}
getAsset('foo').clone()

Should be up to you and flexible, hence no magical module that does this.

^Right, if you were using another format/loader than glTF/GLTFLoader the code would have to be adjusted accordingly. That’s part of why there isn’t an out-of-the-box example already. But if your 3-4 models use the same format it’s fine.

Wait a minute… Wouldn’t loading status need to be reflected somehow before cache returns true? In this solution loading will be re-triggered repeatedly if the second call to the cache is made before the GLTF file is fully loaded, right?

In the code described at this comment, additional calls to the cache will not cause additional loads, even if the first load has not finished yet. This is because the cache stores an unresolved Promise immediately, and subsequent calls wait for that to finish. I think in a previous version of the code I’d forgotten to include that.