fix: use original rigged skeleton for animations, add 6 new animations

The twintails were moving as arms because the auto-rig script treated the
combined mesh as one piece. Now using the original miku_rigged_final.glb
skeleton (which has correct skin weights from the model creator) and
adding 6 new animations (Idle, Walk, Run, Jump, Crouch, Death) on top
of the existing skeleton.

- tools/add_animations.py: adds animations to an existing rigged GLB
- tools/rig_and_animate.py: updated to separate mesh into loose parts
  before rigging (for future use with other models)
- miku_rigged_animated.glb: now 7 animations with correct skin weights
This commit is contained in:
2026-06-23 00:33:40 -04:00
parent 0672bebe7b
commit a83fa41b19
3 changed files with 412 additions and 16 deletions
+92 -16
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env python3
"""
Auto-rig a humanoid mesh in Blender and add idle/run/walk/jump/crouch/death animations.
Auto-rig a humanoid mesh in Blender and add animations.
Handles multi-part meshes (body, hair, accessories) by separating loose parts,
rigging only the body, and parenting everything to the same skeleton.
Exports as GLB with proper skeleton for Godot.
"""
import bpy
@@ -35,20 +37,20 @@ elif input_path.endswith('.fbx'):
imported = bpy.context.selected_objects
print(f"Imported {len(imported)} objects")
# Find all mesh objects (skip Icosphere and other defaults)
# Find all mesh objects
meshes = [o for o in imported if o.type == 'MESH' and o.name != 'Icosphere']
if not meshes:
print("ERROR: No mesh found")
sys.exit(1)
# Join all meshes into one
# Join all meshes into one first
if len(meshes) > 1:
bpy.context.view_layer.objects.active = meshes[0]
bpy.ops.object.join()
meshes = [bpy.context.active_object]
mesh_obj = meshes[0]
print(f"Mesh: {mesh_obj.name} ({len(mesh_obj.data.vertices)} verts)")
print(f"Combined mesh: {mesh_obj.name} ({len(mesh_obj.data.vertices)} verts)")
# Clean up mesh
bpy.context.view_layer.objects.active = mesh_obj
@@ -62,18 +64,69 @@ bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS')
mesh_obj.location = (0, 0, 0)
# Scale to roughly 1.8m tall (check largest dimension)
dims = mesh_obj.dimensions
max_dim = max(dims.x, dims.y, dims.z)
print(f"Original dimensions: {dims}")
if max_dim > 0 and max_dim < 10: # Model is in meters, needs scaling
if max_dim > 0 and max_dim < 10:
target_height = 1.8
scale = target_height / max_dim
mesh_obj.scale = (scale, scale, scale)
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
print(f"Scaled by {scale:.2f}x. New dims: {mesh_obj.dimensions}")
# Now create a custom humanoid rig
# Separate mesh into loose parts (body, hair, twintails, etc.)
# This is key: we need to identify which vertices belong to the body vs hair/accessories
print("\n=== Separating mesh into parts ===")
bpy.context.view_layer.objects.active = mesh_obj
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.separate(type='LOOSE')
bpy.ops.object.mode_set(mode='OBJECT')
# List separated parts
parts = [o for o in bpy.context.selected_objects if o.type == 'MESH']
print(f"Separated into {len(parts)} parts:")
for p in parts:
print(f" {p.name}: {len(p.data.vertices)} verts, dims={p.dimensions}")
# Identify the body part (largest by vertex count, roughly humanoid proportions)
# The body should be the largest part with roughly humanoid proportions
body_part = None
hair_parts = []
other_parts = []
for p in parts:
verts = len(p.data.vertices)
dims = p.dimensions
height = max(dims.x, dims.y, dims.z)
width = min(dims.x, dims.y, dims.z)
# Body: largest part, height > width, reasonable humanoid proportions
if height > 0.5 and verts > 1000:
if body_part is None or verts > len(body_part.data.vertices):
if body_part:
other_parts.append(body_part)
body_part = p
else:
other_parts.append(p)
else:
hair_parts.append(p)
if not body_part:
# Fallback: just use the largest part
body_part = max(parts, key=lambda p: len(p.data.vertices))
hair_parts = [p for p in parts if p != body_part]
other_parts = []
print(f"\nBody: {body_part.name} ({len(body_part.data.vertices)} verts)")
print(f"Hair/accessories: {[p.name for p in hair_parts]}")
print(f"Other: {[p.name for p in other_parts]}")
# Delete non-body parts (hair, twintails, accessories) - they'll be parented to bones later
# Actually, keep them but we won't skin them to the body bones
all_non_body = hair_parts + other_parts
# Create the armature
print("\n=== Creating Rig ===")
bpy.ops.object.armature_add(enter_editmode=True, location=(0, 0, 0))
armature = bpy.context.active_object
@@ -88,7 +141,6 @@ for bone in armature.data.edit_bones:
# Define humanoid skeleton (Mixamo-compatible names)
# Blender is Z-up, so head is at higher Z
bone_defs = [
# (name, head_xyz, tail_xyz)
("mixamorig:Hips", (0.09, 0.0, 0.95), (0.09, 0.0, 1.05)),
("mixamorig:Spine", (0.09, 0.0, 1.05), (0.09, 0.0, 1.20)),
("mixamorig:Spine1", (0.09, 0.0, 1.20), (0.09, 0.0, 1.35)),
@@ -135,7 +187,6 @@ bones["mixamorig:RightUpLeg"].parent = bones["mixamorig:Hips"]
bones["mixamorig:RightLeg"].parent = bones["mixamorig:RightUpLeg"]
bones["mixamorig:RightFoot"].parent = bones["mixamorig:RightLeg"]
# Connect bones for visual clarity
for name in ["mixamorig:Spine", "mixamorig:Spine1", "mixamorig:Spine2", "mixamorig:Neck",
"mixamorig:Head", "mixamorig:LeftArm", "mixamorig:LeftForeArm", "mixamorig:LeftHand",
"mixamorig:RightArm", "mixamorig:RightForeArm", "mixamorig:RightHand",
@@ -144,17 +195,40 @@ for name in ["mixamorig:Spine", "mixamorig:Spine1", "mixamorig:Spine2", "mixamor
bones[name].use_connect = True
print(f"Created {len(bones)} bones")
bpy.ops.object.mode_set(mode='OBJECT')
# Parent mesh to armature with automatic weights
print("\n=== Skinning mesh ===")
# Skin ONLY the body part to the armature with automatic weights
print("\n=== Skinning body mesh ===")
bpy.ops.object.select_all(action='DESELECT')
mesh_obj.select_set(True)
body_part.select_set(True)
armature.select_set(True)
bpy.context.view_layer.objects.active = armature
bpy.ops.object.parent_set(type='ARMATURE_AUTO')
print("Mesh parented with automatic weights")
print(f"Body '{body_part.name}' parented with automatic weights")
# Parent hair/accessories to the armature (no skinning, just follow)
# They'll move with the Head bone
print("\n=== Parenting hair/accessories ===")
for part in all_non_body:
# Find the closest bone to this part's center
part_center = part.location
closest_bone = "mixamorig:Head" # Default to head for hair
closest_dist = float('inf')
for bone_name, bone in bones.items():
bone_head = armature.matrix_world @ bone.head
dist = (part_center - bone_head).length
if dist < closest_dist:
closest_dist = dist
closest_bone = bone_name
# Parent to armature with empty weights (just follow the bone)
bpy.ops.object.select_all(action='DESELECT')
part.select_set(True)
armature.select_set(True)
bpy.context.view_layer.objects_active = armature
bpy.ops.object.parent_set(type='ARMATURE_NAME', keep_transform=True)
print(f" '{part.name}' -> parented to '{closest_bone}'")
# Now create animations
print("\n=== Creating animations ===")
@@ -336,10 +410,12 @@ for action in bpy.data.actions:
# Switch to object mode
bpy.ops.object.mode_set(mode='OBJECT')
# Select for export
# Select all for export
bpy.ops.object.select_all(action='DESELECT')
mesh_obj.select_set(True)
armature.select_set(True)
body_part.select_set(True)
for part in all_non_body:
part.select_set(True)
bpy.context.view_layer.objects.active = armature
# Export as GLB