feat(ui): relief and vegetation texture over the terrain hue (T-1194)

RimWorld technique 4 from the reference map: texture as data, not decoration.

WHY IT WAS FLAT. The base layer reads hue from morphology and lightness from
elev_q. Below Global, morphology resolves to exactly ONE zone per canvas, so
the frame became a single colour whose only variation was a lightness ramp too
subtle to see. Measured on Ferrath at Region: 1 morphology zone, but 49
distinct elev_q values. The information was already on the wire and arriving —
the renderer was discarding it by expressing it in lightness alone.

TWO MARKS, NOT ONE SLIDER — the ticket's design question (b), settled by
looking at the reference rather than reasoning about it:
  - RELIEF stipple: fine, dense, darker, keyed to RUGGEDNESS not height. The
    reference's high flat plains carry none while its ranges are dense with it,
    so the driver is the local elev_q gradient; a high plateau stays clean.
  - VEGETATION blotch: coarser, softer, marked on a half-frequency lattice so
    it reads as patches rather than a second speckle at the same pitch.

Inline in the existing per-cell loop (question (a)) and always-on, base layer
only (question (c)). The TMP/MST/VEG toggles are ANALYTIC reads — stippling a
temperature ramp would corrupt the quantity being read.

THE BASELINE IS MEASURED, NOT GUESSED, and the first attempt got it wrong: a
1-cell ruggedness delta samples mostly quantization noise, reads
near-identically everywhere, and rendered as uniform static over flat green —
grain, not structure. The gradient saturates by about 4 cells (Region: d1 1.40,
d4 2.38, d8 2.41, d16 2.51), so the baseline is 4 and the full scale 4.
Verified by capture at native resolution: on Global the stipple now
concentrates on rugged ground and leaves plains clean.

WATER TAKES NEITHER MARK, and gets a flat tone. An earlier version excluded
Lake alone and stippled the entire ocean — the one surface with no relief to
express. Both open-water zones are excluded now.

The ocean also stops shading by elev_q, which is the same argument T-1188
already made for lakes and never applied here: elev_q on a water cell is the
bedrock UNDER the water, not the surface, so shading the sea by it paints
seabed relief nobody can see. Near a coast that bedrock rises steeply and
quantizes hard, which is exactly where it showed — a pale, pixellated, broken
fringe hugging every shore (Jeroen, on the capture). One tone for the sea reads
as water and lets the coastline be the edge.

D-255(e)-legal throughout: texture-space dithering of already-derived per-cell
values, decided per server cell by a hash of its own coordinates and values —
no sample invented between cells, identical on cache hit and miss.

Two colorize tests updated: both asserted the old ocean shading incidentally
while testing zero-fill/no-crash. Property under test unchanged.

1838 client tests, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-07 15:20:04 +02:00
co-authored by Claude Opus 5
parent 8eb5cb9ff7
commit 566b566519
5 changed files with 170 additions and 12 deletions
+33 -1
View File
@@ -2582,9 +2582,41 @@ mod tests {
_ => 0,
};
// Mean |elev_q delta| at several baselines. T-1194's relief stipple
// needs a driver that VARIES across the frame; a baseline shorter
// than the finest terrain octave measures quantization noise
// instead and comes out near-constant, which renders as uniform
// grain rather than as ruggedness.
let w = raw.width as usize;
let h = raw.height as usize;
let mut grad = String::new();
for base in [1usize, 4, 8, 16] {
let mut sum = 0.0f64;
let mut n = 0.0f64;
let mut hits = 0.0f64;
for r in 0..h {
for c in 0..w.saturating_sub(base) {
let a = raw.elev_q[r * w + c] as i32;
let b = raw.elev_q[r * w + c + base] as i32;
let d = (a - b).abs() as f64;
sum += d;
n += 1.0;
if d >= 2.0 {
hits += 1.0;
}
}
}
grad += &format!(
" d{}[mean {:.2} p>=2 {:.0}%]",
base,
sum / n.max(1.0),
100.0 * hits / n.max(1.0)
);
}
println!(
"{:8?} {:>9.1} m/gu {}x{} elev[uniq {:3} span {:3}] moist[uniq {:3} span {:3}] \
morph[uniq {:2}] veg[uniq {:2}] glac[uniq {:2}] courses {}",
morph[uniq {:2}] veg[uniq {:2}] glac[uniq {:2}] courses {}{grad}",
rung,
spacing,
raw.width,