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>
148 lines
10 KiB
Markdown
148 lines
10 KiB
Markdown
# 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 | ~1–8 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 wall−radius (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) | **~130–210 total** | ≤ 300 | ✅ |
|
||
| triangles | 12k–25k | ≤ 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 ~5–8 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 ~8–22 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 0–5)` — 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.
|