singe/util/packGlb.py
2026-09-10 19:03:29 -05:00

42 lines
2 KiB
Python

# Packs a .gltf with external buffers and images into one self-contained .glb, which is the only
# model format Singe loads. Usage: python3 util/packGlb.py Model.gltf Model.glb
import json, os, struct, sys
src, out = sys.argv[1], sys.argv[2]
base = os.path.dirname(src)
g = json.load(open(src))
binary = bytearray()
views = g.setdefault('bufferViews', [])
def add(data):
while len(binary) % 4: binary.append(0)
off = len(binary); binary.extend(data)
return off
# Buffers: concatenate, remapping views.
offsets = []
for b in g.get('buffers', []):
data = open(os.path.join(base, b['uri']), 'rb').read()
offsets.append(add(data))
for v in views:
v['byteOffset'] = v.get('byteOffset', 0) + offsets[v.get('buffer', 0)]
v['buffer'] = 0
# Images: each becomes a buffer view with a mime type. The table is exhaustive on purpose: a
# two-way png/jpeg guess quietly labelled every .ktx2 as image/jpeg, which Singe survives because it
# sniffs the magic number, and which no other glTF reader has to.
mimeTypes = {'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'ktx2': 'image/ktx2', 'webp': 'image/webp', 'basis': 'image/basis'}
for img in g.get('images', []):
uri = img.pop('uri')
data = open(os.path.join(base, uri), 'rb').read()
ext = uri.lower().rsplit('.', 1)[-1]
if ext not in mimeTypes:
sys.exit('No mime type known for ' + uri + '; add it to mimeTypes in util/packGlb.py.')
views.append({'buffer': 0, 'byteOffset': add(data), 'byteLength': len(data)})
img['bufferView'] = len(views) - 1
img['mimeType'] = mimeTypes[ext]
while len(binary) % 4: binary.append(0)
g['buffers'] = [{'byteLength': len(binary)}]
js = json.dumps(g, separators=(',', ':')).encode()
while len(js) % 4: js += b' '
with open(out, 'wb') as f:
f.write(struct.pack('<4sII', b'glTF', 2, 12 + 8 + len(js) + 8 + len(binary)))
f.write(struct.pack('<II', len(js), 0x4E4F534A)); f.write(js)
f.write(struct.pack('<II', len(binary), 0x004E4942)); f.write(binary)
print(out, os.path.getsize(out), 'bytes')