N64GraphicsInteractive

Texturing for the N64

How texture formats, vertex colors, and the color combiner shape Pandemonium’s N64 boss room.

We built Pandemonium, our N64brew game-jam boss battle, with very little time for optimization. We didn’t get to check every texture’s size or format, and the room wasn’t textured as nicely as we wanted. There was lots of overdraw. Z fighting on the floor. Assets didn't make it into the final build. Usual game-jam chaos.

Revisiting the scene gave us time to improve the artwork and look at what each surface actually needed. The stone floor went from I8 to I4: 4kb down to 2kb, still at 64 × 64. The mesh still supplies its color.

All Pandemonium textures were made by my teammate BoxingBruin.

Room view
After: a more detailed carpet border and patterned stone floor leading toward the door, framed by textured columns.Before: a red carpet with a simple border between pale floor panels, facing the boss-room door.

Small textures, tight budget

A texel (texture element) is one pixel in a texture, so a 64 × 64 texture contains 4,096 texels.

The N64’s graphics hardware has 4kb of texture memory, called TMEM. That is space for textures being used while drawing, rather than a limit on all the artwork in a game. The RDP can only sample texels already loaded there, so textures that exceed that space must be drawn in smaller sections, with new texture data loaded between draws.

IIntensity
A grid of sixteen grayscale values progressing from black to white.

One brightness value per texel.

Color gradient, fine stripes, and alpha fade represented as I4
I4 · 64 × 642kb packed

Grayscale. Opacity follows brightness, so the bottom cannot fade independently.

IAIntensity + alpha
Grayscale rows with independently changing opacity, revealing a checkerboard behind transparent texels.

Brightness and alpha, stored separately.

Color gradient, fine stripes, and alpha fade represented as IA8
IA8 · 64 × 644kb packed

Grayscale, with a separate transparency fade along the bottom.

CIColor index
The stored index 2 points to the blue entry numbered 2 in a four-color example palette.

An index selects a palette color.

Color gradient, fine stripes, and alpha fade represented as CI4
CI4 · 64 × 642.03kb packed2kb texels + 32b palette

The gradient becomes blocks of palette colors. Alpha is on or off with this RGBA16 palette.

RGBADirect color
Red, green, blue, and alpha channels feed one colored texel with transparency.

Color and alpha stored directly.

Color gradient, fine stripes, and alpha fade represented as RGBA32
RGBA32 · 64 × 6416kb packed

The reference: smooth color, distinct magenta/green stripes, and a soft alpha fade.

YUVBrightness + shared color
Two neighboring texels have separate Y brightness samples and share U and V color information.

Two brightness samples share U and V.

Color gradient, fine stripes, and alpha fade represented as YUV16
YUV16 · 64 × 648kb packed

The magenta/green stripes lose their color when paired. The bottom stays opaque.

Fewer bits can leave room for more texels, at the cost of brightness levels, colors, or transparency precision.

FormatBits / texelWhat it stores
I4416 brightness levels
I88256 brightness levels
IA443 intensity bits + 1 alpha bit
IA884 intensity bits + 4 alpha bits
IA16168 intensity bits + 8 alpha bits
CI44An index into a 16-color palette
CI88An index into a 256-color palette
RGBA16165 bits per RGB channel + 1 alpha bit
RGBA32328 bits each for red, green, blue, and alpha
YUV1616Brightness per texel, with U and V shared by a horizontal pair. No alpha

These layouts follow libdragon’s texture-format definitions.

Which format would I use?

I’d start with what the surface needs. Each pair below uses the same source, so you can see what changes when you spend more memory.

Can the mesh supply the color?

Stone detail with sixteen intensity levels and a warm vertex tint.
I4 · 16 brightness levels2kb texels
The same stone detail and tint with finer intensity precision.
I8 · 256 brightness levels4kb texels

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.

Does the artwork need its own colors?

The detailed red and gold carpet border reduced to a sixteen-entry palette.
CI4 · 16 palette entries544b with palette
The same carpet border with a larger palette retaining finer color changes.
CI8 · 256 palette entries1.5kb with palette
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.

Does it need a hard edge or a soft fade?

A glow encoded as IA4 becomes a solid disc with a hard transparency edge.
IA4 · on/off alpha2kb texels
The same glow encoded as IA8 fades gradually into the background.
IA8 · 16 alpha levels4kb texels

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.

How big is it on screen?

The stone texture at thirty-two by thirty-two texels, enlarged for close inspection.
32 × 32 · I4512b texels
The same stone artwork at sixty-four by sixty-four texels, preserving finer detail.
64 × 64 · I42kb texels
Both reduced to 12 × 12 screen pixels, enlarged 8×:
32 × 32 source reduced to twelve by twelve pixels, then enlarged eight times for inspection32 × 32 source
64 × 64 source reduced to twelve by twelve pixels, then enlarged eight times for inspection64 × 64 source

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.

For colored cutouts, RGBA16 or CI with an RGBA16 palette gives you on/off alpha. YUV16 is a separate case for video frames or photographs: it keeps brightness detail but shares color between neighboring texels, and has no alpha. It uses libdragon’s dedicated YUV conversion mode.

