We ship 3D product assets for e-commerce (GLB models that end up in AR Quick Look, <model-viewer>, and Sketchfab). Last week we ran our own delivery gate over one of our own catalog models — a headphone we had already sold — and the report said:
GLBcheck — model_lite.glb
structure : 40 root(s), depth 1, 40 meshes, 40 primitives
geometry : 13540 tris / 10242 verts, span 134.028 units
textures : 24 images, 25.17 MP total
! [WARN] structure/multi-root: 40 root nodes
! [WARN] geometry/scale: model spans ~134.03 units - AR Quick Look expects real-world meters
verdict : PASS_WITH_WARNINGS (0 fails, 2 warnings)
Span 134 units. AR Quick Look renders 1 unit as 1 meter. That is a headphone the size of a twelve-story building appearing in a customer's living room, and the only way they can fix it is pinching for thirty seconds. Desktop viewers never show this problem — they auto-frame. Only real-scale runtimes punish it.
That one report is a good map of the whole class of bugs that pass on desktop and explode on phones. Here are the three that bit us hardest, and the checks that catch each one in ~20 lines of stdlib Python.
1. Real scale: nobody agrees on what a unit is
glTF says 1 unit = 1 meter. DCC tools say "whatever looks right." So the fix belongs in your delivery gate, not your artist's intuition. You already have the numbers: POSITION accessors carry min/max (and viewers use them for framing — if they're missing, add that as its own finding).
# scan mesh primitives; track the largest absolute coordinate
for mesh in doc.get("meshes", []):
for prim in mesh.get("primitives", []):
pos = prim["attributes"].get("POSITION")
acc = accessors[pos]
span = max(abs(v) for v in acc.get("max", []))
if span < 0.01 or span > 100: # cm-class model or building-class model
warn("geometry/scale", f"model spans ~{span:.2f} units; AR expects meters")
A jewelry model at 0.004 units and a furniture model at 134 units are the same bug in opposite directions.
2. mimeType is a claim; bytes are the truth
This one hard-crashes some mobile viewers while every desktop tool passes it silently:
{ "bufferView": 0, "mimeType": "image/jpeg" } // ...but the bytes are PNG
The GLB container gives you everything you need to check it without any image library. Images either live in the BIN chunk (via a bufferView) or inline as data URIs. Read the bytes, read the header:
def real_magic(blob):
if blob[:8] == b"\x89PNG\r\n\x1a\n": return "image/png"
if blob[:2] == b"\xff\xd8": return "image/jpeg"
return None
Claim vs reality mismatch → FAIL, not warn. A viewer that trusts mimeType to pick a decoder will feed PNG bytes to its JPEG path. While you're in there, parse IHDR (bytes 16–24) or the JPEG SOF marker for dimensions — you get texture resolution and a total-megapixels budget for free, which matters because 25 MP of textures on a 8 MB model is its own conversation.
3. The container lies too
A .glb is a tiny container format: 12-byte header (glTF, version, total length), then chunks (JSON first, optional BIN after). Every field is checkable:
-
versionmust be 2, andasset.versionmust be"2.0" - header length must equal the actual file size (truncation is a classic FTP/CDN artifact)
- first chunk type must be
JSON, length 4-byte aligned, space-padded -
extensionsUsedtells you what the file demands — and AR Quick Look will not load Draco or meshopt-compressed meshes. That's a FAIL in delivery-strict mode, not a warning.
magic, version, length = struct.unpack_from("<III", data, 0)
assert magic == 0x46546C67 and version == 2
if length != len(data):
fail("container/length", f"header says {length}, file is {len(data)}")
Why this belongs in CI, not in a viewer
Desktop viewers are forgiving because they want to render something. Your delivery gate is allowed to be rude. The whole auditor is ~330 lines of stdlib Python (container, structure, geometry, materials, textures, extensions), exit codes 0/1/2 so CI can gate on it:
python glbcheck.py model.glb --json --strict && upload_to_store.sh
If you'd rather not write it, that auditor is exactly what GLBcheck is — $29, includes the 14-check self-test that builds its own GLB fixtures with struct (mime spoof, truncation, bad magic, multi-root, Draco strict), so you can see every rule fire. It found the 134-meter headphone in our own catalog; your pipeline probably has one too.
The deeper point: the bugs that cost you customers are the ones the desktop can't see. Audit for the runtime your customer actually uses.
Top comments (0)