diff --git a/docs/LANES/LANE_E_NOTES.md b/docs/LANES/LANE_E_NOTES.md index 4583a6d..b0e0a81 100644 --- a/docs/LANES/LANE_E_NOTES.md +++ b/docs/LANES/LANE_E_NOTES.md @@ -1,9 +1,52 @@ # LANE E — cross-lane notes -## Round 39 — the magpie settled, and the arcade measured ⟨v9 39.4⟩ +## Round 39 — the magpie settled, the arcade measured, and three GLBs that were secretly glass ⟨v9 39.4⟩ Full provenance and method: `pipeline/AUDIT.md` § Round 39. Sheets: -`docs/shots/laneE/r39_magpie_ab.png` (the ruling picture) · `docs/shots/laneE/r39_arcade_kit.png`. +`docs/shots/laneE/r39_magpie_ab.png` (the ruling picture) · `r39_transmission_fix.png` · +`r39_arcade_skins.png` · `r39_arcade_kit.png`. + +### → LANE C + LANE F + FABLE: THE TRANSMISSION PRE-PASS — **FIXED, PUBLISHED, GATED.** And it was **three** assets, not one. + +Lane C measured that zeroing the bookshelf's transmission halves interior draws. The cause, with +vendored line numbers: **three.js runs a transmission PRE-PASS.** `three.module.js:16209` — +`if ( transmissiveObjects.length > 0 ) renderTransmissionPass( opaqueObjects, … )`; `:16433` renders +the **whole opaque list** into a render target, so **every opaque draw in that scene is issued +twice**. The trigger is `material.transmission > 0` and nothing else (`:6890`), set straight from the +glTF by `GLTFLoader.js:1202-1204`. And at 1.0 it is not "a bit glassy": `:494`'s +`transmission_fragment` does `totalDiffuse = mix( totalDiffuse, transmitted.rgb, material.transmission )` +— **the diffuse term is replaced entirely by the refracted backdrop.** + +**I scanned all 103 GLBs in the repo. THREE carried `transmissionFactor: 1`:** + +| file | material | state | +|---|---|---| +| `procity_fit_bookshelf_01.glb` | `mtl_10218_Bookshelves_v1` | **shipped and hurting** — the one C measured | +| `procity_street_longbench_01.glb` | `lambert3SG` | **NOT WIRED** — the 5.86 m arcade/market bench the v9 charter asks for **by name**, and which my own R38 notes call a "true one-liner" for Lane B | +| `procity_street_streetlight_01.glb` | `Streetlight_HighResSG3` | **NOT WIRED** — one per ~18 m of street, also on my R38 recommend list | + +> **→ LANE B: the two unwired ones are the worse finding, and they were pointed at you.** The street +> budget is **292/300 with eight draws of margin**. A transmission pre-pass on the street does not +> cost eight draws, it costs **another ~292**. Both are now fixed on the depot; **if you had a local +> copy cached, re-fetch.** No other GLB in the repo carries `KHR_materials_transmission` or +> `KHR_materials_volume`. + +**Fixed with `pipeline/strip_transmission.py`** — JSON chunk rewritten, BIN chunk copied through, and +the tool prints sha1(BIN) before and after: **`BIN IDENTICAL` on all three.** Tris/dims/materials/ +images/max-tex unchanged. `KHR_materials_ior` deliberately left (ior does not trigger the pre-pass). +Published under the R22 standing authorization and **fetched back off the public depot: 6/6 HTTP 200, +bytes exact, sha1 identical**, and the depot copies re-scanned clean. `_published.json` 56 → 59. + +**→ LANE F, the gate you can now write:** `validate_manifest.py` **hard-fails** on +`transmissionFactor > 0` for every manifest GLB (message carries the three.js line numbers and the +fix command), and `glb_stat.py` grew a **`transmission` column** plus a loud footer. `qa.sh` already +runs the validator, so the control is live. C's measurement is the falsifiable arm: re-run the +interior draw sweep and `opshop`/hall should sit at ~104, not 191. + +**→ LANE C: neither fix you held back is needed now.** The asset is corrected at source, so `glb.js` +needs no `transmission = 0` scrub and no appearance compromise. You were right to hold — it was my +asset and my call, and it was a defect, not a style: **a bookshelf, a park bench and a streetlight +are not glass.** This was an exporter default that rode in with the source meshes. ### → FABLE: THE MAGPIE — **RECOMMENDATION: TAKE THE GLB.** The picture is `r39_magpie_ab.png`. @@ -113,19 +156,41 @@ Ranked by **value per draw**, because the margin is eight and this is one distri | 2 | **`ground-arcade-floor`** | ground skin, 512² | 1 class | **+1 draw town-wide**, **+0 tris** (the 504 m² quad *moves* out of `footGeos`) | | 3 | **`ground-arcade-roof`** | ground skin, 512² | 1 quad | **+1 draw in the 1–2 chunks that hold arcade lots**, **+2 tris** | | 4 | **hanging blade signage** | ***no asset — see below*** | 17 | **+0 draws, +0 assets, +4 tris/shop = 68** | -| 5 | **`aframe_board`** | GLB | ≤16/town | +1 draw as ONE town-wide `InstancedMesh` | -| 6 | **`keycutter_sign`** | GLB | 1 | +1 draw for one object — **lowest value in the kit** | +| 5 | **`aframe_board`** | GLB **366 tris**, 0.68 × 0.64 m, h 0.890 | ≤16/town ⇒ ≤5,856 tris | +1 draw as ONE town-wide `InstancedMesh` | +| 6 | **`blade_sign`** | GLB **299 tris**, 0.66 × 0.10 m, h 0.549 | 17 ⇒ 5,083 tris | +1 draw — **the fallback for #4, not the answer** | +| 7 | **`keycutter_sign`** | GLB **498 tris**, 0.29 × 0.06 m, h 0.599 | 1 | +1 draw for one object — **lowest value in the kit** | + +All three are published and **sha1-verified off the public depot**, in `manifest.json` under +`furniture` (13 → 16), and the local-depot mirror is re-staged for `?localdepot=1`. House GLB law +clean: metres · +Y up · base origin · 1 mtl / 1 img / 512² WebP · no Draco. + +> **⚠ `blade_sign` ships with a flagged defect.** Its FRONT face is the blank white the house law +> requires; its BACK face carries **TRELLIS-hallucinated artwork** invented where the single concept +> photo had no data. A blade sign in a 5 m arcade is read from both sides. It is shipped flagged +> rather than quietly (the R38 magpie-albedo precedent), and it is one more reason **#4's +0-draw +> quad is the recommendation and this GLB is the fallback.** Fix path if wanted: `tint_atlas.py` +> already rasterizes UV→position maps, so flat-whiting every texel with `z < 0` is the same pass +> that repainted the magpie. **#4 is a refusal with a better answer, and it is the most useful thing in this table.** A blade sign does not need a mesh or a texture. `buildings.js` already draws every shop's sign into a per-chunk 512×168 canvas atlas cell and pushes ONE quad into the merged `signQuads` mesh. **Push a SECOND quad, perpendicular to the facade, with the same `signUV.get(shop.id)` rect** and you get a -projecting sign carrying the shop's real name for **+0 draws** (it rides the mesh that already +projecting sign carrying the shop's **real name** for **+0 draws** (it rides the mesh that already exists) and **+2 tris single-sided / +4 back-to-back** — **68 triangles for all 17 shops.** The bracket is two `boxMats` boxes, which is also +0 draws. A GLB blade sign at 17 instances cannot beat -that; it can only look different. Generated anyway (see AUDIT §Round 39) so the option exists, but +that; it can only look different. Generated anyway (AUDIT §Round 39 §6) so the option exists, but **wire the quad first**. +> The insertion point is the block you already have — `buildings.js:620-631`, immediately after +> `const uv = signUV.get(shop.id)`. Today's quad is `sh 0.5 · sy = fh − 0.35 · sw = w×0.92 · +> zs = zf + 0.05`, i.e. spanning **y 2.40–2.90** on a 3.0 m facade. **Do not reuse that y for a +> blade**: the awning slab is `AWNING_Y 2.95` × 0.16 thick ⇒ its underside is **2.87**, so today's +> sign band already grazes it. A blade wants **centre y ≈ 2.35, height ≈ 0.45 ⇒ 2.13–2.58** — clear +> under the slab and well over EYE 1.62. Project it **~0.7 m** from the facade (`z` from `zf+0.05` +> outward), fixed `x` at a lot edge: 0.7 m into a 2.50 m half-lane leaves **1.8 m clear** each side, +> which is more clearance than the awning posts currently leave (§ the post finding above). + **#2 and #3's `use` values name slots that do not exist yet — deliberately.** R38 recorded `gravel` (`use:'verge'`) and `reddust` (`use:'outback'`) as selected by nothing *by accident*. These two are selected by nothing *on purpose and with the price printed*: the arcade cannot ride `footpath`, diff --git a/docs/shots/laneE/r39_arcade_kit.png b/docs/shots/laneE/r39_arcade_kit.png new file mode 100644 index 0000000..ce574ad Binary files /dev/null and b/docs/shots/laneE/r39_arcade_kit.png differ diff --git a/docs/shots/laneE/r39_transmission_fix.png b/docs/shots/laneE/r39_transmission_fix.png new file mode 100644 index 0000000..f9e8ee3 Binary files /dev/null and b/docs/shots/laneE/r39_transmission_fix.png differ diff --git a/pipeline/AUDIT.md b/pipeline/AUDIT.md index 3b558a4..1c64dd8 100644 --- a/pipeline/AUDIT.md +++ b/pipeline/AUDIT.md @@ -997,3 +997,106 @@ photographic grounds already in the pool, seam measured and recorded rather than **$0 spent.** No cloud key touched. The hardyards set stays REJECTED on provenance (R38), and the 3GOD depot stays a read-only archive. + +### 6. THE THREE MESHES — measured, published, byte-verified + +`flux_local` → `bg_remove_local` (RMBG-2.0) → `trellis2_mlx` (1024_cascade, metal baker, 500 k bake +cap) → `normalize.py` → `bake_lowpoly.py`. **3/3 first attempt, 0 retries, 0 OOM-kills.** + +| id | tris | footprint (m) | height (m) | mtl/img/tex | instances the rule implies | sha1 (local == depot) | +|---|---|---|---|---|---|---| +| `aframe_board` | **366** | 0.68 × 0.64 | 0.890 | 1 / 1 / 512² | **≤16/town** (arcade ~4 + v9's unmarked cue ≤12) ⇒ ≤5,856 tris | `2aa9732d23c7…` | +| `blade_sign` | **299** | 0.66 × 0.10 | 0.549 | 1 / 1 / 512² | **17** (one per arcade shop) ⇒ 5,083 tris | `210e270c22ec…` | +| `keycutter_sign` | **498** | 0.29 × 0.06 | 0.599 | 1 / 1 / 512² | **1** | `6f22995d6402…` | + +House GLB law verified on all three: metres · +Y up · base origin · **1 material / 1 image / 512² +WebP** · no Draco. Decimation floors, again confirming R38's finding that collapse cannot reach +these budgets and the bake must: 443,014 → 104,997 (normalize) → **366** (bake) · 490,612 → 94,202 → +**299** · 493,916 → 28,940 → **498**. + +**⚠ `blade_sign` ships with a defect, flagged not hidden.** Its FRONT face is the blank white the +house law requires, but its BACK face carries **TRELLIS-hallucinated artwork** (a plant, a globe, +scattered shapes) invented where the single concept photograph had no data. A blade sign in a 5 m +arcade is read from both sides, so this matters — and it is one more reason the **+0-draw +perpendicular-quad answer (§3 item 4) is the recommendation and this GLB is the fallback.** Fix +path if it is ever wanted, stated not done: the R38 `tint_atlas.py` machinery already rasterizes +UV→position maps, so flat-whiting every texel with `z < 0` is the same pass that painted the magpie. + +**⚠ AND THE OPERATOR'S TAIL IS NOT AN OUTLIER — it looks like memory pressure.** Wall clock: +**516.3 s · 538.7 s · 176.8 s.** R38 reported a single 489 s job against a 128 s median and called +it an unexplained tail. Here the **first two** jobs — the two that followed a `flux_local` skin +batch and a `flux_local` concept batch in the same session — took ~3× the third, which ran after +everything else had exited. Same operator, same params, same box, no error, monotonically improving. +**Hypothesis, not proof: the tail is resident MPS memory from the image model, not the mesh model.** +Worth one controlled A/B in a later round; until then, plan a mesh batch as a *cold* run. + +### 7. THE TRANSMISSION PRE-PASS — three GLBs shipped as glass (R39, from Lane C's measurement) + +Lane C measured that zeroing `bookshelf`'s transmission halves interior draws. The cause is not +subtle once named: **three.js runs a transmission PRE-PASS.** + +> `three.module.js:16209` — `if ( transmissiveObjects.length > 0 ) renderTransmissionPass( opaqueObjects, … )` +> `three.module.js:16433` — `renderTransmissionPass` renders the **whole opaque list** into a +> `WebGLRenderTarget`. **Every opaque draw in that scene is issued twice.** +> `three.module.js:6890` — the trigger is `material.transmission > 0`, nothing else. +> `GLTFLoader.js:1202-1204` — `materialParams.transmission = extension.transmissionFactor`. +> `three.module.js:494` (`transmission_fragment`) — `totalDiffuse = mix( totalDiffuse, transmitted.rgb, +> material.transmission )`, so at **1.0 the diffuse is replaced entirely by the refracted backdrop**: +> the asset is not "slightly glassy", it *is* glass. + +**Lane C's numbers:** `opshop`/hall **191 → 104 (−46%)** · `opshop`/wide 142 → 80 · `book`/hall +**101 → 51 (−50%)** · `dept`/hall 116 → 116 (control — no bookshelf, no change). + +**I scanned all 103 GLBs** in `pipeline/_normalized`, `web/assets/models`, `web/assets` and +`pipeline/packs` for `KHR_materials_transmission` / `KHR_materials_volume`. **Three carried it — +not one:** + +| file | material | state before R39 | +|---|---|---| +| `procity_fit_bookshelf_01.glb` | `mtl_10218_Bookshelves_v1` | **SHIPPED and hurting** — the one C measured | +| `procity_street_longbench_01.glb` | `lambert3SG` (+ ior 1) | **NOT WIRED** — the 5.86 m arcade/market bench the v9 charter names, and LANE_E_NOTES R38 lists as a "true one-liner" for Lane B | +| `procity_street_streetlight_01.glb` | `Streetlight_HighResSG3` (+ ior 1) | **NOT WIRED** — one per ~18 m of street, also on R38's recommend list | + +**The two unwired ones are the worse finding.** They are STREET assets, both recommended to Lane B +by this lane's own notes, and the street budget is **292/300 with eight draws of margin**. A +transmission pre-pass on the street does not cost eight draws — it costs **another ~292**. They were +armed, not fired. No other GLB in the repo carries either extension. + +**The fix: `pipeline/strip_transmission.py`.** Dependency-free GLB surgery — rewrite the JSON chunk, +copy the BIN chunk through untouched, and **print the sha1 of the BIN chunk before and after** so +"geometry unchanged" is a checked claim. All three: `BIN IDENTICAL`. Tris, dims, material and image +counts and max texture size are unchanged (5000 / 1172 / 4999). `KHR_materials_ior` is deliberately +**left alone** — ior does not trigger the pre-pass, and removing more than the defect widens the +diff for no measured gain. + +**Appearance:** before/after through the same rig, `docs/shots/laneE/r39_transmission_fix.png`. In +Blender/EEVEE the difference is small; in three.js it is the whole diffuse term (see the shader line +above). Either way the direction is not a trade: **a bookshelf, a park bench and a streetlight are +not glass.** This was an exporter default that rode in with the source meshes and has been doubling +interior draws for however many epochs it has shipped. + +**Published and verified**, under John's R22 standing authorization, through the passwordless tailnet +ingress (`GOD3_DEPOT=http://100.94.195.115:8788`; no secret read, no cookie). Then fetched back off +the **public** host and compared: **6/6 HTTP 200, byte counts exact, sha1 identical**, and the depot +copies re-scanned — **`clean, nothing to do` on all six.** + +| file | HTTP | bytes (local == depot) | sha1 | +|---|---|---|---| +| `procity_fit_bookshelf_01.glb` | 200 | 356 636 | `1769af492fbf9338…` | +| `procity_street_longbench_01.glb` | 200 | 56 600 | `2813e81c6bb1feb5…` | +| `procity_street_streetlight_01.glb` | 200 | 156 328 | `9c5ec45d4d861db5…` | +| `procity_street_aframe_board_01.glb` | 200 | 88 572 | `2aa9732d23c72c2e…` | +| `procity_street_blade_sign_01.glb` | 200 | 64 196 | `210e270c22ec4dd6…` | +| `procity_street_keycutter_sign_01.glb` | 200 | 79 708 | `6f22995d6402a509…` | + +`_published.json` **56 → 59**; drift check green. `build_manifest.py` re-run (furniture 13 → 16, +GLBs 36 → 39); **`validate_manifest.py`: 0 errors, 0 warnings.** + +**AND IT IS NOW UNGATEABLE-PAST, which matters more than the fix.** +- `glb_stat.py` grew a **`transmission` column** and prints a loud block when any file has it. +- `validate_manifest.py` **HARD FAILS** on `transmissionFactor > 0` for every manifest GLB, with the + three.js line numbers in the message and the fix command. Nobody looked for three epochs because a + bookshelf is obviously not glass; a gate is the only thing that makes "nobody looked" impossible. + +**Licence unchanged 🟢, $0.** No geometry, no texture and no third-party asset was added or altered +— one extension object was deleted from three material blocks. diff --git a/pipeline/_published.json b/pipeline/_published.json index ec50ba2..46d3a68 100644 --- a/pipeline/_published.json +++ b/pipeline/_published.json @@ -33,16 +33,19 @@ "procity_fit_record_crate_01.glb", "procity_fit_wire_shelf_01.glb", "procity_fit_work_table_01.glb", + "procity_street_aframe_board_01.glb", "procity_street_bench_01.glb", "procity_street_bench_01_hi.glb", "procity_street_bench_modern_01.glb", "procity_street_bench_wood_01.glb", "procity_street_bin_01.glb", "procity_street_bin_01_hi.glb", + "procity_street_blade_sign_01.glb", "procity_street_bus_shelter_01.glb", "procity_street_bus_shelter_01_hi.glb", "procity_street_food_cart_01.glb", "procity_street_food_cart_01_hi.glb", + "procity_street_keycutter_sign_01.glb", "procity_street_longbench_01.glb", "procity_street_magpie_01.glb", "procity_street_novelty_record_01.glb", diff --git a/pipeline/_r39_results.json b/pipeline/_r39_results.json new file mode 100644 index 0000000..ffc7955 --- /dev/null +++ b/pipeline/_r39_results.json @@ -0,0 +1,53 @@ +{ + "results": [ + { + "file": "procity_street_aframe_board_01.glb", + "tri_before": 104997, + "tri_after": 366, + "size_m": [ + 0.678, + 0.89, + 0.641 + ], + "footprint": [ + 0.68, + 0.64 + ], + "thumb": "web/assets/thumbs/procity_street_aframe_board_01.png", + "out": "pipeline/_normalized/procity_street_aframe_board_01.glb" + }, + { + "file": "procity_street_blade_sign_01.glb", + "tri_before": 94202, + "tri_after": 299, + "size_m": [ + 0.657, + 0.549, + 0.101 + ], + "footprint": [ + 0.66, + 0.1 + ], + "thumb": "web/assets/thumbs/procity_street_blade_sign_01.png", + "out": "pipeline/_normalized/procity_street_blade_sign_01.glb" + }, + { + "file": "procity_street_keycutter_sign_01.glb", + "tri_before": 28939, + "tri_after": 498, + "size_m": [ + 0.288, + 0.599, + 0.058 + ], + "footprint": [ + 0.29, + 0.06 + ], + "thumb": "web/assets/thumbs/procity_street_keycutter_sign_01.png", + "out": "pipeline/_normalized/procity_street_keycutter_sign_01.glb" + } + ], + "errors": [] +} \ No newline at end of file diff --git a/pipeline/build_manifest.py b/pipeline/build_manifest.py index e5f5924..1afd274 100644 --- a/pipeline/build_manifest.py +++ b/pipeline/build_manifest.py @@ -24,6 +24,10 @@ GIG_RESULTS = os.path.join(ROOT, "pipeline", "_gig_results.json") # roun # `_r38_results.PENDING_PUBLISH.json`. ONE STEP lands them: publish, then rename it to # `_r38_results.json` and re-run this script. See AUDIT.md R38 §4 and LANE_E_NOTES R38. R38_RESULTS = os.path.join(ROOT, "pipeline", "_r38_results.json") # round-38 wave-1 props +# [R39] the arcade kit — aframe_board / blade_sign / keycutter_sign. All three are PUBLISHED and +# sha1-verified off the public depot, so they are catalogued the same round they were generated +# (contrast R38, which held its results file at `.PENDING_PUBLISH` until the push landed). +R39_RESULTS = os.path.join(ROOT, "pipeline", "_r39_results.json") # round-39 arcade kit OUT = os.path.join(ROOT, "web", "assets", "manifest.json") AUDIO = os.path.join(ROOT, "web", "assets", "audio") # round-11 audio pack DEPOT = "https://digalot.fyi/3god" @@ -294,7 +298,7 @@ def build(): fittings, furniture = {}, {} # cube_shelf dropped: horizontal node-scale distortion (6.3x8.4m footprint) — see AUDIT.md DROP = {"procity_fit_cube_shelf_01.glb"} - for results_file in (RESULTS, PROPS_RESULTS, GIG_RESULTS, R38_RESULTS): + for results_file in (RESULTS, PROPS_RESULTS, GIG_RESULTS, R38_RESULTS, R39_RESULTS): if not os.path.exists(results_file): continue for r in json.load(open(results_file)).get("results", []): diff --git a/pipeline/glb_stat.py b/pipeline/glb_stat.py index 99e892a..0ea6549 100644 --- a/pipeline/glb_stat.py +++ b/pipeline/glb_stat.py @@ -140,9 +140,24 @@ def stat(path): tex_sizes.append(wh) exts = gltf.get("extensionsUsed", []) or [] draco = any("draco" in e.lower() for e in exts) + # [R39] TRANSMISSION IS A DRAW-BUDGET PROPERTY, NOT A LOOK. three.js runs a transmission + # PRE-PASS — `renderTransmissionPass(opaqueObjects, …)` at three.module.js:16433, entered from + # :16209 whenever `currentRenderList.transmissive.length > 0` — which re-renders the whole + # OPAQUE list into a render target, so every draw in that scene is issued TWICE. Three PROCITY + # GLBs shipped with `transmissionFactor: 1` and nobody looked, because a bookshelf, a bench and + # a streetlight are not glass. Lane C measured the bookshelf's at −46%/−50% of interior draws. + # It is reported here so it can never hide in a material block again. + transmission = [] + for i, m in enumerate(gltf.get("materials", [])): + ex = m.get("extensions") or {} + t = ex.get("KHR_materials_transmission") + if t and t.get("transmissionFactor", 0) > 0: + transmission.append({"material": i, "name": m.get("name"), + "factor": t.get("transmissionFactor")}) return { "file": os.path.basename(path), "tris": tris, + "transmission": transmission, "dims_m": dims, # x,y,z extent in file units (hopefully metres) "nodes": len(gltf.get("nodes", [])), "meshes": len(meshes), @@ -208,17 +223,26 @@ def main(): if as_json: print(json.dumps(rows, indent=2)) return - hdr = f"{'file':38} {'tris':>7} {'dims (m) x,y,z':>22} {'mtl':>3} {'img':>3} {'maxtex':>6} draco" + hdr = (f"{'file':38} {'tris':>7} {'dims (m) x,y,z':>22} {'mtl':>3} {'img':>3} {'maxtex':>6} " + f"draco transmission") print(hdr) print("-" * len(hdr)) + bad = 0 for r in rows: if "error" in r: print(f"{r['file']:38} ERROR: {r['error']}") continue d = r["dims_m"] dstr = f"{d[0]:.2f},{d[1]:.2f},{d[2]:.2f}" + tx = r.get("transmission") or [] + bad += len(tx) + txs = ("*** " + ",".join(f"{t['name']}={t['factor']}" for t in tx) + " ***") if tx else "-" print(f"{r['file']:38} {r['tris']:>7} {dstr:>22} {r['materials']:>3} " - f"{r['images']:>3} {r['max_tex']:>6} {'YES' if r['draco'] else '-'}") + f"{r['images']:>3} {r['max_tex']:>6} {'YES' if r['draco'] else '-':5} {txs}") + if bad: + print(f"\n*** {bad} material(s) carry KHR_materials_transmission > 0. three.js will run a " + f"transmission PRE-PASS (three.module.js:16209/:16433) and issue EVERY opaque draw in " + f"that scene TWICE. Fix with: python3 pipeline/strip_transmission.py ***") if __name__ == "__main__": diff --git a/pipeline/strip_transmission.py b/pipeline/strip_transmission.py new file mode 100644 index 0000000..879ee4f --- /dev/null +++ b/pipeline/strip_transmission.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""PROCITY Lane E — strip_transmission.py (R39, the transmission pre-pass) + +Remove `KHR_materials_transmission` from a GLB **without touching one byte of geometry**. + +WHY THIS IS NOT A COSMETIC FIX. three.js renders a **transmission pre-pass** — a second full render +of the opaque scene into a render target — as soon as ANY material in the scene has +`transmission > 0` (`WebGLRenderer`'s transmissive-object list). Every opaque draw call in that +scene is therefore issued **twice**. Lane C measured it in R39 by zeroing the bookshelf's: + + opshop/hall 191 -> 104 draws (-46%) · opshop/wide 142 -> 80 + book/hall 101 -> 51 draws (-50%) · dept/hall 116 -> 116 (control, no bookshelf) + +Three PROCITY assets carry `transmissionFactor: 1`, and only one of them has ever been wired: + + procity_fit_bookshelf_01.glb mtl_10218_Bookshelves_v1 SHIPPED — the one C measured + procity_street_longbench_01.glb lambert3SG + ior 1 NOT WIRED — the 5.86 m arcade bench + procity_street_streetlight_01.glb Streetlight_HighResSG3 + ior 1 NOT WIRED — one per ~18 m of street + +The two unwired ones are the worse finding. They are STREET assets, LANE_E_NOTES R38 recommends both +to Lane B, and the street budget is 292/300 with **eight draws of margin** — a transmission pre-pass +there does not cost eight draws, it costs another ~292. Nobody looked because a bookshelf, a bench +and a streetlight are not glass; this is an exporter default that rode in with the source meshes. + + python3 pipeline/strip_transmission.py FILE.glb [FILE ...] [--in-place] [--dry-run] + +The JSON chunk is rewritten and the BIN chunk is copied through untouched — the tool prints the +sha1 of the BIN chunk before and after so "geometry unchanged" is a checked claim, not a promise. +`KHR_materials_ior` is deliberately LEFT ALONE: ior alone does not trigger the pre-pass, and +removing more than the defect widens the diff for no measured gain. +""" +import hashlib +import json +import os +import struct +import sys + +EXT = "KHR_materials_transmission" + + +def read(path): + d = open(path, "rb").read() + assert d[:4] == b"glTF", f"{path}: not a GLB" + off, gltf, binc = 12, None, b"" + while off < len(d): + clen, ctype = struct.unpack(" 0. + + This is a draw-budget check wearing a material's clothes. three.js runs a transmission PRE-PASS + — `renderTransmissionPass(opaqueObjects, …)`, three.module.js:16433, entered at :16209 whenever + `currentRenderList.transmissive.length > 0` — which re-renders the entire OPAQUE list into a + render target, so EVERY draw in that scene is issued twice. Lane C measured the effect of one + such material (the bookshelf's): opshop/hall 191→104 draws, book/hall 101→51, against a control + room with no bookshelf at 116→116. Three GLBs shipped with `transmissionFactor: 1` because a + bookshelf, a park bench and a streetlight are obviously not glass and so nobody ever looked. + A gate is the only thing that makes "nobody looked" impossible. Fix: pipeline/strip_transmission.py + """ + if not os.path.isfile(local_path): + return # depot-only asset: nothing to parse here + try: + import struct + d = open(local_path, "rb").read() + if d[:4] != b"glTF": + return + off, g = 12, None + while off < len(d): + clen, ctype = struct.unpack(" 0: + err(f"{file}: material[{i}] '{mat.get('name')}' has transmissionFactor " + f"{t['transmissionFactor']} — three.js will run a transmission PRE-PASS and " + f"issue every opaque draw in that scene TWICE. Run pipeline/strip_transmission.py") + except Exception as e: # a parse failure is not a licence to skip the check + warn(f"{file}: could not read materials for the transmission check ({str(e)[:60]})") + + def check_glb(entry, depot): file = entry["file"] local = os.path.join(NORM, file) on_disk = os.path.isfile(local) + check_transmission(local, file) live = head_ok(f"{depot}/a/{file}") if STRICT_DEPOT and not live: err(f"GLB not live on depot: {file}") diff --git a/web/assets/manifest.json b/web/assets/manifest.json index 64369d1..7f3c6ec 100644 --- a/web/assets/manifest.json +++ b/web/assets/manifest.json @@ -966,6 +966,33 @@ ], "height": 2.19, "thumb": "thumbs/procity_street_water_tank_01.png" + }, + "aframe_board": { + "file": "procity_street_aframe_board_01.glb", + "footprint": [ + 0.68, + 0.64 + ], + "height": 0.89, + "thumb": "thumbs/procity_street_aframe_board_01.png" + }, + "blade_sign": { + "file": "procity_street_blade_sign_01.glb", + "footprint": [ + 0.66, + 0.1 + ], + "height": 0.549, + "thumb": "thumbs/procity_street_blade_sign_01.png" + }, + "keycutter_sign": { + "file": "procity_street_keycutter_sign_01.glb", + "footprint": [ + 0.29, + 0.06 + ], + "height": 0.599, + "thumb": "thumbs/procity_street_keycutter_sign_01.png" } }, "fittingAliases": { diff --git a/web/assets/thumbs/procity_street_aframe_board_01.png b/web/assets/thumbs/procity_street_aframe_board_01.png new file mode 100644 index 0000000..5bfcaca Binary files /dev/null and b/web/assets/thumbs/procity_street_aframe_board_01.png differ diff --git a/web/assets/thumbs/procity_street_blade_sign_01.png b/web/assets/thumbs/procity_street_blade_sign_01.png new file mode 100644 index 0000000..f607ee1 Binary files /dev/null and b/web/assets/thumbs/procity_street_blade_sign_01.png differ diff --git a/web/assets/thumbs/procity_street_keycutter_sign_01.png b/web/assets/thumbs/procity_street_keycutter_sign_01.png new file mode 100644 index 0000000..f6bc983 Binary files /dev/null and b/web/assets/thumbs/procity_street_keycutter_sign_01.png differ