Skip to content

L09 — Meshes: vertices, triangles, winding

Goal

See 3D (and 2D) shapes as triangles made from vertices. Each vertex can carry extra data (especially color). Learn why winding order is a real thing, not pedantry.

If “mesh” is a new word

A mesh is the drawable shape: a bunch of corners connected into triangles.

  • A triangle has 3 corners.
  • A rectangle is 2 triangles.
  • A low-poly character might be hundreds or thousands of triangles.

GPUs (and the N64 RDP path we’re aiming at) are built around triangles. Almost every 3D model is “triangles all the way down.”

What you will see (ROM)

bash
source scripts/env.sh
make -C lessons/l09-meshes
WhatWhy it’s there
One triangleSmallest interesting mesh
Red / green / blue cornersPer-vertex colors
Colors blending in the middleHardware interpolates across the triangle (Gouraud shading)
Stick XRotate
AFlip winding (swap two corners’ order)

Stare at the middle of the triangle: you don’t store a color for every pixel; the GPU blends corner colors. That’s the same mechanism vertex lighting and vertex paint will use.


Anatomy (build-up)

Vertex

A vertex is a corner. At minimum: a position. Often also:

AttributeNewbie meaning
PositionWhere is this corner?
ColorWhat color is painted on this corner?
NormalWhich way does the surface face? (for lighting — Module 2)
UVWhich pixel of a texture maps here? (Module 2–3)

Triangle

Three vertices (or three indices into a vertex list). One filled polygon.

Mesh

Many triangles, usually sharing vertices so edges match.

text
Vertex  = position + attributes
Triangle = 3 vertices
Mesh     = many triangles

Vertex color (first meeting)

Instead of one color for the whole triangle, each corner can have its own RGBA. The rasterizer blends between them.

Why care on N64?

  • Cheap detail (warm vs cool side of a rock)
  • Later: paint the terrain in Blender and multiply with a tiling texture (L10 + Module 3)

L09 shows the blend with loud RGB so you can’t miss it.


Winding order (the “why is my triangle gone?” topic)

List the three corners in a consistent order when viewed from the front — e.g. counter-clockwise.

text
  0
 / \
1---2     order 0→1→2 might be CCW (example)

Engines can cull (throw away) triangles that face away from the camera, using winding to decide “front” vs “back.” That saves drawing leaves of a tree from the inside, etc.

A in the ROM swaps two vertices. You’re not changing positions of the shape’s idea — you’re changing order. On some setups the triangle would disappear when back-facing; here we still draw it, but you practice the idea that order is data.

Authoring tip (future you)

In Blender, “recalculate normals outside” and consistent normals are cousins of this idea. Inside-out meshes often look dark or missing faces.


Indexed meshes (why games don’t duplicate everything)

Suppose a quad (two triangles, four corners):

text
vertices:  v0, v1, v2, v3
indices:   0,1,2,  0,2,3

Both triangles share v0 and v2. Benefits:

  • Less memory
  • Shared attributes stay seamless (same color/normal at the shared edge)

When you want a hard crease, artists split vertices (two verts in the same place, different normals).


How this ROM cheats (honest teaching)

We draw with libdragon RDP screen-space shaded triangles (TRIFMT_SHADE), and we transform corners with ng_math ourselves.

That’s intentional:

  • You see vertices + colors now
  • Module 2 loads real Tiny3D meshes and lets the RSP help

Same concepts; different pipeline maturity.


Common noob confusions

FeelingReality
“Meshes are files only”A file is stored triangles; in RAM it’s still verts + tris
“Smooth color means a texture”Not necessarily — vertex color alone can blend
“Winding is random”It’s a deliberate front-face convention
“One triangle isn’t a mesh”It’s the smallest mesh; scale up from here

Exercises

  1. Change the three corner colors in main.c; rebuild.
  2. Hold a mental image: cube = 6 faces × 2 tris = 12 triangles (more if smoothed/split).
  3. On paper, square corners 0–3; write indices for two triangles.
  4. Press A a few times — predict what “flip winding” did to the vertex list.

Full lesson source

