# Stage 43: open her in the Blender GUI on a slow, endless turntable. # # blender --python 43_spin.py # # Runs inside the GUI session. Puts a linear 360-degree Z rotation on the body over SECONDS_PER_TURN # and starts playback; the scene loops, and because the last frame is the same pose as the first, # the turn is seamless and runs until you stop it. # # sync_mode is set to FRAME_DROP on purpose: left at the default, Blender plays this 32k mesh as # fast as the viewport can draw it, which is far faster than the fps you asked for. FRAME_DROP # makes playback honour real time, so "slow" actually means slow. import bpy import math SECONDS_PER_TURN = 50.0 FPS = 24 def setup(): ob = max([o for o in bpy.data.objects if o.type == 'MESH'], key=lambda o: len(o.data.vertices)) bpy.context.view_layer.objects.active = ob for o in bpy.data.objects: o.select_set(o is ob) scn = bpy.context.scene scn.render.fps = FPS scn.sync_mode = 'FRAME_DROP' n = int(round(SECONDS_PER_TURN * FPS)) scn.frame_start = 1 scn.frame_end = n # frame n+1 would repeat frame 1, so end at n scn.frame_set(1) # Blender 5.x uses slotted actions, so `action.fcurves` no longer exists and reaching the # curves means walking layers/strips/channelbags. Not needed: set the interpolation default # BEFORE inserting, and let the scene's loop supply the extrapolation. A constant-interpolated # turntable would visibly stutter, so LINEAR here is the whole point. bpy.context.preferences.edit.keyframe_new_interpolation_type = 'LINEAR' ob.animation_data_clear() ob.rotation_mode = 'XYZ' ob.rotation_euler = (0.0, 0.0, 0.0) ob.keyframe_insert("rotation_euler", index=2, frame=1) ob.rotation_euler = (0.0, 0.0, 2.0 * math.pi) ob.keyframe_insert("rotation_euler", index=2, frame=n + 1) for area in bpy.context.screen.areas: if area.type != 'VIEW_3D': continue sp = area.spaces.active sp.shading.type = 'MATERIAL' sp.shading.use_scene_lights = False sp.shading.use_scene_world = False sp.overlay.show_floor = False sp.overlay.show_axis_x = False sp.overlay.show_axis_y = False sp.overlay.show_text = False sp.overlay.show_cursor = False sp.overlay.show_object_origins = False sp.clip_start = 0.01 sp.clip_end = 100.0 for region in area.regions: if region.type == 'WINDOW': with bpy.context.temp_override(area=area, region=region): bpy.ops.view3d.view_axis(type='FRONT') bpy.ops.view3d.view_selected() # pull back a little: her arms swing toward camera through the turn bpy.ops.view3d.zoom(delta=-2) ob.select_set(False) # drop the selection outline for a clean look for window in bpy.context.window_manager.windows: scr = window.screen for area in scr.areas: if area.type == 'VIEW_3D': with bpy.context.temp_override(window=window, screen=scr, area=area): bpy.ops.screen.animation_play() break print(f"SPIN_READY {n} frames at {FPS} fps = {SECONDS_PER_TURN:.0f}s per turn", flush=True) return None bpy.app.timers.register(setup, first_interval=0.6)