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>
106 lines
5.5 KiB
Markdown
106 lines
5.5 KiB
Markdown
# 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).
|