# 05 — Renderer Spec: 3D ASCII That Reads, Moves, and Sings *How the game looks in motion, how we build it, and how we keep it readable at 60fps on a toaster over SSH.* Read this with doc 01 §1/§6 (the feel) and doc 04 §4.5 (the pipeline). This doc is build-to-print for the engine team. --- ## 5.1 The look — "1982 dreaming about 2032" **Core thesis:** ASCII is not a limitation aesthetic — it is a *resolution-independent vector-ish medium*. A wall 2 meters away and a wall 200 meters away both render as characters, but density, color, and motion sell the depth. The reference images in the reader's head: terminal raycasters, `caca`-style demos, phosphor CRTs, DOS ANSI art — but with modern truecolor, smooth motion, and art direction. **The frame, described precisely (120×36 reference target):** - **Depth as density.** Distance ramp per material class, 10 steps, near→far: `█▓▓▒▒░░==--::..·· ` — nearest surfaces nearly solid, far surfaces dissolve into mist-dots. Fog is *diegetic*: dungeon damp + lantern falloff, not a shader trick. Fog color shifts by stratum (Shallows: warm brown-black; Works: teal-black ozone; Deep: violet-black + drifting `·` motes). - **Edges as line-art.** Hard geometry edges (doorframes, stair lips, grate bars, elevator cables) snap to `/ \ | - + ─ │ ┌ ┐ └ ┘ ├ ┤` box-drawing when the surface angle is edge-on to the ray. This single rule makes architecture *pop* out of the density field — a doorway reads instantly at 40m. - **Faces as half-block gradients.** Interior of surfaces uses `▀▄█ ░▒▓` + truecolor foreground/background pairs to fake sub-cell vertical resolution (two vertical samples per cell — effectively doubles perceived vertical res for floors/ceilings/walls). - **Actors as glyphs-with-posture.** Player `@` (gold when lit, dim-blue when dark). NPCs by role-glyph: townsfolk `&` variants, wardens `§`, tinkers `%`, monsters per-species (`r` cable-rat with amber spark-pixel when charged, `V` Vendric with too-clean white glass pixels, `O` Drowned bell with swinging `~` clapper offset by animation phase, cherubs `✦` brass). Posture modifies: climbing `≋`, swimming `≈`, rope-hanging `|o`, prone `–`. Status overlays: burning = red `*` flicker on the glyph cell, blessed = gold `.` halo ring (one-cell radius, sparse), cursed = violet `:` drip. - **Light as truth.** Every cell's brightness = computed light level (doc 03 §3.3) quantized to 24 steps and multiplied into truecolor RGB. Lantern cone is visibly a cone: warm `+*` core → amber `=` falloff → cold blue `.` darkness. Muzzle-flash of a wand whites out 3 frames (with no-flicker palette option clamping this). Phosphor flora (tinker-moss) renders as sparse green `*`/`·` constellations — navigation beacons players learn to love. - **Sound made visible.** Noise events draw expanding `(` `)` arcs for ~6 frames, intensity = arc count + brightness, direction readable from arc center offset. A slammed door two floors up renders as faint arcs leaking through the grate glyphs — the player *sees* upstairs consequences. - **Motion language.** 60fps interpolation of 10Hz sim (doc 04 §4.2): actors glide between ticks, lantern flame shivers (2-frame `~` wobble), wheat `,`/`;`/`'` fields outside ripple in wind bands, elevator cables `│` scroll, water `≈` flows downhill visibly, gas `░` churns upward. The world never sits still — stillness means death or pause, and pause draws a `‖` border. **Three cameras, one sim (switching never pauses in Classic):** 1. **First-person (default):** eye at actor head height, 75° FOV cap (motion-sickness law), subtle head-bob (toggleable, off by default in no-shake mode). Crosshair is a NetHack-voice hint cell, not a shooter reticle. 2. **Chase-orbit (press Tab to toggle; M restores the saved perspective on map exit):** camera 3m behind/up, mouse-drag orbits, auto-collides (pulls in rather than clipping). For ropework, climbing, and admiring the mule. 3. **Top-tactical (M):** orthographic ASCII plan-view, 2.5D stacked-glyph rendering (`^` stairs up, `v` down, `=` bridges, `~` water, `░` gas) with Z-slider (PageUp/PageDown; Home recenters; optional in legacy keymaps). The planner: chalk marks, rope-length preview arcs, thrown-trajectory `·` dots. Pause-step auto-engages here in Story mode; Classic keeps ticking (display a `⏳ LIVE` tag so nobody feels cheated). **The title-screen resolve (doc 01 §1) is a renderer feature:** new-game boot plays a 2-second density ramp from ` ` → full scene, left-to-right like developing film. Cheap (animates fog density scalar), unforgettable. ## 5.2 The algorithm — CPU raycast, DDA, diffed ANSI **Per frame (render thread, reads triple-buffered snapshot — never touches live sim):** 1. **Snapshot acquire** (~200KB: visible chunks + actor list + light/sound overlay buffers + particle sparks). Non-blocking try-read; on contention, re-render last frame (tearing beats stalling). 2. **Primary rays:** one ray per *cell column-half* (half-blocks give 2 vertical samples per cell → cast W rays × 2H samples; at 120×36 that's 120×72 = 8,640 rays/frame — trivial for CPU). DDA through voxel grid (Amanatides & Woo), max distance per quality tier, early-out on opacity. Fixed-point DDA with integer stepping — no float drift, deterministic screenshots for tests. 3. **Shading per hit:** material base color (palette-indexed, truecolor-expanded) × light level (24-step) × distance fog (density ramp select) × facing (top faces +10% warm from lantern law, side faces neutral, bottom faces −15% cold). Edge detection: if the ray grazes a voxel boundary within ε OR neighbor depth discontinuity > threshold → line-art glyph instead of density glyph. Normal estimation via face hit (DDA gives this free). **Terminal exposure convention:** material-table channels are linear reflectance coefficients. Encode the lit foreground product with the sRGB transfer function before emitting RGB bytes; direct linear bytes on an sRGB display double-darken the lantern falloff. Zero-light crush bypasses this encoding. Density and edge glyphs retain a deliberately dark background partner (quarter of the unencoded product); only actual oriented half-block glyphs may use another ray's foreground as background. Fog remains a post-shade display-color blend. The production opening regression requires at least 35% nonblank viewport cells with foreground relative luminance ≥0.10 and fg/bg contrast ≥3:1, in truecolor and decoded ANSI256 at default settings; this is a contrast floor, not certification of the three-glance experience. 4. **Actors & transparents:** billboard glyph selection by species+posture+anim phase, depth-tested against ray depth buffer, drawn brightest-first so overlapping torch-bearers composite sensibly. Transparent/gas/fluid overlays (`≈` water sheen, `░` gas churn, `(` sound arcs, `·` thrown trails, flour-dust `:` puffs) composited after opaques with max-depth ordering. 5. **Post:** vignette (corners −20% brightness beyond 70% radius — focuses the eye, hides narrow-terminal stretch), hint-ribbon glow (key hints brighten when contextually relevant — doc 01 §2), damage flash (red edge `!` ticks, clamped by no-flicker setting), darkness crush (below-light-threshold cells snap to near-black blue, not gray — darkness must feel *total*). 6. **ANSI diff encode:** compare cell buffer (glyph + fg + bg) against last-flushed frame; emit minimal cursor-move + SGR sequences. Typical frame diff <15% of cells → SSH-playable. Full redraw on resize/palette swap. 256-color fallback path (quantize truecolor via precomputed 6×6×6 cube LUT) and *no-color path* (density-only + bold — the game stays fully playable; tests assert this). 7. **Flush + frame pacing:** target 60fps, floor 30fps (slice gate). Adaptive render-scale: if frame time > budget 3 frames running, drop horizontal res 10% (re-expand when headroom returns). Sim fidelity never degrades — only pixels. **Performance budget (slice gate, measured `cargo bench -p hexwork-render` + real terminals):** - Raycast+shade ≤ 6ms @120×36 on 5-yr-old laptop CPU (single thread; worker-pool feature for >2 cores splits screen bands — deterministic band boundaries for test-stable output). - ANSI encode+flush ≤ 4ms typical (diff path), ≤ 12ms worst (full redraw). - Snapshot copy ≤ 0.5ms. Memory: triple buffer ≤ 1MB total. Zero per-frame allocations in hot path (pooled buffers — assert with a debug allocator tripwire). ## 5.3 Terminal reality — backends, capability matrix, degradation Backend: `crossterm` (portability) behind the `hexwork-term` trait so we can swap. Capability probe at boot (truecolor? unicode? mouse? focus? bell? response-time for SSH lag estimate): | Capability | Full | Degraded | Floor | |---|---|---|---| | Color | 24-bit truecolor | 256-color LUT | no-color density+bold (tested) | | Unicode | box-drawing + half-blocks + `✦≋≈` | ASCII-only (`#|/-+\|`) | same, assert playable | | Size | 120×36+ | 80×24 minimum (tighter FOV, larger glyphs) | refuse <80×24 with kind message + resize hint | | Input | keys + mouse-drag orbit | keys only | keys only, full game completable | | Latency | local 60fps | SSH adaptive (render-scale + 30fps floor + input prediction dots) | turn-step mode (`Shift` per tick) always available | | Motion | full anim | reduced (no bob, no flash, static water) | screenreader stream mode (sim→text, §5.5) | Detect-and-adapt automatically; every adaptation logged once ("Downshifted to 256-color — set HEXWORK_FORCE_TC=1 to override"). No silent ugliness. ## 5.4 Readability laws (playtested in slice, non-negotiable) 1. **Three-glance rule:** threat (`red glyph + movement`), exit (`lit edge-art + draft lines`), loot (`glint pixel`) each readable within 3 eye-glances of entering a room. Worldgen + palette tests assert glint/edge contrast minimums. 2. **No camouflage kills:** no lethal monster shares glyph+color with a harmless one at the same light level. Mimics break this *deliberately* (Vendric) and are therefore required to have the tell (off-key hum + too-clean pixels) — tested. 3. **Darkness is honest:** underexposed cells never fake-hide an enemy that the sim says is visible, and never show one the sim says is hidden. Render is a pure function of snapshot + camera — *assert with screenshot-hash tests* on scripted scenes. 4. **FOV ≤ 75°, bob off by default in reduced-motion, flash clamped.** Motion-sickness toggles ship in slice, not 1.0. ## 5.5 Accessibility rendering paths - **Colorblind palettes:** protan/deutan/tritan-safe + high-contrast + no-flicker, each with measured contrast ratios (tests). Threat/loot/exit never distinguished by hue alone — always glyph+brightness redundant. - **Screenreader mode:** bypasses raycast; emits prioritized text event stream (threat > exit > loot > flavor, throttled, interruptible). Fully completable — a blind delver should be able to win; test with scripted playthrough asserting winnability from stream alone. - **Dyslexia/log options:** alternate log font rendering (terminal font is user's — so: shorter lines, more spacing, no italics in critical warnings; all caps reserved for death). ## 5.6 What the engine team builds (ordered) 1. Cell buffer + ANSI diff + crossterm bring-up with a static test scene (slice week 1 — something on screen day 3). 2. DDA raycaster + density ramp + fog on voxel snapshots (grayscale first — color is step 4). 3. Edge-art detection + half-block faces + light-level multiply. 4. Truecolor + palettes + 256/no-color fallbacks. 5. Cameras (FP → orbit → tactical) + interpolation + vignette + sound arcs. 6. Adaptive scale + SSH path + capability probe + benchmark harness. 7. Screenshot-hash test rig + readability bots (three-glance assertions on generated rooms). 8. Screenreader stream + reduced-motion + all palettes certified.