2
$\begingroup$

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?

$\endgroup$
5
  • $\begingroup$ Just curious, is there any particular reason you don't want to use opengl render? $\endgroup$
    – gandalf3
    Commented Mar 17, 2016 at 23:29
  • $\begingroup$ The openGL Render seems to only render the 3DView. But i also need the User interface in the exported sequence. $\endgroup$
    – Gaia Clary
    Commented Mar 18, 2016 at 0:18
  • 2
    $\begingroup$ 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. $\endgroup$
    – batFINGER
    Commented Mar 18, 2016 at 5:07
  • $\begingroup$ @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... $\endgroup$
    – Gaia Clary
    Commented Mar 18, 2016 at 12:16
  • $\begingroup$ 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 ;-) $\endgroup$
    – Samoth
    Commented Mar 18, 2016 at 14:10

2 Answers 2

1
$\begingroup$

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
        self._timer = wm.event_timer_add(0.1, context.window)
        wm.modal_handler_add(self)
        scene.frame_set(scene.frame_start)
        return {'RUNNING_MODAL'}
$\endgroup$
0
$\begingroup$

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)    
$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .