3
$\begingroup$

I am working on a Python-driven scientific animation using Blender 2.83.1. The animation includes four watt meters which are visually represented by Text objects. Each text object has a custom property named "kw" which stands for kilowatts and is the value I wish to display. The text objects are driven to show the corresponding value using a handler and animated with keyframes. The color of the text is also changed to red or green depending on the sign of the number.

The problem

When I scrub through the timeline it works just fine in preview. However, when I attempt to render it, the hander does not seem to do anything and the rendered frames are all identical.

The ugly workaround

After studying Execute Python script between rendering animation frames and Handler frame_change_pre doesn't work in render which seem to be describing the same problem, and finding that the proposed solutions there don't work for me, I came up with an ugly workaround shown in the code below as a Python routine called slow_render. For each frame, it sets the frame, temporarily reduces the animation length to a single frame and renders that animation. This is ugly, slow, and only works if output is a series of still images. Also, I found that I had to set the frame twice to get the intended effect. I don't know why.

Questions

  1. Is there a better way to do this?
  2. Why doesn't frame_change_pre seem to work during renders?
  3. Why did setting the frame twice seem to be necessary?

Screenshot

wattmeter screenshot

Code

The code I'm using to drive this is relatively simple. It has a handler called my_handler which uses the value of the custom kw property to alter the text body. It also has a main routine which installs the handler as a frame_change_pre handler and then sets up key frames for the significant events in this simulation. The meters are named meter.load, meter.solar, meter.batt and meter.net. The first three are driven from data in the script and the last is calculated as the negative net value of the other three. In other words meter.load + meter.solar + meter.batt = -meter.net.

import bpy

def my_handler(scene, depsgraph):
    for meter in bpy.data.objects.data.collections['meters'].objects:
        #meter = meter.evaluated_get(depsgraph)
        kw = meter.get('kw')
        color = 'delivered' if kw < 0 else 'received'
        meter.data.body = "{:+.1f} {}".format(kw, "kW")
        meter.active_material_index = 0
        meter.active_material = bpy.data.materials[color]
        print(scene.frame_current, meter.name, kw, color)
            
def slow_render():
    scene = bpy.context.scene
    lo, hi = scene.frame_start, scene.frame_end
    for f in range(lo, hi):    
        bpy.context.scene.frame_set(f)
        bpy.context.scene.frame_set(f)
        scene.frame_start = f
        scene.frame_end = f+1
        bpy.ops.render.render(animation=True)
    scene.frame_start, scene.frame_end = lo, hi

if __name__ == "__main__":
    alreadyInitialized = len(bpy.app.handlers.frame_change_pre)
    if not alreadyInitialized:
        bpy.app.handlers.frame_change_pre.append(my_handler)
        meter_name = ['meter.net', 'meter.load', 'meter.solar', 'meter.batt']
        # define the power for (load, solar, batt) at each point
        #         predawn    morning           noon           afternoon         evening      night
        kws = [0.2, 0, 0], [4.1, 0, 0], [2.7, -5.5, 2.8], [3.2, -0.9, -2.3], [5.9, 0, 0], [12.1, 0, 0]

        # calculate net value using list comprehension
        [m.insert(0, -sum(m)) for m in kws]
        # now assign values
        framenum = 1
        for pwr in kws:
            print("Frame: {}".format(framenum))
            for i in range(len(pwr)):
                bpy.context.scene.frame_set(framenum)
                meter = bpy.data.collections['meters'].objects[meter_name[i]]
                meter['kw'] = float(pwr[i])
                print("\t{} = {}".format(meter.name, pwr[i]))
                meter.keyframe_insert(data_path='["kw"]')
            framenum += 30
    else:                
        slow_render()

Other details

These are probably not relevant to the solution to this, but here are some additional details that may help reproduce exactly what I have here:

  1. I'm using the Eevee render engine
  2. I'm running on Fedora 32 Linux
  3. My default F-curves are set for linear interpolation
  4. The background is set to use the Holdout shader (black under Eevee, transparent under Cycles)
  5. For Cycles renders, I'm using CUDA and a GeForce GTX 1060 6GB card
  6. System is Intel Core i7-6700 CPU @ 3.40GHz
  7. The two materials used are simple Emission shaders; a red one called "delivered" and a green one called "received" which represent negative and positive numbers, respectively. Both have Fake Users so they will always exist.

Results

Based on the helpful feedback I received here, the video is complete.

$\endgroup$
6
  • 1
    $\begingroup$ Hi. Does this help? blender.stackexchange.com/questions/167551/… $\endgroup$ Commented Jul 15, 2020 at 14:01
  • $\begingroup$ Have you tried using frame change post? $\endgroup$
    – batFINGER
    Commented Jul 15, 2020 at 14:02
  • $\begingroup$ @RayMairlot: I read that too, and tried what I think it suggested (see commented out line in handler) but it didn't work for me. $\endgroup$
    – Edward
    Commented Jul 15, 2020 at 14:05
  • $\begingroup$ @batFINGER: yes, I've tried both post and pre and have the same problem either way. $\endgroup$
    – Edward
    Commented Jul 15, 2020 at 14:05
  • $\begingroup$ Ed Vid looks great. Love Kermit the Frog's narration. $\endgroup$
    – batFINGER
    Commented Jul 23, 2020 at 14:12

1 Answer 1

3
$\begingroup$

Use frame_change_post, and evaluated object

enter image description here

Test script.

Have added a collection to scene collection containing font objects with a "kw" custom property.

This property has been animated using keyframes.

To get the animated custom property value use the evaluated font object.

import bpy

def handler(scene, depsgraph):
    meters = scene.collection.children.get("meters")
    if meters:
        for m in meters.objects:
            print(m.name)
            em = m.evaluated_get(depsgraph)
            m.data.body = f"{em.get('kw', 0):4.2f}  {m.get('kw', 0):4.2f}"
    
#bpy.app.handlers.frame_change_post.clear()
bpy.app.handlers.frame_change_post.append(handler)
$\endgroup$
8
  • $\begingroup$ In your opinion, is this a duplicate of the question I mentioned? $\endgroup$ Commented Jul 15, 2020 at 14:56
  • $\begingroup$ I'm working on applying this to my project, but still don't understand, so I'm unable to fully duplicate it. Why post rather thanpre and why is the evaluated get only used to retrieve the value? $\endgroup$
    – Edward
    Commented Jul 15, 2020 at 15:01
  • $\begingroup$ @RayMairlot 50 / 50 above uses the evaluated objects (em) data, but sets the objects (m) data. m.data.body = str(em["kw"]) Prob should make more of using an object in the scene. Evaluating some object in bpy.data that is not linked to scene is probably not going to work as expected either. $\endgroup$
    – batFINGER
    Commented Jul 15, 2020 at 15:02
  • $\begingroup$ @Edward AFAIK the depsgraph passed to pre is always None. $\endgroup$
    – batFINGER
    Commented Jul 15, 2020 at 15:08
  • $\begingroup$ ... in other words its not yet evaluated for that frame. We are changing the body of the font object linked to the actual object, not that of its evaluated copy. eg em.data.body $\endgroup$
    – batFINGER
    Commented Jul 15, 2020 at 15:39

You must log in to answer this question.

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