137 lines
4.2 KiB
Python
137 lines
4.2 KiB
Python
import bpy
|
|
import math
|
|
|
|
# Clear scene
|
|
bpy.ops.object.select_all(action='SELECT')
|
|
bpy.ops.object.delete()
|
|
|
|
# Import the existing Miku GLB
|
|
input_path = "/home/ai-agent/Papay-Shooter/assets/characters/skins/miku_rigged_animated.glb"
|
|
output_path = "/home/ai-agent/Papay-Shooter/assets/characters/skins/miku_proper_anim.glb"
|
|
|
|
bpy.ops.import_scene.gltf(filepath=input_path)
|
|
print("Imported GLB")
|
|
|
|
# Find the armature
|
|
armatures = [obj for obj in bpy.data.objects if obj.type == 'ARMATURE']
|
|
if not armatures:
|
|
print("ERROR: No armature found!")
|
|
exit(1)
|
|
|
|
armature = armatures[0]
|
|
print(f"Armature: {armature.name}, bones: {len(armature.data.bones)}")
|
|
|
|
# Switch to pose mode
|
|
bpy.context.view_layer.objects.active = armature
|
|
bpy.ops.object.mode_set(mode='POSE')
|
|
|
|
def create_anim_dense(name, bone_rotations_func, length_frames=60):
|
|
action = bpy.data.actions.new(name=name)
|
|
armature.animation_data.action = action
|
|
action.use_fake_user = True
|
|
|
|
for frame in range(1, length_frames + 1):
|
|
bpy.context.scene.frame_set(frame)
|
|
rotations = bone_rotations_func(frame, length_frames)
|
|
for bone_name, euler in rotations.items():
|
|
if bone_name in armature.pose.bones:
|
|
bone = armature.pose.bones[bone_name]
|
|
bone.rotation_mode = 'XYZ'
|
|
bone.rotation_euler = euler
|
|
bone.keyframe_insert(data_path="rotation_euler", frame=frame)
|
|
|
|
return action
|
|
|
|
# WALK: Per-frame keyframes (60 frames, 1 second)
|
|
def walk_fn(frame, length):
|
|
t = frame / length * math.pi * 2
|
|
return {
|
|
"LeftUpperLeg": (math.sin(t) * 1.0, 0, 0),
|
|
"RightUpperLeg": (math.sin(t + math.pi) * 1.0, 0, 0),
|
|
"LeftUpperArm": (math.sin(t + math.pi) * 0.7, 0, 0),
|
|
"RightUpperArm": (math.sin(t) * 0.7, 0, 0),
|
|
"Spine": (0, math.sin(t) * 0.2, 0),
|
|
}
|
|
|
|
create_anim_dense("Walk", walk_fn, 60)
|
|
print("Walk created")
|
|
|
|
# RUN: Faster (40 frames)
|
|
def run_fn(frame, length):
|
|
t = frame / length * math.pi * 2
|
|
return {
|
|
"LeftUpperLeg": (math.sin(t) * 1.3, 0, 0),
|
|
"RightUpperLeg": (math.sin(t + math.pi) * 1.3, 0, 0),
|
|
"LeftUpperArm": (math.sin(t + math.pi) * 1.0, 0, 0),
|
|
"RightUpperArm": (math.sin(t) * 1.0, 0, 0),
|
|
"Spine": (0, math.sin(t) * 0.3, 0),
|
|
}
|
|
|
|
create_anim_dense("Run", run_fn, 40)
|
|
print("Run created")
|
|
|
|
# JUMP: Single jump cycle (40 frames)
|
|
def jump_fn(frame, length):
|
|
t = frame / length
|
|
peak = max(0.0, 1.0 - abs(t - 0.5) * 3.0)
|
|
return {
|
|
"LeftUpperArm": (-2.5 * peak, 0, -0.3 * peak),
|
|
"RightUpperArm": (-2.5 * peak, 0, 0.3 * peak),
|
|
"LeftUpperLeg": (-0.5 * peak, 0, 0),
|
|
"RightUpperLeg": (-0.5 * peak, 0, 0),
|
|
"Spine": (0.15 * peak, 0, 0),
|
|
"Hips": (0, 0.3 * peak, 0),
|
|
}
|
|
|
|
create_anim_dense("Jump", jump_fn, 40)
|
|
print("Jump created")
|
|
|
|
# IDLE: Gentle breathing (60 frames)
|
|
def idle_fn(frame, length):
|
|
t = frame / length * math.pi * 2
|
|
return {
|
|
"Spine": (0, math.sin(t) * 0.5, 0),
|
|
"Head": (math.sin(t * 0.7) * 0.15, math.sin(t * 0.5) * 0.1, 0),
|
|
"LeftUpperArm": (math.sin(t) * 0.2, 0, math.sin(t * 0.5) * 0.5),
|
|
"RightUpperArm": (math.sin(t) * 0.2, 0, -math.sin(t * 0.5) * 0.5),
|
|
"Hips": (0, math.sin(t) * 0.15, 0),
|
|
}
|
|
|
|
create_anim_dense("Idle", idle_fn, 60)
|
|
print("Idle created")
|
|
|
|
# Remove original stripped actions, keep only our custom ones
|
|
for action in list(bpy.data.actions):
|
|
if action.use_fake_user:
|
|
# Rename Blender's auto-suffixed names
|
|
if action.name == "Walk.001":
|
|
action.name = "Walk"
|
|
elif action.name == "Run.001":
|
|
action.name = "Run"
|
|
elif action.name == "Jump.001":
|
|
action.name = "Jump"
|
|
elif action.name == "Idle.002":
|
|
action.name = "Idle"
|
|
else:
|
|
bpy.data.actions.remove(action)
|
|
|
|
print(f"Final actions: {[a.name for a in bpy.data.actions if a.use_fake_user]}")
|
|
|
|
# Export
|
|
bpy.context.scene.render.fps = 60
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
|
|
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,
|
|
export_optimize_animation_size=False,
|
|
)
|
|
|
|
print(f"\nExported to {output_path}")
|
|
print("Done!")
|