TransWikia.com

How do I render with frame_change_pre handler active?

Blender Asked on December 12, 2021

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.

One Answer

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)

Answered by batFINGER on December 12, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP