The Valley Is Real — a WebGL terrain rebuild

2026-06-13 · lib/render/gl/ (WebGL2) + lib/render/sky.ts + atmosphere-model.ts · attempt #2 · every frame below is the live app captured headless via scripts/visual-matrix.mjs at the seed visual-baseline, before = the pre-overhaul Canvas-2D build, after = this rebuild · 4-critic adversarial verification + a fresh-eyes confirmation pass

A first attempt at this overhaul was reverted. It moved the existing flat painted relief onto the GPU byte-for-byte — "blit parity" — then laid a colour filter, a moving sun and cast shadows over the top. Technically clean; artistically timid. It was the same map with the sun switched on, and the owner sent it back: "do it again and 10× better."

This is the second attempt, and it inverts the idea that sank the first. The painted albedo bitmap is demoted from "the image" to one input of five: the fragment shader now recomposes the surface per-pixel from the simulation's own arrays — landcover class, slope, cover, the heightfield — against a procedural material library, lit in linear radiance by the live master-clock sun, grounded by baked ambient occlusion and ray-marched ridge shadows, with a real river, single-scatter aerial perspective, and an ACES film finish. The ground stops being a chart of the valley and becomes a photograph of it — while staying a legible tactical map at every zoom, which is the whole job.

Before → after

Strategic — the whole valleyppm 0.35 · noon · Clear. The satellite plate.
Before — flat tinted heightmap
before strategic
After — recomposed surface
after strategic

The before is a single hill-shaded bitmap with a colour wash. The after has material identity per landcover — irrigated terraces quilting the floor, dusty ochre-and-scree slopes, cedar-dark draws — the drainage network reading as the darkest connected feature (correct for water from above), and ridge shadows giving the spurs and draws real relief. A skeptical eye reads it as drone/ISR imagery.

Operational — the COP on its benchppm 0.9 · noon · Clear. The most-improved (and the hardest-won) frame.
Before
before operational
After
after operational

The river is now dark silt water — darker than its own gravel bars, the way a real Pech/Korengal channel reads — not the bright near-white ribbon a first pass produced (which a reviewer called out as "snow or a salt flat"). Road, terrace, talus and water sit at distinct luminances instead of collapsing into one olive band.

Dawn — raking light down the drawppm 0.9 · 07:00 · Clear.
Before
before dawn
After
after dawn

The sun is the master clock made visible. At first light the east ridge throws a multi-kilometre shadow across the valley floor (the most-documented Korengal morning image), and the lit west-facing slopes warm while the shadowed floor stays cool — the temperature separation a fixed NW bake can never have.

Close — the ground resolvesppm 4.6 · the COP. Surface, not upscaled paint.
Before — upscaled bitmap
before close
After — material + detail-normal rake
after close

At close zoom the old bitmap smeared. The new ground carries per-material luminance tooth and micro detail-normals that the live sun rakes — gravel, dirt, scree resolve as surface. The COP structures and unit chits stay the crisp 2D overlay above it (the layer separation is deliberate and load-bearing).

Fog dawn — weather you can readppm 0.9 · 06:30 · Fog (visibility 600 m).
Before
before fog
After
after fog

Valley fog pools from a local-floor min-field up into the draws; the aerial-perspective veil is keyed to the sacred visibilityM so it can never hide more than the weather declares. The orange contours, labels and unit chits float cleanly above it — the atmosphere does weather, it doesn't hide the map.

How it works

The renderer is a WebGL2 terrain underlayer on its own opaque canvas, beneath a transparent Canvas-2D canvas that still draws contours, units, fire-control and the HUD. That split is the legibility firewall: the tactical symbology is physically a separate layer and is never graded, blurred or fogged. If WebGL2 is unavailable the underlayer reports ok = false and the old 2D bake renders instead — the headless test ecology keeps working.

Each frame is three passes:

