Loading Multiple Assets
Description
You may want to load multiple models, textures, animations, fonts, sounds or other assets in your application.
All Three.js loaders at some point in their hierarchy, extend from the THREE.Loader base class.
The THREE.Loader
base class has a default THREE.LoadingManager object under the hood to keep track of loaded and pending objects and data.
The THREE.LoadingManager
knows when a resource has started loading, has finished loading, the progress of a download (in most browsers), and if an error occurred while attempting to download the resource.
When you instantiate any Threejs loaders, e.g., GLTFLoader, OBJLoader, TextureLoader, etc..., you can pass in a callback function to use when the resource has loaded (onLoad
), is downloading (onProgress
) and if there was any error (onError
) during the download.
E.g., the GLTFLoader
new GLTFLoader().load(
'model.glb',
// onLoad
function (gltf) {
scene.add(gltf)
},
// onProgress (Optional)
function (xhr) {
console.log((xhr.loaded / xhr.total) * 100 + '% loaded')
},
// onError (Optional)
function (err) {
console.error('An error happened')
}
)
It is ok to use the same loader instance to load many models or assets at the same time.
E.g.,
const loader = new GLTFLoader()
loader.load('model1.glb', function (gltf) {
scene.add(gltf.scene)
})
loader.load('model2.glb', function (gltf) {
scene.add(gltf.scene)
})
loader.load('model3.glb', function (gltf) {
scene.add(gltf.scene)
})
This is fine. All models will be downloaded asynchronously (in parallel), independently of each other, and added to the scene in order of whichever onLoad
callback finished first/next.
Loading Dependent Objects
Loading multiple resources asynchronously is normally not a problem if each of your models or assets are capable of being treated independently of each other.
In some situations, you may be downloading other models or assets that are dependent on a resource that has already been fully downloaded and setup in memory first.
Example, you need to add a wheel to a car chassis.
In this case you can load the wheel model in the car chassis onLoad
callback. Now when you add the wheel
to the chassis
, you can be sure that the chassis
has fully downloaded and is ready in memory for use.
E.g.,
const loader = new GLTFLoader()
loader.load('chassis.glb', function (gltf) {
// chassis.glb has downloaded and is ready in memory for use. Lets continue,
const chassis = gltf.scene
loader.load('wheel.glb', function (gltf) {
chassis.add(gltf.scene)
})
scene.add(chassis)
})
In the above example code, the wheel
object is now a child component of the chassis
object. The chassis
needed to be setup in memory first before adding the wheel
to it.
If both models where instead loaded asynchronously, as shown earlier, and the wheels onLoad
callback happened to execute before the chassis onload
callback, then your program would crash and show an undefined error in the browsers JavaScript console.
Uncaught TypeError: Cannot read property 'add' of undefined
The chassis
object wouldn't yet have been fully instantiated, so wouldn't exist in memory yet. This situation is also known as a race condition. Race conditions can be a problem if you are asynchronously loading several objects in parallel and one of them is dependent on the other.
Also, a side note, since the wheel
was added as a child of the chassis
object, any changes to the chassis
transforms, such as position
, scale
or rotation
will automatically be reflected in the wheel
object. Read more about Object3D Hierarchy.
Now, you probably want to see 4 wheels on this chassis and positioned accordingly, so this code below is one way to achieve that.
const loader = new GLTFLoader()
loader.load('chassis.glb', function (gltf) {
const chassis = gltf.scene
loader.load('wheel.glb', function (gltf) {
const wheels = [
gltf.scene,
gltf.scene.clone(),
gltf.scene.clone(),
gltf.scene.clone(),
]
wheels[0].position.set(-1, 0, 1)
wheels[1].position.set(1, 0, 1)
wheels[2].position.set(-1, 0, -1)
wheels[3].position.set(1, 0, -1)
chassis.add(...wheels)
})
scene.add(chassis)
})
LoadAsync
Now, you may not like to use nested callbacks for whatever reason. In this case, another option you have is to load your dependent assets synchronously using await
and the loaders loadAsync method. loadAsync
is equivalent to the load method, except that it returns a promise that can be awaited.
In the example below, loadAsync
will finish loading the dependent chassis
model before continuing to the next load
method.
const loader = new GLTFLoader()
let chassis: Object3D
await loader.loadAsync('chassis.glb').then((gltf) => {
chassis = gltf.scene
})
loader.load('wheel.glb', function (gltf) {
const wheels = [
gltf.scene,
gltf.scene.clone(),
gltf.scene.clone(),
gltf.scene.clone(),
]
wheels[0].position.set(-1, 0, 1)
wheels[1].position.set(1, 0, 1)
wheels[2].position.set(-1, 0, -1)
wheels[3].position.set(1, 0, -1)
chassis.add(...wheels)
scene.add(chassis)
})
In the above example code, the load
method will still run asynchronously, but start only after the awaited loadAsync
has finished.
You also have the option to run that second load
method using loadAsync
instead. In that case, your script would wait for the second model to be fully downloaded before continuing.
Async/Await, Promise.All() & LoadAsync
If you want to load multiple assets at the same time, but wait for them all to be fully downloaded before continuing, then you also have the option to wrap multiple loadAsync() methods in a Promise.all(). You will also need to wrap this in an Async/Await
async function loadCar() {
const loader = new GLTFLoader()
const [...model] = await Promise.all([
loader.loadAsync('chassis.glb'),
loader.loadAsync('wheel.glb'),
])
const chassis = model[0].scene
const wheels = [
model[1].scene,
model[1].scene.clone(),
model[1].scene.clone(),
model[1].scene.clone(),
]
wheels[0].position.set(-1, 0, 1)
wheels[1].position.set(1, 0, 1)
wheels[2].position.set(-1, 0, -1)
wheels[3].position.set(1, 0, -1)
chassis.add(...wheels)
scene.add(chassis)
}
loadCar()
In the above code example, the chassis
and wheel
objects will be instantiated only after all the loadAsync
calls within the Promise.all()
where completed.