How to script the motion blur shutter curve?

I am trying to script the motion blur shutter curve (image below) for each frame of a render. I want to control the "openness" of the shutter curve with a list of values between 0 and 1. Ideally I would also like to be able to change this from one frame to the next within a single animation. I believe the variable I need to change is bpy.context.scene.render.motion_blur_shutter_curve. How do I convert a list of values to a curve and pass it to this variable? • Learned a LOT from both answers. Super helpful y'all, thanks a bunch.
– onb
May 21 '21 at 1:53

I'd suggest read the values and create a dictionary out of point positions per frame, which allows to structure as well as access the values in a reasonable way:

dict = {
frame_number : [
(LOC_X, LOC_Y),
(LOC_X, LOC_Y),
(LOC_X, LOC_Y),
...
]
frame_number : [
(LOC_X, LOC_Y),
(LOC_X, LOC_Y),
(LOC_X, LOC_Y),
...
]
}

One pitfall is that the minimum point count for a curve is 2, means that you can not remove all the points of any existing curve.

The following demo sets the shutter curve based on custom data. Notice that set_curve() is a slightly modified version of the reset_curve() function from this Q&A:

import bpy

frame_dict = {
1: [(0.0, 0.0), (0.07, 0.5), (0.25, 0.9), (0.5, 1.0), (0.75, 0.9), (0.92, 0.5),  (1.0, 0.0)],
2: [(0.0, 0.0), (0.125, 0.44), (0.375, 0.95), (0.5, 1.0), (0.625, 0.95), (0.875, 0.44), (1.0, 0.0)]
}

# Based on 'reset_curve()' from https://blender.stackexchange.com/a/64461/
def set_curve(curvemap, custom_points):

curve = curvemap.curves # Get the curve
# Remove redundant points
if len(curve.points) > len(custom_points):
n = 0
while len(curve.points) > len(custom_points):
curve.points.remove(curve.points[n])
n += 1
# Generate missing points
if len(curve.points) < len(custom_points):
n = 0
while len(curve.points) < len(custom_points):
curve.points.new(0.1, 0.5)
n +=1
# Set the values and update UI
for c, point in enumerate(curve.points):
point.location = custom_points[c]
curvemap.update() # Optional call

C = bpy.context
shutter_curve = C.scene.render.motion_blur_shutter_curve

# test call
set_curve(shutter_curve, frame_dict.get(2))

In order to set the curve and render each frame, go through the key-value pair of the dict and call Scene.frame_set(your_frame) per iteration to update the scene:

for frame, points in frame_dict.items():
# Set the curve
set_curve(shutter_curve, points)
# Set the frame
C.scene.frame_set(frame)
# Render the frame
bpy.ops.render.render(write_still=True)

Alternatively you can setup a frame_change_post handler which allows to preview and set your values on the fly, when changing the frame in the timeline (before rendering): def set_curve_frame(scene):
shutter_curve = scene.render.motion_blur_shutter_curve
if scene.frame_current in frame_dict:
set_curve(shutter_curve, frame_dict.get(scene.frame_current))

bpy.app.handlers.frame_change_post.append(set_curve_frame)

Related:

I think this works (after running, a new text block should appear with a script restoring the curves):

import bpy, json

D = bpy.data
curves = {}
for name, scene in D.scenes.items():
curve = scene.render.motion_blur_shutter_curve.curves
curves[name] = [(p.location.x, p.location.y, p.handle_type) for p in curve.points]

out = "curves = " + json.dumps(curves, indent=4)
out += '''

import bpy
from itertools import zip_longest

for scene_name, source in curves.items():
curve_wrapper = bpy.data.scenes[scene_name].render.motion_blur_shutter_curve
target = curve_wrapper.curves.points
for i in range(len(target) - len(source)):
target.remove(target[-1])
for oldp, newp in zip_longest(target, source):
if not oldp:
oldp = target.new(*newp[:2])  # probably can init with just 0, 0
oldp.location.x, oldp.location.y, oldp.handle_type = newp
curve_wrapper.update()
'''

text = D.texts.get(save_name)
if not text:
text = D.texts.new(save_name)
text.from_string(out)  # OVERWRITE!
• Funny, I understood the question in a completely different way. We'll see, UV'd. May 19 '21 at 11:44
• @brockmann when you run this script, it basically generates your script, though it seems your script uses old solutions that are battle-tested, while mine is fresh and perhaps buggy. May 19 '21 at 12:36
• OH and I didn't put anything for the animation. Also I noticed there's an option to add drivers, will test later if it actually works. May 19 '21 at 12:58