Lane B (Streetscape): round-3 — scene.environment for PBR peds + budget re-measure with citizens

- 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 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-14 16:00:22 +10:00
parent 0985b05790
commit fafd5a700a
3 changed files with 63 additions and 3 deletions

View File

@ -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 (~12 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**

View File

@ -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 (~12 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

View File

@ -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);
}