N64brew · Pandemonium
Texturing for the N64
How texture formats, vertex colors, and the color combiner shape Pandemonium’s boss room.
Kevin Newman · textures by BoxingBruin
Press → or Space to begin

The jam
Built fast. Optimized… later.
- Texture sizes and formats never checked
- Lots of overdraw
- Z-fighting on the floor
- Assets that missed the final build
Coming back gave us time to improve the art, and to ask what each surface actually needed.
Before → after
The same room, revisited


Vocabulary
A texel is one pixel in a texture
64 × 64 = 4,096 texels
1 texelThe budget
The RDP samples from just 4kb of TMEM
Space for the textures being used while drawing. Not a limit on all the artwork in a game.
The RDP can only sample texels already loaded into TMEM.
Draw it in smaller sections, loading new texture data between draws.
Try it
What fits in one load?
64 × 64 RGBA16 = 8kb of texels.
Only 32 rows fit at once, so the texture is drawn in sections, loading new texels between draws.
Illustrative. Ignores row padding, mipmaps, and materials that load two textures at once.
Reading the name
What the letters mean
One brightness value per texel.

Grayscale. Opacity follows brightness, so the bottom cannot fade independently.
Brightness and alpha, stored separately.

Grayscale, with a separate transparency fade along the bottom.
An index selects a palette color.

The gradient becomes blocks of palette colors. Alpha is on or off with this RGBA16 palette.
Color and alpha stored directly.

The reference: smooth color, distinct magenta/green stripes, and a soft alpha fade.
Two brightness samples share U and V.

The magenta/green stripes lose their color when paired. The bottom stays opaque.
Fewer bits, more texels
Every format is a trade
| Format | Bits / texel | What it stores | 64 × 64 |
|---|---|---|---|
| I4 | 4 | 16 brightness levels | 2kb |
| I8 | 8 | 256 brightness levels | 4kb |
| IA4 | 4 | 3 intensity bits + 1 alpha bit | 2kb |
| IA8 | 8 | 4 intensity bits + 4 alpha bits | 4kb |
| IA16 | 16 | 8 intensity bits + 8 alpha bits | 8kb |
| CI4 | 4 | An index into a 16-color palette | 2kb |
| CI8 | 8 | An index into a 256-color palette | 4kb |
| RGBA16 | 16 | 5 bits per RGB channel + 1 alpha bit | 8kb |
| RGBA32 | 32 | 8 bits each for red, green, blue, and alpha | 16kb |
| YUV16 | 16 | Brightness per texel, U and V shared by a pair. No alpha | 8kb |
Which format would I use? · 1 of 4
Can the mesh supply the color?


For a tinted stone floor, I’d start with I4. I8 costs twice as much at the same dimensions. Use it if the brightness steps in I4 spoil the shading. Both examples get the same tint from vertex color.
Which format would I use? · 2 of 4
Does the artwork need its own colors?


One 16 × 64 tile, repeated across each preview.
The carpet’s pattern needs color in the texture. CI4 has 16 palette entries and CI8 has 256. If neither palette keeps the colors you need, try RGBA16, at 2b per texel.
Which format would I use? · 3 of 4
Does it need a hard edge or a soft fade?


IA4 works for a grayscale cutout such as a banner ornament. A glow needs the softer alpha of IA8. IA16 adds precision. For an effect with its own changing colors and smooth alpha, use RGBA32.
Which format would I use? · 4 of 4
How big is it on screen?




Up close, the larger texel grid can help. On a tiny pickup, that detail may disappear. Choose dimensions for the view. A detailed mesh does not automatically need a large texture.
Special cases
A few special cases

For video frames or photographs. Keeps brightness per texel, shares color between neighbors, and has no alpha. Needs a dedicated conversion mode.
Smaller copies for surfaces that recede into the distance. They reduce shimmering, but add storage, so budget for the levels loaded together.

RGBA16, or CI with an RGBA16 palette, gives on/off alpha with color.
Vertex colors
Let the mesh carry the color
surface RGB = texture intensity × interpolated vertex RGB


Try it
Texture × vertex colors



Spend the bits on detail
Same 2kb. Four times the texels.
Vertex colors don’t add texture pixels. Their gradients follow the mesh, and vertex data costs memory too.
The color combiner
One formula mixes every material
(A − B) × C + D
Run separately for RGB and alpha, once or twice per pixel.
Filling the slots
Three recipes
(0 − 0) × 0 + Texture 0Shade is ignored(Texture 0 − 0) × Shade + 0The vertex-colored floor(Texture 1 − Texture 0) × amount + Texture 00 gives Texture 0, 1 gives Texture 1Try it
Build a material
- Cycles
- 2Two-cycle mode
- Textures
- 1I4 · 32 × 32
- Vertex shade
- UsedInterpolated vertex colors
- Primitive
- Color tint#FFFFFF
- Output alpha
- OpaqueForced to 1
- Preview blending
- OffOpaque surface
White primitive color leaves the first cycle’s result unchanged.
Multiply texture color by interpolated vertex shading. Force alpha to 1.
Cycle 1(Texture 0 − 0) × Shade + 0(0 − 0) × 0 + 1Tint the first result with primitive color. Keep its alpha.
Cycle 2(Combined − 0) × Primitive + 0(0 − 0) × 0 + Combined



In code
The column material in libdragon
rdpq_mode_combiner(RDPQ_COMBINER2(
(TEX0, 0, SHADE, 0), (0, 0, 0, 1),
(COMBINED, 0, PRIM, 0), (0, 0, 0, COMBINED)
));
rdpq_set_prim_color(RGBA32(255, 255, 255, 255));- Cycle 1 RGB Texture 0 × Shade
- Cycle 1 alpha Forced to 1
- Cycle 2 RGB Combined × Primitive
- Cycle 2 alpha Keep Combined
Fast64 stores these choices in the exported material. Changing the PNG alone can’t change the formula.
The bigger picture
The combiner is only part of the material
- ShadeVertex colors and lighting, blended across the triangle
- Texture samplesFrom TMEM, picked by the UVs
- Color combiner(A − B) × C + D, for RGB and alpha
- BlenderMixes with the framebuffer. Fog lives here, often borrowing shade alpha
Alpha testing can discard cutout pixels. Fog needs care alongside custom alpha formulas.
Putting it together
Textures, shade, and combiner on a real model




Left drag to orbit · right drag or WASD / arrow keys to pan · pinch, scroll, or + / − to zoom · R to reset
The revision
What changed in the room?




+
Try it
Try the formats yourself
Texels + a full RGBA16 palette for CI. Excludes headers, padding, mipmaps, and compression. PNG file sizes differ.


Each texel selects one of sixteen palette entries. This example generates an RGBA16 palette from the selected artwork.
Conclusion
The floor saved 2kb. The room still grew.
| Room assets | Original | Updated |
|---|---|---|
| Model files | 10 | 12 |
| Triangles | 1,316 | 2,321 |
| Material slots | 32 | 72 |
| Unique textures | 18 | 51 |
| Packed texels (est.) | 69kb | 131.56kb |
Totals include room effects. Texel estimates exclude palettes, external mipmaps, padding, and headers.
Thanks!
Ship the jam. Then revisit.
Getting the game finished came first. Coming back let us choose formats on purpose, and bring the stonework and banners closer to how we wanted them.
Slide 1 / 26: Texturing for the N64