Discover three.js is now open source!
Word Count:3537, reading time: ~17minutes

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.

This entire scene fits in a single .glb file.

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:

Import and create an instance of the GLTFLoader
    
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'

const loader = new GLTFLoader();

  

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.

GLTFLoader.loadAsync
    
const loader = new GLTFLoader();

const loadedData = await loader.loadAsync('path/to/yourModel.glb');

  

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:

You can only use await inside an async function
    
async function loadingSuccess() {
  // inside an async function: OK!
  await loader.loadAsync('yourModel.glb');
}

function loadingFail() {
  // not inside an async function: ERROR!
  await loader.loadAsync('yourModel.glb');
}

  

Another issue is that we cannot mark a constructor as async. A common solution to this is to create a separate .init method.

The constructor of a class cannot be async
    
class Foobazzer {
  constructor() {
    // constructor cannot be async: ERROR!
    await loader.loadAsync('yourModel.glb');
  }

  async init() {
    // inside an async function: OK!
    await loader.loadAsync('yourModel.glb')
  }
}

  

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.

We will create a new World.init method to handle asynchronous setup
    

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.

main.js: mark main as async
    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.

main.js: call both synchronous and asynchronous stages of World setup
    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.js: calling the main() function
    
main();

  

… with:

main.js: add a catch method to handle errors
    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:

birds/birds.js: initial structure
    
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:

World.js: import components
    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:

birds.js: load the Parrot
    
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:

World.js: load the birds!
    
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:

Data return by the GLTFLoader
    
{
  animations: [AnimationClip]
  asset: {generator: "Khronos Blender glTF 2.0 I/O", version: "2.0"}
  cameras: []
  parser: GLTFParser {json: {…}, extensions: {…}, options: {…}, cache: {…}, primitiveCache: {…}, …}
  scene: Scene {uuid: "1CF93318-696B-4411-B672-4C12C46DF7E1", name: "Scene", type: "Scene", parent: null, children: Array(0), …}
  scenes: [Scene]
  userData: {}
  __proto__: Object
}

  
  • 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 the GLTFLoader.
  • gltfData.scene is a Group 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:

birds/setupModel.js: initial structure
    
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.

birds.js: process loaded data
    
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:

setupModel.js: extract the model from the loaded data
    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:

World.js: add the mesh 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:

Load multiple glTF files, the WRONG way
    
// Don't do this!
const parrotData = await loader.loadAsync('/assets/models/Parrot.glb');
const flamingoData = await loader.loadAsync('/assets/models/Flamingo.glb');
const storkData = await loader.loadAsync('/assets/models/Stork.glb');

const parrot = setupModel(parrotData);
const flamingo = setupModel(flamingoData);
const stork = setupModel(storkData);

  

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.

birds.js: load the other two file using 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:

birds.js: load and then process multiple glTF files
    
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:

World.js: add the second two birds to the 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:

birds.js: move the birds into position
      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:

birds.js: 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.

World.js: make controls a module scoped variable
    let camera;
let controls;
let renderer;
let scene;
let loop;

  
World.js: make controls a module scoped variable
      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.

World.js: target the parrot with the camera
      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.

Challenges

Import Style
Selected Texture