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 May 21 at 1:53
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 May 19 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$ – Markus von Broady May 19 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$ – Markus von Broady May 19 at 12:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.