The blocks below are imported from the real repository files at build time (VitePress <<< snippets). They are not hand-copied into this markdown.

lessons/l09-meshes/Makefile · lessons/l09-meshes/src/main.c

lessons/l09-meshes/Makefile
make
ROMNAME   := l09_meshes
ROM_TITLE := "L09 Meshes"
USE_NG_MATH := 1

include ../../common/lesson.mk
lessons/l09-meshes/src/main.c
c
/**
 * L09 — Meshes: vertices, triangles, winding, vertex color
 * ============================================================================
 * Mesh = triangles. Vertex = corner with attributes (here: pos + RGBA).
 * Colors blend across the triangle (Gouraud). A flips winding order.
 * Stick X rotates. RDP screen triangles stand in before Tiny3D meshes.
 * DOCS: docs/guide/m1/l09-meshes.md
 */


#include <libdragon.h>
#include <stdio.h>
#include "ng_math.h"

int main(void)
{
    display_init(RESOLUTION_320x240, DEPTH_16_BPP, 2, GAMMA_NONE,
                 FILTERS_RESAMPLE);
    rdpq_init();
    rdpq_text_register_font(1, rdpq_font_load_builtin(FONT_BUILTIN_DEBUG_VAR));
    joypad_init();

    float rot = 0.f;
    bool flip_winding = false;
    char line[64];

    while (1) {
        joypad_poll();
        joypad_inputs_t in = joypad_get_inputs(JOYPAD_PORT_1);
        joypad_buttons_t pressed = joypad_get_buttons_pressed(JOYPAD_PORT_1);
        rot += (float)in.stick_x * 0.002f;
        if (pressed.a) {
            flip_winding = !flip_winding;
        }

        /* Local triangle */
        ng_vec3 p0 = ng_v3(0.f, -50.f, 0.f);
        ng_vec3 p1 = ng_v3(-55.f, 45.f, 0.f);
        ng_vec3 p2 = ng_v3(55.f, 45.f, 0.f);
        if (flip_winding) {
            ng_vec3 tmp = p1;
            p1 = p2;
            p2 = tmp;
        }

        ng_mat4 M;
        ng_mat4_trs_z(&M, ng_v3(160.f, 120.f, 0.f), rot, ng_v3(1.f, 1.f, 1.f));
        ng_vec3 w0 = ng_mat4_mul_point(&M, p0);
        ng_vec3 w1 = ng_mat4_mul_point(&M, p1);
        ng_vec3 w2 = ng_mat4_mul_point(&M, p2);

        /* TRIFMT_SHADE: X, Y, R, G, B, A */
        float v0[] = { w0.x, w0.y, 1.f, 0.2f, 0.2f, 1.f };
        float v1[] = { w1.x, w1.y, 0.2f, 1.f, 0.3f, 1.f };
        float v2[] = { w2.x, w2.y, 0.3f, 0.4f, 1.f, 1.f };

        surface_t *disp = display_get();
        rdpq_attach(disp, NULL);
        rdpq_clear((color_t){ .r = 18, .g = 14, .b = 28, .a = 255 });

        rdpq_set_mode_standard();
        rdpq_mode_combiner(RDPQ_COMBINER_SHADE);
        rdpq_triangle(&TRIFMT_SHADE, v0, v1, v2);

        rdpq_text_print(NULL, 1, 12, 12, "L09 — Meshes & vertex color");
        snprintf(line, sizeof(line), "winding: %s  (A toggles)",
                 flip_winding ? "flipped" : "default");
        rdpq_text_print(NULL, 1, 12, 28, line);
        rdpq_text_print(NULL, 1, 12, 200, "3 verts → 1 triangle; colors blend across (Gouraud)");
        rdpq_text_print(NULL, 1, 12, 216, "Stick X rotates. Mesh = many of these.");

        rdpq_detach_show();
    }
}

What you learned

  • Mesh = triangles built from vertices
  • Attributes ride on vertices (color first)
  • Colors interpolate across a triangle
  • Winding marks front vs back

Next

L10 — Color & light — ambient light, base color, and why multiply by vertex color is an N64 superpower.

N64 Educator v1.2.2 — libdragon + Tiny3D · branch master