1
$\begingroup$

I animate with lots of holds in between each keyframe so there are lots of frames with no movement throughout my animation. I don't want to render a bunch of duplicate frames (I'd rather stretch them out in post), so I've devised a way of rendering only the frames which have keyframes using a batch script.

"C:\Program Files\Blender Foundation\Blender\blender.exe" -b "C:\Users\Me\Documents\Blendfile.blend" 
-f 1 -f 23 -f 24 -f 25 -f 26 -f 31 -f 32 -f 33 -f 34 -f 37 -f 38 -f 39 -f 45 -f 46 -f 47 -f 51 -f 52 -f 53 -f 59 -f 61 -f 71 -f 72 -f 73 -f 75 -f 76 -f 77 -f 83 -f 84 -f 85 -f 93 -f 94 -f 95 -f 98 -f 99 -f 100 -f 103 -f 104 -f 105 -f 110 -f 111 -f 112 -f 114 -f 115 -f 116 -f 122 -f 123 -f 124 -f 127 -f 128 -f 129 -f 130 -f 131 -f 132 -f 140 -f 141 -f 142 -f 143 -f 144 -f 145 -f 149 -f 150 -f 151 -f 152 -f 153 -f 154 -f 156 -f 157 -f 158 -f 161 -f 162 -f 163 -f 169 -f 170 -f 171 -f 175 -f 176 -f 177 -f 178 -f 179 -f 180 -f 181 -f 182 -f 183 -f 192 -f 193 -f 194 -f 196 -f 198 -f 200 -f 202 -f 204 -f 206 -f 208 -f 210 -f 211 -f 212 -f 213 -f 217 -f 218 -f 219 -f 226 -f 227 -f 229 -f 234 -f 235 -f 236 -f 237 -f 238 -f 239 -f 240 -f 244 -f 245 -f 246 -f 249 -f 250 -f 251 -f 253 -f 254 -f 255 -f 264 -f 265 -f 266 -f 267 -f 268 -f 275 -f 276 -f 277 -f 279 -f 280 -f 281 -f 282 -f 285 -f 286 -f 287 -f 296 -f 297 -f 298 -f 308 -f 309 -f 310 -f 316 -f 317 -f 318 -f 319 -f 323 -f 324 -f 325 -f 332 -f 333 -f 334 -f 343 -f 344 -f 345 -f 352 -f 353 -f 354 -f 356 -f 357 -f 358 -f 361 -f 362 -f 363 -f 367 -f 368 -f 369 -f 370 -f 371 -f 372

Currently I'm manually populating this script with the frame numbers. However, I know that Blender keeps the data of all the keyframes (such as at the top of the Dope Sheet).

Blender Dope Sheet

So my dilemma is, how can I write a Python script that collects this data for me? Does anyone know how to tap into this Super Sheet Summary or some Blender internal function to grab the frame numbers of the frames which have keyframes on them?

All help would be greatly appreciated! Thanks!

$\endgroup$
2
$\begingroup$

Graph editor Operator (for version 2.79.4 and above)

Set up the selections on your graph editor to match those of the dope sheet.

enter image description here

Since version 2.79.4 the graph editor has the very handy context.selected_editable_fcurves property. The operator below uses each of these selected fcurves. Render frames are added to a list sequentially from frame start to end if there is any change (over a tolerance) in the evaluated fcurve between frames current, f, and previous f-1.

The render command line is written to a text block enter image description here

To use, run the script (to register) Use the space bar and search for enter image description here "Report Changed Frames" in the graph editor space. Remember it uses the selected fcurves, press A before running to select all visible in current graph editor selection.

import bpy


