# Driver using self does not update location in render only

I am writing a driver in blender 2.91.2 to simulate a spacecraft's Reaction Control System thrusters based on the movement of a parent object, without having to manually adjust and keyframe each "RCS" object or work with actual physics simulation.

I'm attempting to do this by using a calculated acceleration of each driven child object to determine a scale factor for its z-axis. To calculate the acceleration I am attempting to store previous frame's locations in custom properties of each object.

I am using the following script as a driver expression.

import bpy
from mathutils import Matrix, Vector, Euler

def update_matrices(obj):
if obj.parent is None:
obj.matrix_world = obj.matrix_basis

else:
obj.matrix_world = obj.parent.matrix_world.copy() @ \
obj.matrix_parent_inverse.copy() @ \
obj.matrix_basis.copy()

def rcs_calc(obj):

#update_matrices(obj) # Update stale data, doesn't work. Does change data erratically, occasionally.

# Get current location of each driven object. Doesn't seem to update when being rendered.
currentLocation = obj.matrix_world.to_translation()

# Get locations from object properties
Loc2 = Vector(obj.prevLoc2)
Loc1 = Vector(obj.prevLoc1)

# Calculate two velocity samples
vecVelocity2 = (Loc1 - Loc2)
vecVelocity1 = (currentLocation - Loc1)
#print(vecVelocity1)

# Calculate acceleration sample
vecAccel = (vecVelocity1 - vecVelocity2)

#print(vecAccel)

# Assign old locations, seems to never update...
obj.prevLoc2 = obj.prevLoc1
obj.prevLoc1 = currentLocation
# Update stale data? # Force update, see https://developer.blender.org/T63793#881438, does not work
obj.update_tag()

# Get direction
offsetAmount = Vector([0,0,1])
offsetGlobal = obj.matrix_world.to_3x3() @ offsetAmount
offsetObj = obj.matrix_world.to_translation().copy() + offsetGlobal

direction = offsetObj - obj.matrix_world.translation
#direction = obj.matrix_world.to_quaternion().copy() @ offsetAmount # Same result as above

#Invert direction so that plumes 'push' rather than 'pull'
direction *= -1

# Scale by dot product
scaleFactor = direction.normalized().dot(vecAccel.normalized())

debug = False
# Outputs text of most variables into the file 'scaleFactors' in the current directory, once per frame.
# File is never automatically cleared, be careful to avoid it getting too large!
if obj.name == "RCS.Front.225d" and debug == True:
path = './'
formatStr = ("f:{:03d}, name:{:s} \n---\n" +
"currentLocation: {:6.3f} {:6.3f} {:6.3f}, Loc1: {:6.3f} {:6.3f} {:6.3f}, Loc2: {:6.3f} {:6.3f} {:6.3f} \n" +
"vecVelocity1: {:6.3f} {:6.3f} {:6.3f}, vecVelocity2: {:6.3f} {:6.3f} {:6.3f} \n" +
"offsetGlobal: {:6.3f} {:6.3f} {:6.3f}, offsetObj: {:6.3f} {:6.3f} {:6.3f} \n" +
"vecAccel: {:6.3f} {:6.3f} {:6.3f}, direction: {:6.3f} {:6.3f} {:6.3f} \n" +
"scaleFactor: {:6.3f}\n\n")
with open(path + "scaleFactors.txt", "a") as file:
file.write(formatStr.format(bpy.context.scene.frame_current, obj.name,
currentLocation.x, currentLocation.y, currentLocation.z, Loc1.x, Loc1.y, Loc1.z, Loc2.x, Loc2.y, Loc2.z,
vecVelocity1.x, vecVelocity1.y, vecVelocity1.z, vecVelocity2.x, vecVelocity2.y, vecVelocity2.z,
offsetGlobal.x, offsetGlobal.y, offsetGlobal.z, offsetObj.x, offsetObj.y, offsetObj.z,
vecAccel.x, vecAccel.y, vecAccel.z, direction.x, direction.y, direction.z, scaleFactor))

#bpy.context.view_layer.depsgraph.update() # Update scene to prevent stale data. Causes RuntimeError

if scaleFactor < 0.25:
# Can't be zero or offsetGlobal will become 0
return 0.01
elif scaleFactor < 0.5:
return 0.5
else:
return 0.999

# Create object custom properties.
bpy.types.Object.prevLoc1 = bpy.props.FloatVectorProperty(
name='Previous Location',
description='Location -1 frames ago',
subtype='TRANSLATION',
options={'LIBRARY_EDITABLE', 'ANIMATABLE'})
bpy.types.Object.prevLoc2 = bpy.props.FloatVectorProperty(
name='Previous Previous Location',
description='Location -2 frames ago',
subtype='TRANSLATION',
options={'LIBRARY_EDITABLE', 'ANIMATABLE'})

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


The desired result is that each driven RCS object scales with the dot product of its direction and acceleration vector so that it appears as if the RCS object is a rocket motor pushing the main keyframed object. This works almost flawlessly in the viewport, but when rendered, it does not. When rendered, the RCS objects rarely scale at all, and if they do, they do so in ways inconsistent with the viewport display.

