Feat/14 movement overhaul #20

Merged
Dotts merged 86 commits from feat/14-movement-overhaul into main 2026-07-17 10:44:23 -07:00
5 changed files with 275 additions and 202 deletions
Showing only changes of commit 39063bfe9e - Show all commits
Binary file not shown.
+76
View File
@@ -92,9 +92,85 @@ func load_model(path: String) -> void:
elif anim_list.size() > 0: elif anim_list.size() > 0:
animation_player.play(anim_list[0]) animation_player.play(anim_list[0])
print("SkinnedPlayerModel: playing '%s'" % anim_list[0]) print("SkinnedPlayerModel: playing '%s'" % anim_list[0])
# If the loaded animations are too subtle (only 1 track), create better ones
if animation_player.has_animation("Idle"):
var idle_anim = animation_player.get_animation("Idle")
if idle_anim.get_track_count() <= 1:
print("SkinnedPlayerModel: Idle has too few tracks, creating a proper one")
_create_idle_animation(animation_player)
animation_player.play("Idle")
print("SkinnedPlayerModel: playing newly created Idle")
else: else:
print("SkinnedPlayerModel: WARNING - no AnimationPlayer found") print("SkinnedPlayerModel: WARNING - no AnimationPlayer found")
func _create_idle_animation(ap: AnimationPlayer) -> void:
# Create a proper idle animation with visible movement
var anim = Animation.new()
anim.length = 2.0
anim.loop_mode = Animation.LOOP_LINEAR
# Get the skeleton path from the scene
var skel_path = "MikuRig/Skeleton3D"
# Spine sway
_create_bone_track(anim, skel_path + ":Spine", "rotation",
[
{ "time": 0.0, "value": Vector3(0.1, 0, 0) },
{ "time": 0.5, "value": Vector3(-0.1, 0, 0) },
{ "time": 1.0, "value": Vector3(0.1, 0, 0) },
{ "time": 1.5, "value": Vector3(-0.1, 0, 0) },
{ "time": 2.0, "value": Vector3(0.1, 0, 0) },
])
# Left arm sway
_create_bone_track(anim, skel_path + ":LeftUpperArm", "rotation",
[
{ "time": 0.0, "value": Vector3(0, 0, 0.3) },
{ "time": 1.0, "value": Vector3(0, 0, 0.5) },
{ "time": 2.0, "value": Vector3(0, 0, 0.3) },
])
# Right arm sway
_create_bone_track(anim, skel_path + ":RightUpperArm", "rotation",
[
{ "time": 0.0, "value": Vector3(0, 0, -0.3) },
{ "time": 1.0, "value": Vector3(0, 0, -0.5) },
{ "time": 2.0, "value": Vector3(0, 0, -0.3) },
])
# Neck gentle movement
_create_bone_track(anim, skel_path + ":Neck", "rotation",
[
{ "time": 0.0, "value": Vector3(0.05, 0, 0) },
{ "time": 1.0, "value": Vector3(-0.05, 0, 0) },
{ "time": 2.0, "value": Vector3(0.05, 0, 0) },
])
# Hips subtle bounce
_create_bone_track(anim, skel_path + ":Hips", "position",
[
{ "time": 0.0, "value": Vector3(0, 0, 0) },
{ "time": 0.5, "value": Vector3(0, 0.02, 0) },
{ "time": 1.0, "value": Vector3(0, 0, 0) },
{ "time": 1.5, "value": Vector3(0, 0.02, 0) },
{ "time": 2.0, "value": Vector3(0, 0, 0) },
])
# Add animation to the player
ap.add_animation("Idle", anim)
print("SkinnedPlayerModel: created Idle animation with " + str(anim.get_track_count()) + " tracks")
func _create_bone_track(anim: Animation, bone_path: String, prop: String, keyframes: Array) -> void:
var track_idx = anim.add_track(Animation.TYPE_VALUE)
anim.track_set_path(track_idx, bone_path + ":" + prop)
anim.track_set_interpolation_type(track_idx, Animation.INTERPOLATION_LINEAR)
for kf in keyframes:
var time = kf["time"]
var value = kf["value"]
anim.track_insert_key(track_idx, time, value)
func _setup_first_person() -> void: func _setup_first_person() -> void:
# In first person, hide the head and upper body so only arms/hands/legs show # In first person, hide the head and upper body so only arms/hands/legs show
# This creates the classic FPS arms-only view # This creates the classic FPS arms-only view
+1 -1
View File
@@ -432,7 +432,7 @@ func _spawn_player(pid: int) -> CharacterBody3D:
# Miku: use the rigged GLB model with its own armature # Miku: use the rigged GLB model with its own armature
var skinned = load("res://characters/skinned_player_model.gd").new() var skinned = load("res://characters/skinned_player_model.gd").new()
skinned.name = "SkinnedModel" skinned.name = "SkinnedModel"
skinned.model_path = "res://assets/characters/skins/miku_rigged_animated.glb" skinned.model_path = "res://assets/characters/skins/miku_rigged_final.glb"
skinned.scale_factor = 1.0 # Already scaled to 1.8m in Blender skinned.scale_factor = 1.0 # Already scaled to 1.8m in Blender
skinned.position = Vector3.ZERO # Model origin = player origin skinned.position = Vector3.ZERO # Model origin = player origin
player.add_child(skinned) player.add_child(skinned)
+1 -1
View File
@@ -147,7 +147,7 @@ func _spawn_player(pid: int) -> CharacterBody3D:
# Local player: use skinned Miku model with animations # Local player: use skinned Miku model with animations
var skinned = load("res://characters/skinned_player_model.gd").new() var skinned = load("res://characters/skinned_player_model.gd").new()
skinned.name = "SkinnedModel" skinned.name = "SkinnedModel"
skinned.model_path = "res://assets/characters/skins/miku_rigged_animated.glb" skinned.model_path = "res://assets/characters/skins/miku_rigged_final.glb"
skinned.position = Vector3(0, 0.0, 0) skinned.position = Vector3(0, 0.0, 0)
skinned.first_person_mode = true skinned.first_person_mode = true
player.add_child(skinned) player.add_child(skinned)
+192 -195
View File
@@ -1,8 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Add animations to an already-rigged Miku GLB. Add animations to an already-rigged Miku GLB.
Uses the existing skeleton and skin weights from the original model. Keyframes ALL bones for each animation to ensure proper track export.
Just adds new animation actions and re-exports.
""" """
import bpy import bpy
import sys import sys
@@ -17,249 +16,251 @@ input_path = argv[0] if len(argv) > 0 else ''
output_path = argv[1] if len(argv) > 1 else '' output_path = argv[1] if len(argv) > 1 else ''
if not input_path or not output_path: if not input_path or not output_path:
print("Usage: blender --background --python add_animations.py -- <input.glb> <output.glb>")
sys.exit(1) sys.exit(1)
# Clear scene
bpy.ops.object.select_all(action='SELECT') bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete() bpy.ops.object.delete()
# Import the rigged GLB
print(f"Importing {input_path}...") print(f"Importing {input_path}...")
bpy.ops.import_scene.gltf(filepath=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'] 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'] meshes = [o for o in bpy.data.objects if o.type == 'MESH' and o.name != 'Icosphere']
if not armatures: if not armatures:
print("ERROR: No armature found") print("ERROR: No armature")
sys.exit(1) sys.exit(1)
arm = armatures[0] arm = armatures[0]
print(f"Armature: {arm.name} ({len(arm.data.bones)} bones)") 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.context.view_layer.objects.active = arm
bpy.ops.object.mode_set(mode='POSE') bpy.ops.object.mode_set(mode='POSE')
# Get bone names for reference bone_names = [b.name for b in arm.data.bones if b.name != "neutral_bone"]
bone_names = [b.name for b in arm.data.bones] print(f"Bones: {bone_names}")
print(f"\nBone names: {bone_names}")
# Define animation keyframes using the EXISTING bone names def create_animation(name, frame_end, keyframes_func):
# The bones are: Hips, Spine, Chest, Neck, Head, LeftUpperArm, LeftLowerArm, LeftHand, """Create an animation. keyframes_func(frame, bone_name) -> (loc, rot) or None."""
# RightUpperArm, RightLowerArm, RightHand, LeftUpperLeg, LeftLowerLeg, LeftFoot, print(f" Creating {name}...")
# RightUpperLeg, RightLowerLeg, RightFoot, neutral_bone action = bpy.data.actions.new(name=name)
arm.animation_data.action = action
action.frame_range = (1, frame_end)
def set_rest(): # Keyframe ALL bones at every frame to ensure tracks are exported
for bone in arm.pose.bones: for frame in range(1, frame_end + 1):
for bone_name in bone_names:
bone = arm.pose.bones[bone_name]
result = keyframes_func(frame, bone_name)
if result is None:
# Default: no movement
bone.location = (0, 0, 0) bone.location = (0, 0, 0)
bone.rotation_euler = (0, 0, 0) bone.rotation_euler = (0, 0, 0)
bone.scale = (1, 1, 1) else:
loc, rot = result
def key(bone_name, frame, loc=None, rot=None): bone.location = loc if loc else (0, 0, 0)
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_mode = 'XYZ'
bone.rotation_euler = rot bone.rotation_euler = rot if rot else (0, 0, 0)
bone.keyframe_insert(data_path="location", frame=frame)
bone.keyframe_insert(data_path="rotation_euler", frame=frame) bone.keyframe_insert(data_path="rotation_euler", frame=frame)
# Idle print(f" {name}: {frame_end} frames, {len(bone_names)} bones")
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)) # Idle: visible swaying (large enough to not be optimized away)
key("Spine", 15, rot=(-0.02, 0, 0)) def idle_kf(frame, bone_name):
key("Spine", 30, rot=(0.02, 0, 0)) t = (frame - 1) / 30.0
key("LeftUpperArm", 1, rot=(0, 0, 0.05)) phase = t * 2 * math.pi
key("LeftUpperArm", 15, rot=(0, 0, -0.05)) if bone_name == "Spine":
key("LeftUpperArm", 30, rot=(0, 0, 0.05)) return None, (math.sin(phase) * 0.5, 0, 0)
key("RightUpperArm", 1, rot=(0, 0, -0.05)) elif bone_name == "LeftUpperArm":
key("RightUpperArm", 15, rot=(0, 0, 0.05)) return None, (0, 0, 0.5 + math.sin(phase) * 0.3)
key("RightUpperArm", 30, rot=(0, 0, -0.05)) elif bone_name == "RightUpperArm":
return None, (0, 0, -0.5 - math.sin(phase) * 0.3)
elif bone_name == "Neck":
return None, (math.sin(phase) * 0.3, 0, 0)
elif bone_name == "LeftLowerArm":
return None, (-0.5, 0, 0)
elif bone_name == "RightLowerArm":
return None, (-0.5, 0, 0)
return None, None
create_animation("Idle", 30, idle_kf)
# Walk # Walk
print("Creating Walk...") def walk_kf(frame, bone_name):
action_walk = bpy.data.actions.new(name="Walk") t = (frame - 1) / 24.0
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 phase = t * 2 * math.pi
key("Hips", f, loc=(0, 0, abs(math.sin(phase * 2)) * 0.01)) if bone_name == "Hips":
key("LeftUpperLeg", f, rot=(math.sin(phase) * 0.4, 0, 0)) return (0, 0, abs(math.sin(phase * 2)) * 0.02), None
key("LeftLowerLeg", f, rot=(max(0, -math.sin(phase) * 0.3 + 0.2), 0, 0)) elif bone_name == "LeftUpperLeg":
key("RightUpperLeg", f, rot=(math.sin(phase + math.pi) * 0.4, 0, 0)) return None, (math.sin(phase) * 0.5, 0, 0)
key("RightLowerLeg", f, rot=(max(0, -math.sin(phase + math.pi) * 0.3 + 0.2), 0, 0)) elif bone_name == "LeftLowerLeg":
key("LeftUpperArm", f, rot=(-math.sin(phase) * 0.3, 0, 0.05)) return None, (max(0, -math.sin(phase) * 0.4 + 0.3), 0, 0)
key("LeftLowerArm", f, rot=(-0.3 + max(0, math.sin(phase) * 0.2), 0, 0)) elif bone_name == "RightUpperLeg":
key("RightUpperArm", f, rot=(-math.sin(phase + math.pi) * 0.3, 0, -0.05)) return None, (math.sin(phase + math.pi) * 0.5, 0, 0)
key("RightLowerArm", f, rot=(-0.3 + max(0, math.sin(phase + math.pi) * 0.2), 0, 0)) elif bone_name == "RightLowerLeg":
key("Spine", f, rot=(0, math.sin(phase) * 0.05, 0)) return None, (max(0, -math.sin(phase + math.pi) * 0.4 + 0.3), 0, 0)
elif bone_name == "LeftUpperArm":
return None, (-math.sin(phase) * 0.4, 0, 0.1)
elif bone_name == "LeftLowerArm":
return None, (-0.5 + max(0, math.sin(phase) * 0.3), 0, 0)
elif bone_name == "RightUpperArm":
return None, (-math.sin(phase + math.pi) * 0.4, 0, -0.1)
elif bone_name == "RightLowerArm":
return None, (-0.5 + max(0, math.sin(phase + math.pi) * 0.3), 0, 0)
elif bone_name == "Spine":
return None, (0, math.sin(phase) * 0.08, 0)
return None, None
create_animation("Walk", 24, walk_kf)
# Run # Run
print("Creating Run...") def run_kf(frame, bone_name):
action_run = bpy.data.actions.new(name="Run") t = (frame - 1) / 20.0
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 phase = t * 2 * math.pi
key("Hips", f, loc=(0, 0, abs(math.sin(phase * 2)) * 0.03)) if bone_name == "Hips":
key("LeftUpperLeg", f, rot=(math.sin(phase) * 0.8, 0, 0)) return (0, 0, abs(math.sin(phase * 2)) * 0.04), None
key("LeftLowerLeg", f, rot=(max(0.1, -math.sin(phase) * 0.6 + 0.3), 0, 0)) elif bone_name == "LeftUpperLeg":
key("RightUpperLeg", f, rot=(math.sin(phase + math.pi) * 0.8, 0, 0)) return None, (math.sin(phase) * 0.9, 0, 0)
key("RightLowerLeg", f, rot=(max(0.1, -math.sin(phase + math.pi) * 0.6 + 0.3), 0, 0)) elif bone_name == "LeftLowerLeg":
key("LeftUpperArm", f, rot=(-math.sin(phase) * 0.7, 0, 0.1)) return None, (max(0.1, -math.sin(phase) * 0.7 + 0.4), 0, 0)
key("LeftLowerArm", f, rot=(-1.0 + max(0, math.sin(phase) * 0.3), 0, 0)) elif bone_name == "RightUpperLeg":
key("RightUpperArm", f, rot=(-math.sin(phase + math.pi) * 0.7, 0, -0.1)) return None, (math.sin(phase + math.pi) * 0.9, 0, 0)
key("RightLowerArm", f, rot=(-1.0 + max(0, math.sin(phase + math.pi) * 0.3), 0, 0)) elif bone_name == "RightLowerLeg":
key("Spine", f, rot=(0.1, math.sin(phase) * 0.08, 0)) return None, (max(0.1, -math.sin(phase + math.pi) * 0.7 + 0.4), 0, 0)
elif bone_name == "LeftUpperArm":
return None, (-math.sin(phase) * 0.8, 0, 0.15)
elif bone_name == "LeftLowerArm":
return None, (-1.2 + max(0, math.sin(phase) * 0.4), 0, 0)
elif bone_name == "RightUpperArm":
return None, (-math.sin(phase + math.pi) * 0.8, 0, -0.15)
elif bone_name == "RightLowerArm":
return None, (-1.2 + max(0, math.sin(phase + math.pi) * 0.4), 0, 0)
elif bone_name == "Spine":
return None, (0.15, math.sin(phase) * 0.1, 0)
return None, None
create_animation("Run", 20, run_kf)
# Jump # Jump
print("Creating Jump...") def jump_kf(frame, bone_name):
action_jump = bpy.data.actions.new(name="Jump") if frame <= 5:
arm.animation_data.action = action_jump t = (frame - 1) / 4.0
action_jump.frame_range = (1, 20) if bone_name == "Hips":
set_rest() return (0, 0, -t * 0.3), None
elif bone_name in ("LeftUpperLeg", "RightUpperLeg"):
return None, (-t * 0.7, 0, 0)
elif bone_name in ("LeftLowerLeg", "RightLowerLeg"):
return None, (t * 1.2, 0, 0)
elif bone_name == "LeftUpperArm":
return None, (0, 0, -t * 0.8)
elif bone_name == "RightUpperArm":
return None, (0, 0, t * 0.8)
elif frame <= 8:
t = (frame - 5) / 2.0
if bone_name == "Hips":
return (0, 0, -0.3 + t * 0.3), None
elif bone_name in ("LeftUpperLeg", "RightUpperLeg"):
return None, (-0.7 + t * 0.7, 0, 0)
elif bone_name in ("LeftLowerLeg", "RightLowerLeg"):
return None, (1.2 - t * 0.4, 0, 0)
elif bone_name == "LeftUpperArm":
return None, (-t * 2.5, 0, -0.8 - t)
elif bone_name == "RightUpperArm":
return None, (-t * 2.5, 0, 0.8 + t)
elif frame <= 15:
if bone_name == "LeftUpperArm":
return None, (-2.5, 0, -1.8)
elif bone_name == "RightUpperArm":
return None, (-2.5, 0, 1.8)
elif bone_name in ("LeftLowerArm", "RightLowerArm"):
return None, (-0.3, 0, 0)
elif bone_name == "LeftUpperLeg":
return None, (0.3, 0, 0)
elif bone_name == "LeftLowerLeg":
return None, (0.6, 0, 0)
elif bone_name == "RightUpperLeg":
return None, (0.2, 0, 0)
elif bone_name == "RightLowerLeg":
return None, (0.4, 0, 0)
else:
t = (frame - 15) / 4.0
if bone_name == "Hips":
return (0, 0, -t * 0.3), None
elif bone_name in ("LeftUpperLeg", "RightUpperLeg"):
return None, (-t * 0.6, 0, 0)
elif bone_name in ("LeftLowerLeg", "RightLowerLeg"):
return None, (t * 1.0, 0, 0)
elif bone_name == "LeftUpperArm":
return None, (-2.5 + t * 2.5, 0, -1.8 + t * 1.8)
elif bone_name == "RightUpperArm":
return None, (-2.5 + t * 2.5, 0, 1.8 - t * 1.8)
return None, None
for f in range(1, 6): create_animation("Jump", 20, jump_kf)
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 # Crouch
print("Creating Crouch...") def crouch_kf(frame, bone_name):
action_crouch = bpy.data.actions.new(name="Crouch") if bone_name == "Hips":
arm.animation_data.action = action_crouch return (0, 0, -0.5), None
action_crouch.frame_range = (1, 1) elif bone_name == "Spine":
set_rest() return None, (0.4, 0, 0)
elif bone_name in ("LeftUpperLeg", "RightUpperLeg"):
return None, (-0.9, 0, 0)
elif bone_name in ("LeftLowerLeg", "RightLowerLeg"):
return None, (1.5, 0, 0)
elif bone_name == "LeftUpperArm":
return None, (-0.6, 0, 0.3)
elif bone_name == "RightUpperArm":
return None, (-0.6, 0, -0.3)
return None, None
key("Hips", 1, loc=(0, 0, -0.4)) create_animation("Crouch", 1, crouch_kf)
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 # Death
print("Creating Death...") def death_kf(frame, bone_name):
action_death = bpy.data.actions.new(name="Death") if frame <= 14:
arm.animation_data.action = action_death t = (frame - 1) / 13.0
action_death.frame_range = (1, 30) if bone_name == "Hips":
set_rest() return (0, 0, -t * 0.6), (-t * 1.3, 0, t * 0.3)
elif bone_name == "Spine":
return None, (-t * 0.6, t * 0.4, 0)
elif bone_name == "Chest":
return None, (-t * 0.5, 0, 0)
elif bone_name in ("LeftUpperArm", "RightUpperArm"):
sign = -1 if "Left" in bone_name else 1
return None, (t * 2.2, 0, sign * -t * 0.6)
elif bone_name in ("LeftUpperLeg", "RightUpperLeg"):
return None, (-t * 0.4, 0, 0)
else:
t = (frame - 14) / 15.0
if bone_name == "Hips":
return (0, 0, -0.6 - t * 0.3), (-1.3 - t * 0.6, 0.8 * (1 - t), 0.3 + t * 0.4)
elif bone_name == "Spine":
return None, (-0.8 - t * 0.4, 0.5 * (1 - t), 0)
elif bone_name == "Chest":
return None, (-0.7 - t * 0.3, 0, 0)
elif bone_name == "LeftHand":
return None, (t * 0.4, t * 0.6, -0.8 - t * 0.6)
elif bone_name == "RightHand":
return None, (t * 0.5, -t * 0.7, 0.9 + t * 0.5)
return None, None
for f in range(1, 15): create_animation("Death", 30, death_kf)
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)}") print(f"\nTotal animations: {len(bpy.data.actions)}")
for action in bpy.data.actions: for action in bpy.data.actions:
print(f" {action.name} ({action.frame_range[0]:.0f}-{action.frame_range[1]:.0f})") 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') bpy.ops.object.mode_set(mode='OBJECT')
# Select all for export
bpy.ops.object.select_all(action='DESELECT') bpy.ops.object.select_all(action='DESELECT')
arm.select_set(True) arm.select_set(True)
for m in meshes: for m in meshes:
m.select_set(True) m.select_set(True)
bpy.context.view_layer.objects.active = arm bpy.context.view_layer.objects.active = arm
# Export as GLB
print(f"\nExporting to {output_path}...") print(f"\nExporting to {output_path}...")
bpy.ops.export_scene.gltf( bpy.ops.export_scene.gltf(
filepath=output_path, filepath=output_path,
@@ -272,9 +273,7 @@ bpy.ops.export_scene.gltf(
) )
print(f"Exported! {os.path.getsize(output_path)} bytes") print(f"Exported! {os.path.getsize(output_path)} bytes")
# Patch skeleton reference import struct, json
import struct
import json
with open(output_path, 'rb') as f: with open(output_path, 'rb') as f:
data = f.read() data = f.read()
@@ -293,9 +292,7 @@ for i, n in enumerate(nodes):
if skeleton_idx is not None: if skeleton_idx is not None:
skins = gltf.get('skins', []) skins = gltf.get('skins', [])
if skins: if skins:
old = skins[0].get('skeleton')
skins[0]['skeleton'] = skeleton_idx skins[0]['skeleton'] = skeleton_idx
print(f"Set skeleton from {old} to {skeleton_idx}")
new_json = json.dumps(gltf, separators=(',', ':')).encode('utf-8') new_json = json.dumps(gltf, separators=(',', ':')).encode('utf-8')
while len(new_json) % 4 != 0: while len(new_json) % 4 != 0:
@@ -316,5 +313,5 @@ new_data += data[bin_start:]
with open(output_path, 'wb') as f: with open(output_path, 'wb') as f:
f.write(new_data) f.write(new_data)
print(f"Final: {os.path.getsize(output_path)} bytes, {len(gltf.get('animations', []))} animations") print(f"Final: {os.path.getsize(output_path)} bytes")
print("=== Complete ===") print("=== Complete ===")