welcome back to dolphin diary :)

in my last post i introduced the series and talked about my motivations for making a gamecube game. today i'll talk about the first step toward making the game - rendering 3d models!

the utah teapot rendering in dolphin
the famous utah teapot rendering in dolphin

the OBJ file format

i've chosen OBJ as my file format for working with 3d models. there are many other options out there (FBX and glTF, for example, are two other popular formats), but OBJ has a few advantages: it's simple to understand, it's a plain-text / ASCII format (which makes it easier to inspect and work with than a binary file), and it's widely supported by 3d modelling programs.

it also has a couple of disadvantages that are worth noting: the plain-text format means it has relatively large file sizes compared to an equivalent binary file, and it has no built-in support for animations. these aren't problems for me, since to keep the scope of this project manageable i intend to keep my models simple and avoid complex animations, but if you're considering using OBJ you should be aware of these limitations.

in the OBJ format, a line defining a vertex is indicated by a v. this is then followed by x, y, and z coordinates, separated by spaces (optionally, you can also include a w coordinate).

for example:

v 6.5453 -6.5453 8.1094

faces are indicated by an f, followed by the indices of the constituent vertices. indexing goes from the top of the file to the bottom and starts at one, so the first vertex in the file is index 1, the second is 2, and so on.

so, a triangle could be defined like this:

f 2 5 9

or a quad like this:

f 1 3 12 8

this is all we need to represent some 3d geometry!

as an example, here's a basic cube made of six quad faces (i adapted it from the one on this page of sample OBJ files):

v 0.0 0.0 0.0
v 0.0 0.0 1.0
v 0.0 1.0 0.0
v 0.0 1.0 1.0
v 1.0 0.0 0.0
v 1.0 0.0 1.0
v 1.0 1.0 0.0
v 1.0 1.0 1.0

f 1 5 7 3
f 1 3 4 2
f 3 7 8 4
f 5 7 8 6
f 1 5 6 2
f 2 6 8 4

faces can also include data about textures and normals with their vertices (using forward slashes / to separate the three values), but for the moment i only care about the geometry, so i'm tossing out that information. there are other features of OBJ, such as groups and curves, that i'm similarly ignoring.

i may implement some of these features later in the project (particularly textures), but others i will never get around to. this is one of the tricks to writing your own engine: only doing work you absolutely need to do. partially implementing a file format wouldn't be acceptable in a commercial engine, but when you're making tools for yourself it's fine. or better than fine: arguably, this kind of corner-cutting is an essential skill to finishing a game when you're working on your own.

defining a mesh data structure

in the next section, i'll explain how i get the data from a plain-text OBJ file into the game, but first we need to talk about how that data is represented in the program.

i'm writing this game in C, so any data that isn't a built-in type is defined as a struct. the structures i'm using to represent the geometry of a 3d model are pretty simple, so i'll just include them here verbatim:

typedef struct Vertex {
    Vector position;
} Vertex;

typedef struct Face {
    int vertexIndices[3];
} Face;

typedef struct Mesh {
    int vertexCount;
    int faceCount;
    Vertex vertices[MESH_VERTEX_MAX];
    Face faces[MESH_FACE_MAX];
} Mesh;

you'll notice this corresponds pretty closely to what we see in an OBJ file. we have a Vertex containing a position in 3d space (as a vector), a Face made of vertex indices, and then a Mesh made of those faces.

i want to point out a couple of simplifying assumptions i've made in the design of my data structures:

these are two more examples of doing as much as you need and no more. it may turn out later for performance or memory or other reasons that i need to revisit these decisions, but for now this will do.

converting the OBJ to runtime data

so how do we actually get our OBJ file loaded in our game at runtime? ultimately the game program will be a single binary, and there's no file system on the gamecube to read OBJ files from. so instead of including some kind of OBJ parser in the game itself, i've chosen to convert my OBJ files into the C data structure defined in the previous section and build them directly into the binary.

for that purpose, i've written a short script (in javascript) that takes in an OBJ file and spits out an H file that i can then include in my game program like any other C header file.

the whole script is less than a hundred lines of code:

var fileName = "teapot.obj";
print("file: " + fileName);

var obj = {
    vertices: [],
    faces: [],
};

// read in the source .obj file
var objIn = read("models/" + fileName);

// parse OBJ data
var data = objIn.split("\n");
for (var i = 0; i < data.length; i++) {
    var element = data[i].split(/\s+/);
    var id = element[0];

    if (id === "v") {
        // vertex
        var vertex = {
            x: parseFloat(element[1]),
            y: parseFloat(element[2]),
            z: parseFloat(element[3]),
        };

        obj.vertices.push(vertex);
    }
    else if (id === "f") {
        // face
        var face = [];  

        for (var j = 1; j < element.length; j++) {
            var vertex = element[j].split("/");
            // currently I'm ignoring everything except the vertex *index*
            var vertexIndex = parseFloat(vertex[0]);

            if (!isNaN(vertexIndex)) {
                vertexIndex--; // subtract one to index from zero
                face.push(vertexIndex);             
            }
        }

        if (face.length === 3) {
            // tri
            obj.faces.push(face);
        }
        else if (face.length === 4) {
            // quad (convert to two tris)
            obj.faces.push([face[0], face[1], face[2]]);
            obj.faces.push([face[0], face[2], face[3]]);
        }
        else {
            print("!!! face with invalid number of vertices: " + face.length);
        }
    }
}

// serialize as C struct
var meshName = fileName.split(".")[0];
var hOut = "";
hOut += "Mesh mesh_" + meshName + " = {\n";

