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
+320
View File
@@ -0,0 +1,320 @@
#!/usr/bin/env python3
"""
Add animations to an already-rigged Miku GLB.
Uses the existing skeleton and skin weights from the original model.
Just adds new animation actions and re-exports.
"""
import bpy
import sys
import os
import math
argv = sys.argv
if '--' in argv:
argv = argv[argv.index('--') + 1:]
input_path = argv[0] if len(argv) > 0 else ''
output_path = argv[1] if len(argv) > 1 else ''
if not input_path or not output_path:
print("Usage: blender --background --python add_animations.py -- <input.glb> <output.glb>")
sys.exit(1)
# Clear scene
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
# Import the rigged GLB
print(f"Importing {input_path}...")
bpy.ops.import_scene.gltf(filepath=input_path)
# Find armature and mesh
armatures = [o for o in bpy.data.objects if o.type == 'ARMATURE']
meshes = [o for o in bpy.data.objects if o.type == 'MESH' and o.name != 'Icosphere']
if not armatures:
print("ERROR: No armature found")
sys.exit(1)
arm = armatures[0]
print(f"Armature: {arm.name} ({len(arm.data.bones)} bones)")
# List existing bones
for bone in arm.data.bones:
parent = bone.parent.name if bone.parent else "ROOT"
print(f" {bone.name:25s} parent={parent:25s} head={bone.head_local} tail={bone.tail_local}")
# List meshes
for m in meshes:
print(f"Mesh: {m.name} ({len(m.data.vertices)} verts)")
if m.parent:
print(f" Parent: {m.parent.name} ({m.parent.type})")
for mod in m.modifiers:
if mod.type == 'ARMATURE':
print(f" Armature mod: {mod.object.name if mod.object else 'None'}")
# List existing animations
print(f"\nExisting animations: {len(bpy.data.actions)}")
for action in bpy.data.actions:
print(f" {action.name} ({action.frame_range[0]:.0f}-{action.frame_range[1]:.0f})")
# Check if armature has animation_data
if arm.animation_data:
print(f"\nArmature animation_data: action={arm.animation_data.action}")
print(f" NLA tracks: {len(arm.animation_data.nla_tracks)}")
for track in arm.animation_data.nla_tracks:
print(f" Track: {track.name}")
for strip in track.strips:
print(f" Strip: {strip.name} ({strip.frame_start}-{strip.frame_end})")
# Enter pose mode
bpy.context.view_layer.objects.active = arm
bpy.ops.object.mode_set(mode='POSE')
# Get bone names for reference
bone_names = [b.name for b in arm.data.bones]
print(f"\nBone names: {bone_names}")
# Define animation keyframes using the EXISTING bone names
# The bones are: Hips, Spine, Chest, Neck, Head, LeftUpperArm, LeftLowerArm, LeftHand,
# RightUpperArm, RightLowerArm, RightHand, LeftUpperLeg, LeftLowerLeg, LeftFoot,
# RightUpperLeg, RightLowerLeg, RightFoot, neutral_bone
def set_rest():
for bone in arm.pose.bones:
bone.location = (0, 0, 0)
bone.rotation_euler = (0, 0, 0)
bone.scale = (1, 1, 1)
def key(bone_name, frame, loc=None, rot=None):
bone = arm.pose.bones.get(bone_name)
if not bone:
return
if loc:
bone.location = loc
bone.keyframe_insert(data_path="location", frame=frame)
if rot:
bone.rotation_mode = 'XYZ'
bone.rotation_euler = rot
bone.keyframe_insert(data_path="rotation_euler", frame=frame)
# Idle
print("\nCreating Idle...")
action_idle = bpy.data.actions.new(name="Idle")
arm.animation_data.action = action_idle
action_idle.frame_range = (1, 30)
set_rest()
key("Spine", 1, rot=(0.02, 0, 0))
key("Spine", 15, rot=(-0.02, 0, 0))
key("Spine", 30, rot=(0.02, 0, 0))
key("LeftUpperArm", 1, rot=(0, 0, 0.05))
key("LeftUpperArm", 15, rot=(0, 0, -0.05))
key("LeftUpperArm", 30, rot=(0, 0, 0.05))
key("RightUpperArm", 1, rot=(0, 0, -0.05))
key("RightUpperArm", 15, rot=(0, 0, 0.05))
key("RightUpperArm", 30, rot=(0, 0, -0.05))
# Walk
print("Creating Walk...")
action_walk = bpy.data.actions.new(name="Walk")
arm.animation_data.action = action_walk
action_walk.frame_range = (1, 24)
set_rest()
for f in range(1, 25):
t = (f - 1) / 24
phase = t * 2 * math.pi
key("Hips", f, loc=(0, 0, abs(math.sin(phase * 2)) * 0.01))
key("LeftUpperLeg", f, rot=(math.sin(phase) * 0.4, 0, 0))
key("LeftLowerLeg", f, rot=(max(0, -math.sin(phase) * 0.3 + 0.2), 0, 0))
key("RightUpperLeg", f, rot=(math.sin(phase + math.pi) * 0.4, 0, 0))
key("RightLowerLeg", f, rot=(max(0, -math.sin(phase + math.pi) * 0.3 + 0.2), 0, 0))
key("LeftUpperArm", f, rot=(-math.sin(phase) * 0.3, 0, 0.05))
key("LeftLowerArm", f, rot=(-0.3 + max(0, math.sin(phase) * 0.2), 0, 0))
key("RightUpperArm", f, rot=(-math.sin(phase + math.pi) * 0.3, 0, -0.05))
key("RightLowerArm", f, rot=(-0.3 + max(0, math.sin(phase + math.pi) * 0.2), 0, 0))
key("Spine", f, rot=(0, math.sin(phase) * 0.05, 0))
# Run
print("Creating Run...")
action_run = bpy.data.actions.new(name="Run")
arm.animation_data.action = action_run
action_run.frame_range = (1, 20)
set_rest()
for f in range(1, 21):
t = (f - 1) / 20
phase = t * 2 * math.pi
key("Hips", f, loc=(0, 0, abs(math.sin(phase * 2)) * 0.03))
key("LeftUpperLeg", f, rot=(math.sin(phase) * 0.8, 0, 0))
key("LeftLowerLeg", f, rot=(max(0.1, -math.sin(phase) * 0.6 + 0.3), 0, 0))
key("RightUpperLeg", f, rot=(math.sin(phase + math.pi) * 0.8, 0, 0))
key("RightLowerLeg", f, rot=(max(0.1, -math.sin(phase + math.pi) * 0.6 + 0.3), 0, 0))
key("LeftUpperArm", f, rot=(-math.sin(phase) * 0.7, 0, 0.1))
key("LeftLowerArm", f, rot=(-1.0 + max(0, math.sin(phase) * 0.3), 0, 0))
key("RightUpperArm", f, rot=(-math.sin(phase + math.pi) * 0.7, 0, -0.1))
key("RightLowerArm", f, rot=(-1.0 + max(0, math.sin(phase + math.pi) * 0.3), 0, 0))
key("Spine", f, rot=(0.1, math.sin(phase) * 0.08, 0))
# Jump
print("Creating Jump...")
action_jump = bpy.data.actions.new(name="Jump")
arm.animation_data.action = action_jump
action_jump.frame_range = (1, 20)
set_rest()
for f in range(1, 6):
t = (f - 1) / 4
key("Hips", f, loc=(0, 0, -t * 0.2))
key("LeftUpperLeg", f, rot=(-t * 0.6, 0, 0))
key("LeftLowerLeg", f, rot=(t * 1.0, 0, 0))
key("RightUpperLeg", f, rot=(-t * 0.6, 0, 0))
key("RightLowerLeg", f, rot=(t * 1.0, 0, 0))
key("LeftUpperArm", f, rot=(0, 0, -t * 0.5))
key("RightUpperArm", f, rot=(0, 0, t * 0.5))
for f in range(6, 9):
t = (f - 6) / 2
key("Hips", f, loc=(0, 0, -0.2 + t * 0.2))
key("LeftUpperLeg", f, rot=(-0.6 + t * 0.6, 0, 0))
key("LeftLowerLeg", f, rot=(1.0 - t * 0.3, 0, 0))
key("RightUpperLeg", f, rot=(-0.6 + t * 0.6, 0, 0))
key("RightLowerLeg", f, rot=(1.0 - t * 0.3, 0, 0))
key("LeftUpperArm", f, rot=(-t * 2.0, 0, -0.5 - t))
key("RightUpperArm", f, rot=(-t * 2.0, 0, 0.5 + t))
for f in range(9, 16):
key("LeftUpperArm", f, rot=(-2.0, 0, -1.5))
key("RightUpperArm", f, rot=(-2.0, 0, 1.5))
key("LeftLowerArm", f, rot=(-0.2, 0, 0))
key("RightLowerArm", f, rot=(-0.2, 0, 0))
key("LeftUpperLeg", f, rot=(0.2, 0, 0))
key("LeftLowerLeg", f, rot=(0.5, 0, 0))
key("RightUpperLeg", f, rot=(0.1, 0, 0))
key("RightLowerLeg", f, rot=(0.3, 0, 0))
for f in range(16, 21):
t = (f - 16) / 4
key("Hips", f, loc=(0, 0, -t * 0.2))
key("LeftUpperLeg", f, rot=(-t * 0.5, 0, 0))
key("LeftLowerLeg", f, rot=(t * 0.8, 0, 0))
key("RightUpperLeg", f, rot=(-t * 0.5, 0, 0))
key("RightLowerLeg", f, rot=(t * 0.8, 0, 0))
key("LeftUpperArm", f, rot=(-2.0 + t * 2.0, 0, -1.5 + t * 1.5))
key("RightUpperArm", f, rot=(-2.0 + t * 2.0, 0, 1.5 - t * 1.5))
# Crouch
print("Creating Crouch...")
action_crouch = bpy.data.actions.new(name="Crouch")
arm.animation_data.action = action_crouch
action_crouch.frame_range = (1, 1)
set_rest()
key("Hips", 1, loc=(0, 0, -0.4))
key("Spine", 1, rot=(0.3, 0, 0))
key("LeftUpperLeg", 1, rot=(-0.8, 0, 0))
key("LeftLowerLeg", 1, rot=(1.4, 0, 0))
key("RightUpperLeg", 1, rot=(-0.8, 0, 0))
key("RightLowerLeg", 1, rot=(1.4, 0, 0))
key("LeftUpperArm", 1, rot=(-0.5, 0, 0.2))
key("RightUpperArm", 1, rot=(-0.5, 0, -0.2))
# Death
print("Creating Death...")
action_death = bpy.data.actions.new(name="Death")
arm.animation_data.action = action_death
action_death.frame_range = (1, 30)
set_rest()
for f in range(1, 15):
t = (f - 1) / 13
key("Hips", f, loc=(0, 0, -t * 0.5), rot=(-t * 1.2, 0, t * 0.2))
key("Spine", f, rot=(-t * 0.5, t * 0.3, 0))
key("Chest", f, rot=(-t * 0.4, 0, 0))
key("LeftUpperArm", f, rot=(t * 2.0, 0, -t * 0.5))
key("RightUpperArm", f, rot=(t * 2.0, 0, t * 0.5))
key("LeftUpperLeg", f, rot=(-t * 0.3, 0, 0))
key("RightUpperLeg", f, rot=(-t * 0.3, 0, 0))
for f in range(15, 31):
t = (f - 15) / 15
key("Hips", f, loc=(0, 0, -0.5 - t * 0.2), rot=(-1.2 - t * 0.5, 0.8 * (1 - t), 0.2 + t * 0.3))
key("Spine", f, rot=(-0.7 - t * 0.3, 0.5 * (1 - t), 0))
key("Chest", f, rot=(-0.6 - t * 0.2, 0, 0))
key("LeftHand", f, rot=(t * 0.3, t * 0.5, -0.7 - t * 0.5))
key("RightHand", f, rot=(t * 0.4, -t * 0.6, 0.8 + t * 0.4))
print(f"\nTotal animations: {len(bpy.data.actions)}")
for action in bpy.data.actions:
print(f" {action.name} ({action.frame_range[0]:.0f}-{action.frame_range[1]:.0f})")
# Switch to object mode
bpy.ops.object.mode_set(mode='OBJECT')
# Select all for export
bpy.ops.object.select_all(action='DESELECT')
arm.select_set(True)
for m in meshes:
m.select_set(True)
bpy.context.view_layer.objects.active = arm
# Export as GLB
print(f"\nExporting to {output_path}...")
bpy.ops.export_scene.gltf(
filepath=output_path,
export_format='GLB',
export_apply=True,
export_animations=True,
export_animation_mode='ACTIONS',
export_skins=True,
export_yup=True,
)
print(f"Exported! {os.path.getsize(output_path)} bytes")
# Patch skeleton reference
import struct
import json
with open(output_path, 'rb') as f:
data = f.read()
json_len = struct.unpack('<I', data[12:16])[0]
json_data = data[20:20+json_len]
gltf = json.loads(json_data)
nodes = gltf.get('nodes', [])
skeleton_idx = None
for i, n in enumerate(nodes):
if n.get('name') == 'MikuRig':
skeleton_idx = i
break
if skeleton_idx is not None:
skins = gltf.get('skins', [])
if skins:
old = skins[0].get('skeleton')
skins[0]['skeleton'] = skeleton_idx
print(f"Set skeleton from {old} to {skeleton_idx}")
new_json = json.dumps(gltf, separators=(',', ':')).encode('utf-8')
while len(new_json) % 4 != 0:
new_json += b' '
bin_start = 20 + json_len
bin_len = struct.unpack('<I', data[bin_start:bin_start+4])[0]
new_length = 12 + 8 + len(new_json) + 8 + bin_len
new_data = b'glTF'
new_data += struct.pack('<I', 2)
new_data += struct.pack('<I', new_length)
new_data += struct.pack('<I', len(new_json))
new_data += b'JSON'
new_data += new_json
new_data += data[bin_start:]
with open(output_path, 'wb') as f:
f.write(new_data)
print(f"Final: {os.path.getsize(output_path)} bytes, {len(gltf.get('animations', []))} animations")
print("=== Complete ===")