17
$\begingroup$

I have an animation with lots of text objects flying about.

I'd like to render it with motion blur and stuff, but there are quite a few long sections where nothing is moving so that the text can be read. In these sections the render output (should) remain exactly the same.

Since it's pretty pointless to render the same image over and over, is there some way to automatically detect and skip those sections, only rendering one frame until there are more keyframes?

$\endgroup$
1

3 Answers 3

19
$\begingroup$

I had the same problem, solved it with a bit of code. This script renders your animation frame for frame, skipping frames with no animation and adding them to render destination as copies of their equivalents.

This gives you the same result as a normal animation render, and saves you a lot of processing time.

The script does the following:

  • Take each animated object and store its F-Curve values at each frame in the animation.

  • Create list of still frames, by comparing differences in neighbouring F-Curve values for each frame in each animated object.

  • Render frames in desired range, skipping each still frame.

  • Fill in skipped frames with copies of their equivalents.

      import bpy
      import subprocess
    
      def render_with_skips(start, stop):
          """
          Take start and stop, and render animation only for animated
          frames. Still frames, are substituted into the output folder
          as copies of their equivalents.
          """
    
          render_range = list(range(start, stop))
    
          # create JSON like dictionary to store each
          # animated object's fcurve data at each frame.
          all_obj_fcurves = {}
          for obj in bpy.data.objects:    
              obj_fcurves = {}
    
              try:
                  obj.animation_data.action.fcurves
              except AttributeError:
                  print("--|'%s' is not animated" % obj.name)
                  continue
    
              print("\n--> '%s' is animated at frames:" % obj.name)
    
              for fr in list(range(start,stop+1)):
                  fc_evals = [c.evaluate(fr) for c in obj.animation_data.action.fcurves]
                  obj_fcurves.update({int(fr): fc_evals})
                  print(fr, end=", ")
              print()
    
              all_obj_fcurves.update({obj.name: obj_fcurves})
    
    
          # loop through each animated object and find its
          # animated frames. then remove those frames from
          # a set containing all frames, to get still frames.
          still_frames = set(render_range)
          for obj in all_obj_fcurves.keys():
              obj_animated_frames = []
              for i, fr in enumerate(sorted(all_obj_fcurves[obj].keys())):
                  if i != 0:
                      if all_obj_fcurves[obj][fr] != all_obj_fcurves[obj][fr_prev]:
                          obj_animated_frames.append(fr)
                  fr_prev = fr
    
              still_frames = still_frames - set(obj_animated_frames)
    
          print("\nFound %d still frames" % len(still_frames))
          print(sorted(still_frames), end="\n\n")
    
    
          # render animation, skipping the still frames and
          # filling them in as copies of their equivalents
          filepath = bpy.context.scene.render.filepath
    
          for fr in render_range:
              if fr not in still_frames or fr == render_range[0]:
                  bpy.context.scene.frame_set(fr)
                  bpy.context.scene.render.filepath = filepath + '%04d' % fr
                  bpy.ops.render.render(write_still=True)
              else:
                  scene = bpy.context.scene
                  abs_filepath = scene.render.frame_path(scene.frame_current)
                  abs_path = '/'.join(abs_filepath.split('/')[:-1]) + '/'
                  print("Frame %d is still, copying from equivalent" % fr)
                  subprocess.call(['cp', abs_path + '%04d.png' % (fr-1), abs_path + '%04d.png' % fr])
    
          bpy.context.scene.render.filepath = filepath
    
      start = bpy.data.scenes['Scene'].frame_start
      end = bpy.data.scenes['Scene'].frame_end
      render_with_skips(start,end)
    

I should stress that this will only work if you do not have anything else moving around at the times when everything else in your camera view is still.

NOTE At this moment the code is only Linux/OSX compatible because it uses a subprocess to copy frames which is not Windows compatible. MarcHorstmanns's answer below implements my code so it works with Windows.

$\endgroup$
9
  • $\begingroup$ related: blender.stackexchange.com/questions/1718/… $\endgroup$
    – p2or
    Commented Apr 26, 2015 at 14:30
  • $\begingroup$ First thanks for sharing this. Maybe it's a good idea to mention that this is linux only, because of subprocess.call(["cp" .. ]). Might be better to replace this with shutil module to be os independent like in the linked script above. $\endgroup$
    – p2or
    Commented May 4, 2015 at 6:41
  • $\begingroup$ Oh shoot, I did not think about this AT ALL. You are very welcome to edit the post in order to make the code os independent. $\endgroup$
    – Ulf Aslak
    Commented May 4, 2015 at 8:57
  • $\begingroup$ Can you provide a test scene as starting point? I'll try test it and implement shutil in the next days. $\endgroup$
    – p2or
    Commented May 4, 2015 at 9:19
  • $\begingroup$ Rather than copy the frames, another possibility would be to use the VSE and create strips for the bits with motion and stills for the bits without motion. Another option would be a speed control strip effect strip to stretch out the still parts. $\endgroup$
    – Mutant Bob
    Commented May 4, 2015 at 13:16
