glTF Export Roundtrip

examples/gltf-export-roundtrip/

A sci-fi supply crate exported to glTF and re-imported, verifying the round-trip against the depsgraph-evaluated mesh within float tolerances. Positions, loop normals, box-mapped UVs, and per-triangle material bindings must all survive; the on-disk JSON proves the +Y-up conversion and the V-flipped UV layout.

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

witnesses export_yup bakes (x, y, z) -> (x, z, -y) into the vertex data with no node rotation (probed identical on 4.5.11 and 5.1.2); export_apply ships the evaluated mesh, not the base cage; exporter/importer RNA is guarded so a future kwarg rename fails loudly.

tags export uv

runs on 4.5 LTS · 5.1 · 5.2 LTS

blender --background --python examples/gltf-export-roundtrip/gltf_export_roundtrip.py --

falsify it Breaks the contract on purpose and must exit 9

blender --background --python examples/gltf-export-roundtrip/gltf_export_roundtrip.py -- --no-yup

A runnable example that builds a sci-fi supply crate — 43 beveled box shells (body, lid with stiffening ribs, corner armour, carry handles over vent slats, latches, rivets, a status plate with glowing pips) with three material slots and box-mapped UVs — exports it with bpy.ops.export_scene.gltf, parses the file on disk, re-imports it, and verifies the whole round-trip against the depsgraph-evaluated mesh, following depsgraph-and-evaluated-data and headless-batch-scripting.

Pipeline arc: modeling/LOD in lod-decimate-chain, weighting in vertex-weight-limit, export here.

What it witnesses: the interchange contracts AI-generated export code most often gets silently wrong.

Version witness (probed on Blender 4.5.11 LTS and 5.1.2): the operator signatures are byte-identical — 109 exporter properties, 20 importer properties, same defaults — and the exported JSON differs only in asset.generator ("Khronos glTF Blender I/O v4.5.51" vs "v5.1.20"). The example therefore runs identical kwargs on both versions and guards forward drift explicitly: every kwarg it passes must still exist in the operator's RNA, so a future rename fails loudly instead of drifting silently. One genuine 5.x removal surfaced during authoring: Mesh.calc_normals() is gone (loop normals auto-compute on read) — calling it is itself a cross-version hazard, noted in the code.

Two more authoring hazards are pinned in comments: exact face-plane coincidences between kit-bashed shells weld loops on export (the count check catches it), and read_factory_settings mid-check frees the original mesh — touching a freed RNA raises ReferenceError, so counts are captured before the wipe.

The render shows one crate. The solid crate is the authored one, rebuilt in place after the check. Laid over it is the actual re-imported mesh — the file's own triangles — drawn as a thin amber wire cage (a render-only Weld + Wireframe display stack on the imported object) and masked to the right of a vertical cut, so the left half reads as the authored asset and the right half as the file's geometry sitting on it. Neither object is moved: both keep the identity transform check 13 proved, and the camera does the turning. The cage lands on every bevel, rivet and vent slat only because the importer undid the +Y-up bake exactly and the file carried the evaluated (beveled) mesh. If the axis conversion broke, the cage would be rotated a quarter turn off the solid crate; a probe that re-applies that quarter turn to the imported object before rendering drops the cage through the floor and also fails the framing gate (bottom margin 0.000). If the modifier contract broke, the cage would trace square corners across the rounded ones.

Presentation only, after every check has run: the authored crate's bevel is baked into its display mesh (the asset-quality edge measure reads Object.data, which is still the square cage while the bevel lives in the modifier stack), and its paint and trim get a noise-mottled base colour and roughness. The exported file only ever carried the flat Principled values.

The render path gates framing through gallery_framing.check_framing and the asset floors through gallery_asset_quality.check_asset_quality on the authored crate. Both return their reserved codes (10, 11) directly; the POSITION-count and UV V-flip checks use 22 and 23.

Run#

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

# Falsifier: export_yup=False. Must exit non-zero (bbox is Z-up on disk).
blender --background --python gltf_export_roundtrip.py -- --no-yup

# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts):
blender --background --python gltf_export_roundtrip.py -- --output crate.png
blender --background --python gltf_export_roundtrip.py -- --output crate.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/importer RNA kwargs drifted
4Base cage drifted from its closed form
5Authored UVs drifted from the box-map closed form
6Bevel produced no evaluated geometry
7On-disk node/mesh/generator contract drifted
8On-disk primitive/material binding count drifted
9On-disk POSITION bounds ≠ axis-converted bbox (--no-yup lands here)
10Gallery framing violation (render path only; gallery_framing)
11Gallery asset-quality violation (render path only; gallery_asset_quality)
12Re-import did not produce exactly one mesh
13Re-imported object carries a transform
14Material names drifted on re-import
15Re-import vert count ≠ evaluated loop count
16Round-trip position drift
17Round-trip normal drift
18Round-trip UV drift
19Re-import triangle count drifted
20Per-triangle material bindings drifted
21--output produced no file
22On-disk POSITION count ≠ evaluated loop count
23On-disk UV V-flip failed

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-yup (expects exit 9).

Source

examples/gltf-export-roundtrip/gltf_export_roundtrip.py View the script on GitHub →
One olive sci-fi supply crate with a cyan light strip, its right half traced by an amber wire cage of the re-imported glTF triangles that lands exactly on every bevel, rivet and vent slat.