Page 9: Loading Models

Spring 2026 Sample Solution

Most (graphics) objects in Three will be collections of triangles put into a Mesh (JavaScript) object.

There are three main ways to get the collections of triangles:

  1. We can programmatically build the collection of triangles ourselves. This requires careful assembly of the BufferGeometry data structures. We’ll come back to this later in the course.
  2. We can use the built-in shapes. Three provides a bunch of these, but they are hardly everything we want.
  3. We can load a model in from a file. This allows us to create the object in a design program or find something on the web.

Three includes “loaders” for a number of common file formats.

A Note on File Formats

Graphics file formats (for 3D geometry) are historically problematic.

Describing a collection of triangles shouldn’t be very hard. However, the other information we want along with the triangles can get complicated. This is because different systems might have different ways to describe material properties, grouping, or hierarchy.

Modern file formats try to be very general: they want to capture all information about a model. But this makes them complex. And it means that things may not be portable: a model might use a feature that your system doesn’t support.

To fully capture all the information about a model, the fancy file formats need to store different kinds of information. This means the models can get very large and complicated. Many of the fancy file formats were proprietary, not well-documented, or both.

Historical file formats were less ambitious. They would store just the triangle information, or minimal additional information. But because of that, these formats couldn’t capture appearance information. Or worse, different programs would use non-standard hacks to try to shoehorn in additional information.

All that said, there are some new open standards that promise to provide file formats that can reliably transmit all the information about objects. A problem with these formats is that they often work too well: they have more information than we want/need. We want simple geometry and basic colors, and we get fancy stuff (that we will discuss later in class).

Some file formats you might encounter as you create workbooks:

  • OBJ - This is a historical format for triangle data. It is old and simple, and mainly stores collections of triangles. However, it isn’t always reliable because everyone interpreted it differently.
  • FBX - This is a proprietary format used by game development tools. Files in this format are sometimes broken, because the company that made the format doesn’t document it well.
  • GLTF - This is the emerging modern file format, and probably should be preferred. It is pretty new, but it seems to be well supported. Note that the files often have the .glb extension - so they are a binary format. This has the downside that it is not human-readable, but it can incorporate more kinds of data inside.

In the workbook, we generally discuss OBJ files (for historical reasons). But the methods for loading other object types in three are similar. For example, here is an example loading a .glb file (that I got from Poly Haven).

There are a few lessons hidden here…

  1. While I wanted a simple model, I got a very detailed one with fancy appearance data (textures, which we will learn about later).
  2. I had no idea what the scale of the model was - the program makes a total guess at how big things are. It’s a little small (and I didn’t change it).
  3. You can find nice models on the web! This model from Poly Haven is released under CC0 (public domain).

The code is very similar to how we load OBJ files (explained in a moment). Gemini wrote the code and it was very careful with error checking. It uses callbacks (explained below).

Loading Objects

If we want an object that isn’t built in, THREE gives us the ability to load it from a file.

A common file format for 3D models (as collections of triangles) is the obj file format. The THREE loader is part of their “examples” - we include it in the parts of THREE we provide for class, but the documentation isn’t very complete.

The trick with loading an object is that it may take time. So we perform loading asynchronously. We start the loader, and it runs in the “background” (although, the browser isn’t truly parallel, so it may not get its turn until we return to the event loop). Later, once the object is loaded, we need to handle it correctly.

There are many different ways to handle asynchronous programming in JavaScript. The traditional way to do it is with a callback function. When we start the loader, we also provide a function (the callback) that gets called when loading is finished. This function needs to set the object up (put it into the scene, position and scale it, …). Here’s an example in 04-09-01.js (04-09-01.html):

04-09-01   view Kinds of boxes:
view - look at the box and experiment with it
examine - look at the code for the box
edit - change the box's content
rubric - several steps are suggested in the rubric page - form elements on page in rubric
   04-09-01.html   04-09-01.js

The relevant code:

33
34
35
36
37
38
39
40
let loader = new OBJLoader();
loader.load("./objects/07-astronaut.obj", function(astronaut) {
        astronaut.position.set(1.5, 4, 0);
        astronaut.scale.set(0.5, 0.5, 0.5);
        scene.add(astronaut);
        // note that we have to render
        renderer.render(scene, camera);
    });

Notice that this code creates a loader, and then asks the loader to load the file ./objects/07-astronaut.obj. In this call, it passes another (anonymous) function that will be called when loading is complete. This function is passed the loaded object. Note how the function positions the object (setting its position), scales it, and places it into the scene. We didn’t have to rotate the object since it was already oriented the right way. Also, notice that we have to call render to draw the object after the object is loaded. If render is being called continuously (for example, in an animation loop), that can take care of making sure drawing happens after the object is loaded.

One other tricky thing: notice that the astronaut variable is a local variable. If you want to access the astronaut after it’s loaded (for example, to animate it), you need to have some way to access it from elsewhere in your program. For example, you might want to store it in a global variable. Be careful: remember that your code continues to run while the object is getting loaded.

Hints on loading

Loaders are tricky because they are asynchronous. Learning to deal with asynchronous programming is good practice. It will come up for other things in three, but it comes up in web programming often. For example, when you send a request to get something over the web, you start the process and then do something else until it’s ready.

