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 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:
- the OBJ file format supports faces that are triangles or quads, but my
Face
struct is hard-coded to contain 3 vertices. this helps simplify rendering code since i don't have to manage switching between primitive types, but i'll have to convert any quads in my OBJ files to two triangles, which you'll see in my conversion script below. - similarly, instead of using a dynamically-sized data structure (like a linked list, say) to hold the vertices and faces for the mesh, i opted to use simple arrays. this is a lot easier for me to implement, at the cost of putting a hard cap on the size of my meshes.
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!
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!)
🌊🌊🌊