PROCITY/pipeline/fix_glb_textures.py
m3ultra dafb079ff4 Lane E round 4 (E2): fix record_crate + ship counter-with-till, both live on depot
- record_crate: R3 normalize left a sourceless texture (packed AO/roughness map
  didn't survive WebP export; metallicRoughness+occlusion pointed at it) → three.js
  'undefined uri' crash. New pipeline/fix_glb_textures.py strips material slots that
  reference a sourceless texture. Verified loads in vendored GLTFLoader; re-published.
- counter_till (new fitting): ~2m timber counter + period till, flux_local→trellis_mac
  on-device, 1.61x0.64m / 1.15m tall, 32k tris. Verified loads; published. Lane C maps
  kind counter -> counter_till (balcao 4m 'counter' kept for long-bar use).
- publish.py: --only <name> filter + passwordless tailnet detection (GOD3_DEPOT override).
- manifest rebuilt (15 fittings, alias counter->counter_till); validate --depot 0 errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 19:07:15 +10:00

79 lines
2.7 KiB
Python

#!/usr/bin/env python3
"""Repair a GLB whose material references a texture with no image source.
Blender's WebP glTF export occasionally emits a texture slot whose image failed to encode
(no `source`, no `EXT_texture_webp.source`) — e.g. a packed metallic-roughness/AO map. three.js
GLTFLoader then throws `Cannot read properties of undefined (reading 'uri')` when a material points
at it. This strips every material texture-slot that points at a sourceless texture (base-colour /
normal are kept; the orphaned texture is never loaded because GLTFLoader loads textures lazily),
then repacks the GLB (BIN chunk untouched).
python3 fix_glb_textures.py in.glb out.glb
"""
import json, struct, sys
SLOTS = ("baseColorTexture", "metallicRoughnessTexture", "normalTexture",
"occlusionTexture", "emissiveTexture")
def read_glb(path):
d = open(path, "rb").read()
off, gltf, chunks = 12, None, []
while off < len(d):
clen, ctype = struct.unpack("<II", d[off:off + 8])
body = d[off + 8: off + 8 + clen]
if ctype == 0x4E4F534A:
gltf = json.loads(body.decode("utf-8"))
chunks.append((ctype, body))
off += 8 + clen
return gltf, chunks
def has_source(tex):
if "source" in tex:
return True
ext = tex.get("extensions", {})
return any("source" in v for v in ext.values() if isinstance(v, dict))
def repair(gltf):
textures = gltf.get("textures", [])
bad = {i for i, t in enumerate(textures) if not has_source(t)}
if not bad:
return 0
stripped = 0
for m in gltf.get("materials", []):
pbr = m.get("pbrMetallicRoughness", {})
for container in (m, pbr):
for slot in list(container.keys()):
if slot in SLOTS and isinstance(container[slot], dict) \
and container[slot].get("index") in bad:
del container[slot]
stripped += 1
return stripped
def write_glb(gltf, chunks, out):
new_json = json.dumps(gltf, separators=(",", ":")).encode("utf-8")
new_json += b" " * ((4 - len(new_json) % 4) % 4) # pad to 4 bytes
body = struct.pack("<II", len(new_json), 0x4E4F534A) + new_json
for ctype, data in chunks:
if ctype == 0x4E4F534A:
continue
pad = b"\x00" * ((4 - len(data) % 4) % 4)
body += struct.pack("<II", len(data) + len(pad), ctype) + data + pad
header = struct.pack("<III", 0x46546C67, 2, 12 + len(body))
open(out, "wb").write(header + body)
def main():
inp, out = sys.argv[1], sys.argv[2]
gltf, chunks = read_glb(inp)
n = repair(gltf)
write_glb(gltf, chunks, out)
print(f"stripped {n} material slot(s) pointing at sourceless textures → {out}")
if __name__ == "__main__":
main()