Loading External Models
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
- Setup
- Understanding the
GLTFLoader.load
method - The
onLoad
function - Load The Models
- 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
- Setup
- Understanding the
GLTFLoader.load
Method - The
onLoad
function - Load the Models
- 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
- Setup
- Understanding the
GLTFLoader.load
method - The
onLoad
function - Load The Models
- 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:url
, a string - this tells the loader where to find the file on the serveronLoad
, a callback function - this will get called when the model has finished loadingonProgress
, a callback function that gets called as the loading progressesonError
, 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:
- Load the file specified in the
url
- 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
- Setup
- Understanding the
GLTFLoader.load
method - The
onLoad
function - Load The Models
- 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:
- Extract the single bird model from each of the loaded files
- Move each bird into a unique position
- Set up the animations (we’ll come back to this at the end of the chapter)
- Add the model to the scene
Steps , , and will be the same for each model, while step 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 , however, we’ll need to pass in a second parameter called position
for step , 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:meshA.copy( meshB )
perspectiveCameraA.copy( perspectiveCameraB )
directionalLightA.copy( directionalLightB )
Or, as we are doing here:
vectorA.copy( vectorB )
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 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
- Setup
- Understanding the
GLTFLoader.load
method - The
onLoad
function - Load The Models
- 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:
- Parrot
- Flamingo
- Stork
But there is no way to know which order they will finish loading in… or even ifthey 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.
5. Playback the AnimationClips
- Setup
- Understanding the
GLTFLoader.load
method - The
onLoad
function - Load The Models
- 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-inrequestAnimationFrame to render our scene at around 60 frames per second, meaning that the time that has elapsed between any two frames should be around milliseconds - , to be precise.
However, there are plenty of reasons why it won’t be exactly - 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 frames per second on desktop, although that number may go up to or even 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!
Comments
Post a Comment