Skip to content

Commit 74813ec

Browse files
TMHSDigitalclaude
andauthored
fix(snippets): export Y-up, meter glTF for Godot and Unreal too (#417)
The Godot preset exported Z-up glTF (export_yup=False) and the Unreal glTF preset baked a 100x scale. Both contradict the glTF 2.0 spec (+Y up, meters) and the engines: Godot is Y-up and imports glTF per the spec, so every Godot asset arrived rotated -90 deg about X; Unreal's glTF importer already converts meters to centimeters (Interchange import_scale defaults to 100), so a baked file landed 100x too large. The Z-up choice was justified in the skill only as keeping the axis test from being "untestable". All three engine glTF presets are now the same spec-compliant export (export_yup=True, meters). Engine differences are documented where they really live: Godot's node-name import hints (-convcolonly, -colonly, -rigid) and Unreal's FBX axis_forward/axis_up/global_scale, which is unchanged. The pipeline template's --preset now selects the same export for every engine. examples/export-preset-axis keeps its checks and still image but stops calling the lying mast "Godot": it now contrasts the engine preset with a naive export_yup=False file, which is what any glTF engine would show. Closes #346 Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 6dfa75c commit 74813ec

14 files changed

Lines changed: 179 additions & 209 deletions

File tree

‎CLAUDE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ VERSION - Source of truth for the repo version
3737
| addon-scaffolding | Extensions Platform manifest, file layout, register/unregister symmetry |
3838
| ai-mesh-cleanup | Ordered cleanup for imported generated meshes: units, transform apply, origin, normals, budget, collider |
3939
| bake-high-to-low | Cycles cage-bake of high-poly detail onto a low-poly target as a tangent-space normal map |
40-
| engine-export-presets | Unity Y-up, Godot Z-up, and Unreal centimeter glTF/FBX presets; glTF uses export_yup, FBX uses axis_forward/axis_up |
40+
| engine-export-presets | Unity, Godot and Unreal presets: glTF is +Y up and meters for every engine (export_yup=True; Unreal converts to cm on import), FBX uses axis_forward/axis_up plus centimeter scale |
4141
| operators | `bpy.types.Operator` lifecycle, `bl_idname`, redo, defensive context handling |
4242
| ui-panels | `bpy.types.Panel` declarative `draw()`, layout primitives, conditional UI |
4343
| custom-properties | `bpy.props` annotations, PropertyGroup, PointerProperty, storage tradeoffs |

‎ROADMAP.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ Provider-agnostic GLB-in / engine-ready-out. This repo does not generate meshes.
9898

9999
- **Post-generation cleanup skills.** Import and unit-scale normalization, transform apply and origin, poly-budget decimate, LOD chain, collision mesh. Phase 1 shipped in v0.54.0 as `ai-mesh-cleanup`, four snippets, two rules.
100100
- **High-to-low normal bake.** **Delivered.** Skill `bake-high-to-low`, snippets `setup_bake_target_image.py` / `bake_normal_high_to_low.py` / `save_baked_image.py`, witness `examples/bake-normal-high-to-low/`. Cycles CPU cage bake; statistical gates, not byte-identity. UV transfer and atlas packing remain a later phase. Template integration remains a later phase.
101-
- **Engine export presets.** **Delivered.** Unity (Y-up glTF), Godot (Z-up glTF, meters), Unreal (centimeter glTF bake and FBX `global_scale`). Skill `engine-export-presets`, three snippets, rule `use-correct-axis-rna-per-exporter`, witness `examples/export-preset-axis/`. Draco remains opt-in via `gltf_draco_export.py`.
101+
- **Engine export presets.** **Delivered.** Unity, Godot and Unreal glTF are one spec-compliant export (+Y up, meters; Unreal converts to cm on import, corrected in #346), and Unreal FBX carries `axis_forward`/`axis_up` plus `global_scale`. Skill `engine-export-presets`, three snippets, rule `use-correct-axis-rna-per-exporter`, witness `examples/export-preset-axis/`. Draco remains opt-in via `gltf_draco_export.py`.
102102
- **`ai-asset-pipeline-template/`.** **Delivered.** Third template. Headless: GLB path in; LOD set, convex or box collider, engine-preset export; explicit CI exit codes. Pattern: `templates/headless-batch-script-template/`.
103103
- **Live-session agent bridge.** Research spike, not a committed deliverable. MCP server or socket listener so an agent can execute against a running Blender instance instead of blind `--background` scripts. Built on `templates/extension-addon-template/`. Needs its own design pass. Unpinned.
104104

‎docs/gallery/export-preset-axis/index.html‎

Lines changed: 16 additions & 16 deletions
Large diffs are not rendered by default.

‎docs/gallery/gltf-export-roundtrip/index.html‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,11 @@ <h3><a class="stretch" href="../uv-layer-grid/">UV Layer Grid</a></h3>
115115
</article>
116116
<article class="card mini">
117117
<div class="card-media">
118-
<img src="../assets/export-preset-axis-hero-640.webp" srcset="../assets/export-preset-axis-hero-640.webp 640w, ../assets/export-preset-axis-hero.webp 1280w" sizes="(min-width: 560px) 340px, 100vw" alt="Two re-imports of a red-and-white radio mast with axis gizmos: the Unity copy stands with blue Z up, the Godot copy lies on the floor with green Y up and blue Z along the mast." width="1280" height="720" loading="lazy" decoding="async" />
118+
<img src="../assets/export-preset-axis-hero-640.webp" srcset="../assets/export-preset-axis-hero-640.webp 640w, ../assets/export-preset-axis-hero.webp 1280w" sizes="(min-width: 560px) 340px, 100vw" alt="Two re-imports of a red-and-white radio mast with axis gizmos: the engine-preset copy stands with blue Z up, the naive Z-up copy lies on the floor with green Y up and blue Z along the mast." width="1280" height="720" loading="lazy" decoding="async" />
119119
</div>
120120
<div class="card-body">
121121
<h3><a class="stretch" href="../export-preset-axis/">Export Preset Axis</a></h3>
122-
<p>A radio mast exported under Unity and Godot glTF presets and re-imported, proving the two files have different vertex orientation</p>
122+
<p>A radio mast exported once with the engine glTF preset (export_yup=True, shared by Unity, Godot and Unreal) and once naively Z-up, then re-imported: the Z-up file lies on its back</p>
123123
</div>
124124
</article>
125125
</div>

‎docs/gallery/index.html‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -449,15 +449,15 @@ <h2><a class="stretch" href="gltf-export-roundtrip/">glTF Export Roundtrip</a></
449449
<span class="card-link" aria-hidden="true">View example &rarr;</span>
450450
</div>
451451
</article>
452-
<article class="card" data-tags="export" data-kind="examples" data-name="export-preset-axis" data-search="export preset axis export-preset-axis a radio mast exported under unity and godot gltf presets and re-imported, proving the two files have different vertex orientation export ">
452+
<article class="card" data-tags="export" data-kind="examples" data-name="export-preset-axis" data-search="export preset axis export-preset-axis a radio mast exported once with the engine gltf preset (export_yup=true, shared by unity, godot and unreal) and once naively z-up, then re-imported: the z-up file lies on its back export ">
453453
<div class="card-media">
454-
<img src="assets/export-preset-axis-hero-640.webp" srcset="assets/export-preset-axis-hero-640.webp 640w, assets/export-preset-axis-hero.webp 1280w" sizes="(min-width: 900px) 340px, (min-width: 560px) 50vw, 100vw" alt="Two re-imports of a red-and-white radio mast with axis gizmos: the Unity copy stands with blue Z up, the Godot copy lies on the floor with green Y up and blue Z along the mast." width="1280" height="720" loading="lazy" decoding="async" />
454+
<img src="assets/export-preset-axis-hero-640.webp" srcset="assets/export-preset-axis-hero-640.webp 640w, assets/export-preset-axis-hero.webp 1280w" sizes="(min-width: 900px) 340px, (min-width: 560px) 50vw, 100vw" alt="Two re-imports of a red-and-white radio mast with axis gizmos: the engine-preset copy stands with blue Z up, the naive Z-up copy lies on the floor with green Y up and blue Z along the mast." width="1280" height="720" loading="lazy" decoding="async" />
455455
</div>
456456
<div class="card-body">
457457
<h2><a class="stretch" href="export-preset-axis/">Export Preset Axis</a></h2>
458458
<p class="slug">export-preset-axis</p>
459-
<p class="teaches">A radio mast exported under Unity and Godot glTF presets and re-imported, proving the two files have different vertex orientation</p>
460-
<p class="witnesses"><span class="tag">witnesses</span> Unity export_yup=True round-trips standing (x,y,z); Godot export_yup=False reimports lying along -Y as (x,-z,y); --same-axis exits 9; exporter RNA is guarded so a future kwarg rename fails loudly</p>
459+
<p class="teaches">A radio mast exported once with the engine glTF preset (export_yup=True, shared by Unity, Godot and Unreal) and once naively Z-up, then re-imported: the Z-up file lies on its back</p>
460+
<p class="witnesses"><span class="tag">witnesses</span> The engine preset (export_yup=True) round-trips standing (x,y,z); a naive export_yup=False file reimports lying along -Y as (x,-z,y), as it would in any glTF engine; --same-axis exits 9; exporter RNA is guarded so a future kwarg rename fails loudly</p>
461461
<span class="card-link" aria-hidden="true">View example &rarr;</span>
462462
</div>
463463
</article>

‎examples/export-preset-axis/README.md‎

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,37 @@
11
# Export Preset Axis
22

3-
A runnable example that exports the same radio-mast mesh under the Unity
4-
and Godot glTF presets from
5-
[`engine-export-presets`](../../skills/engine-export-presets/SKILL.md) and
6-
re-imports both files. The check is on coordinates, not a screenshot: Unity
7-
(`export_yup=True`) stands; Godot (`export_yup=False`) lies.
3+
A runnable example that exports the same radio-mast mesh twice: once with
4+
the engine glTF preset from
5+
[`engine-export-presets`](../../skills/engine-export-presets/SKILL.md)
6+
(`export_yup=True`, the call Unity, Godot and Unreal all share), and once
7+
naively with `export_yup=False`. It re-imports both files. The check is on
8+
coordinates, not a screenshot: the preset copy stands; the Z-up copy lies.
89

9-
**What it witnesses:** named engine presets are not comments on the same
10-
kwargs. glTF axis RNA is `export_yup`, not FBX `axis_forward` / `axis_up`.
10+
**What it witnesses:** glTF is +Y up by spec, so `export_yup=False` is not
11+
an engine preset, it is a bug every engine shows the same way. glTF axis RNA
12+
is `export_yup`, not FBX `axis_forward` / `axis_up`.
1113

12-
- **Disk POSITION follows the closed form.** Unity bakes
13-
`(x, y, z) -> (x, z, -y)` with no node rotation. Godot writes raw Z-up
14-
`(x, y, z)`. The check reads accessor min/max from the `.gltf` JSON.
15-
- **Re-import proves the conversion.** Blender's importer always treats the
16-
file as Y-up: `blender = (gltf.x, -gltf.z, gltf.y)`. Unity restores the
17-
source. Godot permutes again, so the mast lies along `-Y`.
18-
- **The two reimports differ.** Unity `z_span` matches source height; Godot
19-
`y_span` matches that height. `--same-axis` exports both with
20-
`export_yup=True`; both stand and the differ check exits 9.
14+
- **Disk POSITION follows the closed form.** The preset bakes
15+
`(x, y, z) -> (x, z, -y)` with no node rotation. The naive export writes
16+
raw Z-up `(x, y, z)`. The check reads accessor min/max from the `.gltf`
17+
JSON.
18+
- **Re-import proves the conversion.** Blender's importer, like Unity's,
19+
Godot's and Unreal's, treats the file as Y-up:
20+
`blender = (gltf.x, -gltf.z, gltf.y)`. The preset restores the source. The
21+
Z-up file permutes again, so the mast lies along `-Y`.
22+
- **The two reimports differ.** The preset's `z_span` matches source
23+
height; the Z-up file's `y_span` matches that height. `--same-axis`
24+
exports both with `export_yup=True`; both stand and the differ check
25+
exits 9.
2126
- **Exporter RNA is guarded.** Every kwarg passed must still exist on
2227
`bpy.ops.export_scene.gltf`.
2328

2429
Neighbor of [`gltf-export-roundtrip`](../gltf-export-roundtrip/) (Y-up bake
2530
vs Z-up on disk for one file) and [`unapplied-scale-gltf`](../unapplied-scale-gltf/)
26-
(`export_apply` is modifiers, not object scale). This example names the
27-
Unity vs Godot presets and asserts the re-imported orientations diverge.
31+
(`export_apply` is modifiers, not object scale). Until 0.143.x this example
32+
called the Z-up export the "Godot preset". That was wrong: Godot is Y-up and
33+
imports glTF per the spec, so the lying mast is what a Godot user would have
34+
seen (#346).
2835

2936
The subject is a radio mast chosen because "up" is unmistakable on it: a
3037
stepped concrete footing, a bolted base flange, a tapered mast in red and
@@ -33,26 +40,25 @@ shrouded microwave dish on a raked arm, an equipment cabinet, and a red
3340
obstruction lamp under a lightning rod. The rod's point is the witnessed tip
3441
vertex.
3542

36-
The still stages the two re-imports side by side: the Unity copy standing on
37-
the left, the Godot copy lying along the floor on the right. Beside each sits
43+
The still stages the two re-imports side by side: the preset copy standing on
44+
the left, the Z-up copy lying along the floor on the right. Beside each sits
3845
a modelled X/Y/Z axis gizmo (red, green, blue) showing that re-import's
3946
frame. The gizmo is measured, not placed: the render path tries all 24
4047
axis-aligned rotations and keeps the one that maps the source vertices onto
4148
the re-imported vertices (worst nearest-vertex distance, printed as
42-
`fit_err`; exit 12 if it exceeds 1 mm). The Unity gizmo comes out as the
43-
identity, blue Z up. The Godot gizmo comes out as `Y -> +Z, Z -> -Y`: green
49+
`fit_err`; exit 12 if it exceeds 1 mm). The preset gizmo comes out as the
50+
identity, blue Z up. The Z-up gizmo comes out as `Y -> +Z, Z -> -Y`: green
4451
Y points up and blue Z runs along the lying mast. The mast didn't fall over.
45-
The Godot file stores Z-up coordinates, and the importer reads them as
46-
Y-up. If `export_yup` were the same on both, the two copies and both gizmos
47-
would match.
52+
The Z-up file stores Z-up coordinates, and the importer reads them as
53+
Y-up, as every glTF consumer does.
4854

4955
## Run
5056

5157
```bash
5258
# Cheap correctness check (no render) - the CI check:
5359
blender --background --python export_preset_axis.py --
5460

55-
# Falsifier: both presets Y-up. Must exit non-zero.
61+
# Falsifier: both exports Y-up. Must exit non-zero.
5662
blender --background --python export_preset_axis.py -- --same-axis
5763

5864
# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts):
@@ -73,10 +79,10 @@ the shared asset-quality helper.
7379
| 2 | argparse / usage; also exporter RNA missing expected glTF kwargs |
7480
| 3 | Source mast is not Z-dominant, or tip drifted |
7581
| 4 | glTF reimport produced no mesh |
76-
| 5 | Unity disk POSITION is not `(x, z, -y)`, or Unity node has rotation |
77-
| 6 | Godot disk POSITION is not raw Z-up; also `--output` produced no file |
78-
| 7 | Unity reimport is not standing |
79-
| 8 | Godot reimport is not lying along Y, or reimported tip mismatch |
82+
| 5 | Preset disk POSITION is not `(x, z, -y)`, or preset node has rotation |
83+
| 6 | Z-up disk POSITION is not raw Z-up; also `--output` produced no file |
84+
| 7 | Preset reimport is not standing |
85+
| 8 | Z-up reimport is not lying along Y, or reimported tip mismatch |
8086
| 9 | Reimported orientations did not differ (`--same-axis` lands here) |
8187
| 10 | Gallery framing violation |
8288
| 11 | `--same-axis` did not collapse the axis difference; also asset-quality floor violation (render path) |

0 commit comments

Comments
 (0)