From fafd5a700a8988c4e4c5a23d185fe01db45538ac Mon Sep 17 00:00:00 2001 From: m3ultra Date: Tue, 14 Jul 2026 16:00:22 +1000 Subject: [PATCH] =?UTF-8?q?Lane=20B=20(Streetscape):=20round-3=20=E2=80=94?= =?UTF-8?q?=20scene.environment=20for=20PBR=20peds=20+=20budget=20re-measu?= =?UTF-8?q?re=20with=20citizens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lighting.js sets scene.environment (neutral PMREM sky->ground gradient, no asset) in attachRenderer, before CitizenSim constructs; Lane D rig-fleet peds bake their impostor atlas from it and render dark if null. Verified set in street/night/interior + after interior-return. - Re-measured worst continuous-walk view WITH citizens (pop 140, 125 active peds) via window.DBG: 263 scene / 275 total midday, 275 night -- <=300 everywhere. Radius-2 auto already the spawn default. - Round-3 task 3 (furniture GLB footprints) standby: Fable s3.4 manifest upgrade not yet landed. - Docs: LANE_B_NOTES.md + B-progress.md round-3 updates. Co-Authored-By: Claude Opus 4.8 --- B-progress.md | 16 ++++++++++++++++ docs/LANES/LANE_B_NOTES.md | 15 +++++++++++++-- web/js/world/lighting.js | 35 ++++++++++++++++++++++++++++++++++- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/B-progress.md b/B-progress.md index e4e15f7..66d90dd 100644 --- a/B-progress.md +++ b/B-progress.md @@ -4,6 +4,22 @@ 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 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 via `window.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 in + `lighting.attachRenderer` (before `CitizenSim` constructs; no asset). Verified **set in + street / night / interior / after interior-return** (`interior_mode` only swaps `scene.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** diff --git a/docs/LANES/LANE_B_NOTES.md b/docs/LANES/LANE_B_NOTES.md index 568f831..5377346 100644 --- a/docs/LANES/LANE_B_NOTES.md +++ b/docs/LANES/LANE_B_NOTES.md @@ -30,8 +30,14 @@ 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. + +**Round-3 re-measure WITH Lane D citizens (pop 140, the shipped default).** Driving the real loop +(`chunks.update` + `citizens.update`) along the dense corridors via `window.DBG`, up to **125 active +peds** at midday: worst continuous-walk view **263 scene / 275 total** at **MIDDAY** and **275 total +at NIGHT** — **≤300 everywhere**, ~25 draw margin. Peds are a batched impostor layer (~1–2 draws for +the whole near tier), so they cost far less than their count. Radius-2 auto is already the spawn +default for a big city; no config change was needed. (Fable's independent integrated read is ~191 on a +typical main-street view.) | metric | measured | gate | status | |---|---|---|---| @@ -85,6 +91,11 @@ exceeds the radius-2 set. Verified day + night, facades still textured, awning c - **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. +- **`scene.environment` is set** (round 3, `lighting.attachRenderer`): a neutral PMREM'd sky→ground + gradient (no asset). Lane D's rig-fleet peds bake their impostor atlas from `scene.environment` + (`sim.js`) and go dark if it's null; set here *before* `CitizenSim` constructs. One env for all + segments (the atlas bakes once; the mostly-non-metal streetscape is barely affected). Verified set + in street/night/interior + after interior-return (`interior_mode` only swaps `scene.background`). - **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 diff --git a/web/js/world/lighting.js b/web/js/world/lighting.js index 036e4ba..0e17216 100644 --- a/web/js/world/lighting.js +++ b/web/js/world/lighting.js @@ -20,6 +20,23 @@ const SEG = [ ]; const HOUR = ['06:30', '09:00', '12:30', '15:30', '18:30', '22:00']; +// A tiny sky→horizon→ground vertical gradient as an equirect env source (canvas, no asset). PMREM'd +// into scene.environment so PBR content is lit by neutral IBL instead of rendering dark — Lane D's +// rig-fleet peds bake their impostor atlas from scene.environment (sim.js), and future GLB props read it. +function gradientEquirect(THREE) { + const c = document.createElement('canvas'); c.width = 8; c.height = 64; + const g = c.getContext('2d'); + const grad = g.createLinearGradient(0, 0, 0, 64); + grad.addColorStop(0.00, '#9fb6d6'); // zenith sky + grad.addColorStop(0.50, '#cdccc0'); // horizon haze + grad.addColorStop(1.00, '#5c5548'); // ground bounce + g.fillStyle = grad; g.fillRect(0, 0, 8, 64); + const tex = new THREE.CanvasTexture(c); + tex.mapping = THREE.EquirectangularReflectionMapping; + tex.colorSpace = THREE.SRGBColorSpace; tex.needsUpdate = true; + return tex; +} + export function createLighting(scene, plan, skins, { radius = 3, shadows = true } = {}) { const group = new THREE.Group(); group.name = 'lighting'; @@ -54,6 +71,7 @@ export function createLighting(scene, plan, skins, { radius = 3, shadows = true const DAY_SECONDS = 240; // one full cycle let onNightChange = null; let renderer = null; + let envRT = null; // PMREM env render target backing scene.environment function apply(i) { const [, sunI, sunC, hSky, hGnd, hI, skyTint, fogC, exposure, isNight] = SEG[i]; @@ -91,10 +109,25 @@ export function createLighting(scene, plan, skins, { radius = 3, shadows = true return { seg, label: SEG[seg][0], hour: HOUR[seg], night, frac }; } - function attachRenderer(r) { renderer = r; r.toneMappingExposure = SEG[seg][8]; } + function attachRenderer(r) { + renderer = r; + r.toneMappingExposure = SEG[seg][8]; + // set scene.environment now (needs the renderer for PMREM). Called before citizens construct, + // so Lane D's ped impostor atlas bakes against a real environment. One neutral env for all + // segments — the atlas bakes once, and the mostly-non-metal streetscape is barely affected. + if (!envRT) { + const pmrem = new THREE.PMREMGenerator(r); + const eq = gradientEquirect(THREE); + envRT = pmrem.fromEquirectangular(eq); + scene.environment = envRT.texture; + eq.dispose(); pmrem.dispose(); + } + } function dispose() { scene.fog = null; + scene.environment = null; + if (envRT) { envRT.dispose(); envRT = null; } dome.geometry.dispose(); skyMat.dispose(); scene.remove(group); }