Loading External Models 1.7

LOADING EXTERNAL MODELS

In the previous chapter, we created a simple toy train model using the built-in three.js geometries.

While doing so, it became apparent that creating anything with much more detail than a simple toy will quickly become very complicated, and creating a realistic model such as a human body is essentially impossible.

To do that, we’ll need to reach for an external modeling program such as 3D Studio Max, Maya, Blender (which is free! ), Cinema 4D, or hundreds of others. We’ll devote all of Section 13: Working with Other Applications to studying this so that we can make the process as smooth and painless as possible.

In this chapter, we’ll take a couple of the pre-prepared models from the three.js repo on Github and load them with one of the loaders, also found in the repo.

If you take a look at the live examples for the loaders, you’ll see that there are a lot of different loaders available, for a lot of different 3D asset formats. Over the years the 3D industry has created many different formats in an attempt to make sharing models between different programs possible, and some of the more common formats are OBJ, Collada, FBX, and so on, each with their own strengths and weaknesses.

However, very recently, one format has taken the industry by storm and is very quickly becoming the standard 3D format for the web, and that is glTF.

The Graphics Layer Transport Format (glTF)

glTF is a relative newcomer to the scene and was created by the Kronos Group, the same people who created the WebGL API that three.js uses under the hood.

Whenever we mention glTF, we are talking about glTF version 2, which was released in June of 2017 and replaced the previous version.

If possible, you should always use glTF format. It’s been specially designed for sharing models on the web, meaning that the file sizes are as small as possible and loading times will be very fast.

Unfortunately, because of the relative newness of this format, not all applications can export models in glTF format yet. This means that you may need to convert your models to glTF before using them - again, we’ll cover how to do this in Section 13: Working With Other Applications.

Over the course of this chapter, we’ll take a look at some of the glTF models that are available for free on the three.js Github repo. You can see all of them here.

In amongst these are three simple but beautiful bird models - a flamingo, a parrot, and a stork, all created by the talented people at mirada.com.

They are very low poly, meaning that they will run on even the most low-power of mobile devices, and they are even animated.

We’ll spend the rest of this chapter looking at how to load these models, add them to our scene, and play back their animations.

Loading Models with the GLTFLoader

  1. Setup
  2. Understanding the GLTFLoader.load method
  3. The onLoad function
  4. Load The Models
  5. Playback The AnimationClips
  scene.add( ambientLight, mainLight );

}

function loadModels() {

  const loader = new THREE.GLTFLoader();

  // A reusable function to set up the models. We're passing in a position parameter
  // so that they can be individually placed around the scene
  const onLoad = ( gltf, position ) => {

    const model = gltf.scene.children[ 0 ];
    model.position.copy( position );

    const animation = gltf.animations[ 0 ];

    const mixer = new THREE.AnimationMixer( model );
    mixers.push( mixer );

    const action = mixer.clipAction( animation );
    action.play();

    scene.add( model );

  };

  // the loader will report the loading progress to this function
  const onProgress = () => {};

  // the loader will send any error messages to this function, and we'll log
  // them to to console
  const onError = ( errorMessage ) => { console.log( errorMessage ); };

  // load the first model. Each model is loaded asynchronously,
  // so don't make any assumption about which one will finish loading first
  const parrotPosition = new THREE.Vector3( 0, 0, 2.5 );
  loader.load( 'models/Parrot.glb', gltf => onLoad( gltf, parrotPosition ), onProgress, onError );

  const flamingoPosition = new THREE.Vector3( 7.5, 0, -10 );
  loader.load( 'models/Flamingo.glb', gltf => onLoad( gltf, flamingoPosition ), onProgress, onError );

  const storkPosition = new THREE.Vector3( 0, -2.5, -10 );
  loader.load( 'models/Stork.glb', gltf => onLoad( gltf, storkPosition ), onProgress, onError );

}

