Lane D round-4: in-shell verify (D1) + decimated-ped validation (D2) + impostor tone-map fix (D3)
D1: peds+keepers upgrade placeholder->rig in the shell; 12 enter/exit shops leak-free (renderer.info.memory back to exact baseline); determinism holds across reload; ?noassets fully placeholder with zero ped fetches. D2 (gate-3 critical path): validated Lane E's decimated fleet — all 19 peds <=3k tris (1564-2805, avg 2450), rigs bind+animate (skinning/skeleton preserved), 0 broken meshes under eviction churn, silhouettes+identity intact, atlas bakes clean. Near fleet ~59.6k tris at the 24-cap (was ~1.2M) — F clear to re-measure gate 3. D3: fixed a real bug found only in-shell — the impostor self-tone-mapped (toneMapped:false + in-shader ACES), correct on the direct-canvas test page but DOUBLE-tone-mapped under the shell's EffectComposer/OutputPass. Reworked to a standard toneMapped:true material outputting linear via three's own tonemapping/colorspace chunks, so it matches the near rigs on both render paths. setExposure is now a no-op (exposure is global). Verified noon+night in-shell. Only web/js/citizens/impostor.js changed (+ NOTES/progress/R4 shots). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
34926324bd
commit
a5e4b64a9d
@ -1,5 +1,33 @@
|
||||
# PROCITY-D — progress (Lane D · Citizens)
|
||||
|
||||
## 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`.
|
||||
|
||||
@ -4,6 +4,58 @@
|
||||
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.*
|
||||
|
||||
---
|
||||
|
||||
## ROUND 4 — in-shell verify + decimation validation (→ Fable, → Lane F)
|
||||
|
||||
**All three R4 tasks done. `tools/qa.sh --strict` GREEN. Sign-off for Lane F below.**
|
||||
|
||||
### D1 — in-shell rig/keeper verification (`web/index.html`) ✅
|
||||
Verified in the real game (not the test page), via `window.PROCITY`:
|
||||
- **Placeholder→rig upgrade** fires for both peds (`citizens.mode` → `'rig'`, 19 fleet, pool active)
|
||||
and keepers (keeper spawns at the counter `keeperStand` pose as a **rig**).
|
||||
- **Leak-free**: baseline `renderer.info.memory` = {geometries 213, textures 198}; after **12
|
||||
enter/exit shop cycles** it returned to **exactly** 213/198 (Δ geom 0, Δ tex 0). Interiors peak
|
||||
~303 geoms inside, drop back every exit. Confirms the R-review dispose fix (free the keeper's
|
||||
Skeleton only, never the fleet-shared geo/mats) holds in-shell alongside Lane C's interior dispose.
|
||||
- **Determinism in-shell**: seed 20261990 → byte-identical identity signature across a full reload.
|
||||
- **`?noassets=1`**: `fleet===null`, mode stays `placeholder`, peds + keeper are placeholder actors,
|
||||
**zero ped GLB fetches** — town fully playable asset-free.
|
||||
|
||||
### D2 — decimated ped validation (critical path — E1 landed `518678d`) ✅ SIGN-OFF FOR F
|
||||
Validated Lane E's decimated fleet (`web/models/peds/`, ref `lane-e/round4-peds`):
|
||||
- **Per-ped tris: 1564–2805, avg 2450 — all ≤3k** (matches E's `_peds_decim.json`).
|
||||
- **Rigs bind + animate**: skinning preserved, the shared `walk.glb`/`idle.glb` retarget unchanged
|
||||
(skeleton/65 joints intact), near actors animate, mixer 0.1–0.3 ms.
|
||||
- **No skinning explosions**: 24-teleport pool-eviction churn → **0 broken meshes**; silhouettes
|
||||
read at street distance; identity variety intact (verified visually — kid/luchador/hi-vis/elder/
|
||||
business all distinct, no melted hands/faces).
|
||||
- **Impostor atlas bakes clean** from the decimated rigs; determinism + leak checks re-pass.
|
||||
- **Gate-3 number for Lane F**: at the 24-rig near cap the fleet now contributes **≈59.6k tris**
|
||||
(was ~1.2M). Test-page whole-view total **88.9k tris** (was ~1.5M). In-shell sample near rig 2729
|
||||
tris, keeper 2073 tris (was 41k). **F is clear to re-measure gate 3** — the fleet leaves ~140k for
|
||||
the town, comfortably ≤200k. (The R1 "peds too heavy" finding is now resolved by E's decimation.)
|
||||
|
||||
### D3 — exposure sync at dusk/night — FOUND + FIXED A REAL BUG ✅
|
||||
Verifying the shell exposed a genuine rendering bug (only correct on my direct-canvas test page):
|
||||
**the impostor material was `toneMapped:false` and applied ACES in-shader**, but the shell renders
|
||||
through an **EffectComposer (RenderPass → bloom → OutputPass)**. three only tone-maps materials when
|
||||
rendering to the **canvas** (`three.module.js:6921–6931`, `currentRenderTarget === null`); into a
|
||||
composer target, materials output **linear** and OutputPass applies ACES once. So my self-tone-mapping
|
||||
impostors were **double-tone-mapped** in the actual game (too dark), matching the rigs only on the
|
||||
test page. **Fix (impostor.js):** the impostor is now a normal `toneMapped:true` material — it outputs
|
||||
**linear** and uses three's own `<tonemapping_fragment>` + `<colorspace_fragment>` call-sites (the
|
||||
`*_pars_*` are auto-injected — including them ourselves double-defines `RRTAndODTFit` and fails to
|
||||
compile), so it tone-maps **identically to the near rigs on both paths**: canvas → ACES per-material;
|
||||
composer → linear → OutputPass ACES. Verified: near rig + mid impostor match brightness at noon AND
|
||||
the scene reads correctly at night 22:00 in the shell (`docs/shots/laneD/r4_shell_*.jpg`), and the
|
||||
test-page canvas path still matches.
|
||||
- **Note for Lane F**: `citizens.setExposure()` is now a **no-op** (exposure is global via
|
||||
`renderer.toneMappingExposure`, which both three and OutputPass read). The call at
|
||||
`web/index.html:245` is harmless — keep or drop it, your call.
|
||||
|
||||
*(Round-1/3 notes below remain valid; the tri-budget finding #1 is superseded by D2's decimation.)*
|
||||
|
||||
## What shipped
|
||||
|
||||
| file | role |
|
||||
|
||||
BIN
docs/shots/laneD/r4_shell_night.jpg
Normal file
BIN
docs/shots/laneD/r4_shell_night.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
BIN
docs/shots/laneD/r4_shell_noon_near+mid.jpg
Normal file
BIN
docs/shots/laneD/r4_shell_noon_near+mid.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 426 KiB |
@ -131,28 +131,29 @@ const IMP_VERT = `
|
||||
gl_Position = projectionMatrix * viewMatrix * vec4(world, 1.0);
|
||||
}
|
||||
`;
|
||||
// atlas holds LINEAR lit colour → ACES tone-map (three's fit) → sRGB OETF, so mid impostors match
|
||||
// the ACES-tonemapped near rigs. Without this the linear texels display ~2× too dark.
|
||||
// The atlas holds LINEAR lit colour. We output linear and let three's own pipeline tone-map +
|
||||
// colour-manage via the injected chunks, so the impostor behaves EXACTLY like every other material:
|
||||
// • direct-to-canvas (test page): three applies ACES + sRGB (matches the near rigs)
|
||||
// • EffectComposer (shell): the RenderPass renders to a target → three uses NoToneMapping → we
|
||||
// output linear → OutputPass applies ACES once (matches the rigs). A self-tone-mapping shader
|
||||
// (toneMapped:false + in-shader ACES) would DOUBLE tone-map under the composer's OutputPass —
|
||||
// correct only on the canvas path. Letting three do it is right on both. (Exposure is global via
|
||||
// renderer.toneMappingExposure, so no per-material exposure plumbing is needed.)
|
||||
// NOTE: three injects tonemapping_pars_fragment + colorspace_pars_fragment into the ShaderMaterial
|
||||
// prefix automatically (WebGLProgram), so we include ONLY the call-sites here — adding the *_pars_*
|
||||
// ourselves would double-define RRTAndODTFit etc. and fail to compile. <tonemapping_fragment> is a
|
||||
// no-op when the render target forces NoToneMapping (composer RenderPass), and applies ACES on the
|
||||
// canvas — exactly the behaviour that keeps impostors matched to the rigs on both paths.
|
||||
const IMP_FRAG = `
|
||||
uniform sampler2D uAtlas;
|
||||
uniform vec3 uTint;
|
||||
uniform float uExposure;
|
||||
varying vec2 vUv;
|
||||
// imp-prefixed to avoid colliding with three's injected tonemapping functions of the same name
|
||||
vec3 impRRTFit(vec3 v){ vec3 a=v*(v+0.0245786)-0.000090537; vec3 b=v*(0.983729*v+0.4329510)+0.238081; return a/b; }
|
||||
vec3 impACES(vec3 color){
|
||||
color *= uExposure / 0.6; // three's ACES normalises exposure by 0.6 — match it exactly
|
||||
const mat3 ACESInput = mat3(0.59719,0.07600,0.02840, 0.35458,0.90834,0.13383, 0.04823,0.01566,0.83777);
|
||||
const mat3 ACESOutput = mat3(1.60475,-0.10208,-0.00327, -0.53108,1.10813,-0.07276, -0.07367,-0.00605,1.07602);
|
||||
color = ACESInput * color; color = impRRTFit(color); color = ACESOutput * color;
|
||||
return clamp(color, 0.0, 1.0);
|
||||
}
|
||||
vec3 impLin2sRGB(vec3 c){ return mix(c*12.92, 1.055*pow(c,vec3(1.0/2.4))-0.055, step(0.0031308,c)); }
|
||||
void main() {
|
||||
vec4 c = texture2D(uAtlas, vUv);
|
||||
if (c.a < 0.5) discard; // hard cut-out — no sorting needed
|
||||
vec3 col = impLin2sRGB(impACES(c.rgb)) * uTint;
|
||||
gl_FragColor = vec4(col, 1.0);
|
||||
gl_FragColor = vec4(c.rgb * uTint, 1.0); // c.rgb is LINEAR
|
||||
#include <tonemapping_fragment>
|
||||
#include <colorspace_fragment>
|
||||
}
|
||||
`;
|
||||
|
||||
@ -178,10 +179,9 @@ export class ImpostorLayer {
|
||||
uAtlas: { value: atlas.texture },
|
||||
uCellScale: { value: atlas.cellScale },
|
||||
uTint: { value: new THREE.Color(1, 1, 1) },
|
||||
uExposure: { value: atlas.exposure ?? 1.0 },
|
||||
},
|
||||
transparent: false, side: THREE.DoubleSide,
|
||||
toneMapped: false, // we tone-map in-shader (impACES); stop three injecting its own ACES fns
|
||||
toneMapped: true, // three tone-maps (canvas: ACES; composer: linear→OutputPass). See IMP_FRAG.
|
||||
});
|
||||
|
||||
this.mesh = new THREE.InstancedMesh(iGeo, this.material, maxInstances);
|
||||
@ -239,8 +239,10 @@ export class ImpostorLayer {
|
||||
}
|
||||
|
||||
setTint(hex) { this.material.uniforms.uTint.value.set(hex); }
|
||||
// keep impostors matched to the rigs if the shell animates renderer.toneMappingExposure (day/night)
|
||||
setExposure(e) { this.material.uniforms.uExposure.value = e; }
|
||||
// No-op: exposure is now global (renderer.toneMappingExposure) — three's tone-mapping (canvas) and
|
||||
// the composer's OutputPass both read it, so impostors track day/night automatically. Kept so the
|
||||
// shell's citizens.setExposure(...) call site stays valid (Lane F may drop it — harmless either way).
|
||||
setExposure(_e) {}
|
||||
|
||||
dispose() {
|
||||
this.mesh.geometry.dispose();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user