1
$\begingroup$

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.

$\endgroup$
0
$\begingroup$

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.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.