0
$\begingroup$

Background & Overview

I've been working on an animation which involves a series of objects changing color via an object attribute material node controlled by a custom property controlled by a driver which utilizes a custom FloatProperty (glowStartTime) attached to each of the objects. This needs to be done over a controllable number of frames starting once a given control empty has been moved from its starting location, but otherwise irrespective of the control empty's animation.

As an example, I want to be able to tell these objects' attribute to change from 0 to 1 over 60 frames starting at whatever frame the control empty has been moved from its starting position on, irrespective of the movement of the control empty otherwise.

This works as expected in the viewport, but when rendering via f12 or the command line, the driver function doesn't seem to be able to read the updated glowStartTime, regardless of if the object reference in the driver has been evaluated_get(depsgraph)'d or not. Instead, it always sees it as the default value (-1, in this case)

Details

I've written a simple python function (calc_glow) which I've then placed into the driver namespace, and used as a driver expression to achieve this effect. At the most basic, it requires a dependency graph, an object (self), a factor (the z location of the control empty), and a duration (in the example file, 60 frames). I've then created a driver on the object property (Glow Factor) with the expression calc_glow(depsgraph, self, var, 60) where var is the z location of the control empty.

The control empty is keyframed to move from 0 on the z-axis to 1 on the z-axis between frames 11 to 13. On frame 12, the function stores the frame that the empty first had a nonzero z-axis value (in this case, frame 12) in glowStartTime, and calculates a 0 to 1 factor based on frame_current_final - glowStartTime / duration. The attribute in the node tree (reading from Glow Factor) is linked into a color ramp (Going from cyan to pink, just to be easier to see changes).

Watching this in the viewport preview, the object changes color from cyan to pink over the expected 60 frames, but when I render it as either a single frame or an animation, the drive seems unable to read the FloatProperty, and as a result stays pure cyan at any frame.

Blender default cube changing color from cyan to pink over around a second

Caveats & Problems

There are of course many easier ways to make a cube change color over time, but this is a deliberately pared down example which still exhibits the issue — I plan to use this effect on several linked collections of several dozen objects each starting at a different arbitrary frame than the others. In the actual scene, the control empties will be replaced with a control bone in a rig. The control bone will be responsible for several carefully timed events, and as such the location of the control cannot be directly responsible for the Glow Factor.

Additionally, I have looked checked the blender manual regarding properties, drivers, and the Gotchas page without finding anything that seems to indicate a solution. Using bpy.context.view_layer.update() or depsgraph.view_layer.update() in the driver causes blender to error out.

I've also found a number of blender bug reports which seem similar, though not identical (See here and here), which suggest using object.update_tag() or other similar things. These seems to have no effect inside of my driver function, or cause crashes or errors.

Most of my testing has been done in Blender 2.93 LTS because that's the version I work in normally, but in my limited testing the same effect can be observed in the (current as of 2022-07-31) stable version 3.2.1.

Script & Blend File

Here's a copy of the script & blender for reference. In the blend file, the numerous print statements, as well as test_handler1, 2, and 3, are all for debugging purposes and could easily be removed.

import bpy


def calc_glow(dp, obj, factor, duration):
    """Calculate glow length for obj of duration frames starting as soon as factor is > 0"""
    curFrame = dp.scene.frame_current_final
    
    evalObj = obj.evaluated_get(dp)
    
    obj.update_tag()
    evalObj.update_tag()    
        
    if (factor > 0) and (evalObj.glowStartTime == -1):
        obj.glowStartTime = curFrame
        retVal =  0
        
    elif (factor > 0):
        retVal = (curFrame - evalObj.glowStartTime) / duration
        
    else:
        obj.glowStartTime = -1
        retVal = 0
    
    obj.update_tag()
    evalObj.update_tag()
    return retVal


def register():
    if not hasattr(bpy.types.Object, 'glowStartTime'):
        print("no obj.glowStartTime, creating")
        bpy.types.Object.glowStartTime = bpy.props.FloatProperty(
            name = "Glow start frame",
            description = "Frame to start glow time calculation from",
            subtype = 'TIME',
            unit = 'TIME',
            default = -1)


    if 'calc_glow' in bpy.app.driver_namespace:
        del bpy.app.driver_namespace['calc_glow']
    bpy.app.driver_namespace['calc_glow'] = calc_glow


def unregister():
    
    if 'calc_glow' in bpy.app.driver_namespace:
        del bpy.app.driver_namespace['calc_glow']
    
    if hasattr(bpy.types.Object, 'glowStartTime'):
        del bpy.types.Object,glowStartTime


if __name__ == "__main__":
    register()

Conclusion

I am unsure of how to make drivers be capable of reading properties properly in rendered view, and any and all assistance would be greatly appreciated, thank you.

$\endgroup$
0

1 Answer 1

0
$\begingroup$

Happened across this answer which explains that the 'self' in the driver namespace is already an evaluated copy, and thus to modify the actual object (or its properties) one must use self.original.

The following code works in both the viewport and rendered images!

import bpy


def calc_glow(dp, obj, factor, duration):
    """Calculate glow length for obj of duration frames starting as soon as factor is > 0"""
    curFrame = dp.scene.frame_current_final
    
    objOrig = obj.original # 'self' in driver space is an evaluated copy! https://blender.stackexchange.com/a/229432
 
    if (factor > 0) and (objOrig.glowStartTime == -1):
        objOrig.glowStartTime = curFrame
        retVal =  0
        
    elif (factor > 0):
        retVal = (curFrame - objOrig.glowStartTime) / duration
        
    else:
        objOrig.glowStartTime = -1
        retVal = 0
    
    return retVal


def register():
    if not hasattr(bpy.types.Object, 'glowStartTime'):
        print("no obj.glowStartTime, creating")
        bpy.types.Object.glowStartTime = bpy.props.FloatProperty(
            name = "Glow start frame",
            description = "Frame to start glow time calculation from",
            subtype = 'TIME',
            unit = 'TIME',
            default = -1)

    if 'calc_glow' in bpy.app.driver_namespace:
        del bpy.app.driver_namespace['calc_glow']
    bpy.app.driver_namespace['calc_glow'] = calc_glow


def unregister():    
    if 'calc_glow' in bpy.app.driver_namespace:
        del bpy.app.driver_namespace['calc_glow']
    
    if hasattr(bpy.types.Object, 'glowStartTime'):
        del bpy.types.Object,glowStartTime


if __name__ == "__main__":
    register()
$\endgroup$

You must log in to answer this question.

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