From 1fe08987a33417ea19902f3238f3ac0e0ed28f4b Mon Sep 17 00:00:00 2001 From: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Date: Sun, 4 Oct 2026 20:05:58 -0400 Subject: [PATCH] fix(snippets): export Y-up, meter glTF for Godot and Unreal too 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 --- CLAUDE.md | 2 +- ROADMAP.md | 2 +- docs/gallery/export-preset-axis/index.html | 32 ++--- docs/gallery/gltf-export-roundtrip/index.html | 4 +- docs/gallery/index.html | 8 +- examples/export-preset-axis/README.md | 66 +++++---- .../export-preset-axis/export_preset_axis.py | 128 +++++++++--------- examples/gallery.json | 6 +- rules/use-correct-axis-rna-per-exporter.mdc | 12 +- skills/engine-export-presets/SKILL.md | 31 ++--- snippets/export_preset_godot.py | 16 ++- snippets/export_preset_unreal.py | 22 ++- .../ai-asset-pipeline-template/README.md | 16 ++- .../ai-asset-pipeline-template/pipeline.py | 43 +----- 14 files changed, 179 insertions(+), 209 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 89e16fd6..9746b3a4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,7 +37,7 @@ VERSION - Source of truth for the repo version | addon-scaffolding | Extensions Platform manifest, file layout, register/unregister symmetry | | ai-mesh-cleanup | Ordered cleanup for imported generated meshes: units, transform apply, origin, normals, budget, collider | | bake-high-to-low | Cycles cage-bake of high-poly detail onto a low-poly target as a tangent-space normal map | -| 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 | +| 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 | | operators | `bpy.types.Operator` lifecycle, `bl_idname`, redo, defensive context handling | | ui-panels | `bpy.types.Panel` declarative `draw()`, layout primitives, conditional UI | | custom-properties | `bpy.props` annotations, PropertyGroup, PointerProperty, storage tradeoffs | diff --git a/ROADMAP.md b/ROADMAP.md index 774b6fbb..e53a4e26 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -98,7 +98,7 @@ Provider-agnostic GLB-in / engine-ready-out. This repo does not generate meshes. - **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. - **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. -- **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`. +- **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`. - **`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/`. - **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. diff --git a/docs/gallery/export-preset-axis/index.html b/docs/gallery/export-preset-axis/index.html index 1a6bb6b6..02c7d27f 100644 --- a/docs/gallery/export-preset-axis/index.html +++ b/docs/gallery/export-preset-axis/index.html @@ -4,7 +4,7 @@ Export Preset Axis (export-preset-axis) — Examples — Blender Developer Tools - + @@ -12,17 +12,17 @@ - + - + - + @@ -45,15 +45,15 @@

Export Preset Axis

examples/export-preset-axis/

-

A radio mast exported under Unity and Godot glTF presets and re-imported, proving the two files have different vertex orientation

+

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

-

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

-
witnesses 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
+
witnesses 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

tags export

blender --background --python examples/export-preset-axis/export_preset_axis.py --
@@ -61,17 +61,17 @@

Export Preset Axis

-

A runnable example that exports the same radio-mast mesh under the Unity and Godot glTF presets from engine-export-presets and re-imports both files. The check is on coordinates, not a screenshot: Unity (export_yup=True) stands; Godot (export_yup=False) lies.

-

What it witnesses: named engine presets are not comments on the same kwargs. glTF axis RNA is export_yup, not FBX axis_forward / axis_up.

-
  • Disk POSITION follows the closed form. Unity bakes (x, y, z) -> (x, z, -y) with no node rotation. Godot writes raw Z-up (x, y, z). The check reads accessor min/max from the .gltf JSON.
  • Re-import proves the conversion. Blender's importer always treats the file as Y-up: blender = (gltf.x, -gltf.z, gltf.y). Unity restores the source. Godot permutes again, so the mast lies along -Y.
  • The two reimports differ. Unity z_span matches source height; Godot y_span matches that height. --same-axis exports both with export_yup=True; both stand and the differ check exits 9.
  • Exporter RNA is guarded. Every kwarg passed must still exist on bpy.ops.export_scene.gltf.
