""" 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)