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
- Is there a better way to do this?
- Why doesn't
frame_change_pre
seem to work during renders? - Why did setting the frame twice seem to be necessary?
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:
- I'm using the Eevee render engine
- I'm running on Fedora 32 Linux
- My default F-curves are set for linear interpolation
- The background is set to use the Holdout shader (black under Eevee, transparent under Cycles)
- For Cycles renders, I'm using CUDA and a GeForce GTX 1060 6GB card
- System is Intel Core i7-6700 CPU @ 3.40GHz
- 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.