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.