8
$\begingroup$

For Windows:

import bpy
import subprocess
import os
import shutil

def render_with_skips(start, stop):
    """
    Take start and stop, and render animation only for animated
    frames. Still frames, are substituted into the output folder
    as copies of their equivalents.
    """

    render_range = list(range(start, stop))

    # create JSON like dictionary to store each
    # animated object's fcurve data at each frame.
    all_obj_fcurves = {}
    for obj in bpy.data.objects:    
        obj_fcurves = {}

        try:
            obj.animation_data.action.fcurves
        except AttributeError:
            print("--|'%s' is not animated" % obj.name)
            continue

        print("\n--> '%s' is animated at frames:" % obj.name)

        for fr in list(range(start,stop+1)):
            fc_evals = [c.evaluate(fr) for c in obj.animation_data.action.fcurves]
            obj_fcurves.update({int(fr): fc_evals})
            print(fr, end=", ")
        print()

        all_obj_fcurves.update({obj.name: obj_fcurves})


    # loop through each animated object and find its
    # animated frames. then remove those frames from
    # a set containing all frames, to get still frames.
    still_frames = set(render_range)
    for obj in all_obj_fcurves.keys():
        obj_animated_frames = []
        for i, fr in enumerate(sorted(all_obj_fcurves[obj].keys())):
            if i != 0:
                if all_obj_fcurves[obj][fr] != all_obj_fcurves[obj][fr_prev]:
                    obj_animated_frames.append(fr)
            fr_prev = fr

        still_frames = still_frames - set(obj_animated_frames)

    print("\nFound %d still frames" % len(still_frames))
    print(sorted(still_frames), end="\n\n")


    # render animation, skipping the still frames and
    # filling them in as copies of their equivalents
    filepath = bpy.context.scene.render.filepath

    for fr in render_range:
        if fr not in still_frames or fr == render_range[0]:
            bpy.context.scene.frame_set(fr)
            bpy.context.scene.render.filepath = filepath + '%04d' % fr
            bpy.ops.render.render(write_still=True)
        else:
            scene = bpy.context.scene
            abs_filepath = scene.render.frame_path(scene.frame_current)
            #abs_path = '/'.join(abs_filepath.split('/')[:-1]) + '/'
            print("Frame %d is still, copying from equivalent" % fr)
            scn = bpy.context.scene
            shutil.copyfile(filepath + '%04d.png' % (fr-1), filepath + '%04d.png' % fr)

    bpy.context.scene.render.filepath = filepath

start = bpy.data.scenes['Scene'].frame_start
end = bpy.data.scenes['Scene'].frame_end
render_with_skips(start,end)
$\endgroup$
4
  • $\begingroup$ Thanks for this! This is exactly what I've been looking for for so long! I edited the post because adding the + 'png' actually causes the script to fail in Blender, but the original poster's script without the edits by hazzey works perfectly. $\endgroup$
    – rioforce
    Commented May 26, 2018 at 4:47
  • $\begingroup$ I know comments that mainly say thanks are frowned upon, but I just tested this, and it works perfectly for me. It's going to save me so much time. No more tradeoff between saving render time and saving video editing time. $\endgroup$ Commented Jun 29, 2018 at 23:19
  • $\begingroup$ I found a script on this thread: blender.stackexchange.com/questions/15649/… It should help skip identical frames in Blender. But Blender crashes when i load the original script for Windows, and it fails to load the script in the two edited version.... someone to my rescue? $\endgroup$ Commented Aug 22, 2018 at 23:31
  • $\begingroup$ Thanks for making this work with Windows! It would have been nice of you to briefly state that this is a Windows compatible version of my code but neverthness I appreciate you taking the effort to make this update. $\endgroup$
    – Ulf Aslak
    Commented Jul 2, 2020 at 11:12
2
$\begingroup$

Automatically, no. You might want to duplicate the scene with Linked objects. Then have each scene render a different frame range which you determine manually. You can then stitch the rendered frames together using yet another scene that uses the VSE (with no 3D objects) and Image Strips.

$\endgroup$

You must log in to answer this question.

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