Skip to content

Loading Multiple Assets

Video Lecture

Loading Multiple Assets Loading Multiple Assets

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.

loading-multiple-assets.zip

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
import './style.css'
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'
import Stats from 'three/addons/libs/stats.module.js'

const scene = new THREE.Scene()

new RGBELoader().load('img/venice_sunset_1k.hdr', (texture) => {
  texture.mapping = THREE.EquirectangularReflectionMapping
  scene.environment = texture
  scene.background = texture
  scene.backgroundBlurriness = 1.0
})

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100)
camera.position.set(2, 1, -2)

const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.toneMapping = THREE.ACESFilmicToneMapping
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight
  camera.updateProjectionMatrix()
  renderer.setSize(window.innerWidth, window.innerHeight)
})

const controls = new OrbitControls(camera, renderer.domElement)
controls.target.y = 0.75
controls.enableDamping = true

const loader = new GLTFLoader()
loader.load('models/suv_body.glb', (gltf) => {
  scene.add(gltf.scene)
})
// loader.load('models/suv_wheel.glb', (gltf) => {
//   gltf.scene.position.set(-0.65, 0.2, -0.77)
//   scene.add(gltf.scene)
// })
// loader.load('models/suv_wheel.glb', (gltf) => {
//   gltf.scene.position.set(0.65, 0.2, -0.77)
//   gltf.scene.rotateY(Math.PI)
//   scene.add(gltf.scene)
// })
// loader.load('models/suv_wheel.glb', (gltf) => {
//   gltf.scene.position.set(-0.65, 0.2, 0.57)
//   scene.add(gltf.scene)
// })
// loader.load('models/suv_wheel.glb', (gltf) => {
//   gltf.scene.position.set(0.65, 0.2, 0.57)
//   gltf.scene.rotateY(Math.PI)
//   scene.add(gltf.scene)
// })

const stats = new Stats()
document.body.appendChild(stats.dom)

function animate() {
  requestAnimationFrame(animate)

  controls.update()

  renderer.render(scene, camera)

  stats.update()
}

animate()

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.

<>

THREE.Loader

Async/Await

Promise.all()

loadAsync

kenney.nl car-kit

First Car Shooter

Straight Car

Comments