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$
  • $\begingroup$ Just curious, is there any particular reason you don't want to use opengl render? $\endgroup$ – gandalf3 Mar 17 '16 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 Mar 18 '16 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 Mar 18 '16 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 Mar 18 '16 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 Mar 18 '16 at 14:10
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$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.