4
$\begingroup$

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?

enter image description here

$\endgroup$
1
  • 2
    $\begingroup$ Learned a LOT from both answers. Super helpful y'all, thanks a bunch. $\endgroup$
    – onb
    Commented May 21, 2021 at 1:53

2 Answers 2

5
+50
$\begingroup$

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[0] # 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):

enter image description here

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:

$\endgroup$
2
$\begingroup$

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

import bpy, json

save_name = "load_blur_curve.py"

D = bpy.data
curves = {}
for name, scene in D.scenes.items():
    curve = scene.render.motion_blur_shutter_curve.curves[0]
    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[0].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!
$\endgroup$
3
  • $\begingroup$ Funny, I understood the question in a completely different way. We'll see, UV'd. $\endgroup$
    – brockmann
    Commented May 19, 2021 at 11:44
  • $\begingroup$ @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. $\endgroup$ Commented May 19, 2021 at 12:36
  • $\begingroup$ 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. $\endgroup$ Commented May 19, 2021 at 12:58

You must log in to answer this question.

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