TURNCRAFT/docs/briefs/LANE_A_ENGINE.md
jing 5a39e3a947 TURNCRAFT: contracts, docs, and all five lane deliverables (pre-integration)
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>
2026-07-13 20:50:56 +10:00

106 lines
5.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 03
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` 03.
## 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).