fix: T-posing skinned models — autorig produced an orphan skin with no weights

The model animated its skeleton but rendered a permanent T-pose: the mesh
wasn't skin-bound. Root cause was in autorig's binding, introduced while
chasing an earlier "skins:0" export:

- The earlier "skins:0" was actually caused by bone-heat weighting failing
  (0 weighted verts → empty skin gets dropped), NOT by the Armature modifier.
- The "fix" then stripped the Armature modifier and used parent_type=ARMATURE.
  That makes the glTF exporter emit a skin OBJECT but with no node.skin
  reference and no per-vertex JOINTS/WEIGHTS — an orphan skin. The mesh then
  renders its bind pose (T-pose) forever while the skeleton animates unseen.

Fix: keep the standard ARMATURE_AUTO result (Armature modifier + vertex
groups). With the nearest-bone fallback ensuring real weights, the exporter
now writes a COMPLETE skin (verified: meshnode.skin=0, JOINTS/WEIGHTS present).

Also:
- skinned_player_model: _ensure_meshes_bound() re-binds any skinned mesh whose
  skeleton NodePath doesn't resolve at load — graceful degradation instead of
  a silent T-pose for imperfect GLBs.
- smoke test now asserts the spawned player's mesh is bound to its skeleton,
  so this class of bug fails the test instead of shipping. 30/30 pass.
- regenerated miku_test.glb with the corrected pipeline.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Nicholas Butzke
2026-07-06 18:11:26 -04:00
co-authored by Claude Fable 5
parent e2fbc424a7
commit 22cb0a58b7
4 changed files with 33 additions and 13 deletions
+10 -13
View File
@@ -269,13 +269,18 @@ def rigid_nearest_bone_weights(mesh, arm):
def bind_mesh_to_armature(mesh, arm):
"""Bind mesh to armature robustly and in a form the glTF exporter skins.
"""Bind mesh to armature so the glTF exporter writes a COMPLETE skin
(skin object + node.skin reference + per-vertex JOINTS/WEIGHTS).
1. Try Blender automatic (bone-heat) weights for smooth deformation.
2. If that assigns (almost) nothing, fall back to rigid nearest-bone.
3. Present the result as a parent_type='ARMATURE' relationship with NO
Armature modifier — the Blender 5.x glTF exporter only emits a skin
for that exact configuration (a lingering modifier yields skins:0).
2. If that assigns (almost) nothing — common on layered hair/clothing
meshes where bone-heat fails — fall back to rigid nearest-bone.
Keep the standard ARMATURE_AUTO result: an Armature modifier plus vertex
groups (parent_type stays OBJECT). That is exactly what the exporter needs
to write the vertex weights. (An earlier version stripped the modifier and
used parent_type='ARMATURE'; that produced an ORPHAN skin with no weights,
so the mesh rendered its bind pose — a permanent T-pose — in game.)
"""
bpy.ops.object.select_all(action="DESELECT")
mesh.select_set(True)
@@ -291,14 +296,6 @@ def bind_mesh_to_armature(mesh, arm):
weighted = rigid_nearest_bone_weights(mesh, arm)
print(f"Bound mesh: {weighted}/{total} verts weighted")
# Normalize to the exporter-friendly form: parent_type=ARMATURE, no modifier.
for m in list(mesh.modifiers):
if m.type == "ARMATURE":
mesh.modifiers.remove(m)
if mesh.parent != arm:
mesh.parent = arm
mesh.parent_type = "ARMATURE"
def main():
clear_scene()