L12 — Depth buffer + multiple objects
Goal
Treat each object as mesh + matrix, draw more than one, and see what the depth buffer does when you toggle it.
In plain English
Painter’s algorithm = draw far things first, near things last (like watercolors). It’s fragile when objects cross.
Z-buffer (depth buffer) = for every pixel, remember the closest surface so far. New pixels only draw if they’re closer. Draw order becomes much less stressful.
What you will see
source scripts/env.sh
make -C lessons/l12-depth-objects| Object | Role |
|---|---|
| Cyan | Farther, bigger |
| Red | Nearer, slides left/right |
| Input | Effect |
|---|---|
| A | Toggle depth test ON/OFF |
Try this
With depth ON, red correctly stays in front even though we sometimes draw cyan first.
With depth OFF, whichever draws last paints on top — red/cyan “pop” wrong.
Entity mindset
Actor / entity ≈ {
geometry (verts or model),
transform (matrix / SRT),
maybe materials later
}Starshard Cove will be a list of entities (player, shards, island), each with a matrix — same idea as two quads here.
API notes
T3D_FLAG_DEPTH— enable depth testing/writing for draws.- Always clear depth each frame (
t3d_screen_clear_depth) or you’ll see ghosts. - Per object:
t3d_matrix_push(modelMat)→t3d_vert_load→t3d_matrix_pop(1)→ tris.viewport_attachleaves the camera matrix on the stack; push multiplies your model with it. Usingt3d_matrix_setinstead can overwrite the camera and you get only the clear color (text may still work). rdpq_mode_combiner(RDPQ_COMBINER_SHADE)— required for hand-built colored tris (same as L11).
Common noob confusions
| Feeling | Reality |
|---|---|
| “Depth is optional decoration” | It’s how 3D stays coherent |
| “I must sort all triangles” | Z-buffer handles most cases; transparency is the hard leftover |
| “Z-fighting flicker” | Two surfaces same depth — separate them or bias later |
| “Blue/purple clear, no quads” | Matrix stack: use push/pop, not matrix_set over the camera |
Exercises
- Toggle A until the difference is obvious; explain it out loud.
- Swap draw order of red/cyan with depth ON — should still look correct.
- Add a third quad (copy-paste pattern) at another Z.
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/l12-depth-objects/Makefile · lessons/l12-depth-objects/src/main.c
lessons/l12-depth-objects/Makefile
ROMNAME := l12_depth
ROM_TITLE := "L12 Depth"
USE_T3D := 1
include ../../common/lesson.mklessons/l12-depth-objects/src/main.c
/**
* L12 — Depth buffer + multiple objects
* ============================================================================
* Each object = geometry + its own model matrix.
* Depth (Z) buffer: nearer pixels win so draw order matters less.
* A toggles T3D_FLAG_DEPTH — watch red/cyan fight when OFF.
* DOCS: docs/guide/m2/l12-depth-objects.md
*/
#include <libdragon.h>
#include <t3d/t3d.h>
static void make_quad(T3DVertPacked *verts, uint32_t rgba)
{
uint16_t norm = t3d_vert_pack_normal(&(fm_vec3_t){{ 0, 0, 1 }});
verts[0] = (T3DVertPacked){
.posA = { -12, -12, 0 }, .rgbaA = rgba, .normA = norm,
.posB = { 12, -12, 0 }, .rgbaB = rgba, .normB = norm,
};
verts[1] = (T3DVertPacked){
.posA = { 12, 12, 0 }, .rgbaA = rgba, .normA = norm,
.posB = { -12, 12, 0 }, .rgbaB = rgba, .normB = norm,
};
}
int main(void)
{
debug_init_isviewer();
debug_init_usblog();
display_init(RESOLUTION_320x240, DEPTH_16_BPP, 3, GAMMA_NONE, FILTERS_RESAMPLE);
rdpq_init();
rdpq_text_register_font(1, rdpq_font_load_builtin(FONT_BUILTIN_DEBUG_VAR));
joypad_init();
t3d_init((T3DInitParams){});
T3DMat4FP *matNear = malloc_uncached(sizeof(T3DMat4FP));
T3DMat4FP *matFar = malloc_uncached(sizeof(T3DMat4FP));
T3DVertPacked *quadRed = malloc_uncached(sizeof(T3DVertPacked) * 2);
T3DVertPacked *quadCyan = malloc_uncached(sizeof(T3DVertPacked) * 2);
make_quad(quadRed, 0xFF4040FF);
make_quad(quadCyan, 0x40FFFFFF);
const fm_vec3_t camPos = {{ 0, 0, -40 }};
const fm_vec3_t camTarget = {{ 0, 0, 0 }};
uint8_t ambient[4] = { 40, 40, 50, 0xFF };
uint8_t dirCol[4] = { 0xFF, 0xFF, 0xFF, 0xFF };
fm_vec3_t lightDir = {{ 0.2f, 0.4f, 1.0f }};
fm_vec3_norm(&lightDir, &lightDir);
T3DViewport viewport = t3d_viewport_create();
bool useDepth = true;
float t = 0.f;
for (;;) {
joypad_poll();
if (joypad_get_buttons_pressed(JOYPAD_PORT_1).a) {
useDepth = !useDepth;
}
t += 0.02f;
t3d_viewport_set_projection(&viewport, T3D_DEG_TO_RAD(70.0f), 5.0f, 120.0f);
t3d_viewport_look_at(&viewport, &camPos, &camTarget, &(fm_vec3_t){{ 0, 1, 0 }});
/* Near red quad drifts left/right; far cyan sits behind and larger path. */
t3d_mat4fp_from_srt_euler(matNear,
(float[3]){ 0.35f, 0.35f, 0.35f },
(float[3]){ 0, t * 0.4f, 0 },
(float[3]){ fm_sinf(t) * 8.f, 0, 0 });
t3d_mat4fp_from_srt_euler(matFar,
(float[3]){ 0.55f, 0.55f, 0.55f },
(float[3]){ 0, -t * 0.25f, 0 },
(float[3]){ 0, 0, 14.f });
rdpq_attach(display_get(), display_get_zbuf());
t3d_frame_start();
t3d_viewport_attach(&viewport);
t3d_screen_clear_color(RGBA32(20, 24, 40, 0xFF));
t3d_screen_clear_depth();
/* Shade from vertex colors (+ lighting when SHADED). Without this, tris often draw black. */
rdpq_mode_combiner(RDPQ_COMBINER_SHADE);
t3d_light_set_ambient(ambient);
t3d_light_set_directional(0, dirCol, &lightDir);
t3d_light_set_count(1);
uint16_t flags = T3D_FLAG_SHADED;
if (useDepth) {
flags |= T3D_FLAG_DEPTH;
}
t3d_state_set_drawflags(flags);
/* Draw FAR first, then NEAR — with depth, order should not matter.
* Each object uses PUSH/POP so its model composes with the camera
* matrix that viewport_attach placed on the stack (t3d_matrix_set
* here would overwrite the camera → invisible geometry). */
t3d_matrix_push(matFar);
t3d_vert_load(quadCyan, 0, 4);
t3d_matrix_pop(1);
t3d_tri_draw(0, 1, 2);
t3d_tri_draw(2, 3, 0);
t3d_tri_sync();
t3d_matrix_push(matNear);
t3d_vert_load(quadRed, 0, 4);
t3d_matrix_pop(1);
t3d_tri_draw(0, 1, 2);
t3d_tri_draw(2, 3, 0);
t3d_tri_sync();
rdpq_set_mode_standard();
rdpq_text_print(NULL, 1, 12, 12, "L12 — Depth + two objects");
rdpq_text_print(NULL, 1, 12, 28,
useDepth ? "Depth ON (A toggles)" : "Depth OFF — draw order wins");
rdpq_text_print(NULL, 1, 12, 44, "Red=near Cyan=far");
rdpq_detach_show();
}
}What you learned
- Multiple model matrices
- Depth buffer purpose
- Clear color and depth every frame
Next
L13 — Orbit camera — you fly the view around a tiny scene.