For surfaces that recede into the distance, mipmaps can reduce shimmering. They add storage, so budget for the levels that need to be loaded together.

Vertex colors

If you can get away with a simple color then this is definitely the way to go. A grayscale stone texture can still produce a colored floor. The texture supplies fine detail. Colors stored at the mesh’s vertices supply broader variation. The renderer blends those corner colors across the entire triangle, then multiplies the texture by that color:

surface RGB = texture intensity × interpolated vertex RGB
Texture holds the details. Vertices hold color.

For 2kb of raw texels, RGBA16 gives you 32 × 32 pixels, while I4 gives you 64 × 64: four times as many texels. Putting broad color changes in the vertices can preserve a larger grid for surface detail.

Vertex colors do not increase the screen resolution or add texture pixels by themselves. Their gradients follow the mesh, and vertex data also costs memory. The original floor already used this technique, and the revision keeps it.

How the color combiner builds the material

The texture format tells us what is stored. The color combiner decides how those samples mix with the other inputs. Vertex colors only affect the result if the material’s formula uses them.

Each cycle runs this formula separately for RGB and alpha:

(A − B) × C + D

The letters are input slots, like the fields in Fast64. Some of the inputs you can choose are:

  • Texture 0 / 1: Color and alpha sampled from the first or second texture.
  • Shade: Vertex color or lighting, blended across the triangle.
  • Primitive: A fixed color set by the material or drawing code. Here it tints the columns.
  • Environment: A second fixed color the formula can use.

The available inputs depend on the slot. See libdragon’s formula and input definitions.

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
RGB(Texture 0 − 0) × Shade + 0
Alpha(0 − 0) × 0 + 1

Tint the first result with primitive color. Keep its alpha.

Cycle 2
RGB(Combined − 0) × Primitive + 0
Alpha(0 − 0) × 0 + Combined
Texture 0 · I4
Texture 0 · I4
Shade · vertex colors
Shade · vertex colors
After cycle 1
After cycle 1
Final result
Final result
This preview does not emulate RDP fixed-point precision, filtering, or fog.

In a two-cycle material, Combined is the first cycle’s output, used as an input to the second. See libdragon’s documentation for the second cycle.

The two-texture blend preset uses

(Texture 1 − Texture 0) × amount + Texture 0

This is a lerp: amount 0 gives Texture 0, amount 1 gives Texture 1. It uses primitive alpha as the mix amount, then applies shade in cycle 2. Both textures must fit in TMEM together. These two 32 × 32 I4 patches use 1kb in total.

On an N64 model, the UVs select the texture samples and the vertex data supplies shade. Fast64 stores the combiner choices in the exported material, which the model pipeline must apply when drawing. Changing the PNG alone cannot change that formula. For the column settings, the equivalent libdragon configuration is:

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));

The combiner is only part of the material. Lighting supplies shade before this step. The later blender handles mixing with the framebuffer. Alpha testing can discard cutout pixels. Standard fog also uses the blender, often borrowing shade alpha for depth, so it needs care alongside custom alpha formulas. See libdragon’s notes on fog and the combiner.

Explore the boss-room columns
Shaft
Shaft intensity texture used by the column model
32 × 128 · I84kb texels
Base
Base intensity texture used by the column model
32 × 32 · I4512b texels
Transition
Transition intensity texture used by the column model
32 × 124 · I41.94kb texels
Four textured boss-room columns, rendered from the game’s mesh with vertex colors
254 tris3 textures · 6.44kb texels

Left drag to orbit · right drag or WASD / arrow keys to pan · pinch, scroll, or + / − to zoom · R to reset

What changed in the room?

The floor’s artwork also changed, so the before-and-after images show more than a format conversion.

SurfaceOriginal → updatedOutcome
Stone floor64 × 64 I8 → I44kb → 2kb of texels
Carpet border16 × 64 CI8 in bothSame 1kb base cost, with revised artwork and added mipmaps
Banner layersSeparate I4 background and IA4 ornament in the local versionDifferent jobs for brightness and transparency

The carpet still uses CI8. Its mipmaps actually add memory, but help stop the fine pattern shimmering in the distance. The banner layers show another tradeoff, spending bits on independent alpha where a decorative cutout needs it.

Try the formats yourself

What does each format keep?
Source artwork
Compare packed sizes · 64 × 64

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

Source PNG · 64 × 64
Generated diagnostic pattern texture
CI4 · 4-bit index → 16-entry palette
Original texture. Enable JavaScript for the format comparison
Texel data2,048b
Full palette32b
Estimated packed size2,080b

Each texel selects one of sixteen palette entries. This example generates an RGBA16 palette from the selected artwork.

Conclusion

The floor saves 2kb. The updated room still has more texture data and geometry overall:

Room assetsOriginalUpdated
Model files1012
Triangles in those models1,3162,321
Material slots across models3272
Unique embedded textures1851
Estimated packed embedded texels69kb131.56kb

Asset totals include room effects. Texel estimates exclude palettes, external mipmaps, padding, and headers.

During the jam, getting the game finished took priority. Coming back gave us time to revisit choices like the floor’s format and bring the stonework and banners closer to how we wanted them to look.

I hope this little deep dive into texture formats and the color combiner helps understand some of the finer details of Nintendo64 graphics.