Populate the town at city scale, on budget, deterministic per-citizen identity: - rigs.js — ported 90sDJsim rig stack: mixamorig canonicalisation so one shared walk clip drives all 19 peds, head-bone height-normalise, feet-plant, pooled near-tier actors with walk<->idle crossfade - impostor.js — 4-yaw sprite-atlas baker + instanced billboards: whole mid crowd in 1 draw call, tone-matched to the ACES near rigs - sim.js — deterministic roster, footpath lanes off the street graph, near/mid/far LOD with hysteresis + hard 24-cap, staggered mixer budget, time-of-day density - placeholder.js — seeded low-poly box humanoids (frame-one population + ?noassets) - keepers.js — one keeper per shop at the counter, idle + greet-turn - 21 ped GLBs (byte-identical from 90sDJsim), citizens_test.html harness, LANE_D_NOTES.md, 5 beauty shots Verified in-browser: 200 citizens at max mixer 0.2ms (budget 2ms), near capped 24, determinism holds (200 identities match seed), no T-pose, ?noassets crash-free. Consumes Lane A plan.streets with zero adapter. Six adversarial-review bugs fixed (non-deterministic fleet order, shared-resource disposal on pool eviction, impostor colour-management). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
125 lines
8.4 KiB
Markdown
125 lines
8.4 KiB
Markdown
# LANE D — NOTES (measured budgets + clip wishlist)
|
||
|
||
*Written by PROCITY-D, 2026-07-14. Standalone verification via `web/citizens_test.html`.
|
||
Reference stack ported from `90sDJsim/web/world/index.html` ~405–520 (loadRig/spawnRig/_canon/
|
||
_rotOnly/head-bone normalize/upgradeStreetPeople). Measurements on the M3 Ultra dev box.*
|
||
|
||
## What shipped
|
||
|
||
| file | role |
|
||
|---|---|
|
||
| `web/js/citizens/rigs.js` | ported rig stack: fleet loader, `_canon`/`canonRig`/`_rotOnly`, `buildFigure` (head-bone height-normalise + feet plant), `spawnRig` (single clip), `makeActor` (walk↔idle crossfade), `pickRig` |
|
||
| `web/js/citizens/placeholder.js` | seeded low-poly box humanoid (POLY lock) — walks/idles, planted at y=0, hot-swaps to a rig |
|
||
| `web/js/citizens/impostor.js` | 4-yaw sprite-atlas baker + instanced billboard layer (mid tier, 1 draw call) |
|
||
| `web/js/citizens/sim.js` | deterministic roster, footpath lanes, near/mid/far LOD, rig pool, staggered mixer budget, time-of-day density |
|
||
| `web/js/citizens/keepers.js` | one keeper per shop at the counter slot, idle + greet head/body-turn |
|
||
| `web/citizens_test.html` | standalone harness: fixture 3×3 street graph, sliders, tier debug, determinism check |
|
||
| `web/models/peds/*` | 19 rigged GLBs + `walk.glb` + `idle.glb`, **byte-identical** copies from 90sDJsim (checksummed) |
|
||
|
||
## The fleet (copied, never edited)
|
||
|
||
19 rigged peds (17 normal + 2 comical) + 2 clip-only GLBs, ~37 MB total, copied from
|
||
`johnking@100.91.239.7:~/Documents/90sDJsim/web/world/models/peds/` (also present locally at
|
||
`~/Documents/90sDJsim/…`). SHA-verified byte-identical (house law: canonical source stays upstream).
|
||
|
||
- Skeletons canonicalise cleanly: `mixamorig1…`→`mixamorig…`, so **one shared walk clip drives all 19**
|
||
and one shared idle clip too. Verified in-scene — a single `walk.glb` animates every ped.
|
||
- `walk.glb`/`idle.glb` position tracks + `Hips.quaternion` are stripped (`_rotOnly`) — without this
|
||
the different-scale root track inflates peds to giants. Confirmed: no giants/ants across all 19.
|
||
|
||
## Measured budgets (M3 Ultra, `citizens_test.html`)
|
||
|
||
Preview note: the in-app browser reports `visibilityState:'hidden'`, which throttles `requestAnimationFrame`,
|
||
so the on-screen fps counter reads low (47–84). True cost was measured by timing 140 manual
|
||
`update()+render()` frames.
|
||
|
||
**200 citizens, midday (the acceptance fixture):**
|
||
|
||
| metric | measured | CITY_SPEC / Lane-D budget | verdict |
|
||
|---|---|---|---|
|
||
| near (rigged) actives | 24 (hard cap) | ≤ 24 | ✅ |
|
||
| mid (impostor) | ~122 | — | — |
|
||
| far (culled) | ~54 | — | — |
|
||
| **mixer update / frame** | **0.4 ms max, ~0.2 ms avg** | < 2 ms | ✅ comfortably |
|
||
| sim logic / frame | 0.24 ms avg | 4 ms build budget | ✅ |
|
||
| full frame CPU (update+render) | ~2.6 ms → ~380 fps headroom | 60 fps (16.6 ms) | ✅ huge margin |
|
||
| mid tier draw calls | **1** (69 instances, 138 tris) | 1 per atlas | ✅ |
|
||
| impostor atlas texture | 2048×640 (16×5 cells, 4 yaws × 19 peds) | ≤ 2048, < 512 MB total | ✅ (~5 MB) |
|
||
| determinism | live roster ≡ seed recompute, stable while walking | same seed → same crowd | ✅ |
|
||
|
||
**Time-of-day density** (200 base): 00:00 → 12 active, 06:00 → 70, 12:00 → 200, 15:00 → 150,
|
||
21:00 → 36. Curve in `sim.js DAY_CURVE`.
|
||
|
||
## ⚠️ Findings for Lane E / Lane F (need a decision)
|
||
|
||
1. **The inherited peds are ~49.7k tris each — too heavy for the CITY_SPEC 200k-tri "typical view".**
|
||
24 near rigs ≈ **1.2M tris**; a realistic 16-rig street view already measured **1.02M tris**.
|
||
Framerate holds on M-series (GPU eats it), but this leaves *nothing* for Lane B buildings.
|
||
**Recommendation:** decimate the ped fleet to ~5–8k tris in the asset pipeline (Lane E) — at 6k/ped
|
||
the near cap costs ~144k tris, back within budget. The rig stack is indifferent to poly count;
|
||
this is purely an asset job. Until then, `NEAR_MAX` (sim.js) is the throttle.
|
||
|
||
2. **Draw calls scale with near rigs (~6 calls/ped — each GLB has several sub-meshes/materials).**
|
||
16 rigs ≈ 144 calls; 24 rigs ≈ ~190. Under the 300-call street budget *today*, but tight once
|
||
Lane B adds shells. **Recommendation:** merge each ped's sub-meshes by material on load
|
||
(`BufferGeometryUtils.mergeGeometries`) → ~1–2 calls/ped. Cheap win, deferred (not blocking).
|
||
|
||
3. **Peds are metallic-PBR (`metalness≈0.5`) and render black without a `scene.environment`.**
|
||
The test scene builds a neutral PMREM; the impostor bake is fed the same env. **Lane B's shell must
|
||
set `scene.environment`** (a sky PMREM) or every citizen — near and mid — goes dark. This is wired:
|
||
`CitizenSim` reads `scene.environment` and passes it to the impostor baker.
|
||
|
||
4. **Impostor material is `toneMapped:false` and does ACES + sRGB itself** (matching
|
||
`ACESFilmicToneMapping`, exposure `/0.6`). If Lane B changes the tone-mapping curve or animates
|
||
`renderer.toneMappingExposure`, call `sim.impostor.setExposure(e)` (and, for a non-ACES curve,
|
||
the in-shader ACES in `impostor.js IMP_FRAG` would need to match). Mid billboards otherwise drift
|
||
from the near rigs at the LOD seam.
|
||
|
||
*Hardened by a 4-dimension adversarial code review (see D-progress.md): 6 confirmed defects fixed —
|
||
non-deterministic fleet order, shared-geometry disposal, impostor under-exposure, unrestored
|
||
viewport, missing modelMatrix, frozen exposure.*
|
||
|
||
## Integration hooks for Lane F
|
||
|
||
- `new CitizenSim({ renderer, scene, camera, citySeed, graph, fleet })` — `graph = { nodes:[{id,x,z}],
|
||
edges:[{id,a,b,width,kind}] }`. Swap the fixture graph for Lane A's `CityPlan.streets`. Footpath
|
||
lanes are derived from edge `width` + a 0.9 m margin; pedestrians keep to the right of travel.
|
||
- `sim.setPopulation(n)` (slider / density) · `sim.setTimeOfDay(t01)` (drive from the shell's day
|
||
segment) · `sim.setPaused(bool)` (wire to `visibilitychange`) · `sim.update(dt)` each frame.
|
||
- **Chunk streaming (TODO for F):** roster is currently whole-graph. The identity fn `identityOf(citySeed,
|
||
edgeCount, id)` is chunk-ready — key it by `chunkKey+i` per CITY_SPEC and spawn/despawn rosters with
|
||
Lane B's chunk build/dispose. The LOD + pool machinery is already per-frame and chunk-agnostic.
|
||
- **Keepers:** `keepers.spawn(roomGroup, { x, z, ry, shopId, type })` — feed `x,z,ry` from Lane C's
|
||
interior counter `places`; call on interior build, `keepers.remove(handle)` on dispose.
|
||
`keepers.update(dt, playerPos)` each interior frame.
|
||
- `?noassets=1` verified: full placeholder town, mixers=0, zero crashes.
|
||
|
||
## Clip wishlist (for the mixamo-fetch run — CHECK THE 34 EXISTING CLIPS FIRST)
|
||
|
||
Only `walk` + `idle` are wired today (they ship with the ped fleet). CITY_SPEC says
|
||
`~/Documents/mixamo-fetch/out/` already holds **34 clips (sit, lean, look-around, phone, …)** — these
|
||
bind directly through the canonical `mixamorig` skeleton exactly like walk/idle, so wiring them is a
|
||
data task, not new rig work. **Verify each exists before requesting; never re-download.**
|
||
|
||
Wanted for richer loiter/keeper behaviour, in priority order:
|
||
1. `sit` — bench-sit at benches/verandah steps (sim loiter already has the hook; needs the clip).
|
||
2. `lean` — loiter against a shopfront (window-shopping stops).
|
||
3. `look-around` / `idle-look` — window-shopping variety + a better keeper greet than the body-turn.
|
||
4. `talk` / `gesture` — pairs chatting on the footpath (spawn in seeded 2-groups).
|
||
5. `carry` / laden walk — shoppers with bags after a purchase (content-phase tie-in).
|
||
6. `browse` / `reach` — keeper stocking shelves; customer reaching a shelf inside interiors.
|
||
|
||
Bespoke (won't be on Mixamo, hand-author later): crate-riffle at market stalls, till-operation.
|
||
|
||
## Known limitations (honest)
|
||
|
||
- Near↔mid swap is a hard LOD switch at ~25 m (hysteresis 24/27 m). At that range a 128px impostor
|
||
and the full rig subtend near-identical screen size, so it reads as seamless — but it is a switch,
|
||
not a cross-fade (rig materials are shared across SkeletonUtils clones, so per-instance opacity fade
|
||
isn't free). If a pop is ever visible after buildings land, the fix is a short dither/fade band.
|
||
- In `?noassets` mode, mid impostors use 8 generic placeholder variants while near placeholders are
|
||
per-citizen coloured — so a citizen's colour can shift slightly crossing 25 m. Fallback-only; with
|
||
the real fleet, near and mid are the same baked ped.
|
||
- Loiter *timing* is dt-driven (cosmetic), so exact positions at time T aren't reproducible across
|
||
runs; **identity** (who, which ped, height, speed, spawn beat) is fully seeded and asserted.
|