Lanes A (engine), B (player), C (worldgen), D (machines/quest), E (audio/fx/ui) as landed, each with HANDOFF.md + update docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
9.1 KiB
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 | IVoxelWorld impl — 32³ Uint8Array chunks (lazy), dirty tracking, fillBox bulk paint, readChunkPadded for the mesher |
| atlas.ts | buildAtlas() — procedural 16×16 texture atlas (all 10 patterns) + emissive atlas + the two block materials + setEmissiveBoost |
| mesher.ts | greedy mesher: 6 face dirs, (blockId, AO) merge, baked per-vertex AO, opaque/transparent split, atlas-tiled UVs |
| ChunkManager.ts | buildAll() + budgeted per-frame update() that remeshes dirty chunks and swaps geometry in place |
| renderer.ts | createRenderer() — the one place Three.js is set up: renderer, scene, camera, lighting, fog, setEmissiveBoost |
| index.ts | public barrel |
Plus the standalone demo (engineDemo.ts + demo-engine.html) and a HANDOFF.md with the 10-line API surface.
Public API (what other lanes / integration consume)
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-repeatuv(0..w, 0..h) plus anaTileattribute (atlas cell). A smallonBeforeCompilepatch onMeshStandardMaterialfracts the UV into the block's cell.NearestFilter, no mips, no tile padding needed —fractkeeps 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;setEmissiveBoostjust setsemissiveIntensityon 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)
- Unblock the shared typecheck — get Lane C's
anchors.tsdeckA/deckBtype fixed sonpm run typecheckis green for everyone (integration pre-flight depends on it). - I'm ready to integrate. The engine API is stable and matches
INTEGRATION.md. When Lane C'sbuildBoothand Lane B/D/E land, I can take the integrator seat (wiresrc/main.ts) or support whoever does — the risk items inINTEGRATION.md(dirty-chunk floods, platter well depth) touch my meshing/dirty paths. - 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
buildBoothfills 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)
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)
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.__demoglobal 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.