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>
5.5 KiB
LANE A — Voxel Engine & Rendering
Read first: docs/DESIGN.md, docs/CONTRACTS.md, everything in src/core/.
You own: src/engine/**, src/demo/engineDemo.ts, demo-engine.html.
You may not touch anything else.
Mission
Build the voxel core: chunked block storage, greedy meshing with a procedural texture atlas and per-vertex AO, and the Three.js renderer/scene/lighting rig. Everything the player sees that is made of blocks goes through you. Target: the full 448×160×256 world renders at 60 fps on a mid-range laptop.
Provides (consumed by other lanes)
VoxelWorld— implementsIVoxelWorldfromsrc/core/types.tscreateRenderer(container: HTMLElement)→{ renderer, scene, camera, setEmissiveBoost(v: number), render() }— the one place Three.js is set upChunkManager— builds/updates chunk meshes from aVoxelWorld, adds them to the scene, and remeshes dirty chunks with a per-frame budgetbuildAtlas()— the procedural texture atlas (CanvasTexture)
Tasks
A1. VoxelWorld storage
- One
Uint8Arrayper 32³ chunk, allocated lazily (empty chunks read as AIR). getBlock: out of bounds → AIR for y ≥ 0, a solid sentinel below y < 0 (per theIVoxelWorlddoc comment).setBlock: ignore out-of-bounds.setBlockmarks the containing chunk dirty, plus each adjacent chunk when the voxel lies on a shared face (otherwise cross-chunk faces go stale).- Expose
forEachDirtyChunk(cb)/clearDirty(cx,cy,cz)for the ChunkManager. - Bulk-fill fast path:
fillBox(x0,y0,z0,x1,y1,z1,id)(worldgen will paint millions of voxels; per-voxel setBlock with dirty bookkeeping is too slow — fillBox writes raw and dirties each touched chunk once).
A2. Procedural texture atlas
- One canvas atlas of 16×16 px tiles, one tile per block id (index = id),
built from
BLOCKSinsrc/core/blocks.ts: paint each tile with itspattern+tint. Implement all patterns in thePatternunion:solid(±6% value noise),brushed(fine horizontal streaks),plywood(wavy grain lines, slightly darker forply_edgewith laminate stripes),speckle,grooves(fine concentric-ish dark lines — straight parallel lines are fine at tile scale),pcb(traces + pads),mesh(dark hole grid),glass(faint diagonal streaks, low alpha),led(bright core, radial falloff),felt(soft high-frequency noise). NearestFilter, no mips (orNearestMipmapLinearwith generated mips if you handle bleeding via 1px tile padding — your call, document it).- Two materials: opaque (alphaTest off) and transparent pass (for
glass,vinyl_blue) — both from the same atlas. Emissive: vertex-color or a second emissive atlas soemissive > 0blocks glow; exposesetEmissiveBoost(v)multiplying emissive intensity (Lane E pulses it to the beat; default 1.0).
A3. Greedy mesher
- Per chunk: greedy meshing over the 6 face directions, merging coplanar
faces with identical (blockId, AO tuple). Neighbor lookups read through
VoxelWorldso chunk borders cull correctly. - Transparent blocks: don't occlude opaque neighbors; mesh them into the separate transparent geometry, don't merge across different transparent ids.
- Per-vertex AO: classic 3-neighbor corner darkening (the Minecraft 0–3 levels), baked into vertex colors. Flip quad triangulation on the AO anisotropy case.
- Output non-indexed or indexed BufferGeometry — your call — with position,
uv (atlas tile + repeat handled by writing uvs per merged quad using a
tiny shader patch or by splitting quads; greedy + atlas needs one of:
(a) uv wrap via shader
fract, or (b) cap merge length and tile uvs. Choose (a): patchonBeforeCompileto fract the uv within the tile. Document the approach in code).
A4. ChunkManager
- Initial full-world mesh build must complete < 3 s (parallelize nothing — just be efficient; greedy over 560 mostly-empty chunks is fast).
- Per-frame remesh budget (e.g. 4 chunks/frame) draining the dirty set;
listens to nothing — poll
forEachDirtyChunkfrom itsupdate(). - Frustum culling is Three.js default per chunk mesh; nothing fancier needed.
A5. Renderer rig
createRenderer(container): WebGLRenderer (antialias off, pixelRatio capped at 2), sRGB output, ACES tone mapping,PerspectiveCamera75°.- Lighting per DESIGN §7: warm key
DirectionalLight, cool ambient/hemi fill, fog colored warm plywood-brown, background near-black warm. - No OrbitControls in the delivered API (demo may use one).
Demo (demo-engine.html)
Standalone scene: build a VoxelWorld, fill a test pattern that exercises
everything — a 60×60 plywood floor, stepped mesas of every block id in a
labeled row (one pillar per id), a glass arch, an LED wall, a hollowed cave —
OrbitControls to fly around, on-screen text: draw calls, triangles, remesh
time. A button "torture" edits 500 random blocks/sec to prove dirty remeshing
keeps 60 fps. A slider drives setEmissiveBoost 0→3.
Acceptance
- Full-size empty-ish world + test pattern ≥ 60 fps, < 300 draw calls
- AO visibly darkens inner corners; no cracks/z-fighting at chunk borders
- Transparent blue vinyl & glass render correctly against opaque blocks
- Torture button: no frame drops below ~55 fps, edits appear within 2 frames
- All 10 patterns visually distinct at
demo-engine.html npm run typecheckclean;HANDOFF.mdwritten
Out of scope
Player, physics, worldgen content, machines, audio, UI. No web workers in v1 (note in HANDOFF if meshing budget suggests they're needed later).