def main(context):
    scene = context.scene
    selected = context.selected_editable_fcurves[:]
    # all selected fcurves
    f = scene.frame_start
    render_frames = [f]
    f += 1
    while f <= scene.frame_end:
        def changed(fc, tol=0.0001):
            return abs(fc.evaluate(f) - fc.evaluate(f - 1)) > tol

        if any(changed(fc) for fc in selected):
            render_frames.append(f)
        f += 1

    report = bpy.data.texts.new("Changed Frames")

    report.write('"%s" -b "%s" ' % (bpy.app.binary_path, bpy.data.filepath)) 
    for f in render_frames:
        report.write(" -f %d" % f)

class GRAPH_OT_report_frames(bpy.types.Operator):
    """Tooltip"""
    bl_idname = "graph.report_frames"
    bl_label = "Report changed frames"

    @classmethod
    def poll(cls, context):
        return context.area.type == 'GRAPH_EDITOR'

    def execute(self, context):
        main(context)
        return {'FINISHED'}

def register():
    bpy.utils.register_class(GRAPH_OT_report_frames)

def unregister():
    bpy.utils.unregister_class(GRAPH_OT_report_frames)

if __name__ == "__main__":
    register()

As stated this doesn't list all frames with a keyframe, rather it lists all frames that have some animated change on that frame, (for any of all selected fcurves).

If you are just after keyframe frames, add all kfp.co.x (keyframe point frame) points to a list, for all selected fcurves. Converting to a set s = set() will remove doubles.

all_frames = set(kfp.co.x 
        for fc in context.selected_editable_fcurves
        for kfp in fc.keyframe_points)

Oldschool (all versions)

You can also iterate through all actions and check the state of Keyframe.select_control_point property to get the corresponding frame of all keys in the selection.

enter image description here

import bpy
from math import ceil


class ACTION_OT_report_selection(bpy.types.Operator):
    """Report Selected Keyframes"""
    bl_idname = "action.report_selected_keyframes"
    bl_label = "Report Selected Keyframes"

    def selected_keys(self):
        """Returns the frames of all selected keys""" 
        ctrl_points = set()
        for action in bpy.data.actions:
            for channel in action.fcurves: 
                for key in channel.keyframe_points:       
                    if key.select_control_point:
                        ctrl_points.add(ceil(key.co.x))
        return sorted(ctrl_points)

    def cli_block(self, frame_list, block_name):
        """Creates a new textblock with all necessary command line arguments"""
        report = bpy.data.texts.new(block_name)
        report.write('"%s" -b "%s" ' % (bpy.app.binary_path, bpy.data.filepath)) 
        for f in frame_list:
            report.write(" -f %d" % f)

    @classmethod
    def poll(cls, context):
        area_types = ('DOPESHEET_EDITOR', 'GRAPH_EDITOR', 'TIMELINE')
        return context.area.type in area_types

    def execute(self, context):
        render_frames = self.selected_keys()

        if not render_frames:
            self.report({'ERROR'}, "Nothing selected")
            return {'CANCELLED'}
        else:
            textblock_name = "Render Arguments"
            self.cli_block(render_frames, textblock_name)
            self.report({'INFO'}, "'%s' text block created" % textblock_name)

        return {'FINISHED'}


def register():
    bpy.utils.register_class(ACTION_OT_report_selection)

def unregister():
    bpy.utils.unregister_class(ACTION_OT_report_selection)

if __name__ == "__main__":
    register()
$\endgroup$
  • $\begingroup$ Thanks for the answer! Unfortunately, no matter what i do, I always get this error. I've tried selecting all in all windows to no avail. Traceback (most recent call last): File "C:\Users\Me\Blenderfile.blend\Text.py", line 35, in execute File C:\Users\Me\Blenderfile.blend\Text.py", line 6, in main AttributeError: 'Context' object has no attribute 'selected_editable_fcurves' $\endgroup$ – rioforce May 17 '18 at 23:59
  • $\begingroup$ I'm using 2.79b, which is why I commented. $\endgroup$ – rioforce May 19 '18 at 0:04
  • $\begingroup$ Docs for 2.79.4 thought it was earlier Commit (August 2017) See version via bpy.app.version $\endgroup$ – batFINGER May 19 '18 at 2:51

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.