The value bracket that was never on screen
The design doc said the lit wall was brighter than the floor. On screen it never had been.
docs/visual-design.md had a sound rule in it. Because the camera spins through
a full circle under an orthographic projection, hue has to carry what you can
roll on. The lit wall was supposed to be brighter than the floor, so
“bright means walkable” would stop being a shortcut the eye could
take.
Those numbers were albedo. They described Palette.swift, not the screen.
Palette.swift. The floor lands between the two wall
brightnesses on purpose, so no brightness threshold anywhere separates
walkable from wall. These are albedo — the gap between them and the
screen is what the rest of this entry is about.What the screenshots said
The sun is steep, so a vertical face catches little of it, and the compensating albedo bump was short by roughly a factor of two. On a device screenshot the lit wall measured 140 against the top’s 191 — the old hierarchy the rule existed to remove.
Two more things came out of the same pass:
- 63–69% of the playfield was one flat pale grey. Luma 191, a 25% dip where the grid line lands, no other variation. Graph paper over a blockout.
- Two perpendicular sunlit walls barely broke at the corner — under
10%, against 24% for a grid line. The fill light travels
(0.55, -0.38, 0.42)and separates the −x/−z pair cleanly, while the sun has almost no horizontal component and does nothing for +x/+z. All the corner separation was coming from the fill light, on the half of the walls that did not need it.
Three fixes
Walls split by world axis as well as sun side. Six index buffers instead of
four, and corners now break at 19.7%. A fourth light lost on maintenance
rather than looks: the sun-side classification already existed one line above,
so the axis split is three lines and a switch, where a light’s direction
and category mask would need keeping in sync with sunDirection forever.
Per-cell value jitter at 0.05, hashed from the world-space UV. Tuned on screenshots: 0.03 vanishes at frame scale, 0.06 reads as a faint second grid. The jitter is per-cell rather than per-block, which does not show — the grid line already draws a boundary at every cell edge, so the shade step lands where the eye expects one.
Tops dropped from 0.68 to 0.47. The floor now sits between the two sunlit wall values, so no brightness threshold anywhere separates walkable from wall. Hue carries it alone, which is what the doc claimed all along. Checked at four camera yaws.
Costs
The marble used to read dark-against-bright one way and bright-against-dark the other; contrast is now similar in both, so the eyes and wisp carry more of the job of finding it. The floor is no longer brighter than the sky, and the skyline reads on saturation instead.
Frame time is unmeasured: six draw calls instead of four, about six ALU added to a modifier covering most of the screen, no numbers. Recorded rather than assumed away.
Every measurement above needed the same frame twice, and the demo driver spins
the camera — so the commit also adds -yaw <degrees> to pin it. Without a
way to hold the frame still you are comparing memories of renders.