From what I can tell from debugging by printing all the variables in the script into a file each frame, it is because obj.matrix_world.to_translation() does not update when rendering the file. Additionally, the two custom object properties (obj.prevLoc1 and obj.prevLoc2) do not seem to update either. I have very little idea as to why this is occurring.

I have found a handful of reports online of similar issues in which objects driven by their own transform channels fail to render properly, but none of them have had resolutions that have worked for me, if they had solutions at all.

### Things I've tried, without success:

• Using object.update_tag() inside the driver as per this bug report. This appears to have no effect.
• Using object.evaluated_get(depsgraph) inside the driver to get values, instead of object.matrix_world.to_translation(). This causes blender to segfault when the script is run.
• Parenting each RCS object to its own corresponding empty, then modifying the script to use the empties' transform channels, instead of self. This doesn't appear to change anything, and is far more clunky to manage and set up for dozens of RCS objects.
• A lot of manual tweaking of values, as well as inspecting objects in the blender console. This was helpful in determining that values weren't updating.

As an example of what I mean, here are two samples of the debug files. The first was generated by playing the animation in the viewport, and the second by rendering from the terminal.

Viewport:

f:005, name:RCS.Front.225d
---
currentLocation: -0.622 -2.700 -0.622, Loc1: -0.630 -2.700 -0.630, Loc2: -0.635 -2.700 -0.635
vecVelocity1:  0.008  0.000  0.008, vecVelocity2:  0.005  0.000  0.005
offsetGlobal: -0.706  0.000 -0.706, offsetObj: -1.328 -2.700 -1.328
vecAccel:  0.003  0.000  0.003, direction:  0.706 -0.000  0.706
scaleFactor:  1.000

f:006, name:RCS.Front.225d
---
currentLocation: -0.611 -2.700 -0.611, Loc1: -0.622 -2.700 -0.622, Loc2: -0.630 -2.700 -0.630
vecVelocity1:  0.011  0.000  0.011, vecVelocity2:  0.008  0.000  0.008
offsetGlobal: -0.706  0.000 -0.706, offsetObj: -1.317 -2.700 -1.317
vecAccel:  0.003  0.000  0.003, direction:  0.706 -0.000  0.706
scaleFactor:  1.000

f:007, name:RCS.Front.225d
---
currentLocation: -0.597 -2.700 -0.597, Loc1: -0.611 -2.700 -0.611, Loc2: -0.622 -2.700 -0.622
vecVelocity1:  0.014  0.000  0.014, vecVelocity2:  0.011  0.000  0.011
offsetGlobal: -0.706  0.000 -0.706, offsetObj: -1.303 -2.700 -1.303
vecAccel:  0.003  0.000  0.003, direction:  0.706 -0.000  0.706
scaleFactor:  1.000


Rendered:

f:005, name:RCS.Front.225d
---
currentLocation: -0.636 -2.700 -0.636, Loc1:  0.000  0.000  0.000, Loc2:  0.000  0.000  0.000
vecVelocity1: -0.636 -2.700 -0.636, vecVelocity2:  0.000  0.000  0.000
offsetGlobal: -0.007  0.000 -0.007, offsetObj: -0.643 -2.700 -0.643
vecAccel: -0.636 -2.700 -0.636, direction:  0.007 -0.000  0.007
scaleFactor: -0.316

f:006, name:RCS.Front.225d
---
currentLocation: -0.636 -2.700 -0.636, Loc1:  0.000  0.000  0.000, Loc2:  0.000  0.000  0.000
vecVelocity1: -0.636 -2.700 -0.636, vecVelocity2:  0.000  0.000  0.000
offsetGlobal: -0.007  0.000 -0.007, offsetObj: -0.643 -2.700 -0.643
vecAccel: -0.636 -2.700 -0.636, direction:  0.007 -0.000  0.007
scaleFactor: -0.316

f:007, name:RCS.Front.225d
---
currentLocation: -0.636 -2.700 -0.636, Loc1:  0.000  0.000  0.000, Loc2:  0.000  0.000  0.000
vecVelocity1: -0.636 -2.700 -0.636, vecVelocity2:  0.000  0.000  0.000
offsetGlobal: -0.007  0.000 -0.007, offsetObj: -0.643 -2.700 -0.643
vecAccel: -0.636 -2.700 -0.636, direction:  0.007 -0.000  0.007
scaleFactor: -0.316


In the rendered sample, the variables have not changed at all between frames, whereas they change as expected in the viewport sample. The most important variables for demonstration's purpose are currentLocation, Loc1, and Loc2. They correspond to the object's position at the current frame, frame - 1, and frame -2, respectively.

Here is my testing file:

Any advice on how to work around or fix this issue would be greatly appreciated, thank you.

I managed to solve the issue and get a (usually) reliable effect. I ended up moving the parts of the driver that handled assigning previous locations and calculating acceleration into a frame change handler, similar to how this answer does. I'll attach the example blend file to the end of this answer. The following script is the main component of this effect:

