Load 3D Models in glTF Format
In the last chapter, we created a simple toy train model using some of the built-in three.js geometries, and it quickly became clear that it would be hard to build anything complex or organic using just these. To create beautiful 3D models, a sophisticated modeling program is required. You can use three.js to build any kind of 3D application, however, building a modeling app from scratch would be a huge amount of work. A much simpler solution is to use an existing program and export your work for use in three.js… or, cheat, and download any of the millions of amazing models and other scene assets that are available for free in many places around the web.
In this chapter, we’ll show you how to load some models that were created in
Blender, an open-source 3D graphics application that can be used for modeling, scene building, material creation, animation authoring, and more. Once you have created a model in Blender, you can export your work using a 3D format such as glTF, then use the
GLTFLoader
plugin to bring the model into three.js.
The Best Way to Send 3D Assets Over the Web: glTF
There have been many attempts at creating a standard 3D asset exchange format over the last thirty years or so. FBX, OBJ (Wavefront) and DAE (Collada) formats were the most popular of these until recently, although they all have problems that prevented their widespread adoption. For example, OBJ doesn’t support animation, FBX is a closed format that belongs to Autodesk, and the Collada spec is overly complex, resulting in large files that are difficult to load.
However, recently, a newcomer called glTF has become the de facto standard format for exchanging 3D assets on the web. glTF (GL Transmission Format), sometimes referred to as the JPEG of 3D, was created by the Kronos Group, the same people who are in charge of WebGL, OpenGL, and a whole host of other graphics APIs. Originally released in 2017, glTF is now the best format for exchanging 3D assets on the web, and in many other fields. In this book, we’ll always use glTF, and if possible, you should do the same. It’s designed for sharing models on the web, so the file size is as small as possible and your models will load quickly.
However, since glTF is relatively new, your favorite application might not have an exporter yet. In that case, you can convert your models to glTF before using them, or use another loader such as the FBXLoader
or OBJLoader
. All three.js loaders work the same way, so if you do need to use another loader, everything from this chapter will still apply, with only minor differences.
Whenever we mention glTF, we mean glTF Version 2. The original glTF Version 1 never found widespread use and is no longer supported by three.js
glTF files can contain models, animations, geometries, materials, lights, cameras, or even entire scenes. This means you can create an entire scene in an external program then load it into three.js.
Types of glTF Files
glTF files come in standard and binary form. These have different extensions:
- Standard .gltf files are uncompressed and may come with an extra .bin data file.
- Binary .glb files include all data in one single file.
Both standard and binary glTF files may contain textures embedded in the file or may reference external textures. Since binary .glb files are considerably smaller, it’s best to use this type. On the other hand, uncompressed .gltf are easily readable in a text editor, so they may be useful for debugging purposes.
Free glTF Files on the three.js Repo
There are lots of free glTF models available on the three.js repo, and amongst these are three simple and beautiful models of a parrot, a flamingo, and a stork, created by the talented people at mirada.com. These three models are low poly, meaning they’ll run on even the most low-power of mobile devices, and they are even animated.
You can find these three files in the editor, in the assets/models/ folder. In this chapter, we’ll load Parrot.glb, Flamingo.glb, and Stork.glb and then add the bird-shaped meshes each file contains to our scene. In the next chapter, we’ll show you how to play the flying animation that is included with each bird.
If you’re working locally rather than using the inline code editor, you’ll need to set up a webserver. Otherwise, due to browser security restrictions, you won’t be able to load these files from your hard drive.
The GLTFLoader
Plugin
To load glTF files, first, you need to add
the GLTFLoader
plugin to your app. This works the same way as adding the
OrbitControls
plugin. You can find the loader in
examples/jsm/loaders/GLTFLoader.js on the repo, and we have also included this file in the editor. Go ahead and locate the file now.
Importing and creating an instance of the loader works like this:
You can use one instance of the loader to load any number of glTF files.
The .load
and .loadAsync
Methods
All three.js loaders have two methods for loading files: the old callback-based
.load
method, and the newer Promise based .loadAsync
method. Again, refer to chapter
A.5 where we cover the difference between these two approaches in detail. Promises allow us to use async functions, which in turn results in much cleaner code, so throughout this book, we will always use .loadAsync
.
Set Up Main.js and World.js to Handle Async/Await
The await
keyword means “wait here until the model has loaded”. If you have previously dealt with loading models using callbacks or Promises, then await
will seem almost magical in its simplicity. However, we need to make a few adjustments to our code before we can use it since we can only use await
inside a function that has been marked as async
:
Another issue is that we cannot mark a constructor as async. A common solution to this is to create a separate .init
method.
This way, the constructor can handle the synchronous setup of the class, as usual, and then the init method will take over for asynchronous setup. We will use this approach, so we need to create a new World.init
method.
class World {
constructor() {
// synchronous setup here
// create camera, renderer, scene, etc.
}
async init() {
// asynchronous setup here
// load bird models
}
}
Go ahead and add an empty .init
method to World now, and make sure you mark it async
. Splitting the setup into synchronous and asynchronous stages like this gives us full control over the setup of our app. In the synchronous stage, we will create everything that doesn’t rely on loaded assets, and in the asynchronous stage, we’ll create everything that does.
Mark the main
Function as Async
Over in main.js, first, we must also mark the main function as async. This is required so that we can call the async World.init
method.
async function main() {
Now we can call both stages of setting up the World app. First, the synchronous constructor, as usual, then the new .init
method to handle asynchronous tasks.
async function main() {
// Get a reference to the container element
const container = document.querySelector('#scene-container');
// create a new world
const world = new World(container);
// complete async tasks
await world.init();
// start the animation loop
world.start();
}
Catch Errors
No method of loading files is complete unless we can also handle any errors that occur. Errors can be as simple as a typo in the file name, or something more complex like a network error. Fortunately, with async functions, error handling is also simple. At the bottom of main.js, replace this line:
main();
… with:
main().catch((err) => {
console.error(err);
});
Now any errors will be logged to the console. In a real app, you might want to do more sophisticated error handling, such as displaying a message to the user to let them know that something went wrong. However, while we are in development mode, the most important thing is that all errors are logged to the console where we can see them.
Create the birds.js Module
Now everything is set up and we can go ahead and load our first model. Open (or create) the components/birds/birds.js module. Start by importing the GLTFLoader
, then create an async loadBirds
function. Inside the function, create an instance of the loader, and finally, export the function at the bottom of the file:
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
async function loadBirds() {
const loader = new GLTFLoader();
}
export { loadBirds };
The structure of this new module should be familiar to you since it’s the same as nearly
every other component we have created so far. The only difference is the async
keyword.
Over in World, update the list of imports:
import { loadBirds } from './components/birds/birds.js';
import { createCamera } from './components/camera.js';
import { createLights } from './components/lights.js';
import { createScene } from './components/scene.js';
Load the Parrot
Now, we’re ready to load the Parrot.glb file using .loadAsync
. Once you have done so, log the loaded data to the console:
async function loadBirds() {
const loader = new GLTFLoader();
const parrotData = await loader.loadAsync('/assets/models/Parrot.glb');
console.log('Squaaawk!', parrotData);
}
Next, call the loadBirds
in World.init
:
async init() {
await loadBirds();
}
Data Returned by the GLTFLoader
We need to take a deeper look at the data we have just loaded before we can add the model to our scene, so for now we’ve simply logged the data to the console. Open up the browser console (press F12). You should see the word Squaaawk! followed by an Object containing the loaded data. This Object contains meshes, animations, cameras, and other data from the file:
gltfData.animations
is an array of animation clips. Here, there’s a flying animation. We’ll make use of this in the next chapter.gltfData.assets
contains metadata showing this glTF file was created using the Blender exporter.gltfData.cameras
is an array of cameras. This file doesn’t contain any cameras, so the array is empty.gltfData.parser
contains technical details about theGLTFLoader
.gltfData.scene
is aGroup
containing any meshes from the file. This is where we’ll find the parrot model.gltfData.scenes
: The glTF format supports storing multiple scenes in a single file. In practice, this feature is rarely used.gltfData.userData
may contain additional non-standard data.
__proto__
is a standard property that every JavaScript object has, you can ignore that.
Usually, all you need is .animations
, .cameras
, and .scene
(not .scenes
!) and you can safely ignore everything else.
Process the Loaded Data
Extracting data from a glTF file usually follows a predictable pattern, especially if the file contains a single animated model, as these three files do. This means we can create a setupModel
function and then run it on each of the three files. We’ll do this in a separate module. Open or create the birds/setupModel.js module, and create the function, following the now-familiar pattern:
function setupModel(data) {}
export { setupModel };
The idea of this function is that we can pass in the loaded data and get back the bird model, ready to be added to the scene. Next, import this new module into birds.js, then pass in the loaded data. Finally, return the results for use within World.
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { setupModel } from './setupModel.js';
async function loadBirds() {
const loader = new GLTFLoader();
const parrotData = await loader.loadAsync('/assets/models/Parrot.glb');
console.log('Squaaawk!', parrotData);
const parrot = setupModel(parrotData);
return { parrot }
}
Extract the Mesh from the Loaded Data
At this point, we have the unprocessed loaded data within the setupModel
function. The next step is to extract the model, and then do any processing to prepare it for use. The amount of work we need to do here depends on the model, and what we want to do with it. Here, all we need to do is extract the mesh, but in the next chapter, we’ll have a bit more work to do as we connect the animation clip to the mesh.
Look at the loaded data in the console again, and expand the gltfData.scene
. This a
Group
, and any meshes that are in the file will be
children of the group. These can be accessed using the
group.children
array. If you look inside there, you’ll see that glTF.scene.children
has only one object inside it, so that must be our parrot model.
Using this knowledge, we can finish the setupModel
function:
function setupModel(data) {
const model = data.scene.children[0];
return model;
}
Note A: if you click the toggle to complete the scene in the editor, then view the gltfData.scene.children
array in the console, it will be empty. This is because, by the time you look at it, the mesh has already been removed and added to the scene.
Note B: you could also just add the gltf.scene
to your scene since it’s a group. That would add an additional node to your scene graph but everything will still work. However, it’s best practice to keep your scene graph as simple as possible, since every node means additional calculations are required to render the scene.
Add the Mesh to the Scene
Over in World, loadBirds
now returns the parrot mesh and you can add it to the scene:
async init() {
const { parrot } = await loadBirds();
scene.add(parrot);
}
Load the Other Two Birds
You can use a single instance of the GLTFLoader
to load any number of files. When performing multiple asynchronous operations with async functions, you should (in most cases) use Promise.all
. We go into the reason for this in more detail
in the appendix, but here’s the short version.
First, here’s the obvious way of loading the other two files:
There’s a problem with this approach.
As we stated above, await
means wait here until the file has loaded. This means the app will wait until the parrot has fully loaded, then start to load the flamingo, wait until that has fully loaded, and finally start to load the stork. Using this approach, loading will take nearly three times longer than it should.
Instead, we want all three files to load at the same time, and the simplest way of doing this is to use Promise.all
.
const [parrotData, flamingoData, storkData] = await Promise.all([
loader.loadAsync("/assets/models/Parrot.glb"),
loader.loadAsync("/assets/models/Flamingo.glb"),
loader.loadAsync("/assets/models/Stork.glb"),
]);
Then we can process each file’s loaded data using the setupModel
function. Once we do that, here’s our (nearly complete) loadModels
function:
async function loadBirds() {
const loader = new GLTFLoader();
const [parrotData, flamingoData, storkData] = await Promise.all([
loader.loadAsync('/assets/models/Parrot.glb'),
loader.loadAsync('/assets/models/Flamingo.glb'),
loader.loadAsync('/assets/models/Stork.glb'),
]);
console.log('Squaaawk!', parrotData);
const parrot = setupModel(parrotData);
const flamingo = setupModel(flamingoData);
const stork = setupModel(storkData);
return {
parrot,
flamingo,
stork,
};
}
Over in World, you now have all three models. Add them to your scene:
async init() {
const { parrot, flamingo, stork } = await loadBirds();
scene.add(parrot, flamingo, stork);
}
Great! Well…
Just like visiting the zoo!
Move the Birds into Position
It is possible for models loaded from a glTF file to have a position already specified, but that’s not the case here, so all three models start at the point $(0,0,0)$, all jumbled together on top of each other. We’ll adjust the position of each bird to make it look like they are flying in formation:
const parrot = setupModel(parrotData);
parrot.position.set(0, 0, 2.5);
const flamingo = setupModel(flamingoData);
flamingo.position.set(7.5, 0, -10);
const stork = setupModel(storkData);
stork.position.set(0, -2.5, -10);
Final birds.js Module
The birds.js module is now complete. Here’s the final code:
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { setupModel } from './setupModel.js';
async function loadBirds() {
const loader = new GLTFLoader();
const [parrotData, flamingoData, storkData] = await Promise.all([
loader.loadAsync('/assets/models/Parrot.glb'),
loader.loadAsync('/assets/models/Flamingo.glb'),
loader.loadAsync('/assets/models/Stork.glb'),
]);
console.log('Squaaawk!', parrotData);
const parrot = setupModel(parrotData);
parrot.position.set(0, 0, 2.5);
const flamingo = setupModel(flamingoData);
flamingo.position.set(7.5, 0, -10);
const stork = setupModel(storkData);
stork.position.set(0, -2.5, -10);
return {
parrot,
flamingo,
stork,
};
}
export { loadBirds };
Center the Camera on the Parrot
The very last thing we’ll do is
adjust the OrbitControls
target. Currently, this is in its default position, the center of the scene. Now that we have moved the birds into formation, this ends up being somewhere around the tail of the parrot. It would look better if the camera focused on the center of the bird rather than its tail. We can easily set this up by copying the parrot.position
into controls.target
. However, to do so, we need to access controls
within .init
, so first, let’s convert it to a module-scoped variable.
let camera;
let controls;
let renderer;
let scene;
let loop;
constructor(container) {
camera = createCamera();
renderer = createRenderer();
scene = createScene();
loop = new Loop(camera, scene, renderer);
container.append(renderer.domElement);
controls = createControls(camera, renderer.domElement);
const { ambientLight, mainLight } = createLights();
loop.updatables.push(controls);
scene.add(ambientLight, mainLight);
const resizer = new Resizer(container, camera, renderer);
}
Now, the controls are accessible from .init
and we can move the target to the center of the parrot.
async init() {
const { parrot, flamingo, stork } = await loadBirds();
// move the target to the center of the front bird
controls.target.copy(parrot.position);
scene.add(parrot, flamingo, stork);
}
Next up, we’ll introduce the three.js animation system and show you how to play the animation clips that were loaded alongside the bird models.