Skip to content

Commit 8960f3f

Browse files
TMHSDigitalclaude
andauthored
fix(templates): apply rotation as well as scale before grounding the origin (#412)
ai-mesh-cleanup and the pipeline template applied transforms only when scale was non-identity. origin_to_base() shifts geometry along local Z, so an imported glTF node with a rotation and unit scale (common) was grounded along the wrong axis and moved in world space: a cube rotated 90 deg on X at z=5 shifted -1 in Y and Z. The gate is now rot_scale_is_identity(), which checks the whole 3x3 of matrix_basis, so rotation is applied too and local Z equals world Z before grounding. origin_to_base() states the precondition and the skill explains why steps 3 and 4 must run in that order. Closes #389 Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent e1d8310 commit 8960f3f

2 files changed

Lines changed: 28 additions & 10 deletions

File tree

‎skills/ai-mesh-cleanup/SKILL.md‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,16 @@ def scene_units_are_meters(scene):
4141
return abs(units.scale_length - 1.0) < 1e-6
4242

4343

44-
def scale_is_identity(obj, tol=1e-6):
45-
sx, sy, sz = obj.scale
46-
return abs(sx - 1.0) < tol and abs(sy - 1.0) < tol and abs(sz - 1.0) < tol
44+
def rot_scale_is_identity(obj, tol=1e-6):
45+
# Rotation and scale both: origin_to_base() shifts along local Z, which is
46+
# world Z only once rotation is applied. A GLB node often carries a
47+
# rotation with identity scale.
48+
m = obj.matrix_basis.to_3x3()
49+
return all(
50+
abs(m[i][j] - (1.0 if i == j else 0.0)) < tol
51+
for i in range(3)
52+
for j in range(3)
53+
)
4754

4855

4956
def apply_transforms(objs):
@@ -61,6 +68,7 @@ def apply_transforms(objs):
6168

6269

6370
def origin_to_base(obj):
71+
# Precondition: rotation and scale applied (local Z == world Z).
6472
mesh = obj.data
6573
n = len(mesh.vertices)
6674
flat = [0.0] * (n * 3)
@@ -112,18 +120,20 @@ if not scene_units_are_meters(scene):
112120
scene.unit_settings.system = "METRIC"
113121
scene.unit_settings.scale_length = 1.0
114122

115-
apply_transforms([o for o in imported_meshes() if not scale_is_identity(o)])
123+
apply_transforms([o for o in imported_meshes() if not rot_scale_is_identity(o)])
116124
```
117125

118126
`export_apply=True` on glTF applies **modifiers**, not object scale. Unapplied object scale lands on the glTF node. Witness: [`examples/unapplied-scale-gltf/`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/unapplied-scale-gltf).
119127

120128
### 3. Apply transforms
121129

122-
`transform_apply` needs a real object in context. Use `temp_override`, not `bpy.context.copy()`. After apply, `obj.scale` is `(1, 1, 1)` and `obj.data` vertex positions hold the world size.
130+
`transform_apply` needs a real object in context. Use `temp_override`, not `bpy.context.copy()`. After apply, `obj.scale` is `(1, 1, 1)`, rotation is zero, and `obj.data` vertex positions hold the world size and orientation.
131+
132+
Gate the apply on rotation **and** scale (`rot_scale_is_identity`). Imported glTF nodes often carry a rotation with identity scale; skipping the apply then makes step 4 ground the mesh along its local Z, which moves it in world space (a cube rotated 90° on X at z=5 shifted by −1 in Y and Z).
123133

124134
### 4. Set origin
125135

126-
Origin at the lowest Z of the mesh (sit-on-ground) via `foreach_get` / `foreach_set`, not a Python loop on `mesh.vertices`. See [`examples/prop-origin-transform/`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/prop-origin-transform) for origin-to-base plus `matrix_parent_inverse`.
136+
Origin at the lowest Z of the mesh (sit-on-ground) via `foreach_get` / `foreach_set`, not a Python loop on `mesh.vertices`. This works in local space, so run it only after step 3. See [`examples/prop-origin-transform/`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/prop-origin-transform) for origin-to-base plus `matrix_parent_inverse`.
127137

128138
### 5. Recalculate normals
129139

‎templates/ai-asset-pipeline-template/pipeline.py‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,16 @@ def scene_units_are_meters(scene):
103103
return abs(units.scale_length - 1.0) < 1e-6
104104

105105

106-
def scale_is_identity(obj, tol=1e-6):
107-
sx, sy, sz = obj.scale
108-
return abs(sx - 1.0) < tol and abs(sy - 1.0) < tol and abs(sz - 1.0) < tol
106+
def rot_scale_is_identity(obj, tol=1e-6):
107+
# Rotation and scale both: origin_to_base() shifts along local Z, which is
108+
# world Z only once rotation is applied. A GLB node often carries a
109+
# rotation with identity scale.
110+
m = obj.matrix_basis.to_3x3()
111+
return all(
112+
abs(m[i][j] - (1.0 if i == j else 0.0)) < tol
113+
for i in range(3)
114+
for j in range(3)
115+
)
109116

110117

111118
def apply_object_transform(obj):
@@ -116,6 +123,7 @@ def apply_object_transform(obj):
116123

117124

118125
def origin_to_base(obj):
126+
# Precondition: rotation and scale applied (local Z == world Z).
119127
mesh = obj.data
120128
n = len(mesh.vertices)
121129
if n == 0:
@@ -370,7 +378,7 @@ def main():
370378
print("Set scene units to metric meters")
371379

372380
hero = max(meshes, key=evaluated_triangle_count)
373-
if not scale_is_identity(hero):
381+
if not rot_scale_is_identity(hero):
374382
apply_object_transform(hero)
375383
print(f"Applied object scale/rotation on {hero.name}")
376384
origin_to_base(hero)

0 commit comments

Comments
 (0)