# Create a screencast with exactly one image per animation frame

I need to export an animation that runs in the 3D Viewport as a sequence of screenshots with one image per frame. So i created a small class :

class MyAnimationCast(bpy.types.Operator):

bl_idname = "my.animation_cast"
bl_label = "Animation to Image sequence"
bl_description = "Capture an Animation from 3D View to image sequence"

def execute(self, context):
scene   = context.scene
folder = scene.render.filepath
format = scene.render.image_settings.file_format.lower()

for frame in range (scene.frame_start, scene.frame_end):
filepath = os.path.join(folder,"%04d.%s" % (frame, format))
print("Render to file", filepath)

scene.frame_set(frame)
scene.update()

bpy.ops.screen.screenshot(
filepath=filepath,
check_existing=False)

return{'FINISHED'}


This class creates the expected number of images at the expected location. But all images contain the frame that i can see when i call the operator. What is missing in the code?

• Just curious, is there any particular reason you don't want to use opengl render?
– gandalf3
Mar 17, 2016 at 23:29
• The openGL Render seems to only render the 3DView. But i also need the User interface in the exported sequence. Mar 18, 2016 at 0:18
• I doubt the UI is being updated running the operator as shown, ie the scene.update() will update the data, not the interface. A tag redraw on all areas may give you the desired result, or run from a modal operator, (render frame change frame wait (maybe)), or frame_change handler. There is a snazzy example of handling events while running a process by ideasman42 will post link. Mar 18, 2016 at 5:07
• @batfinger: ah, yes. I now see why this can not work. I just got the idea to enable an on frame change handler which would export a screenshot of the curent frame at each call. The operator could be split into 2: "enable frame dump" and "disable frame dump" for adding/removing the handler. Then i just need to run the animation and all gets captured... Mar 18, 2016 at 12:16
• Use the search function - there has already been an answer to this problem, but I don't exactly know the exact keywords right now to find it ;-) Mar 18, 2016 at 14:10

My preferred method would be to use a frame_change handler and an update method on a bool to turn it on off.

import bpy
import os

from bpy.props import BoolProperty

def remove_handler_function(handler, name):
''' Removes all functions with name starting with name '''
fns = [f for f in handler
if f.__name__.startswith(name)]
for f in fns:
handler.remove(f)

def screenshot(scene):
frame = scene.frame_current
in_frame =  scene.frame_start <= frame <= scene.frame_end
if not in_frame:
return None

folder = scene.render.filepath
format = scene.render.image_settings.file_format.lower()
filepath = os.path.join(folder,"%04d.%s" % (frame, format))
print("render %d" % frame)
bpy.ops.screen.screenshot(
filepath=filepath,
check_existing=False)

def capture_screen(self, context):
# turn on the handler
handler = bpy.app.handlers.frame_change_post
if self.render_screencast:
handler.append(screenshot)
else:
remove_handler_function(handler, "screenshot")

bpy.types.Scene.render_screencast = BoolProperty(default=False,
name="Screen Shot Cast",
update=capture_screen)

#testing
bpy.context.scene.render_screencast = False


My testing shows that the operator will do as desired with

        bpy.ops.screen.screenshot(
filepath=filepath,
check_existing=False)
for area in context.screen.areas:
area.tag_redraw()


but it will lock up blender until finished. Pretty sure there is no need to call scene.update() after scene.frame_set(f), and not sure how much this effects performance, if at all.

Another is a modal timer operator, which enables the chance to cancel. (The handler is much better). The link of @ideasman42 I mentioned is here How to handle modal operator events in a single loop? Which may make handling canceling easier.

class ModalTimerOperator(bpy.types.Operator):
"""Operator which runs its self from a timer"""
bl_idname = "wm.modal_timer_operator"
bl_label = "Modal Timer Operator"
wait = 10
_timer = None
renderframe = -1
def modal(self, context, event):
scene = context.scene
rendering = self.renderframe == scene.frame_current
folder = scene.render.filepath
format = scene.render.image_settings.file_format.lower()

if event.type in {'RIGHTMOUSE', 'ESC'} or scene.frame_current > scene.frame_end:
self.cancel(context)
return {'CANCELLED'}
if event.type == 'TIMER' and self.wait:
self.wait -= 1
return {'PASS_THROUGH'}

if rendering:
return {'PASS_THROUGH'}

else:
self.renderframe = scene.frame_current

print("render %d" % scene.frame_current)
self.report({'INFO'}, "Render: %4d" % scene.frame_current)
filepath = os.path.join(folder,"%04d.%s" % (scene.frame_current, format))
bpy.ops.screen.screenshot(
filepath=filepath,
check_existing=False)

scene.frame_set(scene.frame_current + 1)
self.wait = 10
for area in context.screen.areas:
area.tag_redraw()

return {'PASS_THROUGH'}

def execute(self, context):
scene = context.scene
wm = context.window_manager
scene.frame_set(scene.frame_start)
return {'RUNNING_MODAL'}


Here is a variation of Answer 1. I simply add/remove the screenshot frame handler based on a property that i can set from within the toolshelf (for example). The cool thing about this is: i do not need to think about escaping from a render. I just start playing the animation and when i made a mistake somewhere in the animation, i just fix it and replay that part of the animation again.

This solution has a caveat as it records frames upon Entering and not upon Leaving the frame. However those pitfalls can be fixed with a bit of extra work.

@batFINGER: Thanks for the inspiration :)

# This is the Frame handler (called on frame change)
def screenshot_frame_handler(scene):
frame = scene.frame_current
in_frame =  scene.frame_start <= frame <= scene.frame_end
if not in_frame:
return None

folder = scene.render.filepath
format = scene.render.image_settings.file_format.lower()
filepath = os.path.join(folder,"%04d.%s" % (frame, format))
print("render %d" % frame)
bpy.ops.screen.screenshot(
filepath=filepath,
check_existing=False)

#Enable/disable frame handler as needed
def manage_handler_function(handler, func, disable):
''' Removes all functions with name starting with name '''
fns = [f for f in handler if f.__name__.startswith(func.__name__)]
if disable:
for f in fns:
print("remove handler", f.__name__)
handler.remove(f)
elif len(fns) == 0:
print("append handler", func.__name__)
handler.append(func)

#Called on update AnimationProps.animation_cast
def manage_screenshot_handler(self, context):
disable  = not context.scene.AnimationProps.animation_cast
handler = bpy.app.handlers.frame_change_post
manage_handler_function(handler, screenshot_frame_handler, disable)

class AnimationProps(bpy.types.PropertyGroup):
#Enable animation casting when this property is set to True
animation_cast = BoolProperty(
default=False,
name = "Animation Cast",
description = "Enable automatic Snapshot on Frame Change",
update = manage_screenshot_handler
)

bpy.types.Scene.AnimationProps = PointerProperty(type=AnimationProps)