function createRenderer() {

  // create a WebGLRenderer and set its width and height
  renderer = new THREE.WebGLRenderer( { antialias: true } );

We will be loading these three files from the three.js repo:

The .glb extension means that these files are in glTF Binary format.

Binary glTF files are compressed for a smaller size and can contain everything that the model needs, including any textures. The loader can also load uncompressed .gltf files, however, these will always come with a separate .bin data file and possibly some textures as well

It’s best to use the binary version if possible since this will generally download faster.

1. Setup

  1. Setup
  2. Understanding the GLTFLoader.load Method
  3. The onLoad function
  4. Load the Models
  5. Playback the AnimationClips

1.1. Include the Loader Script

  <script src="js/vendor/three/OrbitControls.js"></script>

  <!--

    Include the GLTFLoader script. This also needs to be loaded after the three.js script

  -->

  <script src="js/vendor/three/GLTFLoader.js"></script>

</head>

The GLTFLoader is not part of the three.js core, so we need to include the GLTFLoader.js script separately, just as we did with the OrbitControls.js script back in Chapter 1.5.

Make sure that the GLTFLoader.js script is in the correct folder, then add the above line to your index.html file and we’ll be good to go.

1.2. Delete createMaterials,createGeometries, createMeshes and add loadModels

Create the empty loadModels function

  scene.add( frontLight, backLight );

}

function loadModels() {

  // ready and waiting for your code!

}

