tests & can charge up thrust

This commit is contained in:
2026-05-25 20:51:48 +03:00
parent bfb026c7ae
commit 6abf5355c2
10 changed files with 1022 additions and 15 deletions
+151
View File
@@ -0,0 +1,151 @@
"""
Blender 5.1 script: create walk + idle animations for matador_v02.
Run: blender --background matador_v02.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_v02.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')
+136
View File
@@ -0,0 +1,136 @@
"""
Blender 5.1 script: render preview frames of matador animations to PNG.
Run: blender --background matador_v02.blend --python render_anim_preview.py
Outputs to Blender/preview/ directory:
walk_frame_00.png .. walk_frame_09.png
idle_frame_00.png
Use this to visually verify bone orientations BEFORE exporting to Godot.
"""
import bpy
import os
import math
# ── Configuration ─────────────────────────────────────────────────────────────
PREVIEW_DIR = os.path.join(os.path.dirname(bpy.data.filepath), "preview")
RENDER_RES_X = 512
RENDER_RES_Y = 512
WALK_FRAMES = 10 # number of evenly-spaced frames to render from walk cycle
CAMERA_DISTANCE = 5.0
CAMERA_HEIGHT = 1.0
CAMERA_ANGLES = [0, 90] # degrees — front and side views
os.makedirs(PREVIEW_DIR, exist_ok=True)
# ── Find armature ─────────────────────────────────────────────────────────────
arm_obj = next((o for o in bpy.data.objects if o.type == 'ARMATURE'), None)
if arm_obj is None:
raise RuntimeError("No armature found in scene")
# ── Setup render settings ─────────────────────────────────────────────────────
scene = bpy.context.scene
scene.render.resolution_x = RENDER_RES_X
scene.render.resolution_y = RENDER_RES_Y
scene.render.resolution_percentage = 100
scene.render.film_transparent = True
scene.render.image_settings.file_format = 'PNG'
scene.render.image_settings.color_mode = 'RGBA'
scene.render.engine = 'BLENDER_EEVEE'
# ── Create or reuse camera ────────────────────────────────────────────────────
cam_data = bpy.data.cameras.get("PreviewCam") or bpy.data.cameras.new("PreviewCam")
cam_data.lens = 50
cam_obj = bpy.data.objects.get("PreviewCamObj")
if cam_obj is None:
cam_obj = bpy.data.objects.new("PreviewCamObj", cam_data)
scene.collection.objects.link(cam_obj)
scene.camera = cam_obj
# ── Create or reuse key light ─────────────────────────────────────────────────
light_data = bpy.data.lights.get("PreviewLight") or bpy.data.lights.new("PreviewLight", type='SUN')
light_data.energy = 3.0
light_obj = bpy.data.objects.get("PreviewLightObj")
if light_obj is None:
light_obj = bpy.data.objects.new("PreviewLightObj", light_data)
scene.collection.objects.link(light_obj)
light_obj.rotation_euler = (math.radians(45), math.radians(30), 0)
# ── Create or reuse fill light ────────────────────────────────────────────────
fill_data = bpy.data.lights.get("PreviewFill") or bpy.data.lights.new("PreviewFill", type='SUN')
fill_data.energy = 1.0
fill_obj = bpy.data.objects.get("PreviewFillObj")
if fill_obj is None:
fill_obj = bpy.data.objects.new("PreviewFillObj", fill_data)
scene.collection.objects.link(fill_obj)
fill_obj.rotation_euler = (math.radians(60), math.radians(-45), 0)
# ── Ensure armature is visible ────────────────────────────────────────────────
arm_obj.hide_render = False
arm_obj.hide_viewport = False
for child in arm_obj.children:
child.hide_render = False
child.hide_viewport = False
def position_camera(angle_deg: float) -> None:
"""Place camera at given horizontal angle around the armature center."""
rad = math.radians(angle_deg)
cam_obj.location = (
math.sin(rad) * CAMERA_DISTANCE,
CAMERA_HEIGHT,
math.cos(rad) * CAMERA_DISTANCE,
)
# Point camera at armature center (roughly hip height)
target = arm_obj.location.copy()
target.y = 0.9
direction = target - cam_obj.location
rot_quat = direction.to_track_quat('-Z', 'Y')
cam_obj.rotation_euler = rot_quat.to_euler()
def render_action(action_name: str, frame_count: int) -> None:
"""Render frame_count evenly-spaced frames of the named action."""
action = bpy.data.actions.get(action_name)
if action is None:
print(f"WARNING: Action '{action_name}' not found, skipping")
return
# Assign action
if arm_obj.animation_data is None:
arm_obj.animation_data_create()
arm_obj.animation_data.action = action
frame_start = int(action.frame_range[0])
frame_end = int(action.frame_range[1])
total_frames = max(frame_end - frame_start, 1)
for angle_deg in CAMERA_ANGLES:
position_camera(angle_deg)
angle_label = f"{angle_deg:03d}deg"
for i in range(frame_count):
t = i / max(frame_count - 1, 1) if frame_count > 1 else 0
frame = frame_start + int(t * total_frames)
scene.frame_set(frame)
filename = f"{action_name}_{angle_label}_frame_{i:02d}.png"
filepath = os.path.join(PREVIEW_DIR, filename)
scene.render.filepath = filepath
bpy.ops.render.render(write_still=True)
print(f" Rendered: {filename}")
# ── Render all animations ─────────────────────────────────────────────────────
print("=" * 60)
print("RENDERING ANIMATION PREVIEWS")
print("=" * 60)
render_action("walk", WALK_FRAMES)
render_action("idle", 1)
# ── Cleanup preview objects (optional — keep them for re-runs) ────────────────
print("=" * 60)
print(f"Preview images saved to: {PREVIEW_DIR}")
print(f"Total actions rendered: {len([a for a in ['walk', 'idle'] if bpy.data.actions.get(a)])}")
print("=" * 60)