R9 blocker: interior rig figures were ~2x too tall (keeper crown 3.83m in a 3.4m room, clipping the ceiling). Root cause in rigs.js buildFigure: it normalised scale by height/headY, where headY is the head bone's world Y in BIND POSE — but the Mixamo skeleton origin is the hips (~mid-body), so headY is head-above-hips (~half the standing height), not feet->crown. Every rig came out ~1.83-2.0x too tall. (The head does NOT move when posed — bind crown 3.209m vs idle 3.205m — so it's a hips-relative measure, not an animation issue.) Fix: normalise by the feet->crown span (headY - minY, both already computed). buildFigure(rig, h) now lands the crown at exactly h above planted feet. Fixes keepers, browser rigs, near-tier street rigs AND the impostor bake (all ride buildFigure). Verified: all 19 fleet rigs crown==1.75m; live keeper 3.83m -> 1.82m; determinism + dispose-leak soak unchanged. Regression guard (required): new tools/qa/interior_scale_check.py (Playwright) enters a sample of shops, injects browser rigs at browse points, asserts every interior fig crown in [1.4,2.0]m AND < room dims.H AND feet planted. Wired into tools/qa.sh --strict (run_gate => a giant fails qa). Proven bidirectional: PASS on the fix (24 figs/6 shops), FAIL on a reintroduced giant. Full qa.sh --strict: 6 passed, 0 failed. Re-shot laneD/r10_browsers_fixed.jpg. Figures human-sized relative to doors/fittings: yes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
334 lines
24 KiB
Markdown
334 lines
24 KiB
Markdown
# PROCITY-D — progress (Lane D · Citizens)
|
||
|
||
## Round 10 (blocker fix) — interior giants → human-sized. qa --strict GREEN. (2026-07-15)
|
||
|
||
Fixed the R9 ship-blocker: interior rig figures were ~2× too tall (keeper crown at 3.83 m in a
|
||
3.4 m room, clipping the ceiling). Measured live, not from notes.
|
||
|
||
- **Root cause (rigs.js `buildFigure`)**: it normalised scale by `height / headY`, where `headY` is
|
||
the head bone's world Y in bind pose. But the Mixamo skeleton's ORIGIN is the hips (~mid-body), so
|
||
`headY` is head-**above-hips** (~half the standing height) — not the feet→crown height. Result:
|
||
every rig ~1.83–2.0× too tall. (NB Fable's "bind-pose 82 vs posed 180" hypothesis was close on the
|
||
numbers but the head does NOT move when posed — bind crown 3.209 m vs idle-posed 3.205 m. The 82 is
|
||
head-above-hips; the 175 is the true feet→crown span. So it's a hips-relative measure, not animation.)
|
||
- **Fix (one logic line)**: normalise by the feet→crown **span** = `headY - minY` (both already
|
||
computed). Now `buildFigure(rig, h)` lands the crown at exactly `h` above planted feet. Verified all
|
||
19 fleet rigs → crown == 1.75 m (feet 0); live keeper in "Second Time Redfern" 3.83 m → **1.82 m**.
|
||
This one change fixes keepers, browser rigs, near-tier street rigs AND the impostor bake (all ride
|
||
`buildFigure`) — street peds looked fine only because they're mostly impostors, which are sized by
|
||
requested height and so masked the bug.
|
||
- **Regression guard (required)** — new `tools/qa/interior_scale_check.py` (Playwright): enters a
|
||
sample of open shops, injects browser rigs at every browse point, and asserts EVERY interior fig's
|
||
crown ∈ [1.4, 2.0] m AND < room `dims.H` AND feet planted. Wired into `tools/qa.sh --strict` (gate 6,
|
||
`run_gate` ⇒ a giant fails qa). Proven **bidirectional**: PASS on the fix (24 figs / 6 shops), FAIL
|
||
on a reintroduced giant (crowns 3.4–4.05 m, all 6 shops red). `qa.sh --strict` = **6 passed, 0 failed**.
|
||
- **Determinism + leak re-verified**: same seed → same keeper height + ped (shop 18 → 1.823 m, idx 10,
|
||
identical twice; buildFigure crown stable & == requested); 15 spawn/dispose cycles → geo/tex delta 0.
|
||
- **Re-shot** `laneD/r9_browsers_in_shop.jpg` → `laneD/r10_browsers_fixed.jpg` (kept r9 as the
|
||
documented before-state).
|
||
|
||
**Figures human-sized relative to doors/fittings: yes** — verified live (measured crowns 1.6–1.86 m
|
||
across 6 shop types under 3.4–4.0 m ceilings) and visible in `r10_browsers_fixed.jpg` (a hi-vis worker
|
||
and a hoodie browser both well under the ceiling, in proportion to the shelves and door).
|
||
|
||
Files touched (mine only): `web/js/citizens/rigs.js` (fix), `tools/qa/interior_scale_check.py` (new
|
||
gate), `tools/qa.sh` (+gate block, authorized by ROUND10 §Lane D), `docs/shots/laneD/r10_*.jpg`, docs.
|
||
|
||
## Round 9 (v2.0 ship) — interior presence: occupancy truth + browser rigs. qa GREEN.
|
||
|
||
Built the D side of the one cross-lane seam (C browse points → D occupancy+browsers → F handoff).
|
||
Follow a ped into a shop and find them browsing. Full numbers + F wiring in `LANE_D_NOTES.md`.
|
||
|
||
- **Occupancy truth (D1)**: patronage records who's inside which shop by shopId. New
|
||
`citizens.occupancyOf(shopId) → {count, occupants:[{seed,enteredAt,pedIndex}]}`. Cleaned on
|
||
emerge/chunk-drop/stream-toggle. **Deterministic** (17 shops + occupants byte-equal across two runs).
|
||
- **Browser rigs**: `keepers.js` gained `browse` (faces shelf, no greet) + `pedIndex` (the browser IS
|
||
the ped who ducked in) + `seedKey`. Validated 12 enter/exit cycles: count == min(occupancy,3) **12/12**,
|
||
worst **61 draws ≤350**, **leak-free** (0 GPU delta), disposed every exit. Merged peds = ~1 draw each.
|
||
- **Consistency (D2)**: inside peds hidden on the street (0 rendered), re-emerge at the door.
|
||
- **F handoff** (verified by running F's exact wiring): enter → `occupancyOf` → `browsePoints.slice(0,
|
||
min(count,3))` → `keepers.spawn({...browse:true, pedIndex})`. `web/index.html` `setShops` door points
|
||
need `shopId: s.id` added. Closing-time handled by the dwell model (pending A's explicit ruling).
|
||
- Evidence: `docs/shots/laneD/r9_browsers_in_shop.jpg`. v1 path untouched.
|
||
|
||
Changed `web/js/citizens/sim.js` (occupancy) + `keepers.js` (browser mode) + `citizens_test.html`
|
||
(shopId in synthetic shops).
|
||
|
||
---
|
||
|
||
## Round 8 (the crowd comes alive) — shop patronage v0 + weather reaction. qa GREEN.
|
||
|
||
Also did the R7 post-flip shell verification (F's flip landed) + pushed the tags to origin (main
|
||
1a20501→900b274; v1.0/v1.1/v2.0-alpha/v2.0-beta all on origin).
|
||
|
||
- **Post-flip verify** ✅ (real shell): no-flag boot → streamMode true, determinism (188 ids), leak-free
|
||
(0 delta), setNightLivelyChunks wired; `?roster=v1` → fixed roster, golden identity byte-identical.
|
||
- **Shop patronage v0 (D1)** ✅: streamed peds duck into open shops they pass (going→inside→emerge, hidden
|
||
while inside), hours-aware — noon variety (up to ~38 inside, 7 types), **night video-only** (the
|
||
open-late shop draws the crowd). Deterministic (150 chunk-keyed ids), leak-free (0 GPU delta over the
|
||
churn), **budget-neutral** (draws −1, inside peds hidden). Behind `?patronage=0`, default-on for stream.
|
||
- **Weather reaction (D2)** ✅: reads B's `PROCITY.weather` (no import). Rain → density **−56%** (40–60%
|
||
target), walk speed +~14%, more sheltering (inside 6→25); overcast −10%; clear = v1.
|
||
- v1 path untouched (no patron fields, golden identity). Full numbers + F's `setShops`/`setWeather`/
|
||
`?patronage=0` wiring in `LANE_D_NOTES.md`.
|
||
|
||
Only `web/js/citizens/sim.js` + `web/citizens_test.html` (synthetic-shop demo + flags) changed.
|
||
|
||
---
|
||
|
||
## Round 7 (the flip) — MERGE VALIDATED, GO given to F, defaults + escape hatch ready. qa GREEN.
|
||
|
||
Validated Lane E's ped-merge (the gate my R6 memo named) and handed F the **GO** for the roster flip.
|
||
Full go/no-go numbers + F's two-line wiring at the top of `LANE_D_NOTES.md`.
|
||
|
||
- **Merge validated (D1)**: all 19 peds now **1 mesh / 1 material** (was 8/2) → ~1 draw/near-rig
|
||
(fleet 116→24 at the cap); bind+animate/no-T-pose, silhouettes+atlas materials crisp (hi-vis/suit/
|
||
kid), impostor bake clean, identity variety + determinism preserved. GPU memory dropped too.
|
||
- **The flip numbers (full density, perChunk 16)**: worst street view **241 draws ≤300** (was 356
|
||
unmerged), **65,899 tris ≤200k**, 17 near-rigs; 30-chunk soak (726 builds/disposes) **leak-free, 0
|
||
GPU delta**, heap stable. → **GO.**
|
||
- **Full-density defaults (D2)**: `enableStream` default flipped 8→**16** (the merge affords it);
|
||
near-cap stays 24 (now 24 draws not 168).
|
||
- **Post-flip + escape hatch (D3)**: test page + shell now default to stream; **`?roster=v1` restores
|
||
the fixed roster** (verified: streamMode false, determinism ✓). No-flag boot → streamMode true,
|
||
determinism ✓. F's `web/index.html` flip wiring (invert to default-on + `?roster=v1` escape)
|
||
documented; the in-shell no-flag-URL boot completes when F lands F1 (roster proven running in-shell
|
||
at the shipping default via the in-shell soak above).
|
||
|
||
Only `web/js/citizens/sim.js` (perChunk default) + `web/citizens_test.html` (default-on + escape
|
||
hatch) changed. Committed atomically.
|
||
|
||
---
|
||
|
||
## Round 6 (harden streamed roster toward default-on) — DONE. qa GREEN. Flag still default-off.
|
||
|
||
Hardened `?roster=stream` and wrote the **default-on readiness memo** (top of `LANE_D_NOTES.md`) so
|
||
Fable + F can decide the R7 flip on evidence. In-shell (real town), stream runtime-enabled:
|
||
|
||
- **Soak green**: 242 chunk builds/disposes over 3 street-graph walks → **0 GPU geom/tex delta**; 16
|
||
shop enter/exit → 0 delta; heap oscillates 50→77→54 MB (GC recovers, no leak); 188 chunk-keyed
|
||
identities re-derive from seed; 0 console errors; flag-off byte-identical (citizen-0 = R4 golden).
|
||
- **Hours-aware density (new)**: added `setNightLivelyChunks` + a night floor so the open-late video
|
||
block stays lively at night (31→16) while ordinary streets go near-empty (65→6). F call-site documented.
|
||
- **Composition with `?dig=1`**: roster is street-tier, fully inert while interior/dig is open (frame
|
||
loop street-branch-only); enter/exit record shops leak-free with stream on. No shared state with any
|
||
v2 flag → all-on combo safe.
|
||
- **Perf/budget — the one gap**: each decimated ped = 8 sub-meshes / 2 materials = ~7 draws/near-rig, so
|
||
full-density stream (`perChunk 16`) hits **356 draws** (over the 300 street budget). Set the default to
|
||
`perChunk 8` (**291 draws, ≤300, budget-safe today**) and documented the clean unlock — merge ped
|
||
sub-meshes by material (8→2 → ~2 draws/rig → perChunk 16 fits at ~260) — as the single thing gating
|
||
full-density default-on in R7 (F1-safe: v1 spawn view has ~0 near-rigs).
|
||
|
||
Only `web/js/citizens/sim.js` (night floor + default) + `web/citizens_test.html` (default perChunk)
|
||
changed. Committed atomically; flag default-off; v1 path untouched.
|
||
|
||
---
|
||
|
||
## Round 5 (v2 foundations) — DONE. Chunk-streamed roster behind `?roster=stream`. qa GREEN.
|
||
|
||
Implemented the R3 design note (was design-only): the chunk-streamed roster, default-off, v1-identical
|
||
when off. Full numbers + Lane F wiring in `docs/LANES/LANE_D_NOTES.md` (top).
|
||
|
||
- **Chunk-local identity** keyed `(seed, chunkKey, i)` → same seed + chunk → same residents regardless
|
||
of town size / visit order; **windowed spawn/despawn** hook-driven (Lane B's `onChunkBuilt`/
|
||
`onChunkDisposed`, sim auto-detects) with a camera-poll fallback; **global near-cap** across live
|
||
chunks; **owner-chunk despawn** (no cross-boundary re-keying); v1 path untouched (`chunkStream` opt-in).
|
||
- **Proven** on a big 9×9 fixture grid: (a) constant far-field density — ~58–129 citizens within 70m
|
||
at spawn AND 150m out, vs v1's uniform ~12–15 (fixed roster spread thin); (b) deterministic — 507
|
||
chunk-keyed identities re-derive from seed; (c) leak-free — 2 full grid walks (~380 chunk cycles) →
|
||
**0 geom/tex delta**, pool capped 30, chunks bounded 25; (d) budget — same near-cap + 1 impostor
|
||
draw as v1, 2.31 ms/frame; (e) **flag-off byte-identical** — shell citizen-0 signature exactly
|
||
matches the R4 golden. Hours-aware density included (per-chunk tod thinning).
|
||
- Only `web/js/citizens/sim.js` + `web/citizens_test.html` (bigger fixture + stream toggle/HUD/
|
||
determinism) changed. Flag + F call-site documented in LANE_D_NOTES; F wires `web/index.html`.
|
||
Evidence: `docs/shots/laneD/r5_stream_tiers.jpg`.
|
||
|
||
---
|
||
|
||
## Round 4 (v1-tag round) — DONE. qa.sh --strict GREEN.
|
||
|
||
All three Lane-D R4 tasks complete; details + numbers in `docs/LANES/LANE_D_NOTES.md` (top).
|
||
|
||
- **D1 — in-shell verify** ✅ In the real game (`web/index.html`): peds + keepers upgrade
|
||
placeholder→rig; **12 enter/exit shops leak-free** (`renderer.info.memory` back to exactly
|
||
baseline, Δ0); determinism holds across reload; `?noassets=1` fully placeholder, zero ped fetches.
|
||
- **D2 — decimated peds** ✅ (critical path; E1 landed) Validated E's fleet: **all 19 peds ≤3k tris
|
||
(1564–2805, avg 2450)**, rigs bind + animate (skinning/skeleton preserved, walk/idle retarget
|
||
unchanged), **0 broken meshes** under eviction churn, silhouettes + identity variety intact,
|
||
impostor atlas bakes clean, determinism + leak re-pass. **Gate-3 sign-off for Lane F: near fleet
|
||
now ≈59.6k tris at the 24-cap (was ~1.2M); whole test view 88.9k (was ~1.5M).**
|
||
- **D3 — exposure sync** ✅ Verifying it **found + fixed a real bug**: the impostor material
|
||
self-tone-mapped (`toneMapped:false` + in-shader ACES), which is correct on my direct-canvas test
|
||
page but **double-tone-mapped under the shell's EffectComposer/OutputPass** (three only tone-maps
|
||
materials on the canvas path). Reworked it to a standard `toneMapped:true` material that outputs
|
||
linear and uses three's own tone-map/colorspace chunks → tone-maps **identically to the near rigs
|
||
on both paths**. Verified near+mid match at noon and correct night render in-shell
|
||
(`docs/shots/laneD/r4_shell_*.jpg`). `citizens.setExposure()` is now a harmless no-op (exposure is
|
||
global); Lane F can keep or drop the call at `web/index.html:245`.
|
||
|
||
Only `web/js/citizens/impostor.js` changed this round (+ my NOTES/progress/R4 shots). Committed
|
||
atomically; `web/index.html`, `dbg.js`, and other lanes' in-flight files left untouched.
|
||
|
||
---
|
||
|
||
# (Round 1) — initial Lane D build
|
||
|
||
**Status: complete and verified in-browser.** 2026-07-14, by PROCITY-D (Opus 4.8).
|
||
Standalone test page runs, all Lane-D acceptance gates pass. One cross-cutting finding for Lane E
|
||
(ped poly-count) is flagged below and in `docs/LANES/LANE_D_NOTES.md`.
|
||
|
||
### Re-verify after session resume (2026-07-14, PROCITY-D)
|
||
Prior session hit the token limit; resumed on a fresh account. Confirmed nothing rotted — all 5
|
||
citizen modules intact on disk with every adversarial-review fix present. Re-ran the gates live in
|
||
the browser on this machine:
|
||
- **perf @ pop 200**: max mixer **0.2 ms**, sim CPU **0.148 ms/frame**, near capped at **24** ✓
|
||
- **determinism**: `✓ 200 identities match seed 20261990` (live vs two fresh recomputes) ✓
|
||
- **no T-pose / one shared walk clip / keeper at counter** ✓
|
||
- **`?noassets`**: placeholder town, 0.00 ms mixer, 2.4k tris, crash-free ✓
|
||
- **Integration contract PROVEN**: built Lane A's real `generatePlan(1234)` → "North Cressy Reach"
|
||
(27 nodes / 22 edges) and ran `new CitizenSim({ …, graph: plan.streets })` **with zero adapter** —
|
||
0 dangling edges, 0 non-finite citizen coords, no errors. `plan.streets` is exactly the `{nodes,
|
||
edges}` shape the sim consumes. Lane F §3.3 is a one-line construction (see below).
|
||
|
||
### Round 3 — support round (2026-07-14, PROCITY-D) — all 3 tasks done
|
||
Per `docs/LANES/ROUND3_INSTRUCTIONS.md` §Lane D. Committed Lane D landed as `9eb1acc` first; then:
|
||
|
||
1. **Keeper GLB-rig upgrade + leak-free dispose (task 1).** Gated on F's §3.4 (keepers are still
|
||
placeholder-only in the shell — F builds `KeeperManager` with no `fleet`). Verified **my side** of
|
||
the contract in `citizens_test.html`: a fleet-backed keeper spawns as a **rig**, and 15 enter/exit
|
||
spawn→greet→`remove()` cycles leave `renderer.info.memory` **exactly at baseline** (geo 140→140,
|
||
tex 175→175) → leak-free. Enablement for F is one arg (`fleet`); documented in LANE_D_NOTES.
|
||
2. **`ImpostorLayer.setExposure()` wired to day/night (task 2).** Added a clean `CitizenSim.setExposure(e)`
|
||
passthrough (sim.js) that also **survives the placeholder→rig atlas re-bake** (stored + re-applied).
|
||
Confirmed the shell *does* animate `renderer.toneMappingExposure` per segment (`lighting.js`), so
|
||
this is a real fix, not defensive. Verified: uniform tracks 0.55↔2.3 and persists across the upgrade.
|
||
Left F the exact one-line call site (`index.html:225`, after `setTimeOfDay`) in LANE_D_NOTES — it's
|
||
an F-owned seam edit, so I documented rather than edited it.
|
||
3. **Chunk-streamed roster design note (task 3).** Written in LANE_D_NOTES ("Chunk-streamed roster —
|
||
v1.5 design note"): chunk-local identity keying, poll- vs hook-driven windowing (with the `chunks.js`
|
||
reality), global near-cap across live chunks, ownership/hand-off, budget, and an opt-in migration that
|
||
leaves v1 untouched. **Design only, no implementation** as instructed.
|
||
|
||
Post-edit re-verify: no console errors, determinism ✓ (200 match seed), near capped 24, mixer 0.3 ms.
|
||
Round-3 code touches **only sim.js** (my file); the rest is docs. No sibling files edited.
|
||
|
||
---
|
||
|
||
## What I built (owns `web/js/citizens/*`, `web/citizens_test.html`, `web/models/*`)
|
||
|
||
| file | lines-ish | what |
|
||
|---|---|---|
|
||
| `web/js/citizens/rigs.js` | ~185 | the ported crown-jewel rig stack — fleet loader, `_canon`/`canonRig`/`_rotOnly`, `buildFigure` (head-bone height-normalise + feet plant), `spawnRig`, `makeActor` (walk↔idle crossfade for the near tier), `pickRig` |
|
||
| `web/js/citizens/placeholder.js` | ~150 | seeded low-poly box humanoid (the POLY lock) — walks/idles, so the town is populated from frame one and in `?noassets` mode |
|
||
| `web/js/citizens/impostor.js` | ~210 | mid-tier LOD: bakes each ped to a 4-yaw sprite atlas on an offscreen RT, one InstancedMesh of camera-facing billboards → **1 draw call** for the whole mid crowd |
|
||
| `web/js/citizens/sim.js` | ~330 | the simulation — deterministic roster, footpath lanes off the street graph, near/mid/far tiers with hysteresis + hard near-cap, a reusable rig pool, staggered mixer budget, time-of-day density |
|
||
| `web/js/citizens/keepers.js` | ~95 | one keeper per shop at the counter, idle clip + greet turn toward the player |
|
||
| `web/citizens_test.html` | ~310 | standalone harness (fixture 3×3 street graph, no Lane A/B imports) |
|
||
| `web/models/peds/*` | 21 GLBs, 37 MB | 19 rigged peds + walk + idle, **byte-identical** copies from 90sDJsim (SHA-verified) |
|
||
| `docs/LANES/LANE_D_NOTES.md` | — | measured budgets + clip wishlist |
|
||
| `docs/shots/laneD/*.jpg` | 5 shots | near / mid / main-street / tier-debug / noassets |
|
||
|
||
Everything is plain ES modules, three from `web/vendor/`, all randomness via `core/prng.js`
|
||
(`Math.random()` used nowhere in identity/placement). Loaders come from `core/loaders.js`.
|
||
|
||
## Acceptance — every gate met
|
||
|
||
- ✅ **200-citizen fixture holds 60fps** — full frame CPU ~2.6 ms (≈380 fps of headroom on the M3
|
||
Ultra; GPU trivial). Preview's throttled rAF shows 47–84 fps, but timed frames prove the margin.
|
||
- ✅ **Mixer update < 2 ms/frame** — measured **0.4 ms max** at 200 citizens (staggered: nearest 8
|
||
every frame, ≤4 more per frame).
|
||
- ✅ **No T-pose ever** — fixed a real bug: freshly-promoted rigs were showing bind-pose for a frame;
|
||
actors now evaluate their mixer at acquire time. Verified across the fleet in `01_near_tier.jpg`.
|
||
- ✅ **No giants/ants** — head-bone height normalisation verified across all 19 peds (1.55–1.95 m).
|
||
- ✅ **One shared walk clip animates every ped** — the `mixorig` canonicalisation binds a single
|
||
`walk.glb` to all 19; confirmed in-scene.
|
||
- ✅ **Same seed → same crowd** — determinism button asserts the live roster equals a fresh seed
|
||
recompute; holds while citizens walk (identity ≠ live position).
|
||
- ✅ **Screenshots** into `docs/shots/laneD/` (near, mid, busy main street, + tier-debug & noassets).
|
||
- ✅ **`docs/LANES/LANE_D_NOTES.md`** — measured budgets + the mixamo-fetch clip wishlist.
|
||
|
||
Plus the LOD design points from the lane brief: near<25m rig+mixer walking footpath lanes & turning
|
||
at nodes on a seeded choice with loiter stops; mid 25–70m instanced impostors baked from the fleet
|
||
(auto-in-sync); far culled; hysteresis so nobody pops; time-of-day density; `?noassets` placeholder
|
||
town; keepers at counters. All exercised in the test page.
|
||
|
||
## Decisions worth your eyes
|
||
|
||
1. **Rig pool, not per-spawn clone.** Near actors are pooled per ped-type and reused across citizens
|
||
(SkeletonUtils.clone + mixer are the expensive bits) — bounded at 30 instances, cap 24 active,
|
||
≤3 new clones/frame to avoid hitches. Height is applied as an outer-group scale so pooled rigs are
|
||
height-agnostic and reusable.
|
||
2. **Impostor colour management.** The mid atlas is baked *linear* (no tone-map) and the billboard
|
||
shader applies ACES + sRGB itself, so mid impostors match the ACES-tonemapped near rigs exactly —
|
||
fixed an early bug where they rendered ~2× too dark.
|
||
3. **Figure origin at the feet.** Both rigs and placeholders present a `fig` Group whose origin is the
|
||
ground point, so the sim moves/rotates walkers without re-planting them each frame.
|
||
|
||
## ⚠️ One thing I need a ruling on (Lane E territory)
|
||
|
||
**The inherited 90sDJsim peds are ~49.7k tris each.** 24 near rigs ≈ 1.2M tris — far past the
|
||
CITY_SPEC "≤200k tris typical view". It runs fine on Apple Silicon today, but it leaves no tri budget
|
||
for Lane B's buildings. The rig stack is indifferent to poly count — this is an **asset-pipeline
|
||
decimation job (Lane E)**: at ~6k tris/ped the near cap fits budget. Until then `NEAR_MAX` in `sim.js`
|
||
is the throttle. Same story, smaller, for draw calls (each ped is ~6 sub-meshes → merge on load).
|
||
Details + numbers in the NOTES. Not blocking Lane D; flagging so it's on the integration radar.
|
||
|
||
## Adversarial review pass (6 real bugs found + fixed)
|
||
|
||
After the build passed in-browser, I ran a 4-dimension multi-agent code review with an adversarial
|
||
verify stage (14 agents). It confirmed **6 genuine defects** (4 others refuted); all 6 are fixed and
|
||
re-verified:
|
||
|
||
1. **[HIGH] Non-deterministic fleet order** — `fleet.normal/all` were filled in GLB load-completion
|
||
order, so `pickRig`'s index (→ every identity) could differ across reloads/machines, silently
|
||
breaking "same seed → same crowd". Fixed: fill fixed slots by `PED_NAMES` index, compact after.
|
||
2. **[HIGH] Shared-resource disposal** — `_disposeInner` disposed geometry/materials that
|
||
SkeletonUtils.clone *shares* across all clones + the base rig; pool eviction would tear down GPU
|
||
buffers still in use by sibling citizens. Fixed: dispose only the clone's own `Skeleton` (its
|
||
bone texture) — never the shared geo/mats. Stress-tested (24 teleports forcing eviction churn) →
|
||
0 broken meshes.
|
||
3. **[HIGH] Impostor under-exposure** — the in-shader ACES omitted three's `/0.6` exposure
|
||
normalisation, making mid billboards ~1.67× darker than the identical near rig at the swap
|
||
boundary. Fixed: `color *= uExposure / 0.6`.
|
||
4. **[HIGH] Renderer viewport not restored** — the atlas bake set per-cell viewport/scissor but only
|
||
restored scissor-*test*; the async re-bake left the viewport on a 128px cell → the main scene
|
||
could render into a corner. Fixed: save/restore viewport + scissor rect.
|
||
5. **[MED] Billboard ignored `modelMatrix`** — impostors would desync from rigs if the citizens group
|
||
carries a transform (Lane B chunk offsets). Fixed: fold `modelMatrix` into the billboard base.
|
||
6. **[LOW] Frozen tone-map exposure** — added `ImpostorLayer.setExposure()` for day/night.
|
||
|
||
Also caught during re-verification: three injects its own `RRTAndODTFit`/ACES functions into any
|
||
tone-mapped `ShaderMaterial`, colliding with mine. Fixed with `toneMapped:false` (I tone-map
|
||
in-shader) + `imp`-prefixed helper names. Console is clean; determinism still passes; 60fps holds.
|
||
|
||
## For Lane F integration (hooks are ready)
|
||
|
||
- **Street graph — verified, no adapter.** `plan.streets` (Lane A) is byte-shape-compatible with the
|
||
`graph` the sim wants. In the street branch of the shell:
|
||
```js
|
||
const sim = new CitizenSim({ renderer, scene, camera, citySeed: plan.citySeed, graph: plan.streets, fleet });
|
||
// per frame (street mode): sim.update(dt); // LOD is by sim.camera.position — pass the render camera
|
||
```
|
||
Note `update(dt)` takes **only dt** (it reads `this.camera.position` for LOD, which is what you want —
|
||
LOD by what the viewer sees). Lane F §3.3's `sim.update(dt, player.position)` sig is loose; the
|
||
1-arg form is correct as long as the constructed `camera` is the render camera.
|
||
- **Density on big towns**: a flat `setPopulation(N)` spreads citizens uniformly across *all* edges, so
|
||
on a large plan most sit far-culled from any one viewpoint (correct + cheap, but streets read empty
|
||
if N is small). For v1 pick N to suit town size; the durable fix is chunk-streamed rosters (below).
|
||
- Roster identity `identityOf(citySeed, edgeCount, id)` is chunk-ready — key by `chunkKey+i` and
|
||
stream rosters with Lane B's chunk build/dispose.
|
||
- Keepers pull `x,z,ry` from Lane C's counter `places`; `sim.setTimeOfDay` from the shell day segment;
|
||
`sim.setPaused` from `visibilitychange`; **Lane B must set `scene.environment`** (PBR peds go dark
|
||
without it — already wired to be read from the scene).
|
||
|
||
## Try it
|
||
|
||
```
|
||
cd web && python3 -m http.server 8130
|
||
# open http://localhost:8130/citizens_test.html (full fleet)
|
||
# open http://localhost:8130/citizens_test.html?noassets=1 (placeholder town)
|
||
# open http://localhost:8130/citizens_test.html?seed=1234 (any seed)
|
||
```
|
||
Sliders: population 0–200, time-of-day. Buttons: tier colours (green=near, yellow=mid, pink=far),
|
||
determinism check. Drag=look, wheel=zoom, WASD=move focus.
|