glTF Skin Roundtrip

examples/gltf-skin-roundtrip/

A rigged mech scorpion exported to glTF with skins and re-imported, verifying the skinning contract the geometry round-trip left uncovered. Skeleton, weights, and deformation must all survive the format.

Rendered headless by the example itself. Select it to enlarge.

witnesses skins[0].joints names all 21 bones; JOINTS_0/WEIGHTS_0 per primitive with unit sums; every split copy's weights bit-exact on re-import; deformation matches within 6.0e-07. The mesh must be parented to the armature or the exporter binds by name.

tags export armature

runs on 4.5 LTS · 5.1 · 5.2 LTS

blender --background --python examples/gltf-skin-roundtrip/gltf_skin_roundtrip.py --

falsify it Breaks the contract on purpose and must exit 5

blender --background --python examples/gltf-skin-roundtrip/gltf_skin_roundtrip.py -- --no-skins

A runnable example that rigs a mech scorpion — 21 bones: a body, a five-segment tail and stinger, two three-bone claws (arm, hand, movable finger), eight legs; two-bone blends in the rubber boots at every flexing joint — exports it with bpy.ops.export_scene.gltf (export_skins=True), parses the file, re-imports it, and verifies the whole skinning contract against the authored rig, following depsgraph-and-evaluated-data.

Pipeline arc: modeling/LOD in lod-decimate-chain, weighting in vertex-weight-limit, export in gltf-export-roundtrip — this is the skinning counterpart to the crate's geometry round-trip. Tangent frames for the normal maps are in triangulate-tangents.

What it witnesses: the skinned-mesh export contract the geometry round-trip left uncovered.

What each check catches on failure: exporting with export_skins=False (exit 5 — no skin on disk), stripping the weights (exit 4 — no vertex groups), nudging one re-imported weight (exit 18 — 0.99), moving one re-imported bone's rest tail 1 mm (exit 14 — 3.9e-03), and posing the re-imported rig differently (exit 19 — deformation deviates 0.13).

Straddle-safe keys, for real. A float32 round-trip can land a coordinate on the far side of a 1e-4 rounding boundary, so lookups also try the 26 neighbouring keys. Those neighbours must themselves be rounded: 0.3601 + 1e-4 in binary floating point is not the key 0.3602. The first version of this example added the offset without rounding, so only the exact key ever matched. It passed by luck until the redesign lowered the body to z = 0.36, and then it reported a phantom weight loss (exit 18, 1.0).

Version witness: the skins pipeline is stable across Blender 4.5 LTS, 5.1 and 5.2 LTS — every measured value matches to the digit on 4.5.11, 5.1.2 and 5.2.1.

The render is a standoff. On the left, the authored scorpion holds the guard pose the check compares: tail coiled tight over its back, claws tucked, pincers shut. On the right is the actual re-imported mesh, driven through its imported armature into a different pose: the tail rears high to strike, the claws are raised and the pincers gape. A file that lost the skin, the joints, or the weights could not follow that pose. The rubber boots at the joints bend smoothly and the plates stay rigid, which is the linear blend skinning surviving the format. The two poses differ on purpose, so the eye has something to compare. Two identical copies would render the same whether or not the round-trip worked.

Both rigs are posed by rest-space axes in quaternions, and the yaw is composed into each armature's world matrix, because the glTF importer leaves its armature in quaternion mode, where rotation_euler does nothing. Framing is measured by gallery_framing (fill 0.856 x, minimum margin 0.072). The asset floors are measured by gallery_asset_quality (5 materials: hazard paint, gunmetal, chrome, rubber, venom glow; edge90 0.001).

Run#

# Cheap correctness check (no render) — the CI check:
blender --background --python gltf_skin_roundtrip.py --

# Falsifier: export_skins=False. Must exit non-zero.
blender --background --python gltf_skin_roundtrip.py -- --no-skins

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts):
blender --background --python gltf_skin_roundtrip.py -- --output scorp.png
blender --background --python gltf_skin_roundtrip.py -- --output scorp.png --engine cycles

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Exporter RNA missing expected kwargs
4Vertex group count ≠ bone count
5Disk skins ≠ 1 (--no-skins lands here)
6Skin joints ≠ bone names
7Missing JOINTS_0/WEIGHTS_0, or accessor length mismatch
8Disk weight sums off 1.0
9Disk verts exceed evaluated loops
10Gallery framing violation (render path only; gallery_framing)
11Gallery asset-quality violation (render path only; gallery_asset_quality)
12Named bone lost
13Bone parent drifted
14Rest matrices drifted
15Skinned mesh count after import ≠ 1
16Re-import vert count ≠ disk
17Vertex group names drifted
18Weight round-trip drifted
19Deformation round-trip drifted
20--output produced no file
21Armature count after import ≠ 1
22Bone count drifted

On the --output path only, which runs after every check has passed, 10 also means a gallery_framing violation and 11 a gallery_asset_quality floor violation. The shared helpers own those codes, and the log line says which one fired.

The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --no-skins (expects exit 5).

Source

examples/gltf-skin-roundtrip/gltf_skin_roundtrip.py View the script on GitHub →
Two orange-plated mech scorpions face off: the authored one coils its tail with claws tucked; the re-imported one, driven by its imported bones, rears its tail to strike with claws raised.