Nintendo 64 console with EverDrive cartridge

Nintendo 64 Part 15: A Spinning Cube

, Nintendo 64, Programming

No 3D project would be complete without making a spinning cube appear on the screen. It turns out that this is very easy on the Nintendo 64.

A cube in all its glory.

Vertex Data

First, I need to specify the vertexes of the cube. I can’t tell you how many times I’ve hard-coded the vertexes for a cube into a program, but I can tell you that I typed this in quickly and didn’t have to take out a sheet of paper to do it.

#define SZ 100
static const Vtx cube_vertex[4 * 6] = {
    // -X face, bright red.
    {{{-SZ, -SZ, -SZ}, 0, {0, 0}, {255, 128, 128, 255}}},
    {{{-SZ, -SZ, +SZ}, 0, {0, 0}, {255, 128, 128, 255}}},
    {{{-SZ, +SZ, -SZ}, 0, {0, 0}, {255, 128, 128, 255}}},
    {{{-SZ, +SZ, +SZ}, 0, {0, 0}, {255, 128, 128, 255}}},
    // +X face, dark red.
    {{{+SZ, -SZ, -SZ}, 0, {0, 0}, {128, 0, 0, 255}}},
    {{{+SZ, -SZ, +SZ}, 0, {0, 0}, {128, 0, 0, 255}}},
    {{{+SZ, +SZ, -SZ}, 0, {0, 0}, {128, 0, 0, 255}}},
    {{{+SZ, +SZ, +SZ}, 0, {0, 0}, {128, 0, 0, 255}}},
    // -Y face, bright green.
    {{{-SZ, -SZ, -SZ}, 0, {0, 0}, {128, 255, 128, 255}}},
    {{{+SZ, -SZ, -SZ}, 0, {0, 0}, {128, 255, 128, 255}}},
    {{{-SZ, -SZ, +SZ}, 0, {0, 0}, {128, 255, 128, 255}}},
    {{{+SZ, -SZ, +SZ}, 0, {0, 0}, {128, 255, 128, 255}}},
    // +Y face, dark green.
    {{{-SZ, +SZ, -SZ}, 0, {0, 0}, {0, 128, 0, 255}}},
    {{{+SZ, +SZ, -SZ}, 0, {0, 0}, {0, 128, 0, 255}}},
    {{{-SZ, +SZ, +SZ}, 0, {0, 0}, {0, 128, 0, 255}}},
    {{{+SZ, +SZ, +SZ}, 0, {0, 0}, {0, 128, 0, 255}}},
    // -Z face, bright blue.
    {{{-SZ, -SZ, -SZ}, 0, {0, 0}, {128, 128, 255, 255}}},
    {{{-SZ, +SZ, -SZ}, 0, {0, 0}, {128, 128, 255, 255}}},
    {{{+SZ, -SZ, -SZ}, 0, {0, 0}, {128, 128, 255, 255}}},
    {{{+SZ, +SZ, -SZ}, 0, {0, 0}, {128, 128, 255, 255}}},
    // +Z face, dark blue.
    {{{-SZ, -SZ, +SZ}, 0, {0, 0}, {0, 0, 128, 255}}},
    {{{-SZ, +SZ, +SZ}, 0, {0, 0}, {0, 0, 128, 255}}},
    {{{+SZ, -SZ, +SZ}, 0, {0, 0}, {0, 0, 128, 255}}},
    {{{+SZ, +SZ, +SZ}, 0, {0, 0}, {0, 0, 128, 255}}},
};
#undef SZ

Display List

For the display list, I want to use flat shading taken from the vertex colors. The vertex colors are sent to the color combiner using the RSP geometry mode G_SHADE, and the color combiner will write them to the framebuffer with the G_CC_SHADE mode.

The vertex data is loaded into the vertex cache with. The cube is small enough that the entire cube fits in cache—it’s only 24 vertexes. Note that the F3DEX2 XBUS microcode has a vertex cache with only 32 entries, so for more complex models, you would need to load the vertex data in chunks.