import bpy
from mathutils import Matrix, Vector, Euler

# Calculate and store acceleration, previous locations. Works in animation renders and viewport reliably, single
#   frames are substantially less reliable due to the calculation relying on previous frames
def store_locations(scn, dp):
for obj in bpy.data.collections['RCS'].all_objects: # Use to calculate the accelerations of just the RCS Collection
#for obj in scn.objects: # Use this if you want all objects to have their acceleration calculated

# Get current location
currLoc = obj.evaluated_get(dp).matrix_world.to_translation()
#currLoc = obj.matrix_world.to_translation() # Doesn't update right

# Get locations from object properties
Loc2 = Vector(obj.prevLoc2)
Loc1 = Vector(obj.prevLoc1)

# Calculate velocity samples
vecVelocity2 = (Loc1 - Loc2)
vecVelocity1 = (currLoc - Loc1)

# Calculate acceleration sample
vecAccel = (vecVelocity1 - vecVelocity2)

# Assign acceleration vector
obj.vecAccel = vecAccel

# Assign old locations
obj.prevLoc2 = Loc1
obj.prevLoc1 = currLoc
obj.currLoc = currLoc

# Make sure objects are updated
obj.update_tag()

# Calculate how scaled a given plume should be, based on its obj.vecAccel property & orientation
def rcs_calc(obj):

# Get Accel
vecAccel = obj.vecAccel

# Get direction, maybe move this to store_location?
offsetAmount = Vector([0,0,1])
direction = obj.matrix_world.to_quaternion().copy() @ offsetAmount

#Invert direction so that plumes 'push' rather than 'pull'
direction *= -1

# Scale by dot product
scaleFactor = direction.normalized().dot(vecAccel.normalized())

#bpy.context.view_layer.depsgraph.update() # Update scene to prevent stale data. Causes RuntimeError

# Could do other comparisons to scaleFactor
if scaleFactor < 0.25:
return 0.001 # Setting this to zero results in some plumes not scaling, for some reason
elif scaleFactor < 0.5:
return 0.5
else:
return 1

# Create object custom properties.

if not hasattr(bpy.types.Object, 'currLoc'):
bpy.types.Object.currLoc = bpy.props.FloatVectorProperty(
name='Current Location',
description='Location 0 frames ago',
subtype='TRANSLATION')

if not hasattr(bpy.types.Object, 'prevLoc1'):
bpy.types.Object.prevLoc1 = bpy.props.FloatVectorProperty(
name='Previous Location',
description='Location -1 frames ago',
subtype='TRANSLATION')

if not hasattr(bpy.types.Object, 'prevLoc2'):
bpy.types.Object.prevLoc2 = bpy.props.FloatVectorProperty(
name='Previous Previous Location',
description='Location -2 frames ago',
subtype='TRANSLATION')

if not hasattr(bpy.types.Object, 'vecAccel'):
bpy.types.Object.vecAccel = bpy.props.FloatVectorProperty(
name='Acceleration Vector',
description='Direction of acceleration',
subtype='ACCELERATION')

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

if store_locations in bpy.app.handlers.frame_change_post:
bpy.app.handlers.frame_change_post.remove(store_locations)
bpy.app.handlers.frame_change_post.append(store_locations)


### Instructions on how to use this effect

1. Import this script into your blend file of choice, run it, and register it so it'll run each time the file is opened. (Text -> Register in the text editor).
2. Decide what objects you want to be scaled, and on which of their axes.
3. Give each of these objects a driver on the axis you chose set to the value rcs_calc(self). Make sure to check Use Self in the driver options!. Hitting ctrl+l and selecting Animation Data with all the objects selected will make this a lot less tedious!
4. Parent these objects to the object you want them to appear to control the rotation of, positioning the objects in the locations and orientations you prefer on the parent object. Be sure to place them slightly under the surface of the parent object to avoid the plumes showing when at their minimal (0.001) scale!
5. Add the objects to a collection named "RCS" (Or change the collection reference in the first line of store_locations). Child collections under this collection will also be effected!
6. Animate the parent object doing whatever motion you'd like.
7. Save and reopen the file. This might not be strictly neccesary, but won't hurt.
8. Render an animation and enjoy the effect!

### Caveats:

• Purely linearly interpolated motion will often behave erratically
• Some rotation (especially rotation that is along an axis orthogonal to the orientation of the RCS objects) will display incorrect behavior, as shown in the spinning section of the above demo video.
• This will not work reliably for single frame renders, as it requires the location information from the previous two frames.
• Similarly, the effect is not physically accurate, it is always delayed by 2 frames.
• If there is a way to "look ahead" some number of frames in blender without using fcurves, this might work as a way around this, but it's beyond the scope of this answer currently.
• There are likely additional edge cases I've yet to encounter
• I believe that I've dealt with the majority of the issues that caused Blender to crash, but all the same, be careful and keep backup files! If you have an important file that won't open, try launching Blender with the -Y/--disable-autoexec flag, which will prevent the script from automatically running.