137 lines
5.9 KiB
Python
137 lines
5.9 KiB
Python
"""
|
|
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)
|