PassWhat happens
A — terrain → HDROne fullscreen triangle. The fragment reads the heightmap (a bit-faithful port of Terrain.elevAt, so the 2D contours stay welded to the GL relief), recomposes the surface from a per-landcover material array (albedo tooth + micro detail-normals, slope-blended, two octaves), and lights it in linear radiance: sun·NdotL·castShadow·cloud + sky-hemisphere·AO + moon. Output is unclamped half-float (RGBA16F).
B — bloomA half-res bright-pass (only true over-1.0 highlights) + a separable Gaussian. Sober: it catches the river glint, not the whole frame.
C — compositeHDR + bloom → ACES filmic tonemap → the time-of-day grade (exposure folds the weather light exactly once; white-balance, saturation, shadow-lift) → ordered Bayer dither (kills 8-bit sky banding) → sRGB, to the visible canvas.

The single most important control is u_detailGain, ramped by zoom (ppm 1.0→3.0): every added high-frequency effect — detail-normals, the material second octave, the rake — multiplies by it, so at strategic zoom the surface collapses to the byte-faithful relief of the old map (verified: strategic mid-grey luma moved 132.2 → 132.4, Δ+0.2) while close zoom resolves into ground. The band the old map got right is provably preserved; the band it failed is transformed. Reused untouched from the verified prior groundwork: the solar model (sky.ts, δ=+21° at Kunar's latitude — geometric sunrise 04:58 inside the gameplay light ramp), the exponential-stride cast-shadow march (a 780 m ridge at low sun throws a multi-km shadow — the math the timid attempt also got right), and the floor-field valley fog.

The build, one layer at a time

Nine commits, each one green (tsc / build / smoke / balance) and visibly better than the last, on the proven scaffold:

#Layer
C1Restore the WebGL skeleton; verify the HDR pipeline on headless SwiftShader (RGBA16F renderable, R8UI/RG8 uploads clean, usampler2D links) before writing a line of it
C2Linear-space lighting + RGBA16F HDR + ACES tonemap + dither — replaced the albedo·(0.34+0.95·shade) hillshade multiply
C3–C4The centerpiece — material-ID spine (landcover/slope/cover as GPU textures) + procedural per-landcover material array + detail-normal raking
C5Baked horizon ambient occlusion (contact darkening in draws, cliff bases, footprints) + softened PCF cast-shadow penumbra
C6Flow-advected specular river (dark silt, sober glint, clock-driven so it freezes on pause)
C7Single-scatter aerial perspective keyed to visibilityM + in-fog god-rays from the shadow map
C8Wet-ground darkening after rain + threshold bloom
C9Sun-tracked shadows grounding every structure and tree on the lit terrain

Verifying it — and the bug the critics caught

The design itself was set by a fan-out of four independent proposers scored by four adversarial judges; the result was synthesized into a build contract (photoreal-drone surface recomposition as the spine, water and atmosphere grafted on). After the build, a second four-critic panel — art-director, legibility-skeptic, graphics-artifacts, anti-timidity — reviewed the full matrix against the baseline.

The first verdict was 1 of 4. Three critics independently flagged the same thing: the river read as a bright near-white ribbon — "snow / salt flat" — the single biggest realism tell. The daytime haze, the same veil that sold the wide shot, was crushing the operational zoom into a monochrome olive mush (the old flat map genuinely read cleaner there). Two real, specific, high-severity bugs an unaided eye — mine, after hours of staring — had rationalized away.
Both fixed, then re-confirmed by a fresh reviewer. The river was re-authored to dark low-albedo silt with a thin sun-azimuth glint (and three deeper bugs fixed underneath: scattered Marsh cells were checkerboarding the water mask, and a NEAREST-sampled flow field × the clock-advection offset was decorrelating the ripple into 5-metre blocks). The aerial veil now scales with zoom — full at strategic, faded to 12% by tactical — so the mid-zoom keeps its contrast. A fresh skeptical pass confirmed: river reads as dark water, operational contrast restored, clears the bar — ships, no high-severity blockers.

Honest residuals

Numbers-first, partial stated as partial:

Standing checks at ship: tsc clean · next build compiled · smoke OK (no-NaN + serialize round-trip + material-library hash) · balance green (12/12 firefights, 0 stranded) · sun-determinism probe identity maxErr 0.0. The render layer stays pure w.r.t. (clock, weather, seed): clouds, fog, shadows and the river all advect on the sim clock and freeze on pause — only screen-space precip runs on the wall clock.