1.14 Loading 3D Models

In this tutorial, we'll explore how to load 3D models from files. Until now, we've been rendering simple geometries like planes and cubes, which are easily defined manually through vertex positions. However, real-world 3D applications, such as games, involve much more complex geometries that can't be crafted by hand. Thus, we need to learn how to load these geometries from files.

Launch Playground - 1_14_loading_models

We covered index buffers in the previous tutorial because using indices and a vertex pool is the standard method for storing geometries in most 3D file formats. Consequently, using an index buffer is the most natural approach to rendering geometry loaded from a file.

While numerous 3D file formats exist, many of which are highly complex and often use binary encoding for storage efficiency, we'll focus on one of the simplest: the .obj file format. Despite its simplicity, .obj is widely used and has the advantage of being human-readable, making it helpful for debugging model loading and rendering. However, it lacks support for advanced features like lighting and animations, making it primarily suitable for storing geometries.

Here's a snippet of an .obj file:

v 0.000000 3.000000 1.800000
...
vn 0.2867 0.6914 -0.6631
...
f 2152//24 2158//24 2358//24

An .obj file typically consists of consecutive data sections:

  1. Vertex positions (prefixed with 'v')

  2. Optional texture coordinates (prefixed with 'vt')

  3. Optional vertex normals (prefixed with 'vn')

  4. Face definitions (prefixed with 'f')

In this tutorial, we'll load a teapot model from an .obj file that includes normals but no texture coordinates. We'll discuss normals in detail when covering lighting.

The face section defines triangles using vertex indices. Each line begins with 'f', followed by indices referencing the vertex, texture coordinate, and normal sections, separated by slashes (/). This structure aligns well with the index buffer and vertex buffer we introduced earlier.

Rather than writing our own .obj file parser, we'll use an open-source parser called OBJFile.js. This parser generates a JavaScript object that allows us to query vertices and triangles.

Let's examine the code. The shader code remains unchanged. In the JavaScript code, we use the fetch API to load the teapot .obj file:

const objResponse = await fetch('../data/teapot.obj');
const objBody = await objResponse.text();

let obj = await (async () => {
    return new Promise((resolve, reject) => {
        let obj = new OBJFile(objBody);
        obj.parse();
        resolve(obj);
    })
})();

We use an asynchronous function because parsing large models can be time-consuming. Once the .obj file is loaded, we assemble our position buffer:

let positions = [];

for (let v of obj.result.models[0].vertices) {
    positions.push(v.x);
    positions.push(v.y);
    positions.push(v.z);
}

positions = new Float32Array(positions);

let positionBuffer = createGPUBuffer(device, positions, GPUBufferUsage.VERTEX);

let indices = [];

for (let f of obj.result.models[0].faces) {
    for (let v of f.vertices) {
        indices.push(v.vertexIndex - 1)
    }
}

const indexSize = indices.length;

indices = new Uint16Array(indices);

const indexBuffer = createGPUBuffer(device, indices, GPUBufferUsage.INDEX);

We query for vertices from the first model (index 0) in the .obj file. We loop through the vertex pool, pushing all vertex locations into our positions buffer. For the index buffer, we loop through all faces and their vertices, pushing the vertex indices into our indices array.

Note that we decrement the vertex index by one because .obj file format indices start from one, not zero. Remember to store the index size, which indicates how many indices need to be rendered.

Loaded Teapot Model
Loaded Teapot Model

With these updates, we now have a more interesting geometry to work with. For those new to computer graphics, you might be surprised to learn that the humble teapot has been a ubiquitous presence in graphics experiments, often serving as a canvas to demonstrate various lighting techniques and other graphic innovations. Its history within the realm of computer graphics is quite intriguing. If you're curious to explore this further, you can find more details by following this link.

Leave a Comment on Github