Loading Multiple Assets
Video Lecture
Description
All Three.js loaders 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
(gltf) => {
scene.add(gltf.scene)
},
// onProgress (Optional)
(xhr) => {
console.log((xhr.loaded / xhr.total) * 100 + '% loaded')
},
// onError (Optional)
(err) => {
console.error('Error : ' + err)
}
)
Lesson Script
Download the lesson assets.
After extracting, your public folder should resemble,
|-- Three.js-TypeScript-Boilerplate
|-- public
|-- img
|-- grid.png
|-- venice_sunset_1k.hdr
|-- models
|-- suv_body.glb
|-- suv_wheel.glb
|-- suzanne_no_material.glb
./src/main.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
|
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.
In this case you can load the dependent model in the parent models onLoad
callback. Now when you add the dependent model to the parent model, you can be sure that the parent model has fully downloaded and is ready in memory for use.
E.g.,
const loader = new GLTFLoader()
loader.load('parent.glb', function (gltf) {
// parent.glb has downloaded and is ready in memory for use. Lets continue,
const parent = gltf.scene
loader.load('dependent.glb', function (gltf) {
parent.add(gltf.scene)
})
scene.add(parent)
})
In the above example code, the dependent object is now a child component of the parent object. The parent needed to be setup in memory first before adding the dependent to it.
If both models where instead loaded asynchronously, as shown earlier, and the dependents onLoad
callback happened to execute before the parents 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 parent 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 dependent model was added as a child of the parent, any changes to the parents transforms, such as position
, scale
or rotation
will automatically be reflected in the dependent. Read more about Object3D Hierarchy.
In the lesson, I add 4 wheels to the SUV body and position them accordingly. The code below shows one way of achieving that.
const loader = new GLTFLoader()
loader.load('models/suv_body.glb', (gltf) => {
const suvBody = gltf.scene
loader.load('models/suv_wheel.glb', function (gltf) {
const wheels = [gltf.scene, gltf.scene.clone(), gltf.scene.clone(), gltf.scene.clone()]
wheels[0].position.set(-0.65, 0.2, -0.77)
wheels[1].position.set(0.65, 0.2, -0.77)
wheels[1].rotateY(Math.PI)
wheels[2].position.set(-0.65, 0.2, 0.57)
wheels[3].position.set(0.65, 0.2, 0.57)
wheels[3].rotateY(Math.PI)
suvBody.add(...wheels)
})
scene.add(suvBody)
})
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 suv_body.glb
model before continuing to the next load
method.
const loader = new GLTFLoader()
let suvBody: THREE.Object3D
await loader.loadAsync('models/suv_body.glb').then((gltf) => {
suvBody = gltf.scene
})
loader.load('models/suv_wheel.glb', function (gltf) {
const wheels = [gltf.scene, gltf.scene.clone(), gltf.scene.clone(), gltf.scene.clone()]
wheels[0].position.set(-0.65, 0.2, -0.77)
wheels[1].position.set(0.65, 0.2, -0.77)
wheels[1].rotateY(Math.PI)
wheels[2].position.set(-0.65, 0.2, 0.57)
wheels[3].position.set(0.65, 0.2, 0.57)
wheels[3].rotateY(Math.PI)
suvBody.add(...wheels)
scene.add(suvBody)
})
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('models/suv_body.glb'), loader.loadAsync('models/suv_wheel.glb')])
const suvBody = model[0].scene
const wheels = [model[1].scene, model[1].scene.clone(), model[1].scene.clone(), model[1].scene.clone()]
wheels[0].position.set(-0.65, 0.2, -0.77)
wheels[1].position.set(0.65, 0.2, -0.77)
wheels[1].rotateY(Math.PI)
wheels[2].position.set(-0.65, 0.2, 0.57)
wheels[3].position.set(0.65, 0.2, 0.57)
wheels[3].rotateY(Math.PI)
suvBody.add(...wheels)
scene.add(suvBody)
}
await loadCar()
In the above code example, the suvBody
and wheels
will be instantiated only after all the loadAsync
calls within the Promise.all()
where completed.