From White Paint to a Reactive 3D Material

Published:

← Back to Blog


A smoother mesh and a better camera can still produce an unattractive result. In one deterministic ShoggothOx render, an eight-turn NearRing spent much of its time as a featureless white solid. Thin colored bands proved that the audio texture was present, but most of the surface looked like white plastic.

The selected hippy palette supplied the immediate explanation: entry zero is (252, 252, 252), while inactive simulation cells contain index zero. The deeper problem was semantic. A palette lookup tells a 2D framebuffer exactly which color to emit. On a curved object, the same color is only one input to a material.

PALETTE AS UNLIT PAINT
NearRing rendered as a mostly featureless white surface
REACTIVE MATERIAL
NearRing with a dark base and colorful audio activity
Matching frames at 1:05 with identical audio, palette, and playlist. Geometry and camera are unchanged; only material and texture-coverage semantics differ.

Keep Exact Color Where Exact Color Belongs

Screen mode remains a literal indexed-color display. Buffer index n must produce palette entry n, including an intentionally white zero. Changing the palette globally would make the ordinary visualizer disagree with its authored configuration.

Object modes now take a separate, exhaustively selected path. A new uniform is zero for Screen and one for every 3D display. Adding a future display mode therefore requires a compile-time decision about its material semantics rather than silently inheriting either behavior.

A Base Coat, Lighting, and Emission

The renderer already interpolates each fragment's world position. Screen derivatives of that position give the two local surface directions; their cross product gives a geometric normal:

let normal =
    normalize(cross(dpdx(world_pos), dpdy(world_pos)));
let diffuse = abs(dot(normal, light_direction));
let surface_light = 0.28 + 0.72 * diffuse;

This avoids storing and maintaining another normal vector on every mesh vertex. It also works for HeightField displacement because the derivatives follow the displaced position delivered to the fragment shader.

Buffer values zero through twelve fade smoothly from inactive to active. Quiet fragments use a subdued base coat derived from palette entry 16, rather than treating entry zero as opaque paint. Active fragments retain their exact indexed color as a brighter emissive layer:

let activity =
    smoothstep(0.0, 12.0 / 255.0, index_value);
let base_color = palette(16.0 / 255.0);
let surface_albedo =
    mix(base_color * 0.18, indexed_color, activity);
let material_color =
    surface_albedo * surface_light
    + indexed_color * activity * 0.24;

The result is neither a hard black color key nor a recolored framebuffer. A quiet object remains visible as a dark palette-related shape, surface curvature remains readable, and music-driven regions keep the palette's vivid colors.

Eight Coils Were Sharing One Texture

Material shading removed the white slab, but some views were still mostly an inactive base color. The reason was geometric coverage. NearRing's centerline winds around the origin eight times while its longitudinal parameter moves from zero to one, yet its texture coordinate also moved only from zero to one. A sparse vertical band in the simulation therefore occupied only a narrow wedge of the whole object.

ShoggothOx already uses mirrored texture wrapping to make non-tileable simulation edges continuous. One forward-and-reverse mirrored cycle spans two texture-coordinate units. Advancing by two units per turn gives each coil a complete, seam-safe copy:

let uv = [
    u * f32::from(NEAR_RING_TURNS) * 2.0,
    v * 2.0,
];
ONE TEXTURE ACROSS EIGHT TURNS
NearRing with colorful audio activity confined to one edge
ONE MIRRORED CYCLE PER TURN
Colorful audio activity repeated around the NearRing coils
Matching frames at 1:10. Repetition changes only texture coverage; the indexed buffer, palette, mesh position, and camera are identical.

Verification Is an Interval, Not a Hero Frame

The existing deterministic playlist was kept unchanged and rendered with the normal repository command:

nix develop --command cargo xtask video \
  "Aces High" --reuse-playlist

Matching frames were checked at 1:05 and 1:10, then contact sheets sampled the complete NearRing interval from 0:45 through 1:27. That wider check matters: one frame can flatter almost any animation. Across the interval, quiet moments now read as a shaded green surface and active moments spread multicolored audio structure around the full ring.