Because the cube is convex, G_CULL_BACK is enough to draw it correctly.

static const Gfx cube_dl[] = {
    gsDPPipeSync(),
    gsDPSetCycleType(G_CYC_1CYCLE),
    gsSPTexture(0, 0, 0, 0, G_OFF),
    gsSPSetGeometryMode(G_SHADE | G_CULL_BACK),
    gsDPSetCombineMode(G_CC_SHADE, G_CC_SHADE),
    gsSPVertex(cube_vertex, 4 * 6, 0),
    gsSP2Triangles(0, 1, 2, 0, 2, 1, 3, 0),
    gsSP2Triangles(4, 6, 5, 0, 5, 6, 7, 0),
    gsSP2Triangles(8, 9, 10, 0, 10, 9, 11, 0),
    gsSP2Triangles(12, 14, 13, 0, 13, 14, 15, 0),
    gsSP2Triangles(16, 17, 18, 0, 18, 17, 19, 0),
    gsSP2Triangles(20, 22, 21, 0, 21, 22, 23, 0),
    gsSPEndDisplayList(),
};

Graphics State and Matrixes

I need a place to store the matrixes in RAM where the RSP can read them. This storage should be duplicated for each graphics RSP task, so the renderer doesn’t overwrite a previous frame’s matrix data.

I created a structure to encapsulate all storage needed for dynamically changing graphics data. This now includes the display list pointers and a pointer to the framebuffer, just so everything needed by game_render() is in one structure:

struct graphics {
  Gfx *dl_start;
  Gfx *dl_end;
  uint16_t *framebuffer;

  Mtx projection;
  Mtx camera;
  Mtx model;
  Mtx rotate_y;
  Mtx rotate_x;
};

The actual drawing code mostly involves setting up the matrix state. The Nintendo 64 has two matrix stacks, reminiscent of old versions of OpenGL (naturally… both the Nintendo 64 and OpenGL were developed by SGI). One stack is the projection stack and the other is the modelview stack.

Gfx *game_render(struct graphics *restrict gr) {
  Gfx *dl = gr->dl_start;

  ...

  u16 perspNorm;
  guPerspective(&gr->projection, &perspNorm, 33, 320.0f / 240.0f,
                16, 1024, 1.0);
  guLookAt(&gr->camera,            //
           200.0f, 200.0f, 700.0f, // eye
           0.0f, 0.0f, 0.0f,       // look at
           0.0f, 1.0f, 0.0f);      // up
  gSPMatrix(dl++, K0_TO_PHYS(&gr->projection),
            G_MTX_PROJECTION | G_MTX_LOAD | G_MTX_NOPUSH);
  gSPMatrix(dl++, K0_TO_PHYS(&gr->camera),
            G_MTX_PROJECTION | G_MTX_MUL | G_MTX_NOPUSH);
  guMtxIdent(&gr->model);
  gSPMatrix(dl++, K0_TO_PHYS(&gr->model),
            G_MTX_MODELVIEW | G_MTX_LOAD | G_MTX_NOPUSH);
  guRotate(&gr->rotate_x, gs->rotate_x, 1.0f, 0.0f, 0.0f);
  gSPMatrix(dl++, K0_TO_PHYS(&gr->rotate_x),
            G_MTX_MODELVIEW | G_MTX_MUL | G_MTX_NOPUSH);
  guRotate(&gr->rotate_y, gs->rotate_y, 0.0f, 1.0f, 0.0f);
  gSPMatrix(dl++, K0_TO_PHYS(&gr->rotate_y),
            G_MTX_MODELVIEW | G_MTX_MUL | G_MTX_NOPUSH);

  ...

  gDPFullSync(dl++);
  gSPEndDisplayList(dl++);
  osWritebackDCache(gr, sizeof(*gr));
  return dl;
}

It Works

My first version left off the gsDPPipeSync at the beginning of the display list, and so it worked fine on emulators but gave me a completely black screen on real hardware. Remember to test on real hardware!

Here is the build:

Game rev 160 41.3 kB