-

Neighbor of gltf-export-roundtrip (Y-up bake vs Z-up on disk for one file) and unapplied-scale-gltf (export_apply is modifiers, not object scale). This example names the Unity vs Godot presets and asserts the re-imported orientations diverge.

+

A runnable example that exports the same radio-mast mesh twice: once with the engine glTF preset from engine-export-presets (export_yup=True, the call Unity, Godot and Unreal all share), and once naively with export_yup=False. It re-imports both files. The check is on coordinates, not a screenshot: the preset copy stands; the Z-up copy lies.

+

What it witnesses: glTF is +Y up by spec, so export_yup=False is not an engine preset, it is a bug every engine shows the same way. glTF axis RNA is export_yup, not FBX axis_forward / axis_up.

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

Neighbor of gltf-export-roundtrip (Y-up bake vs Z-up on disk for one file) and unapplied-scale-gltf (export_apply is modifiers, not object scale). Until 0.143.x this example called the Z-up export the "Godot preset". That was wrong: Godot is Y-up and imports glTF per the spec, so the lying mast is what a Godot user would have seen (#346).

The subject is a radio mast chosen because "up" is unmistakable on it: a stepped concrete footing, a bolted base flange, a tapered mast in red and white aviation bands with steel collars, three sector panel antennas, a shrouded microwave dish on a raked arm, an equipment cabinet, and a red obstruction lamp under a lightning rod. The rod's point is the witnessed tip vertex.

-

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

+

The still stages the two re-imports side by side: the preset copy standing on the left, the Z-up copy lying along the floor on the right. Beside each sits a modelled X/Y/Z axis gizmo (red, green, blue) showing that re-import's frame. The gizmo is measured, not placed: the render path tries all 24 axis-aligned rotations and keeps the one that maps the source vertices onto the re-imported vertices (worst nearest-vertex distance, printed as fit_err; exit 12 if it exceeds 1 mm). The preset gizmo comes out as the identity, blue Z up. The Z-up gizmo comes out as Y -> +Z, Z -> -Y: green Y points up and blue Z runs along the lying mast. The mast didn't fall over. The Z-up file stores Z-up coordinates, and the importer reads them as Y-up, as every glTF consumer does.

Run#

# Cheap correctness check (no render) - the CI check:
 blender --background --python export_preset_axis.py --
 
-# Falsifier: both presets Y-up. Must exit non-zero.
+# Falsifier: both exports Y-up. Must exit non-zero.
 blender --background --python export_preset_axis.py -- --same-axis
 
 # Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts):
@@ -79,7 +79,7 @@ 

Run blender --background --python export_preset_axis.py -- --output mast.png --engine cycles

Exit codes#

Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper and 11 on the render path is the shared asset-quality helper.

-
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage; also exporter RNA missing expected glTF kwargs
3Source mast is not Z-dominant, or tip drifted
4glTF reimport produced no mesh
5Unity disk POSITION is not (x, z, -y), or Unity node has rotation
6Godot disk POSITION is not raw Z-up; also --output produced no file
7Unity reimport is not standing
8Godot reimport is not lying along Y, or reimported tip mismatch
9Reimported orientations did not differ (--same-axis lands here)
10Gallery framing violation
11--same-axis did not collapse the axis difference; also asset-quality floor violation (render path)
12Render path: no axis-aligned rotation maps the source onto a re-import, so the gizmo frame could not be measured
+
CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage; also exporter RNA missing expected glTF kwargs
3Source mast is not Z-dominant, or tip drifted
4glTF reimport produced no mesh
5Preset disk POSITION is not (x, z, -y), or preset node has rotation
6Z-up disk POSITION is not raw Z-up; also --output produced no file
7Preset reimport is not standing
8Z-up reimport is not lying along Y, or reimported tip mismatch
9Reimported orientations did not differ (--same-axis lands here)
10Gallery framing violation
11--same-axis did not collapse the axis difference; also asset-quality floor violation (render path)
12Render path: no axis-aligned rotation maps the source onto a re-import, so the gizmo frame could not be measured

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

@@ -125,7 +125,7 @@

glTF Skin Roundtrip - 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. + 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.