- 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>
79 lines
2.7 KiB
Python
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()
|