Skip to content

Commit cceb22e

Browse files
TMHSDigitalclaude
andcommitted
fix(templates): apply rotation as well as scale before grounding the origin
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 ea853a1 commit cceb22e

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
@@ -102,9 +102,16 @@ def scene_units_are_meters(scene):
102102
return abs(units.scale_length - 1.0) < 1e-6
103103

104104

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

109116

110117
def apply_object_transform(obj):
@@ -115,6 +122,7 @@ def apply_object_transform(obj):
115122

116123

117124
def origin_to_base(obj):
125+
# Precondition: rotation and scale applied (local Z == world Z).
118126
mesh = obj.data
119127
n = len(mesh.vertices)
120128
if n == 0:
@@ -356,7 +364,7 @@ def main():
356364
print("Set scene units to metric meters")
357365

358366
hero = max(meshes, key=evaluated_triangle_count)
359-
if not scale_is_identity(hero):
367+
if not rot_scale_is_identity(hero):
360368
apply_object_transform(hero)
361369
print(f"Applied object scale/rotation on {hero.name}")
362370
origin_to_base(hero)

0 commit comments

Comments
 (0)