You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit f59ff3f
Browse filesBrowse the repository at this point in the historyBrowse files
fix(templates): keep every mesh, handle shared and parented imports, bake modifier stacks in order (#477)
The pipeline template exported only the largest mesh with exit 0. Its unit
check could never fire, and LOD budgets were printed but never checked.
Shared (instanced) mesh data made transform_apply raise. A child of a rotated
glTF root passed the matrix_basis identity test and was grounded along a local
axis. The headless template's non-first modifier_apply reversed the stack
order, contrary to its README.
The pipeline now isolates mesh data, unparents keeping world placement,
applies transforms and joins every part. It checks the asset's extent (exit 8)
and each LOD budget (exit 9), and exits 12 when the apply or join raises.
The cleanup and export-preset skills and the three preset snippets get the
same isolation and unparent guards. The headless template bakes the whole
stack through new_from_object. check_example_rules now scans templates/ and
flags object operators reached from a loop. Smoke covers the shared-mesh
parented GLB, the modifier order, and the out-of-range extent.
Signed-off-by: TMHSDigital <tmhospitalitystrategies@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
# glTF instancing imports as several objects sharing one Mesh (users > 1).
46
+
# transform_apply refuses multi-user data, and origin_to_base() on shared
47
+
# data moves every other instance. One copy per object; the last user
48
+
# keeps the original.
49
+
for obj in objs:
50
+
if obj.data.users >1:
51
+
obj.data = obj.data.copy()
52
+
53
+
54
+
defclear_parent_keep_transform(objs):
55
+
# glTF node trees import meshes under a root empty that is often rotated
56
+
# or scaled. Applying the child's own rotation leaves the root's in
57
+
# matrix_world, so local Z is still not world Z.
58
+
for obj in objs:
59
+
if obj.parent isnotNone:
60
+
world = obj.matrix_world.copy()
61
+
obj.parent =None
62
+
obj.matrix_world = world
63
+
bpy.context.view_layer.update()
64
+
65
+
44
66
defrot_scale_is_identity(obj, tol=1e-6):
45
67
# Rotation and scale both: origin_to_base() shifts along local Z, which is
46
68
# 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()
69
+
# rotation with identity scale. Read matrix_world, not matrix_basis: a
70
+
# child of a rotated root has an identity basis.
71
+
m = obj.matrix_world.to_3x3()
49
72
returnall(
50
73
abs(m[i][j] - (1.0if i == j else0.0)) < tol
51
74
for i inrange(3)
@@ -68,7 +91,8 @@ def apply_transforms(objs):
68
91
69
92
70
93
deforigin_to_base(obj):
71
-
# Precondition: rotation and scale applied (local Z == world Z).
94
+
# Precondition: unparented, rotation and scale applied (local Z == world
95
+
# Z), and single-user data (isolate_mesh_data), or other instances move.
72
96
mesh = obj.data
73
97
n =len(mesh.vertices)
74
98
flat = [0.0] * (n *3)
@@ -120,9 +144,14 @@ if not scene_units_are_meters(scene):
120
144
scene.unit_settings.system ="METRIC"
121
145
scene.unit_settings.scale_length =1.0
122
146
123
-
apply_transforms([o for o in imported_meshes() ifnot rot_scale_is_identity(o)])
147
+
meshes = imported_meshes()
148
+
isolate_mesh_data(meshes)
149
+
clear_parent_keep_transform(meshes)
150
+
apply_transforms([o for o in meshes ifnot rot_scale_is_identity(o)])
124
151
```
125
152
153
+
The scene check above only catches a scene someone changed. A fresh or factory scene is always metric at 1.0, so it cannot see a prop written in centimeters as meters. Also check the geometry: after the apply, the largest bounding-box extent of a prop should be within a plausible range (the pipeline template uses 0.01 m to 100 m and exits non-zero outside it).
154
+
126
155
`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).
127
156
128
157
### 3. Apply transforms
@@ -131,6 +160,11 @@ apply_transforms([o for o in imported_meshes() if not rot_scale_is_identity(o)])
131
160
132
161
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).
133
162
163
+
Two import shapes break a naive apply, and both are ordinary glTF:
164
+
165
+
-**Shared mesh data.** Instanced nodes import as several objects on one Mesh (`users == 2` after a round trip). `transform_apply` raises `RuntimeError: Cannot apply to a multi user` on both 4.5.11 and 5.2.1. Run `isolate_mesh_data` first. `transform_apply(isolate_users=True)` also exists on both lines (measured on 4.5.11 and 5.2.1), but it only helps the apply. `origin_to_base` rewrites vertices and still needs single-user data.
166
+
-**Parented meshes.** A child of a rotated or scaled root has an identity `matrix_basis`, so a basis check skips it and the root's rotation stays in `matrix_world`. Unparent with the world matrix kept, then test `matrix_world`. Measured with the pipeline template's parented smoke fixture on 4.5.11 and 5.2.1 (a root rotated 90° on X and scaled 2x at z=3). With the unparent step removed, LOD0 exported with its origin at z=3.0 while the geometry's minimum was z=2.0.
167
+
134
168
### 4. Set origin
135
169
136
170
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`.
@@ -182,6 +216,7 @@ Draco, selected-only, explicit `export_yup`, and `export_apply=True` so the deci
182
216
5.**`bm.normal_update()` for flipped faces.** Use `recalc_face_normals`.
183
217
6.**Import then `bpy.ops.mesh.*` with no scale check.** Rule `validate-imported-mesh-scale`.
184
218
7.**Export with a live DECIMATE and `export_apply=False`.** The engine gets the dense mesh. Rule `no-unapplied-modifiers-on-export`.
219
+
8.**Assuming one imported object, single-user and unparented.** Picking the largest mesh drops every other part. Applying shared data raises, and testing `matrix_basis` misses a rotated parent. Isolate the data, unparent, apply, then join if the engine wants one asset.
The two guards are for ordinary imports. Instanced glTF nodes come back as several objects sharing one Mesh, and `transform_apply` then raises `Cannot apply to a multi user` (4.5.11 and 5.2.1). A mesh under a rotated root has an identity `matrix_basis`. Applying it leaves the root's rotation in `matrix_world`, and `use_selection=True` writes that rotation onto the exported node. Copying the data means instances no longer share one mesh. That is the cost of baking transforms into vertices.
56
+
47
57
`use_selection=True` on every preset. Draco is opt-in on glTF; do not copy `gltf_draco_export.py` wholesale.
0 commit comments