PROCITY/pipeline/strip_transmission.py
m3ultra 12eb571357 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>
2026-08-03 21:11:34 +10:00

108 lines
4.5 KiB
Python

#!/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()