# 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` — implements `IVoxelWorld` from `src/core/types.ts` - `createRenderer(container: HTMLElement)` → `{ renderer, scene, camera, setEmissiveBoost(v: number), render() }` — the one place Three.js is set up - `ChunkManager` — builds/updates chunk meshes from a `VoxelWorld`, adds them to the scene, and remeshes dirty chunks with a per-frame budget - `buildAtlas()` — the procedural texture atlas (CanvasTexture) ## Tasks ### A1. VoxelWorld storage - One `Uint8Array` per 32³ chunk, allocated lazily (empty chunks read as AIR). - `getBlock`: out of bounds → AIR for y ≥ 0, a solid sentinel below y < 0 (per the `IVoxelWorld` doc comment). `setBlock`: ignore out-of-bounds. - `setBlock` marks 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 `BLOCKS` in `src/core/blocks.ts`: paint each tile with its `pattern` + `tint`. Implement all patterns in the `Pattern` union: `solid` (±6% value noise), `brushed` (fine horizontal streaks), `plywood` (wavy grain lines, slightly darker for `ply_edge` with 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 (or `NearestMipmapLinear` with 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 so `emissive > 0` blocks glow; expose `setEmissiveBoost(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 `VoxelWorld` so 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): patch `onBeforeCompile` to 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 `forEachDirtyChunk` from its `update()`. - 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, `PerspectiveCamera` 75°. - 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 typecheck` clean; `HANDOFF.md` written ## 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).