17

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?

David
  • 49,291
  • 38
  • 159
  • 317
gandalf3
  • 157,169
  • 58
  • 601
  • 1,133

3 Answers3

19

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.

Ulf Aslak
  • 541
  • 3
  • 13
  • related: http://blender.stackexchange.com/questions/1718/is-it-possible-to-render-only-keyframes-from-dope-sheet/23196#23196 – p2or Apr 26 '15 at 14:30
  • 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. – p2or May 04 '15 at 06:41
  • 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. – Ulf Aslak May 04 '15 at 08:57
  • Can you provide a test scene as starting point? I'll try test it and implement shutil in the next days. – p2or May 04 '15 at 09:19
  • 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. – Mutant Bob May 04 '15 at 13:16
  • @poor I'll get to it once I can. – Ulf Aslak May 04 '15 at 13:26
  • @MutantBob That sounds like it beats the purpose of outputting the exact same files as a normal render would, or am I missing your point? – Ulf Aslak May 04 '15 at 13:26
  • The tactic I suggested would allow you to save some disk space and avoid spawning a subprocess, although the extra code to create VSE strips is somewhat burdensome. For a scene where there was motion from 1-100, still from 101-150 and motion from 151-200 you would create 1 strip with frames 1-100, then a still image spanning 101-150, and another strip for frames 151-200. You would still have to perform the logic that figures out where the stills are. – Mutant Bob May 04 '15 at 18:20
  • The answer mentions "iOS". Do you mean OS X / macOS? – SilverWolf Feb 25 '19 at 03:15
8

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)
rioforce
  • 2,042
  • 2
  • 22
  • 34
  • 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. – rioforce May 26 '18 at 04:47
  • 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. – Justin Helps Jun 29 '18 at 23:19
  • I found a script on this thread: https://blender.stackexchange.com/questions/15649/can-frames-with-no-animation-be-automatically-skipped/63241#63241?newreg=c98392162b80427cb3cd4c674af6790a 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? – Emil Andersen Aug 22 '18 at 23:31
  • 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. – Ulf Aslak Jul 02 '20 at 11:12
2

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.

Mutant Bob
  • 9,243
  • 2
  • 29
  • 55