function createRenderer() {

Our setup step here is very similar to our setup step from the previous chapter. Just delete the functions we will no longer be using: createMaterials,createGeometries, and createMeshes, and create a new, empty, function called loadModels.

1.3 Update the camera.position

Reposition the camera

function createCamera() {

  camera = new THREE.PerspectiveCamera( 35, container.clientWidth / container.clientHeight, 1, 100 );

  camera.position.set( -1.5, 1.5, 6.5 );

}

The bird models are reasonably bird-sized, so we’ll need to move the camera in a bit closer.

1.4 Instantiate the loader

Create an instance of the loader

function loadModels() {

  const loader = new THREE.GLTFLoader();

  // A reusable function to set up the models. We're passing in a position parameter

The GLTFLoader constructor takes an optional parameter specifying which LoadingManager to use. If we omit the parameter then it will use the DefaultLoadingManager, which is fine for now.

We’ll store the created loader in a variable called loader, and we can reuse this to load as many models as we like using the loader.load() method. Let’s take a look at that now.

2. Understanding the GLTFLoader.load Method

  1. Setup
  2. Understanding the GLTFLoader.load method
  3. The onLoad function
  4. Load The Models
  5. Playback The AnimationClips

The GLTFLoader.load function takes 4 parameters

loader.load(

  // parameter 1: The URL
  'models/Parrot.glb',

  // parameter 2:The onLoad callback
  gltf => onLoad( gltf, parrotPosition ),

  // parameter 3:The onProgress callback
  onProgress,

  // parameter 4:The onError callback
  onError

);

Taking a look at the GLTFLoader.load docs page, we can see that it takes four parameters:

  1. url, a string - this tells the loader where to find the file on the server
  2. onLoad, a callback function - this will get called when the model has finished loading
  3. onProgress, a callback function that gets called as the loading progresses
  4. onError, a callback function that gets called if there is an error loading the model

2.1 The url Parameter

The url parameter points to a file on your server that you want to the loader to load.

As we mentioned back in Chapter 1.4, if you are running your files locally - for example, if you are just double-clicking on index.html to load it up in your browser - then you will run into problems when it comes to loading files through JavaScript.

We’ll download these files from the three.js repo and put them in a /models folder next to our index.html file, so the url parameter for the parrot will be the string "/models/Parrot.glb".

2.2 The onLoad Callback Function

Here’s a bare minimum setup for loading a file using the GLTFLoader:

const loader = new THREE.GLTFLoader();

const url = '/models/Parrot.glb';

// Here, 'gltf' is the object that the loader returns to us
const onLoad = ( gltf ) => {

  console.log( gltf );

};

loader.load( url, onLoad );

The model specified in the url gets loaded asynchronously by the loader, meaning that the rest of your JavaScript can continue to run while the loading is happening.

This also means that you can’t know exactly when the file will finish loading, or if you are loading several files, which one will finish first.

The loader has two separate jobs:

  1. Load the file specified in the url
  2. Parse the loaded data and turn it into three.js objects

The data loaded from the file is in glTF format, and the main job of the loader is to parse glTF data and create three.js objects such as meshes, groups and lights.

Once finished, the loader returns the result of the parsing operation back to the onLoad callback function as a single variable which we’re calling gltf here, although it’s also common practice to give this a more generic name such as result.

In the above minimal example, assuming everything has worked correctly, this gltf object gets logged to the browser console so that we can take a look at it.

The gltf Object Returned by the Loader

If you check your console after running the above code, you should see something like this:

gltf = {
  animations: [ AnimationClip ]
  asset: {version: "2.0", generator: "THREE.GLTFExporter"}
  cameras: []
  parser: GLTFParser {  }
  scene: Scene {
    ...,
    children: [ Mesh ]
    ...,
  }
  scenes: [Scene]
  userData: {}
}

In this chapter, we’re interested in just two properties from the loaded object.

gltf.animations


animations: [ AnimationClip ]

If there is any animation data in the file, it gets stored here, in the form of an array of AnimationClips. Each of our bird models contains a single animation of the bird flapping its wings, created using morph targets, which we’ll explain Section 11: Animating Your Scenes.

gltf.scene


scene: Scene {
  ...,
  children: [ Mesh ]
  ...,
}

The loader returns an entire Scene for us, with any models placed inside it. If we wish to, we can just replace our scene entirely with this one.

In this case, we’re going to load three models, so we’ll need to extract the model we want from each of the three loaded scenes and add them to our own scene.

We’ll ignore the other entries in the loaded gltf object for now.

2.3 The onProgress and onError Callback Functions

Just like onLoad, onProgress and onError are callback functions.

onProgress gets called repeatedly as the loading progresses, so if we wished we could use it to create a loading icon. We’ll just pass in an empty function below since we don’t care about the loading progress for now. We could also pass in null or undefined for the same result.

onError is normally used to log any errors to the console, especially during development. The most likely errors you will get are 404 errors if the file was not found, or cross-origin errors meaning that your server is not set up correctly.

Both of these callback functions are optional, so you can leave them out if you prefer, although omitting the onError function is not recommended.

3. The onLoad function

  1. Setup
  2. Understanding the GLTFLoader.load method
  3. The onLoad function
  4. Load The Models
  5. Playback The AnimationClips

The onLoad function

  // A reusable function to set up the models. We're passing in a position parameter
  // so that they can be individually placed around the scene
  const onLoad = ( gltf, position ) => {

    const model = gltf.scene.children[ 0 ];
    model.position.copy( position );

    const animation = gltf.animations[ 0 ];

    const mixer = new THREE.AnimationMixer( model );
    mixers.push( mixer );

    const action = mixer.clipAction( animation );
    action.play();

    scene.add( model );

  };

We want our onLoad function to be reusable. For this to work, we need to think about what we’ll do with each loaded object.

In particular, in what respects will we treat each loaded model the same, and in what respects differently?

Here, we want to do the following four steps:

  1. Extract the single bird model from each of the loaded files
  2. Move each bird into a unique position
  3. Set up the animations (we’ll come back to this at the end of the chapter)
  4. Add the model to the scene

Steps $1$, $3$, and $4$ will be the same for each model, while step $2$ will require a different position for each model.

By default, the onLoad function has a single parameter, the loaded gltf object. This is fine for $1-3$, however, we’ll need to pass in a second parameter called position for step $2$, so the first line of our function will look like this:


const onLoad = ( gltf, position ) => {

By default, the onLoad callback function gets called with a single argument, gltf. We want to pass in two parameters, so we’re wrapping onLoad in an anonymous outer function that then calls our inner function.

In old style JavaScript, which you might be more familiar with, we would have done this:


function( gltf ) {

  onLoad( gltf, parrotPosition );

}

In this case, the two ways of doing this are identical, but using am arrow function is shorter.

Next, we need to get a reference to the model from with the gltf.scene. Fortunately, each of our bird models is located in the same place:


const model = gltf.scene.children[ 0 ];

However, this won’t always be the case, and for this step, and you may need to examine the gltf object in the console to find your models.

We’ll copy the data from the position vector into the model.position using a method called Vector3.copy.

Most objects in three.js have a .copy method, which can be used to quickly copy the properties from another object of the same type into this one. So you can do:

Or, as we are doing here:

Here, the first Vector3 is our model.position and the second is the position parameter we are passing in:


model.position.copy( position );

Skipping step $3$ for now (we’ll cover animation in a moment), the last thing that we need to do is add the model to our scene. As usual, we’ll use the scene.add method to do this:


scene.add( model );

4. Load the Models

  1. Setup
  2. Understanding the GLTFLoader.load method
  3. The onLoad function
  4. Load The Models
  5. Playback The AnimationClips

Load the three models, passing in a unique position for each

  const onError = ( errorMessage ) => { console.log( errorMessage ); };

  // load the first model. Each model is loaded asynchronously,
  // so don't make any assumption about which one will finish loading first
  const parrotPosition = new THREE.Vector3( 0, 0, 2.5 );
  loader.load( 'models/Parrot.glb', gltf => onLoad( gltf, parrotPosition ), onProgress, onError );

  const flamingoPosition = new THREE.Vector3( 7.5, 0, -10 );
  loader.load( 'models/Flamingo.glb', gltf => onLoad( gltf, flamingoPosition ), onProgress, onError );

  const storkPosition = new THREE.Vector3( 0, -2.5, -10 );
  loader.load( 'models/Stork.glb', gltf => onLoad( gltf, storkPosition ), onProgress, onError );

}

Now that we understand how the loader works, we can load the models and add them to our scene!

There are a couple of things to take note of here. First, as we mentioned, the loading takes place asynchronously.

We are loading the files in the order:

  1. Parrot
  2. Flamingo
  3. Stork

But there is no way to know which order they will finish loading in… or even if they will all finish loading! It’s the nature of the web that things can go wrong with loading files, and your app should always handle this as gracefully as possible.

In this app, if any of the files fails to load for some reason, there will be an error logged to the console, and that bird will not be displayed, but everything else will still work.

Next, we’ll create a new Vector3 object that will be used to set the position of the Parrot.

We’ve made quite a bit of use of the Vector3 object, but until now we’ve always been using the ones that were automatically created for Mesh.position and Mesh.scale.

Here we are creating our own Vector3 which will then be copied into the parrot’s position inside the onLoad callback function.


const parrotPosition = new THREE.Vector3( 0, 0, 2.5 );

The call to GLTFLoader.load() happens next on one long line.

If everything is working correctly, the birds should now show up in our scene. So far though, we have not set up the animations that were included in the files, so the models will be frozen in their initial position.

Frozen birds

5. Playback the AnimationClips

  1. Setup
  2. Understanding the GLTFLoader.load method
  3. The onLoad function
  4. Load The Models
  5. Playback The AnimationClips

We’re going to cover the three.js animation system in detail in Section 11: Animating Your Scenes. For now, we’ll just cover the minimum that we need to know in order to playback the AnimationClip included in each file.

5.1 A Little Setup

Create a mixers array at the top of the file. You’ll see why in a few moments

let scene;

const mixers = [];
const clock = new THREE.Clock();

function init() {

The setup here is quite simple - create an array called mixers at the top of the file. This array will hold one AnimationMixer for each model, which is the part of the three.js animation system responsible for attaching animations to models.

Get a reference to the animation clip for each loaded model inside the onLoad function

    model.position.copy( position );

    const animation = gltf.animations[ 0 ];

    const mixer = new THREE.AnimationMixer( model );
    mixers.push( mixer );

Next, get a reference to the AnimationClip from each loaded glTF file inside the onLoad function, and store it in a variable called animation. This contains the actual animation data.

5.2 Create a Clock

Create a clock at the top of the file

let scene;

const mixers = [];
const clock = new THREE.Clock();

function init() {
[

We need some way of accurately controlling the timing for our animations. The three.js Clock, which is a basic stopwatch, has us covered here so let’s create one now. When we call the constructor with no parameters, the timer will immediately start running.

We’ll use this clock to keep our animations in sync and updated at a steady rate even if our frame rate is fluctuating.

5.3 Create an AnimationMixer for Each Model

Create an animation mixer for each loaded object

    model.position.copy( position );

    const animation = gltf.animations[ 0 ];

    const mixer = new THREE.AnimationMixer( model );
    mixers.push( mixer );

    const action = mixer.clipAction( animation );
    action.play();

    scene.add( model );

  };

We’ll need an AnimationMixer for each of our three loaded bird models, so go ahead and create one inside the onLoad callback function.

The mixer’s job is to update the model as the animation progresses.

We’ll push this mixer into our array of mixers, and then later we’ll loop over the array once at the start of each frame, in the update function and tell the mixer how much time has passed since the previous frame.

The mixer takes a single parameter, which is the model whose animation it will control.

5.4 Create an AnimationAction for each clip

Create an AnimationAction using the mixer.clipAction method and tell it to play immediately

    model.position.copy( position );

    const animation = gltf.animations[ 0 ];

    const mixer = new THREE.AnimationMixer( model );
    mixers.push( mixer );

    const action = mixer.clipAction( animation );
    action.play();

    scene.add( model );

  };

We need to associate an AnimationAction with each AnimationClip. This controls the state of the clip - whether it is playing, stopped, paused, etc.

Instead of calling the AnimationAction constructor directly though, we’ll use AnimationMixer.clipAction.

You should always set up the action this way, since it associates the action with the mixer, and means that when we call mixer.update, the action will be updated too.

Finally, call AnimationAction.play() to set the state of the animation to playing - note that it won’t actually start playing until we start passing time values into the mixer, though. Let’s set that up now.

5.5 Update the Mixers at the Start of Every Frame

Get the elapsed (delta) time since the last frame and update each of the mixers

function update() {

  const delta = clock.getDelta();

  mixers.forEach( ( mixer ) => { mixer.update( delta ); } );

}

Now that everything is set up, the final piece of the puzzle is to update each of the mixers at the start of every frame in the update function. We’ll be using Array.forEach to loop over the array of mixers.

If you recall from Chapter 2, we’re using the browser’s built-in requestAnimationFrame to render our scene at around 60 frames per second, meaning that the time that has elapsed between any two frames should be around $16$ milliseconds - $16.666…$, to be precise.

However, there are plenty of reasons why it won’t be exactly $16.666ms$ - for example, the hardware we are running on may be too slow to run our app smoothly. Also, for VR applications the current target is $90$ frames per second on desktop, although that number may go up to $120$ or even $240$ FPS in the future.

For these reasons, we shouldn’t make any assumptions about the amount of time that will elapse between two frames in our application. Instead, we’ll use the Clock.getDelta method to count the elapsed time, or delta, from frame to frame, then update our animations by that much.

At this point, if everything is set up correctly, your birds should take flight!

Final Result

Beautiful

Well done, you made it to the end of the first section!

We’ve barely scratched the surface of what three.js can do, but we’ve covered a lot here - cameras, geometry, textures, physically based materials, global illumination, meshes, vectors, affine transformations, loading external models, the glTF asset format, and even the animation system, which is a complex beast.

I hope you’ve enjoyed the journey so far! The rest of the book will be available in a few months, and if you have any feedback, or just want to say hi, then feel free to get in touch with me on Twitter. I’d love to hear from you!