# Lane A — Voxel Engine & Rendering — HANDOFF Status: **complete.** All of A1–A5 implemented, browser-verified, and typechecking clean in isolation. No stubs. ## Public API (10 lines) ```ts import { createRenderer, VoxelWorld, ChunkManager, buildAtlas, meshChunk } from './engine'; const { renderer, scene, camera, atlas, setEmissiveBoost, render, resize, dispose } = createRenderer(container); const world = new VoxelWorld(WORLD_X, WORLD_Y, WORLD_Z); // implements IVoxelWorld world.fillBox(x0,y0,z0,x1,y1,z1,id); // bulk paint (worldgen) world.setBlock(x,y,z,id); world.getBlock(x,y,z); world.isSolid(x,y,z); const chunks = new ChunkManager(world, scene, atlas, /*remeshBudget*/ 4); chunks.buildAll(); // once, after worldgen fills the world (< 3 s; ~25 ms here) chunks.update(); // once per frame — drains dirty chunks, budgeted setEmissiveBoost(v); // Lane E pulses LED glow 0..3 (default 1) ``` `atlas` also carries `opaqueMaterial` / `transparentMaterial` (ChunkManager uses them). `meshChunk(world,cx,cy,cz)` is exported for anyone who wants raw geometry. ## What's done - **VoxelWorld** — 32³ Uint8Array chunks, lazily allocated; oob reads AIR for y≥0 and a solid sentinel for y<0; `setBlock` marks the chunk + any bordering neighbour dirty; `fillBox` writes raw with a chunk-resolve cache and dirties the touched range +1; `readChunkPadded` gives the mesher a 1-voxel-padded id buffer (fast interior copy, getBlock only on the border shell). Dirty API: `forEachDirtyChunk`/`clearDirty`/`dirtyCount`. - **Atlas** — procedural 8×4 grid of 16×16 tiles, one per block id, all 10 patterns painted deterministically; a parallel emissive atlas (black except LED/strobe). Two `MeshStandardMaterial`s (opaque, transparent) with an `onBeforeCompile` patch that fracts the per-quad tile-repeat UV into the block's atlas cell. `NearestFilter`, no mips, no padding needed (fract keeps the sample strictly inside a cell → no bleed). - **Greedy mesher** — per-chunk, 6 face directions, cells keyed by `(blockId, 4×AO)`, coplanar equal cells merged into one quad; classic 3-neighbour AO baked to vertex colour; quad diagonal flips on AO anisotropy; correct outward winding per direction; opaque and transparent faces split into separate geometries; absolute world coords baked in (bounding sphere → Three.js frustum culling per chunk). - **ChunkManager** — `buildAll()` meshes every non-empty chunk once; `update()` drains the dirty set at `remeshBudget` chunks/frame, disposing+swapping geometry in place; removes emptied meshes; `meshCount`/`lastRemeshMs`/`lastRemeshCount` for HUDs. - **Renderer** — WebGLRenderer (AA off, pixelRatio ≤2), sRGB output, ACES tone mapping, 75° camera, warm key `DirectionalLight` + cool `HemisphereLight` + small ambient, warm plywood fog, near-black warm background; self-wiring `ResizeObserver`; the single `setEmissiveBoost` hook Lane E drives. ## Verified in `demo-engine.html` (all acceptance boxes) Full 448×160×256 world + test pattern: **build ~25 ms, 46–47 draw calls, ~1.5 k tris, 86–128 fps**. AO visibly darkens the steel cave's inner corners with no chunk-seam cracks. Glass arch is see-through onto the geometry behind it; blue vinyl blends over the cream wall. All 31 block ids render with distinct patterns (labeled pillar row); LEDs glow and the emissive slider scales them 0→3. Torture (500 edits/s): **min fps 128**, dirty set drains, edits appear within ~1 frame. ## Post-build adversarial review (done) A 5-dimension correctness review + programmatic verification caught and fixed two bugs: - **HIGH (fixed):** Z-axis winding table (`AXIS[2]`) had flip flags swapped → every opaque block's +Z/−Z faces were wound backwards and back-face-culled. Verified via a per-triangle geometric-vs- stored-normal check: 520 Z tris inverted before, **0 mismatches after** (all six normals, opaque and transparent). If you ever touch the mesher, re-run that check (see `LANE_A_update.md`). - **LOW (fixed):** `renderer.ts` resize fallback used `??` where `clientWidth` returns `0` (not nullish); switched to `||` so a 0-width container falls back to the window. ## Contract friction / notes for the integrator 1. **Repo-wide `npm run typecheck` currently FAILS — but not on Lane A code.** The only error is in Lane C's `src/worldgen/anchors.ts` (a `LAYOUT.deckB` assigned where a `deckA`-typed value is expected). Lane A compiles clean on its own; verified with an isolated tsconfig over `src/core` + `src/engine` + `src/demo/engineDemo.ts`. Not my file to touch — flagging for whoever owns worldgen / integration. 2. **Dev-only console warning** "Multiple instances of Three.js" appears on the demo page because `OrbitControls` (demo convenience only, not in the shipped engine API) pulls `three` through a second Vite resolution. Harmless; it will not appear in the integrated game (no OrbitControls there). If desired, add `resolve.dedupe: ['three']` to `vite.config` — but that's a shared config file, so I left it to the integrator. 3. **Draw-call budget at full booth scale.** The demo (empty-ish world) is ~47 calls, far under 300. The *dense* authored booth could push non-empty-chunks × 2 (opaque+transparent) upward; if it ever nears 300, the cheap wins are merging tiny transparent geometries or worker-threaded meshing. **No web workers in v1** (per brief); meshing is fully synchronous and comfortably inside the per-frame budget so far. 4. **Wiring reminders** (match `docs/INTEGRATION.md`): call `chunks.buildAll()` once after `buildBooth(world)`, then `chunks.update()` every frame in the loop. `createRenderer` builds the atlas for you and exposes it as `.atlas`; hand that same object to `ChunkManager` so `setEmissiveBoost` and the meshes share one material set. Do **not** call `buildAtlas()` a second time — you'd get materials `setEmissiveBoost` can't reach. ## Owned paths (no writes outside these) `src/engine/**`, `src/demo/engineDemo.ts`, `demo-engine.html`. Core untouched. `src/demo/engineDemo.ts` exposes a tiny dev-only `window.__demo.look()` camera hook, clearly commented — used for inspection, harmless in normal play.