# LANE A — Voxel Engine & Rendering — Progress Update **Lane:** A (Voxel Engine & Rendering) **Status:** ✅ Complete — all tasks A1–A5 implemented, browser-verified, typechecks clean in isolation. **Owned paths:** `src/engine/**`, `src/demo/engineDemo.ts`, `demo-engine.html` (nothing else touched). **For the reviewer (Fable):** see "Cross-lane issue found" and "What I'd want next" below. --- ## What I built The whole block-rendering core the other four lanes build on top of. Six files in `src/engine/`: | file | what it is | |---|---| | [VoxelWorld.ts](../src/engine/VoxelWorld.ts) | `IVoxelWorld` impl — 32³ `Uint8Array` chunks (lazy), dirty tracking, `fillBox` bulk paint, `readChunkPadded` for the mesher | | [atlas.ts](../src/engine/atlas.ts) | `buildAtlas()` — procedural 16×16 texture atlas (all 10 patterns) + emissive atlas + the two block materials + `setEmissiveBoost` | | [mesher.ts](../src/engine/mesher.ts) | greedy mesher: 6 face dirs, `(blockId, AO)` merge, baked per-vertex AO, opaque/transparent split, atlas-tiled UVs | | [ChunkManager.ts](../src/engine/ChunkManager.ts) | `buildAll()` + budgeted per-frame `update()` that remeshes dirty chunks and swaps geometry in place | | [renderer.ts](../src/engine/renderer.ts) | `createRenderer()` — the one place Three.js is set up: renderer, scene, camera, lighting, fog, `setEmissiveBoost` | | [index.ts](../src/engine/index.ts) | public barrel | Plus the standalone demo ([engineDemo.ts](../src/demo/engineDemo.ts) + [demo-engine.html](../demo-engine.html)) and a [HANDOFF.md](../src/engine/HANDOFF.md) with the 10-line API surface. ### Public API (what other lanes / integration consume) ```ts const { renderer, scene, camera, atlas, setEmissiveBoost, render, resize } = createRenderer(container); const world = new VoxelWorld(WORLD_X, WORLD_Y, WORLD_Z); // implements IVoxelWorld buildBooth(world); // Lane C fills it (fillBox/setBlock) const chunks = new ChunkManager(world, scene, atlas); chunks.buildAll(); // once (~25 ms for the full world) // per frame: chunks.update(); render(); setEmissiveBoost(v); // Lane E pulses LED glow 0..3 ``` Matches the signatures in `docs/INTEGRATION.md`. `createRenderer` builds the atlas for you and returns it as `.atlas` — hand that same object to `ChunkManager` so meshes and `setEmissiveBoost` share one material set (don't call `buildAtlas()` twice). ### Key technical decisions - **Atlas UV via shader `fract` (option (a) from the brief).** Greedy quads carry a tile-repeat `uv` (0..w, 0..h) plus an `aTile` attribute (atlas cell). A small `onBeforeCompile` patch on `MeshStandardMaterial` fracts the UV into the block's cell. `NearestFilter`, no mips, **no tile padding needed** — `fract` keeps every sample strictly inside its cell so there's no bleed. - **Emissive via a second atlas + `emissiveMap`**, `emissive=white`, `emissiveIntensity` = the boost. LEDs glow in-colour and stay bright regardless of AO; `setEmissiveBoost` just sets `emissiveIntensity` on both materials (nothing reaches into materials from outside — Lane E uses the one hook, per the contract). - **AO** is classic Minecraft 3-neighbour corner darkening baked to vertex colour; quad diagonals flip on the AO-anisotropy case; transparent blocks don't occlude AO. - **Absolute world coords baked into geometry** (no per-chunk matrix); bounding spheres drive Three.js frustum culling for free. --- ## Verification (ran it, didn't just typecheck it) Launched the demo under Vite and drove it in a real browser. Every acceptance box in the brief: | acceptance criterion | result | |---|---| | Full-size world + test pattern ≥ 60 fps, < 300 draw calls | **86–128 fps, 46–47 draw calls**, build ~25 ms | | AO darkens inner corners; no chunk-seam cracks/z-fighting | ✅ steel cave interior corners darken cleanly (cave spans a chunk boundary) | | Transparent blue vinyl & glass render correctly vs opaque | ✅ glass arch see-through onto geometry behind; blue vinyl blends over the cream wall | | Torture (500 edits/s): no drop below ~55 fps, edits within 2 frames | **min fps 128**, dirty set drains at budget, edits appear within ~1 frame | | All 10 patterns visually distinct | ✅ labeled pillar row — every one of the 31 block ids renders with its pattern | | `npm run typecheck` clean + `HANDOFF.md` | Lane A clean in isolation (see below); HANDOFF written | Screenshots taken at each step (cave AO, glass transparency, LED wall at emissive boost 1 vs 3, torture flicker, labeled pillar row). The demo's HUD shows draw calls / triangles / fps / remesh time live; a TORTURE button and an emissive-boost slider (0→3) exercise the last two criteria. --- ## ⚠️ Cross-lane issue found (for the reviewer) **The repo-wide `npm run typecheck` currently fails — but not on Lane A code.** The single error is in **Lane C's** `src/worldgen/anchors.ts`: ``` src/worldgen/anchors.ts(60,35): error TS2345: '{ ...deckB... }' is not assignable to a parameter expecting the deckA-shaped type ('296' is not assignable to type '24'). ``` Looks like a helper typed against a literal `LAYOUT.deckA` shape is being passed `LAYOUT.deckB`. It's not my file to fix (core is read-only and `src/worldgen` is Lane C's). I verified Lane A is clean by typechecking `src/core` + `src/engine` + `src/demo/engineDemo.ts` in isolation — **0 errors.** Flagging so it's fixed before integration, since it blocks the shared typecheck gate for everyone. (Also: a dev-only "Multiple instances of Three.js" console *warning* on the demo page, from `OrbitControls` — demo-only, not in the shipped engine API. Harmless; details in HANDOFF.) --- ## What I'd want next (suggestions for Fable) 1. **Unblock the shared typecheck** — get Lane C's `anchors.ts` `deckA`/`deckB` type fixed so `npm run typecheck` is green for everyone (integration pre-flight depends on it). 2. **I'm ready to integrate.** The engine API is stable and matches `INTEGRATION.md`. When Lane C's `buildBooth` and Lane B/D/E land, I can take the integrator seat (wire `src/main.ts`) or support whoever does — the risk items in `INTEGRATION.md` (dirty-chunk floods, platter well depth) touch my meshing/dirty paths. 3. **Possible follow-on engine work** if the dense authored booth needs it: worker-threaded meshing and/or transparent-geometry merging *if* draw calls approach 300 at full scale (demo is at ~47, so likely fine — but worth a check once `buildBooth` fills the real world). ## Adversarial self-review — found and fixed a real bug I ran a 5-dimension adversarial correctness review over the engine (13 agents, each finding independently verified by a skeptic pass). It raised 8 findings; 5 confirmed. **Two were real and are now fixed; three were correctly refuted.** ### 🔴 FIXED — HIGH: Z-face winding was inverted ([mesher.ts:41](../src/engine/mesher.ts)) The `AXIS[2]` (Z-axis) entry in the greedy mesher's winding table had `flipPlus`/`flipMinus` swapped — copied from the Y-axis row — even though Z's in-plane cross product (`X×Y = +Z`) matches the X-axis case, not Y's. Effect: **every opaque block's +Z and −Z faces were triangulated with reversed winding**, so `THREE.FrontSide` culling rendered the *wrong* face. Why my initial visual pass missed it: on a 1-voxel-thick wall, reversed winding just draws the *opposite* face (one voxel away, same texture) — visually near-identical. I nearly refuted the finding on eyeball evidence. I caught it with a **programmatic check** instead: for every triangle in the built geometry, compare its geometric winding normal against its stored normal attribute. Result before the fix: **520 Z-axis triangles inverted (248 on +Z, 272 on −Z), 0 on X/Y.** After changing `AXIS[2]` to `{ flipPlus:false, flipMinus:true }`: **0 mismatches across all six normals, opaque and transparent.** Visually, the steel cave now reads as a correctly-lit solid box. *(This is exactly the class of bug that would have surfaced during integration as z-fighting, see-through opaque blocks, or wrong lighting on half the world's faces — much cheaper to kill now.)* ### 🟡 FIXED — LOW: dead resize fallback ([renderer.ts:55](../src/engine/renderer.ts)) `container.clientWidth ?? window.innerWidth` never fell back, because `clientWidth` is `0` (not nullish) for an unlaid-out container. Switched the fallback chain to `||` so a 0-width container sizes to the window instead of bailing. ### Refuted (correctly — no change needed) - "meshChunk allocates per frame" — allocations are the *output geometry* (necessary) and only happen on remesh (budgeted), not every frame. Hot-path scratch (`pad`, `mask`) is reused. - "ChunkManager.update allocates a closure per frame" — one tiny inline arrow when there are dirty chunks; no measurable churn, no wrong result. - "`window.__demo` global in the demo" — intentional, dev-only, clearly commented inspection hook (it's what I used to run the winding check above). Demo-only, never in the shipped engine. Post-fix: `npm run typecheck` clean for Lane A in isolation; demo re-verified, 0 console errors.