Files
Bullosseum/art/blender/create_matador_anims.py
T
richard 5caccd318c
CI / Decide whether to run (push) Has been cancelled
CI / Lint & headless tests (push) Has been cancelled
Drop version suffixes from Blender source filenames
Rename the Blender working files to plain role names, dropping the version
noise: bull_v11 -> bull, matador_v03 -> matador, publik_mees_v01 ->
publik_mees, and collapse anim_bull2 into anim_bull (identical rig, newer
save kept; older duplicate dropped). Repoint create_matador_anims.py (blend
input + fbx export name), render_anim_preview.py and CLAUDE.md at the new
names.

The anim_* and sword files link the model blends as Blender libraries
(//bull_v11.blend, //matador_v03.blend, //publik_mees_v01.blend), so those
links need repointing once in Blender. The game is unaffected — it ships the
exported fbx/glb, not the .blend sources (art/blender/ is Godot-ignored).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FBbGXbzg3SD6ZBSadPPF5H
2026-09-07 01:05:09 +03:00

152 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Blender 5.1 script: create walk + idle animations for matador.
Run: blender --background matador.blend --python create_matador_anims.py
"""
import bpy, math, os
# ── setup ──────────────────────────────────────────────────────────────────────
arm_obj = next(o for o in bpy.data.objects if o.type == 'ARMATURE')
bpy.context.view_layer.objects.active = arm_obj
bpy.ops.object.mode_set(mode='POSE')
if arm_obj.animation_data is None:
arm_obj.animation_data_create()
pb = arm_obj.pose.bones
# Ensure all animated bones use XYZ Euler
for bname in ['leg_L', 'leg_R', 'shin_L', 'shin_R',
'arm_L', 'arm_R', 'forearm_L', 'forearm_R',
'collarbone_L', 'collarbone_R']:
pb[bname].rotation_mode = 'XYZ'
CYCLE = 30 # frames (1 s at 30 fps)
LEG_SWING = 25 # degrees
KNEE_BEND = 30
ARM_SWING = 15
ARM_DOWN = 55 # lower arms from T-pose around bone-local Y
ELBOW_BEND = 25
# ── WALK action ────────────────────────────────────────────────────────────────
# Remove old actions if they exist
for old_name in ['walk', 'idle']:
if old_name in bpy.data.actions:
bpy.data.actions.remove(bpy.data.actions[old_name])
walk = bpy.data.actions.new(name='walk')
walk.use_fake_user = True
walk.use_cyclic = True
arm_obj.animation_data.action = walk
# Clear all pose bone transforms first
bpy.ops.pose.select_all(action='SELECT')
bpy.ops.pose.rot_clear()
arm_down_l = math.radians(ARM_DOWN)
arm_down_r = math.radians(-ARM_DOWN)
for f in range(0, CYCLE + 1, 2):
t = (f / CYCLE) * math.tau
# Legs – swing forward/back on bone-local X
pb['leg_L'].rotation_euler = (math.sin(t) * math.radians(LEG_SWING), 0, 0)
pb['leg_L'].keyframe_insert(data_path='rotation_euler', frame=f)
pb['leg_R'].rotation_euler = (math.sin(t + math.pi) * math.radians(LEG_SWING), 0, 0)
pb['leg_R'].keyframe_insert(data_path='rotation_euler', frame=f)
# Shins – only bend backward when leg is back
val_l = max(0.0, -math.sin(t)) * math.radians(KNEE_BEND)
val_r = max(0.0, -math.sin(t + math.pi)) * math.radians(KNEE_BEND)
pb['shin_L'].rotation_euler = (val_l, 0, 0)
pb['shin_L'].keyframe_insert(data_path='rotation_euler', frame=f)
pb['shin_R'].rotation_euler = (val_r, 0, 0)
pb['shin_R'].keyframe_insert(data_path='rotation_euler', frame=f)
# Arms – lower from T-pose (Y) + swing (X, opposite phase to legs)
swing_l = math.sin(t + math.pi) * math.radians(ARM_SWING)
swing_r = math.sin(t) * math.radians(ARM_SWING)
pb['arm_L'].rotation_euler = (swing_l, arm_down_l, 0)
pb['arm_L'].keyframe_insert(data_path='rotation_euler', frame=f)
pb['arm_R'].rotation_euler = (swing_r, arm_down_r, 0)
pb['arm_R'].keyframe_insert(data_path='rotation_euler', frame=f)
# Forearms – constant slight bend
pb['forearm_L'].rotation_euler = (math.radians(ELBOW_BEND), 0, 0)
pb['forearm_L'].keyframe_insert(data_path='rotation_euler', frame=f)
pb['forearm_R'].rotation_euler = (math.radians(ELBOW_BEND), 0, 0)
pb['forearm_R'].keyframe_insert(data_path='rotation_euler', frame=f)
# Collarbones – identity
pb['collarbone_L'].rotation_euler = (0, 0, 0)
pb['collarbone_L'].keyframe_insert(data_path='rotation_euler', frame=f)
pb['collarbone_R'].rotation_euler = (0, 0, 0)
pb['collarbone_R'].keyframe_insert(data_path='rotation_euler', frame=f)
print("Created 'walk' action")
# ── IDLE action ────────────────────────────────────────────────────────────────
idle = bpy.data.actions.new(name='idle')
idle.use_fake_user = True
arm_obj.animation_data.action = idle
bpy.ops.pose.select_all(action='SELECT')
bpy.ops.pose.rot_clear()
# Arms lowered, everything else at rest – 2 frames for a valid action
for f in [0, 1]:
pb['arm_L'].rotation_euler = (0, arm_down_l, 0)
pb['arm_L'].keyframe_insert(data_path='rotation_euler', frame=f)
pb['arm_R'].rotation_euler = (0, arm_down_r, 0)
pb['arm_R'].keyframe_insert(data_path='rotation_euler', frame=f)
pb['forearm_L'].rotation_euler = (math.radians(ELBOW_BEND), 0, 0)
pb['forearm_L'].keyframe_insert(data_path='rotation_euler', frame=f)
pb['forearm_R'].rotation_euler = (math.radians(ELBOW_BEND), 0, 0)
pb['forearm_R'].keyframe_insert(data_path='rotation_euler', frame=f)
for bname in ['collarbone_L', 'collarbone_R', 'leg_L', 'leg_R', 'shin_L', 'shin_R']:
pb[bname].rotation_euler = (0, 0, 0)
pb[bname].keyframe_insert(data_path='rotation_euler', frame=f)
print("Created 'idle' action")
# ── NLA tracks for export ─────────────────────────────────────────────────────
bpy.ops.object.mode_set(mode='OBJECT')
# Clear existing NLA tracks
if arm_obj.animation_data.nla_tracks:
for t in list(arm_obj.animation_data.nla_tracks):
arm_obj.animation_data.nla_tracks.remove(t)
tw = arm_obj.animation_data.nla_tracks.new()
tw.name = 'walk'
sw = tw.strips.new('walk', 0, walk)
ti = arm_obj.animation_data.nla_tracks.new()
ti.name = 'idle'
si = ti.strips.new('idle', 0, idle)
# ── export FBX ─────────────────────────────────────────────────────────────────
fbx_path = os.path.join(os.path.dirname(bpy.data.filepath), 'matador.fbx')
bpy.ops.export_scene.fbx(
filepath=fbx_path,
use_selection=False,
bake_anim=True,
bake_anim_use_all_actions=True,
bake_anim_use_nla_strips=False,
bake_anim_force_startend_keying=True,
bake_anim_step=1.0,
bake_anim_simplify_factor=0.0,
add_leaf_bones=False,
primary_bone_axis='Y',
secondary_bone_axis='X',
apply_scale_options='FBX_SCALE_ALL',
)
print('Exported FBX to:', fbx_path)
bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)
print('Saved .blend')