4

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

onb
  • 93
  • 5

2 Answers2

5

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:

brockmann
  • 12,613
  • 4
  • 50
  • 93
2

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!

Markus von Broady
  • 36,563
  • 3
  • 30
  • 99
  • Funny, I understood the question in a completely different way. We'll see, UV'd. – brockmann 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. – Markus von Broady 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. – Markus von Broady May 19 '21 at 12:58