With file loaders in three, there are two other things to be aware of (spoiler alert, discovering these on your own is good practice for learning how to understand a program or API):

  1. Sometimes THREE requires you to tell it when you change an object. THREE aggressively avoids re-computing things - so you need to tell it when something changes so it knows to recompute. Matrices and materials are two examples.

  2. The loaders don’t always create the object types you think they will. Inspect what object you get (for example, look at the type of astronaut after it is loaded (lines 37 and 49). Use the debugger to place a breakpoint (good practice!). You can do this for astro on line 60 as well. At the end of 04-09-01.js you will see examples that change the material to a yellow color.

In both cases, THREE is doing something smart. For 1, it is trying to be efficient. For 2, it allows loading in files that contain multiple meshes.

If the loader gives you a Group object, you can look through its tree of objects to find the Mesh objects if you need them. For example, to color the loaded astronaut yellow, I used the following code:

75
76
77
78
79
astro.traverse(obj => {
        if (obj instanceof T.Mesh) {
            obj.material = new T.MeshStandardMaterial({color:"yellow"});
        }
    });

A Note on File Formats: In this example, we are using a “.OBJ” file - which is a historic format for triangle data. Three.JS has importers for many different file formats.

Hint on Asynchronous Programming

Three loaders are asynchronous. Loading doesn’t happen instantly. Instead, we start the process, and then continue once the object is ready.

Asynchronous programming is tricky because we need to stop what we are doing, and then start again when the time comes. Fortunately, JavaScript provides mechanisms that make this cleaner to read.

The “classic” mechanism for asynchronous programming is the callback function (as mentioned above - but repeated here). The idea is that we provide a function that is called when things are ready.

33
34
35
36
37
38
39
40
let loader = new OBJLoader();
loader.load("./objects/07-astronaut.obj", function(astronaut) {
        astronaut.position.set(1.5, 4, 0);
        astronaut.scale.set(0.5, 0.5, 0.5);
        scene.add(astronaut);
        // note that we have to render
        renderer.render(scene, camera);
    });

The callback mechanism is also how Gemini loaded the traffic cone using the GLB loader. We can look at its code (some extra stuff removed):

 1const loader = new GLTFLoader();
 2        loader.load(
 3            'trafficcone.glb', 
 4            (gltf) => {
 5                // Success
 6                const model = gltf.scene;
 7                scene.add(model);
 8                status.innerText = 'trafficcone.glb from poly.cam loaded';
 9            },
10            (xhr) => {
11                // Progress
12                const percent = (xhr.loaded / xhr.total) * 100;
13                status.innerText = `Loading: ${Math.round(percent)}%`;
14            },
15            (error) => {
16                // Error
17                console.error('An error happened', error);
18                status.innerText = 'Error loading test.glb - Check console';
19                status.style.color = '#ff4444';
20            }
21        );
The first difference is that this uses GLTFLoader not OBJLoader. But the more important difference is that this code defines 3 callbacks! The first callback (lines 4-9) is very similar to the one in the astronaut loader: once the object successfully loads, add it to the scene. Note: it doesn’t have to call render, since the program is running an animation loop. The next two callbacks allow the program to provide periodic updates and to handle errors.

The callback method works fine. Gemini chose it. However, there are newer ways to do asynchronous programming in JavaScript. JavaScript provides two mechanisms (one actually builds on the other). They are meant to make the code cleaner. For something simple like these loader callbacks, they offer little advantage. However, for more complicated situations, they can really help.

The second method for asynchronous programming is a JavaScript concept called a promise. A promise is an object that a function can return. The idea is that rather than being the actual thing you expect the function to provide, the function provides a “promise” that you can check later to see what the actual thing you wanted is.

Here is the astronaut loader using a promise:

45
46
47
48
49
50
51
52
let obj = loader.loadAsync("./objects/07-astronaut.obj");
obj.then(function(astronaut) {
    astronaut.position.set(-2, 4, 0);
    astronaut.scale.set(0.5, 0.5, 0.5);
    scene.add(astronaut);
    // note that we have to render
    renderer.render(scene, camera);
    });

What happens: we call the loader. We expect to get an object, but what it really provides us with is a promise. (We used a special version of the loader method that provides a promise.) We could go off to do other stuff until we want the object to “make good” on its promise. When we’re ready for the promised object, we access the then method of the promise, where we provide a callback that receieves the object.

In this case, the promise version is very similar to the callback version. The then clause is basically the same as the callback. Promises have a number of advantages in how they encapsulate the asynchronous behavior. For example, they provide a uniform way to handle errors, and they can be passed around (since they are objects).

The third approach to asynchronous programming builds on promises. If something returns a promise to us, we can use the await keyword to resolve it. What await does is it waits for the promise to resolve and then returns the value that the promise resolved to. For loading the astronaut, this looks like:

59
60
61
62
let astro = await loader.loadAsync("./objects/07-astronaut.obj");
astro.position.set(-0, 4, -2);
astro.scale.set(0.5, 0.5, 0.5);
scene.add(astro);

This looks almost like non-asynchronous programming: the loader simply gives us the value we want. The downside is that our program “stops” until the promise is ready. It doesn’t actually block: the function returns and then restarts at the appropriate time.

While the await method is very clean in simple cases, it gives up control of what we do while waiting.

Next: Page  10 - Transformation in Three.js