hOut += "\t//vertex count:\n";
hOut += "\t" + obj.vertices.length + ",\n";
hOut += "\t//face count:\n";
hOut += "\t" + obj.faces.length + ",\n";

hOut += "\t//vertices:\n";
hOut += "\t{\n";
for (var i = 0; i < obj.vertices.length; i++) {
    var vertex = obj.vertices[i];
    hOut += "\t\t{ { " + vertex.x + ", " + vertex.y + ", " + vertex.z + " } },\n";
}
hOut += "\t},\n";

hOut += "\t// faces:\n";
hOut += "\t{\n";
for (var i = 0; i < obj.faces.length; i++) {
    var face = obj.faces[i];

    if (face.length > 3) {
        print("!!!! not a tri");
    }

    hOut += "\t\t{ { " + face[0] + ", " + face[1] + ", " + face[2] + " } },\n";
}
hOut += "\t},\n";

hOut += "};"

// write out the .h file
write("source/" + meshName + ".h", hOut);

and here is the output for the cube OBJ data from earlier (notice all the quad faces have been converted into triangles):

Mesh mesh_cube = {
    //vertex count:
    8,
    //face count:
    12,
    //vertices:
    {
        { { 0, 0, 0 } },
        { { 0, 0, 1 } },
        { { 0, 1, 0 } },
        { { 0, 1, 1 } },
        { { 1, 0, 0 } },
        { { 1, 0, 1 } },
        { { 1, 1, 0 } },
        { { 1, 1, 1 } },
    },
    // faces:
    {
        { { 0, 4, 6 } },
        { { 0, 6, 2 } },
        { { 0, 2, 3 } },
        { { 0, 3, 1 } },
        { { 2, 6, 7 } },
        { { 2, 7, 3 } },
        { { 4, 6, 7 } },
        { { 4, 7, 5 } },
        { { 0, 4, 5 } },
        { { 0, 5, 1 } },
        { { 1, 5, 7 } },
        { { 1, 7, 3 } },
    },
};

rendering the 3d model

we have our data in the game, so now it's time to finally draw it on screen!

once all the boilerplate to initialize the gamecube's video system is out of the way, the actual code to draw the model is pretty straightforward. first i set the gamecube into triangle-drawing mode, then iterate over all the faces of the mesh and send the position and color information for each vertex to the GPU for rendering. (since i haven't implemented any lighting yet, i'm just cycling through a set of pre-determined colors so i can tell the triangles apart for debugging purposes.)

float debugMeshColors[6][3] = {
    { 1.0f, 0.0f, 0.0f, },
    { 0.0f, 1.0f, 0.0f, },
    { 0.0f, 0.0f, 1.0f, },
    { 1.0f, 1.0f, 0.0f, },
    { 1.0f, 0.0f, 1.0f, },
    { 0.0f, 1.0f, 1.0f, },
};

void drawMesh(Mesh* mesh) {
    int debugMeshColorIndex = 0;

    GX_Begin(GX_TRIANGLES, GX_VTXFMT0, mesh->faceCount * 3);

    for (int i = 0; i < mesh->faceCount; i++) {
        // face
        Face face = mesh->faces[i];

        // vertices
        Vertex v1 = mesh->vertices[face.vertexIndices[0]];
        Vertex v2 = mesh->vertices[face.vertexIndices[1]];
        Vertex v3 = mesh->vertices[face.vertexIndices[2]];

        // cycle through different colors to help visualize triangles for debugging
        float r = debugMeshColors[debugMeshColorIndex % 6][0];
        float g = debugMeshColors[debugMeshColorIndex % 6][1];
        float b = debugMeshColors[debugMeshColorIndex % 6][2];
        debugMeshColorIndex++;

        // set positions
        GX_Position3f32(v1.position.x, v1.position.y, v1.position.z);
        GX_Color3f32(r, g, b);

        GX_Position3f32(v2.position.x, v2.position.y, v2.position.z);
        GX_Color3f32(r, g, b);

        GX_Position3f32(v3.position.x, v3.position.y, v3.position.z);
        GX_Color3f32(r, g, b);
    }

    GX_End();
}

the function calls starting with GX_ are what send instructions to the GPU (or at least, define the instructions to send). if you've ever used an older version of OpenGL (or another fixed function pipeline) this should look pretty familiar. in fact, the way i've been learning how to use the gamecube graphics functions is by referencing these example programs, which are adaptations of the examples from the classic NeHe OpenGL tutorials. reading through the NeHe tutorials and comparing the code with the devkitPro examples has been a good way to get started.

what exactly is going on under the hood of these GX functions? at a high level, my understanding from reading this excellent article on the architecture of the gamecube is that there's a special area in RAM where the CPU can write instructions known as "display lists" that define the geometry to render. the GPU then reads the display lists from RAM, and goes on to executes those instructions and do the graphics calculations that result in what you see on screen.

beyond that, well... this is where we reach the limits of my current knowledge of the hardware. the functions i'm using are part of libogc, a low-level gamecube library that comes with the toolchain i'm using, devkitPPC, so i haven't tried directly working with the display lists themselves. i'm interested in digging deeper into the nitty gritty technical details of communicating with the GPU later, but that will have to wait for another day.

thanks for reading!

gif of 3d models rotating in dolphin
the final result!


🐬 dolphin diary 🐬

this post is part of a series documenting my progress making a game for the gamecube. to see a full list of previous entries (and related resources), please click here! (p.s. you can also follow this blog via rss or email - thanks for reading!)

🌊🌊🌊