Lane E R39 (6/n): THREE GLBs shipped as GLASS, not one — transmissionFactor 1 stripped, published, sha1-verified, and now GATED. Plus the arcade's three meshes.
Lane C measured the bookshelf's transmission halving interior draws (opshop/hall 191->104, book/hall
101->51, dept/hall 116->116 control). The cause, with vendored line numbers: three.module.js:16209
enters renderTransmissionPass(opaqueObjects, ...) whenever currentRenderList.transmissive is
non-empty, and :16433 re-renders the WHOLE OPAQUE LIST into a render target — every opaque draw
issued twice. Trigger is material.transmission > 0 (:6890), set from the glTF at GLTFLoader:1202.
And at 1.0 it is not 'a bit glassy': :494 does totalDiffuse = mix(totalDiffuse, transmitted.rgb,
material.transmission) — the diffuse term is REPLACED by the backdrop.
I SCANNED ALL 103 GLBs. Three carried it:
procity_fit_bookshelf_01 mtl_10218_Bookshelves_v1 shipped and hurting (C's)
procity_street_longbench_01 lambert3SG NOT WIRED — the 5.86 m arcade bench
the v9 charter names, on my R38
recommend list for Lane B
procity_street_streetlight_01 Streetlight_HighResSG3 NOT WIRED — one per ~18 m of street
The two unwired ones are the worse finding: the street budget is 292/300 with EIGHT draws of margin,
and a pre-pass there costs another ~292, not eight. Armed, not fired.
FIXED SURGICALLY. pipeline/strip_transmission.py rewrites only the JSON chunk and copies BIN through,
printing sha1(BIN) before and after: BIN IDENTICAL on all three, tris/dims/mtl/img/tex unchanged.
KHR_materials_ior deliberately left (ior does not trigger the pre-pass).
PUBLISHED + VERIFIED under John's R22 standing authorization via the passwordless tailnet ingress:
6/6 HTTP 200 off the PUBLIC depot, byte counts exact, sha1 identical, depot copies re-scanned clean.
_published.json 56 -> 59.
GATED, which matters more than the fix: validate_manifest.py now HARD FAILS on transmissionFactor>0
with the three.js line numbers and the fix command in the message; glb_stat.py grew a transmission
column and a loud footer. Nobody looked for three epochs because a bookshelf is obviously not glass.
ALSO: the arcade's three meshes, published and catalogued the same round they were generated —
aframe_board 366 tris (<=16 inst), blade_sign 299 (17 inst), keycutter_sign 498 (1 inst). manifest
furniture 13->16, GLBs 36->39, validate 0 errors 0 warnings. blade_sign ships FLAGGED: its back face
carries TRELLIS-hallucinated art where the concept had no data, which is one more reason the
+0-draw perpendicular-quad answer is the recommendation and this GLB is the fallback.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
0ee7a84710
commit
12eb571357
@ -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`,
|
||||
|
||||
BIN
docs/shots/laneE/r39_arcade_kit.png
Normal file
BIN
docs/shots/laneE/r39_arcade_kit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 498 KiB |
BIN
docs/shots/laneE/r39_transmission_fix.png
Normal file
BIN
docs/shots/laneE/r39_transmission_fix.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 600 KiB |
@ -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.
|
||||
|
||||
@ -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",
|
||||
|
||||
53
pipeline/_r39_results.json
Normal file
53
pipeline/_r39_results.json
Normal file
@ -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": []
|
||||
}
|
||||
@ -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", []):
|
||||
|
||||
@ -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 <file> ***")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
107
pipeline/strip_transmission.py
Normal file
107
pipeline/strip_transmission.py
Normal file
@ -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("<II", d[off:off + 8])
|
||||
chunk = d[off + 8: off + 8 + clen]
|
||||
if ctype == 0x4E4F534A:
|
||||
gltf = json.loads(chunk.decode("utf-8"))
|
||||
elif ctype == 0x004E4942:
|
||||
binc = chunk
|
||||
off += 8 + clen
|
||||
return gltf, binc
|
||||
|
||||
|
||||
def write(path, gltf, binc):
|
||||
j = json.dumps(gltf, separators=(",", ":")).encode("utf-8")
|
||||
j += b" " * ((4 - len(j) % 4) % 4)
|
||||
b = binc + b"\0" * ((4 - len(binc) % 4) % 4)
|
||||
total = 12 + 8 + len(j) + (8 + len(b) if b else 0)
|
||||
out = bytearray()
|
||||
out += b"glTF" + struct.pack("<II", 2, total)
|
||||
out += struct.pack("<II", len(j), 0x4E4F534A) + j
|
||||
if b:
|
||||
out += struct.pack("<II", len(b), 0x004E4942) + b
|
||||
open(path, "wb").write(bytes(out))
|
||||
|
||||
|
||||
def main():
|
||||
files = [a for a in sys.argv[1:] if not a.startswith("--")]
|
||||
dry = "--dry-run" in sys.argv
|
||||
for f in files:
|
||||
gltf, binc = read(f)
|
||||
before = hashlib.sha1(binc).hexdigest()
|
||||
hits = []
|
||||
for i, m in enumerate(gltf.get("materials", [])):
|
||||
ex = m.get("extensions") or {}
|
||||
if EXT in ex:
|
||||
hits.append((i, m.get("name"), ex[EXT]))
|
||||
del ex[EXT]
|
||||
if not ex:
|
||||
m.pop("extensions", None)
|
||||
if not hits:
|
||||
print(f"{os.path.basename(f)}: clean, nothing to do")
|
||||
continue
|
||||
# prune extensionsUsed/Required only if no material still declares it
|
||||
still = any(EXT in (m.get("extensions") or {}) for m in gltf.get("materials", []))
|
||||
for key in ("extensionsUsed", "extensionsRequired"):
|
||||
if not still and EXT in (gltf.get(key) or []):
|
||||
gltf[key] = [e for e in gltf[key] if e != EXT]
|
||||
if not gltf[key]:
|
||||
del gltf[key]
|
||||
for i, name, val in hits:
|
||||
print(f"{os.path.basename(f)}: material[{i}] '{name}' — removed {EXT} {val}")
|
||||
if dry:
|
||||
print(" (--dry-run, not written)")
|
||||
continue
|
||||
write(f, gltf, binc)
|
||||
_, binc2 = read(f)
|
||||
after = hashlib.sha1(binc2).hexdigest()
|
||||
ok = "BIN IDENTICAL" if after == before else "*** BIN CHANGED ***"
|
||||
print(f" wrote {f} {os.path.getsize(f)} B {ok} sha1(BIN) {after[:12]}")
|
||||
if after != before:
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
main()
|
||||
@ -49,10 +49,47 @@ def check_skin(file):
|
||||
err(f"skin missing: {file}")
|
||||
|
||||
|
||||
def check_transmission(local_path, file):
|
||||
"""[R39] HARD FAIL on KHR_materials_transmission > 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("<II", d[off:off + 8])
|
||||
if ctype == 0x4E4F534A:
|
||||
g = json.loads(d[off + 8: off + 8 + clen].decode("utf-8"))
|
||||
break
|
||||
off += 8 + clen
|
||||
for i, mat in enumerate(g.get("materials", []) if g else []):
|
||||
t = (mat.get("extensions") or {}).get("KHR_materials_transmission")
|
||||
if t and t.get("transmissionFactor", 0) > 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}")
|
||||
|
||||
@ -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": {
|
||||
|
||||
BIN
web/assets/thumbs/procity_street_aframe_board_01.png
Normal file
BIN
web/assets/thumbs/procity_street_aframe_board_01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
BIN
web/assets/thumbs/procity_street_blade_sign_01.png
Normal file
BIN
web/assets/thumbs/procity_street_blade_sign_01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
BIN
web/assets/thumbs/procity_street_keycutter_sign_01.png
Normal file
BIN
web/assets/thumbs/procity_street_keycutter_sign_01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Loading…
Reference in New Issue
Block a user