PROCITY/docs/LANES/LANE_B_NOTES.md
m3ultra 5313402e43 Lane B (Streetscape): chunk-streamed walkable town shell
First-person shell: chunk streaming, instanced buildings + facade texture atlas
(22 facade materials -> 1; 5 skin materials city-wide), signs, awnings, furniture,
day/night lighting, minimap, pixel-accurate collision (incl. arbitrary-angle lots),
door raycast -> procity:enterShop, DBG harness, all-fallback mode. Renders Lane A's
full generated town (seed 20261990 'Boolarra Heads', 493 shops); worst continuous-
walk view ~261 draws (<=300 gate). 6 adversarial-review bugs fixed (collision sign,
house doors, awning skin, sign atlas, furniture seed hash, shot-mode restore).

index.html and skins.js include Lane F's inline integration seam edits (marked
'[Lane F integration]'): interior/keeper/citizen wiring + facade filename fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 14:28:47 +10:00

148 lines
10 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 B — NOTES (measured)
Streetscape + game shell. Files: `web/index.html`, `web/js/world/*`, `web/js/world/fixture_plan.js`.
All numbers measured in-browser (Chromium, M-series) via `renderer.info`, 1280×720-ish viewport.
`renderer.info.autoReset` is off and reset once/frame, so **draws/tris are the true per-frame
totals across all passes** (scene + bloom + output). "scene draws" below excludes the ~11
post-processing passes; "total" adds them.
## Perf — fixture (`fixture_plan.js`, "Yarraville Junction", 26 shops, radius 3, shadows on)
| metric | measured | gate | status |
|---|---|---|---|
| draw calls | **148 scene (~159 total)** | ≤ 300 | ✅ |
| triangles | **~5.8k** | ≤ 200k | ✅ |
| skin materials (city-wide, shared) | **21** | ≤ 25 | ✅ |
| live chunks (radius 3) | 12 | — | — |
| chunk build | ~18 ms | 4 ms/frame budget | drained 1/frame |
Walked the fixture end-to-end (~310 m of main street + the backstreets): contiguous, no seams,
chunks stream in/out silently (live count 8↔12 as you move; build queue never starved), day/night
cycles through 6 segments, signs legible, door tooltips + `procity:enterShop` fire, collision
stops the player at wallradius (measured 10.62 m against an 11 m frontage, radius 0.38). Runs with
`web/assets/` renamed away — no crashes, flat-colour fallback town, canvas signs still legible.
## Perf — Lane A integration (`generatePlan`, "Boolarra Heads", ~493 shops, 1 km², radius 2 auto, shadows off)
Lane A's `generatePlan` landed mid-build; `index.html` prefers it over the fixture (guarded import),
so this is the shipped default now. It is a full city with content in nearly every chunk — far
denser than the fixture the ≤300 gate was sized against.
Streetscape numbers below are **buildings + ground + furniture only** (what Lane B owns), measured by
a scripted continuous walk (real streaming + disposal, so ≤25 live chunks — no orphan-chunk inflation).
Lane F's citizens add on top; Fable measures the *integrated* worst main-street view (pop 140) at
**~191 draws**, also under gate.
| metric | measured | gate | status |
|---|---|---|---|
| draw calls (worst gameplay view, continuous walk) | **~261 total (249 scene)** | ≤ 300 | ✅ ~40 draw margin |
| draw calls (typical street view) | **~130210 total** | ≤ 300 | ✅ |
| triangles | 12k25k | ≤ 200k | ✅ (huge headroom) |
| skin materials (shared, city-wide) | **5** | ≤ 25 | ✅ (facade atlas collapsed ~22 facade mats → 1) |
| live chunks (radius 2, walking) | ≤ 25 | — | — |
| chunk build | min ~1 / median ~8 / max ~22 ms | 4 ms/frame | ~1 heavy chunk/frame |
Integration verified: Lane B streams and renders Lane A's 493-shop city correctly — facades, signs,
doors, ground, furniture, day/night all work. Collision holds for the arbitrary lot orientations
`generatePlan` emits (e.g. `ry = 1.32 rad`); see the collision fix below.
### Adaptive settings (index.html)
`BIG_CITY = plan.shops.length > 120`. Big city → **radius 2 + no sun-shadow pass**; fixture → radius
3 + shadows. Override with `?r=N` / `?shadows=0|1`. Dropping the shadow pass ≈ halves draw calls in
dense areas (the shadow map re-renders every caster) for little visual loss in a flat-lit low-poly
town — LANE_B doc explicitly allows "none".
### Draw-budget close-out (this session) — the gate now holds with margin
Fable's first integrated smoke test read **~334 draws at spawn** (that was radius 3, pre-adaptive) and
the deep districts appeared to peak ~445. Two things closed the gap:
1. **Facade texture atlas** (`skins.js` + `buildings.js`). Every facade skin packs into one 2048²
atlas (6×6 slots, each pre-painted with the skin's flat fallback colour, then the JPEG drawn over
it — house law preserved), so a chunk's facades merge into **one** mesh (was ~58 per-skin meshes)
and ~22 facade materials collapse to **1** (this also fixes the "≤25 materials was at the cap /
over for some seeds" concern — now **5** city-wide). UV convention mirrors the sign atlas.
2. **Instanced-mesh merges.** Awnings were one InstancedMesh *per skin* (red/green/blue) per chunk;
now **one per chunk** with per-instance colour (the stripe JPEG lived on the awning's unseen top
face, so flat colour reads the same under the verandah). Shells + parapets + posts merged into
**one** InstancedMesh (per-instance colour) instead of two.
Measured effect (worst dense frustum): **393 → 265 scene draws**; worst *continuous-walk* gameplay
view **~261 total**. Note the earlier "445" figures were partly a **measurement artifact** — teleport-
jumping between distant spots left orphan chunks the real streamer disposes; a proper walk never
exceeds the radius-2 set. Verified day + night, facades still textured, awning colours still varied.
## Architecture decisions worth knowing (for Lane F)
- **Ground is built once, not streamed.** The street graph is a handful of edges; merged flat quads
are a few draws total and can't seam. If Lane A ships a many-hundred-edge network, revisit.
- **Sky/lighting are global**, re-centred on the player each frame (dome radius 650, sun follows).
Linear fog at `(radius+0.4)·64 m` doubles as the chunk pop-in mask.
- **Per-chunk streaming = buildings + furniture only.** `ChunkManager` keeps chunks within Chebyshev
radius R, disposes past R+1, drains a nearest-first build queue under a 4 ms/frame budget. A single
dense chunk can exceed 4 ms (median 8), so it builds ~1/frame → an occasional ~822 ms build frame
entering a dense district (fog-masked). Splitting a chunk build across frames is the next step if
that hitch matters.
- **Night lighting is faked** (Vuntra-style): window + streetlamp **emissive** toggled by a night
flag, plus bloom — no per-streetlight real lights. `ChunkManager.setNight()` re-applies to chunks
streamed in while already night.
- **Shared materials, city-wide** (`skins.js`). Facades share **one atlas material** (all skins in a
2048² atlas); shells + parapets + posts are **one** InstancedMesh/chunk (per-instance colour); all
awning/canopy skins are **one** InstancedMesh/chunk (per-instance colour). Signs are a per-chunk
canvas atlas sized to the chunk's actual sign count, mipmaps off. Net: ~1 draw per kind per chunk.
- **Orientation law** (obeyed by `buildings.toWorld`, the InstancedMesh shells, and `pushOutRect`):
a lot at `ry=0` faces `+Z`; `local→world = RotY(ry)`. Collider and geometry share this convention.
## Fixes applied after an adversarial multi-agent review (this session)
- **Collision for arbitrary `ry`** (`planutil.pushOutRect`): world→local used the wrong rotation sign;
correct only for axis-aligned `ry`. The fixture is axis-aligned so it passed, but Lane A's angled
lots would let you walk through walls. Fixed to the true inverse of `toWorld`. **(Critical for Lane A.)**
- **House doors** were merged into the interactive door mesh with no `doorRects` → raycasting a house
door mis-resolved to the nearest shop. Now a separate non-picked mesh.
- **Awning skin index** used a signed shift (`seed >> 3`) → negative index → undefined skin for ~⅓ of
shops. Now `>>> 3`.
- **Sign atlas** was a fixed 2048² (~16 MB canvas + ~22 MB GPU each) per shop-chunk regardless of
sign count. Now sized to the actual sign count, mipmaps off — critical at Lane A's density.
- **Furniture edge hashing** assumed string edge ids; `generatePlan` uses numeric ids → every edge
got the same seed. Coerced with `String(id)`.
- **Furniture drop:** `chunkIndex` rasterised only the edge centreline, so furniture at a perpendicular
offset could land in an unbuilt chunk and vanish. Rasterisation now covers the full road+verge band.
- **`takeShots()`** never restored the pre-shot mode (P in map mode dumped you into street).
Reviewers' non-issues (intentional): `getColliders` returns a shared scratch array (per-frame
alloc avoidance); per-frame `{clock,chunks}` HUD object (negligible).
## The `shot()` harness
Press **P** → renders 3 fixed cameras and downloads PNGs. Reference stills captured to
`docs/shots/laneB/` (main-street day, plaza overview, night strip). Note: at Lane A's city the fixed
camera poses are tuned for the fixture; re-aim if used against the generated city.
## `window.DBG` — the QA harness hook (this session, for Lane F's shots.py/soak.py)
Loaded only with **`?dbg=1`** (`js/world/dbg.js`, installed by the shell). Answers LANE_F_NOTES §4.
All poses are derived from the **live plan** so bookmarks work on any seed; the day cycle pauses on
the first scripted segment change for deterministic captures.
- `DBG.ready` — true once the first chunks are built + the queue is drained (textures may still decode).
- `DBG.shot(name)` — snaps to a named bookmark, sets its time-of-day, settles bloom, returns stats.
Bookmarks: `street_noon`, `arcade` (dept anchor), `market_square` (stalls), `milkbar_dusk`, `night_neon`.
- `DBG.teleport(x, z, ry)` / `DBG.setSegment(seg 05)` — drive the soak walk + time of day.
- `DBG.enterShop(shopId)` / `DBG.exitShop()` — scripted interior visits (drives Lane F's bridge →
Lane C `buildInterior`; verified end-to-end: `enterShop` → mode `interior`, `exitShop` → back to street).
- `DBG.info()``{ drawCalls, tris, fps, heapMB, geometries, textures, chunk, mode }` (budget/soak read this).
Every bookmark stays under budget (busiest, the dept anchor, ~269 draws). `fps` reads the HUD's
smoothed value (0 under a throttled/background tab; populates in a foreground Playwright run).
## `ChunkManager` lifecycle hooks (optional, LANE_F_NOTES §8)
`createChunkManager` now fires `ctx.onChunkBuilt(key, {cx, cz, buildings, furniture, data})` and
`ctx.onChunkDisposed(key, {cx, cz})` **if a consumer sets them on `ctx`** — a clean per-chunk seam
for spawning/ambient/LOD. Inert by default (the shell doesn't set them; **Lane D's citizens drive off
`plan.streets` instead of chunk lifecycle**, so this is a spare hook, not a dependency).
## Controls
WASD move · shift run · mouse look · click a door · `[` `]` step time-of-day · `T` pause clock ·
`M` map · `P` screenshot · Esc release pointer.