Loading External Models 1.7

LOADING EXTERNAL MODELS

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

Even for this simple model, our initMeshes function came in at around 50 lines. Creating anything with much more detail will quickly become very complicated, and creating a realistic model like a human body is essentially impossible.

To do something like that, we will 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 10: Working with other applications to studying the best practices for making this process as smooth as possible.

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

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, especially 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 is rapidly becoming the format for sharing 3D assets.

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 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 10.

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 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( frontLight, backLight );

}

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, 50 );
  loader.load( 'models/Parrot.glb', gltf => onLoad( gltf, parrotPosition ), onProgress, onError );

  // load the second model
  const flamingoPosition = new THREE.Vector3( 150, 0, -200 );
  loader.load( 'models/Flamingo.glb', gltf => onLoad( gltf, flamingoPosition ), onProgress, onError );

  // load the third model
  const storkPosition = new THREE.Vector3( 0, -50, -200 );
  loader.load( 'models/Stork.glb', gltf => onLoad( gltf, storkPosition ), onProgress, onError );

}

function initRenderer() {

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="https://threejs.org/examples/js/controls/OrbitControls.js"></script>

		<!--

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

		-->

		<script src="https://threejs.org/examples/js/loaders/GLTFLoader.js"></script>

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

Once again, we’ll include the file directly from the three.js website, so just add the above line to your index.html file and we’ll be good to go.

1.2. Rename initMeshes to loadModels

Rename initMeshes to loadModels

  scene.background = new THREE.Color( 0x8FBCD4 );

  initCamera();
  initControls();
  initLights();
  loadModels();
  initRenderer();

  renderer.setAnimationLoop( () => {

…and delete everything from inside it

  scene.add( frontLight, backLight );

}

function loadModels() {

  // ready and waiting for your code!

}

function initRenderer() {

Our setup step here is very similar to our setup step from the previous chapter. Just delete everything inside the initMeshes function. This time though, we’re not creating meshes manually so we’ll rename this function more accurately to loadModels.

1.3 Update the Camera

Update the camera’s far clipping plane and move it back so that we can see the models

function initCamera() {

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

  camera.position.set( -50, 50, 150 );

}

We’ve mentioned several times that units in three.js are meters, by convention. Now we see why the by convention part of that statement is important. Other applications and modeling programs (or 3D artists) may not honor this convention and may make very huge or very tiny models.

This is the cause of a common point of frustration - you load up a model and add it to your scene. Everything is set up and running correctly, there are no console errors… but where is your model?

Well, it may be so big, or so tiny, that it’s outside of the bounds of your camera’s viewing frustum.

Here, the bird models are on the scale of 10 to 100 meters long - these are some BIG birds! Either the artist who made these didn’t care about correct sizes, or they were using a different scale than us, say, one unit to one centimeter.

We’ll get around this here by increasing the size of the camera’s frustum and moving the camera back quite a bit. We could also (and perhaps, more properly) scale down the birds to be the size of actual birds.

We’ll examine a few methods for dealing with this in Section 10: Working with other applications, but for this introductory chapter we’ll cheat and set the camera’s position to the above pre-calculated value.

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

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

2.1 The url Parameter

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.

The url parameter points to a file on your server that you want to the loader to load. In this case, we will be loading three files called Parrot.glb, Flamingo.glb, and Stork.glb, where 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, so it’s best to stick with the binary version if possible.

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 );

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: {}
}

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.

Whenever the file does finish loading, the result gets passed to the onLoad function, which is a special kind of function known as a callback function.

The loader returns the result of the loading operation back to this function as a single variable which we’ll call gltf.

Once the loader finished the loading the file, assuming everything has worked correctly, it calls the onLoad function, which then logs the loaded object to the console.

Take a look at the gltf object that was logged to the console. It looks kind of complicated, but in this chapter, we’re interested in just two of its properties:

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. We’ll explore these and the other components of the three.js animation system in Section 8: Animation.

gltf.scene

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

The loader returns an entire Scene for us, with any models already placed inside, so if we wish we can just replace our scene 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 can find each of the models at gltf.scene.children[ 0 ] in the respective files.

We’ll ignore the other entries 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 bar. 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, for example, if you typed the url wrong, or cross-origin errors meaning that either your server is not set up correctly or you are trying to load the file from your local file system.

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

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
  // 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 = () => {};

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 of them the same, and in what respects differently?

Here, we want to do the following four steps:

  1. Extract the single bird model from the loaded gltf object
  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 definition for our function will look like this (position will be a Vector3):


const onLoad = ( gltf, position ) => {

Next, we need to get a reference to the model from with the gltf.scene. Fortunately, it looks like each of these files was created by the same person, so we can find all of the models in the same place:


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

This won’t always be the case, and for this step, you will usually need to examine the gltf object in the console to see where the models you want are located.

Next, we’ll copy the data from the position vector into the model.position using a new 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, 50 );
  loader.load( 'models/Parrot.glb', gltf => onLoad( gltf, parrotPosition ), onProgress, onError );

  // load the second model
  const flamingoPosition = new THREE.Vector3( 150, 0, -200 );
  loader.load( 'models/Flamingo.glb', gltf => onLoad( gltf, flamingoPosition ), onProgress, onError );

  // load the third model
  const storkPosition = new THREE.Vector3( 0, -50, -200 );
  loader.load( 'models/Stork.glb', gltf => onLoad( gltf, storkPosition ), onProgress, onError );

}

function initRenderer() {

Now that everything is set up, 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, 50 );

The call to GLTFLoader.load() happens next on one long line. Splitting it over a few lines to make it more readable, it looks like this:


loader.load(
  'models/Parrot.glb', // url
  gltf => onLoad( gltf, parrotPosition ), // onLoad callback
  onProgress, // onProgress callback
  onError // onError callback
);

Take a look at the way we are passing in the onLoad function:


gltf => onLoad( gltf, parrotPosition )

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 );

}

Aside from the technical differences between arrow functions and regular functions, in this case, the two ways of doing this are identical.

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 8. For now, we’ll just cover the minimum that we need to know in order to play back the AnimationClips that were included in each file.

The process of playing back an animation in three.js goes like this:

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. We’ll loop over the mixers in our update function to update each animation at the start of every frame, as we’ll see below.

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

  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 );

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’ll create a Clock, which is a basic stopwatch. When we call the constructor with no parameters, the timer will immediately start running.

We’ll use it below 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

  // 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’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 animated model whenever we set the animation to a new time.

We’ll push the mixer into our array of mixers, and then later we’ll loop over them once at the start of each frame, in the update function, and move each of them forwards by the amount of time that has passed since the previous frame.

5.4 Create an AnimationAction for each clip

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

  // 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 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.

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

  container.appendChild( renderer.domElement );

}

function update() {

  const delta = clock.getDelta();

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

}

function render() {

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.

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.666ms$.

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.

Final Result

Beautiful

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

Even though we’ve barely scratched the surface of what three.js can do, we’ve covered a lot - cameras, geometry, textures, physically based materials, global illumination, meshes, vectors, transformations, loading external models, 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!