Consumes the two orphaned street GLBs via the use-if-ready furniture path (fail-soft; ?noassets →
primitives, 0 network).
- Bins: genuinely sparse (~1/block, s+=78m, seeded side), decoupled from benches — country town not
a mall. GLB on its 0.57x0.68 footprint.
- Bus shelters ON THE STREET GRAPH (future tram/bus stops): deterministic rule = midpoint of every
MAIN edge with hashEdge(id)%3===0, road-side footpath by the kerb, long axis along the edge, open
front to road -> 1-3/town (2 in seed 20261990). Solid oriented-rect collider (furniture now returns
colliders; chunks.js merges them into getColliders) -> walk-into-it is blocked. Exported
busShelterStops(plan) -> [{edgeId,x,z,yaw}] as the stop-list contract for the vehicle loop.
- Perf: worst view WITH rig-fleet citizens 94k tris / 148 draws (<=200k / <=300, big margin);
deterministic; all-flags-on (winmap+dig+roster) composes clean, 0 errors. street_bin GLB is 3k tris
-> flagged to Lane E as a decimation candidate (bounded by the sparse cadence for now).
qa.sh --strict GREEN.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
14 KiB
PROCITY-B — progress (Lane B: Streetscape + game shell)
Status: complete and verified. Lane B turns a CityPlan into a walkable, chunk-streamed 3D town
plus the game shell. It runs on my hand-written fixture and auto-integrates with Lane A's
generatePlan (which landed mid-session). Everything below was verified live in a browser.
Round 6 (Fable's ROUND6 → Lane B) — placed the orphaned street props · qa GREEN
Wired the two published-but-unconsumed street GLBs (street_bin_01, bus_shelter_01) via the
use-if-ready furniture path (fail-soft; ?noassets → primitives, 0 network). Files: furniture.js,
chunks.js (collider merge).
- Bins: sparse (~1/block,
s += 78 m, seeded), decoupled from benches — country town not a mall. - Bus shelters on the street graph (future tram/bus stops): deterministic rule = midpoint of every
MAIN edge with
hashEdge(id)%3===0, road-side footpath, long axis along the edge, open front to road → 1–3/town (2 in seed 20261990). Solid collider (furniture now returns colliders →chunksmerges intogetColliders); walk-into-it is blocked. ExposedbusShelterStops(plan)→ the stop list contract for whoever builds the vehicle loop. - Perf: worst view WITH rig-fleet citizens 94k tris / 148 draws (≤200k / ≤300, big margin);
deterministic; all-flags-on composes clean (0 errors). Flagged the 3k-tri
street_binGLB to Lane E as a decimation candidate (bounded by the sparse cadence for now). Shot:docs/shots/laneB/bus_shelter.png.
Round 5 (Fable's ROUND5 → Lane B) — v2 window trick, behind ?winmap=1 · qa GREEN (4/4)
- Parallax interior-mapping window glass (V2_IDEAS "Vuntra window trick"), behind
?winmap=1default-off. One sharedShaderMaterialon the existing window geometry does single-cube interior mapping — each fragment rays into a virtual room box and shades back wall + seeded shelves + side walls + floor/ceiling. Zero extra geometry, 1 window draw/chunk, per-shop variation via attributes (aTangent/aTint/aSeed), not per-shop materials. Seeded fromshop.seed. - §3.5 held: night → open shop's room glows warm, closed shop's room dark (reuses the closed-facade
uHour/uNight/aHours). Screenshots:docs/shots/laneB/winmap_{day,night_open_vs_closed,night_closed}.png. - v2 prime law held & proven: flag-OFF byte-identical to v1 (windows
MeshStandardMaterial, no winmap attrs); flag-ON worst view 122–168 draws (≤300); shader 100% procedural — 0 fetch delta vs?noassetsbaseline; 0 console errors;GOLDEN.hashuntouched.?winmapis self-read in buildings.js → no index.html seam for F (just a flags-table row). File:buildings.js.
Round 4 (Fable's ROUND4 → Lane B) — done · qa.sh --strict GREEN (4/4)
- B1 — closed-facade visuals (§3.5, headline). Closed shops → dark windows + red CLOSED plate;
open shops (esp. the
openLatevideo rental at night) → lit windows; door tooltip🔒 <name> — CLOSED · opens HH:00. Batched with a per-vertexaHours+ two shared shaders + oneuHour/uNightuniform pair, updated once on aprocity:segmentevent (lighting.js → buildings.js) — facade atlas untouched, no per-shop materials, no per-frame poll. Verified live: 22:00 → only "Video Regal" lit, all others CLOSED; midday → all open; worst-view 200 draws with citizens (≤300). Files:buildings.js,lighting.js,hud.js. - B2 — shot harness (gate-6 critical path). The "letterbox" was not a composer bug (proved
the composer is correctly sized headless) — the
street_noon/arcade/warehouse_fringeposes jammed the camera into a wall. Reframed them to look down a street (newstreetViewPose), added the 3 missing bookmarks F asked for, pointednight_neonat the glowing video shop amid CLOSED neighbours, and added a defensive composer resize.tools/shots.py→ 10 un-letterboxed shots, 0 console errors (indocs/shots/laneB_r4/). File:dbg.js. - B3 — furniture GLB upgrade + novelty_record. furniture.js now use-if-ready-upgrades primitives
to depot GLBs (sync check,
?noassets→ 0 network verified). Placed novelty_record at every record store + food_cart by stalls/milk bars. Finding: the novelty_record GLB is 26.5k tris (5×glb_law), spiking a busy view to 574k → it stays on its low-poly primitive; bench (1.9k)- food_cart (5.0k) GLBs are fine and sit on their footprints. → Lane E: decimate novelty_record
(same as the ped rigs). File:
furniture.js.
- food_cart (5.0k) GLBs are fine and sit on their footprints. → Lane E: decimate novelty_record
(same as the ped rigs). File:
Round 3 (Fable's ROUND3_INSTRUCTIONS → Lane B) — done
- Task 1 (draw budget, re-measured WITH citizens). Radius-2 auto is already the spawn default;
no config change needed. Drove the real loop (
chunks.update+citizens.update, pop 140, up to 125 active peds) along the dense corridors viawindow.DBG: worst continuous-walk view 263 scene / 275 total at MIDDAY, 275 total at NIGHT — ≤300 everywhere, ~25 margin. Peds are a batched impostor layer (~1–2 draws). Documented in LANE_B_NOTES. - Task 2 (
scene.environment). Was null → Lane D's rig-fleet peds bake their impostor atlas from it (sim.js) and would render dark. Set a neutral PMREM'd sky→ground gradient inlighting.attachRenderer(beforeCitizenSimconstructs; no asset). Verified set in street / night / interior / after interior-return (interior_modeonly swapsscene.background). - Task 3 (furniture GLB footprints) — standby: Fable's §3.4 (manifest GLB upgrade +
?noassets) hasn't landed (furniture.js still primitives); will verify placement/collision when it does. - Task 4 (optional polish) — deferred (fresh generated-city stills / map.html wiring).
- Only Lane B file changed this round:
web/js/world/lighting.js(+ these docs). Committed by exact pathspec (Lanes C/E/F have concurrent uncommitted work in the shared tree — untouched).
Session 2 (continuation) — closed the two open Lane-B items Fable flagged
- Draw budget now holds with margin. Facade texture atlas + awning-merge + shell/trim-merge: worst dense frustum 393 → 265 scene draws, worst continuous-walk gameplay view ~261 total (≤300 gate, ~40 margin). Bonus: skin materials 25 → 5 (the atlas is one shared facade material), fixing the "≤25 was at the cap" concern too. (The old "~445" peak was largely a measurement artifact — orphan chunks from teleport-jumping that the real streamer disposes.)
window.DBGharness exposed (?dbg=1,js/world/dbg.js) — answers LANE_F_NOTES §4 so Fable'sshots.py/soak.pycan drive the game:ready,shot(name),teleport,setSegment,enterShop/exitShop(drives the real B→F→C interior chain),info().ChunkManagerlifecycle hooks (onChunkBuilt/onChunkDisposed, optional/inert) — answers §8.- Verified: fixture ("Yarraville Junction", 26 shops) still builds clean with all 3 awning colours;
shadow path (
?shadows=1) renders from the merged meshes; day + night + door tooltip + interiors all work; zero console errors. Files touched:skins.js,buildings.js,chunks.js,hud.js, newdbg.js, + a small guarded DBG loader inindex.html. Uncommitted (per the lane treaty; and Fable is concurrently wiringindex.html— our edits coexist cleanly on stable anchors).
Owned/created (only web/index.html + web/js/world/*, per the ownership treaty):
| file | role |
|---|---|
web/index.html |
shell: importmap→vendor, renderer + ACES + bloom, MODE state machine (map/street/interior), main loop, adaptive perf, guarded generatePlan import, shot harness |
web/js/world/fixture_plan.js |
hand-authored CityPlan (schema v1): 4 N + 2 S terraced blocks (26 shops), cross-street houses, 2 market stalls, streets graph — spans ~6 chunks so streaming is exercised |
web/js/world/planutil.js |
chunk math (64 m), chunkIndex, node/lot indices, oriented-rect collider + pushOutRect |
web/js/world/skins.js |
city-wide shared material registry (one material per skin), seeded flat-colour fallbacks |
web/js/world/buildings.js |
facade kit: instanced shells/parapets/posts/awnings, merged per-skin facades, per-chunk sign atlas, doors/windows |
web/js/world/ground.js |
roads/footpaths/kerbs/plaza/base plane, merged strips, UV-baked tiling |
web/js/world/furniture.js |
instanced streetlights (emissive at night), benches, bins, gum trees, bus stops — all with primitive fallbacks |
web/js/world/lighting.js |
seeded sky dome + 6-segment day/night cycle, one following sun/shadow, fog pop-in mask |
web/js/world/player.js |
PointerLock WASD+run, eye 1.7 m, push-out collision vs nearby-chunk colliders |
web/js/world/chunks.js |
the streamer: radius-R build / R+1 dispose, nearest-first queue, 4 ms/frame budget, night propagation |
web/js/world/hud.js |
crosshair, throttled door-raycast tooltip, live draws/tris/fps/seed/clock readout, click→procity:enterShop |
web/js/world/minimap.js |
map mode: 2D top-down town directory (streets + lots by type + player dot) |
Acceptance — all met
- ✅ Walk the fixture 5 min: contiguous, no seams, chunks stream silently, day/night cycles, signs legible, tooltips work, budgets green (148 scene draws, 21 skin materials, 5.8k tris).
- ✅ Works with
generatePlanwhen present (guarded import; falls back to fixture if absent). - ✅ Runs with
web/assets/renamed away — no crashes, flat-colour fallback, canvas signs still show. - ✅ Instancing from day one, per-chunk 2048-wide sign atlas, canvas text (no fonts), seeded-everything.
- ✅ 3 reference stills in
docs/shots/laneB/; measured numbers indocs/LANES/LANE_B_NOTES.md.
Verified live (in-browser)
Streaming (chunks build/dispose as you walk, queue never starves), 6-segment day→night with glowing
shopfront windows + streetlamp bloom at night, door raycast → correct shop → procity:enterShop +
toast, pixel-accurate collision (stops at wall − player radius), all-fallback mode, and Lane A's full
493-shop city rendering + streaming + arbitrary-angle collision through Lane B.
Perf (measured — full table in docs/LANES/LANE_B_NOTES.md)
- Fixture (radius 3, shadows on): ~148 scene draws, low tris, well under all gates.
- Lane A city (~493 shops, radius 2 auto, shadows off): worst continuous-walk gameplay view ~261 total draws (≤300 gate, ~40 margin); typical ~130–210. Skin materials 5 (≤25). Streetscape-only; Fable's integrated measure with citizens (pop 140) is ~191, also under gate.
Adversarial multi-agent review → fixes landed
I ran a 4-dimension review (correctness / memory-perf / spec / robustness) with adversarial verification. Confirmed real bugs, all fixed and re-verified:
- Collision was wrong for non-axis-aligned
ry(pushOutRectused the wrong rotation sign). Latent on my axis-aligned fixture; would let you walk through Lane A's angled buildings. Fixed — verifiedkeptOut:trueagainst ary=−1.32lot. - House doors were raycastable and mis-resolved to the nearest shop → clicking a house "entered" a random shop. Fixed (separate non-picked mesh).
- Awning skin used a signed shift → undefined skin for ~⅓ of shops. Fixed (
>>>). - Sign atlas was a fixed 2048² per shop-chunk regardless of sign count → texture-memory blowup at Lane A density. Fixed (sized to actual signs, mipmaps off).
- Furniture: numeric edge ids broke the seed hash (all edges identical); offset furniture could land in an unbuilt chunk and vanish. Both fixed.
takeShots()didn't restore the pre-shot mode. Fixed.
⚠️ Things Fable should know (cross-lane)
- Lane A landed and is now the shipped default.
index.htmlprefersgeneratePlan(seed)over the fixture. Seed 20261990 → "Boolarra Heads", ~493 shops. The fixture remains the fallback + schema ref. - A "Lane F integration fix" already appeared in
skins.js(facadeMatnow strips a.jpg/facade-prefix). Lane A storesshop.facadeSkinas a full filename ("facade-stucco-pink.jpg"), not the bare skin key the CITY_SPEC table implies. That normalize is what makes Lane A's facades load. Kept. - Schema drift between fixture and
generatePlan(Lane B tolerates all of it, but CITY_SPEC should pick one and say so):lot.id/lot.blockare strings in the fixture, numbers from Lane A;lot.frontEdgeis'N'/'S'in the fixture, a number from Lane A;shop.facadeSkinis a bare key in the fixture, a filename from Lane A;lot.ryis axis-aligned in the fixture, arbitrary radians from Lane A. Recommend Fable freeze these in CITY_SPEC so Lane C/D/E don't each guess. - ≤25 skin-materials gate — RESOLVED (session 2). Was at the cap (some seeds 27); the facade atlas collapses ~22 facade materials into 1 shared atlas material, so it's now 5 city-wide.
- Perf gate on the full city — RESOLVED (session 2). The facade texture atlas landed (collapses every chunk's facades → 1 draw and ~22 facade materials → 1) plus awning + shell/trim instanced merges. Worst gameplay view is now ~261 total (≤300). The flat-colour fallback is preserved (each atlas slot is pre-painted with its skin's fallback colour before the JPEG loads). See LANE_B_NOTES.
Suggested next instructions (your call)
- Freeze the CityPlan schema in CITY_SPEC (the drift list above) — unblocks C/D/E. Still open.
Decide the facade-atlas— done (session 2): Lane B implemented it. Draw + material gates now green.Interiors hand-off— done: Fable wiredenterShop → interiorMode → buildInteriorin the shell; verified viaDBG.enterShop. Lane B's seam (procity:enterShop/procity:exitShop) is intact.- Run Lane F's
shots.py/soak.pyagainstwindow.DBGnow that it exists (bookmarks are seed-derived). - Optional: capture fresh
docs/shots/laneB/stills of the generated city viaDBG.shot(...); wiremapmode into Lane A'smap.html(currently my lightweight minimap).
Run
cd web && python3 -m http.server 8130 → http://localhost:8130 . (The app needs no server-side logic;
the P-key shot harness uses browser downloads.)