Neutralize Parent Inverse

The parent inverse is pesky for my style of working so I’m developing a tool to eliminate it from child objects while maintaining their ultimate location, rotation and scale in the world. To apply the parent inverse matrix to the transforms of an object and then clear it is straightforward enough:


import bpy 

# Apply the parent inverse matrix to the object's transforms
bpy.context.object.matrix_basis = bpy.context.object.matrix_parent_inverse * ob.matrix_basis

# Clear the parent inverse matrix
bpy.context.object.matrix_parent_inverse.identity()

However, prior animation of the location, rotation, or scale properties of the child throws a wrench into the simple solution. Preexisting key frame values do not change when applying the parent inverse matrix to the basis matrix of the object. Then when the parent inverse is cleared, it causes an undesired shift in the location/rotation/scale of the object in relation to how it situated in the world before.

So, it is necessary to iterate through each key frame of the transform properties and apply the parent inverse. This is where I am having problems. My current code works on each selected object. First, it records the parent inverse for later reference. Then the parent inverse matrix of each object is applied to its basis matrix and cleared. Then, if the object is animated, it applies the stored parent inverse to each location, rotation, and scale key frame value.


import bpy


for ob in bpy.context.selected_objects: # Repeat for each selected object
    if ob.parent:   # Only alter objects that have a parent


        # Store the parent inverse matrix in separate location, rotation, and scale variables
        parent_location_inverse, parent_rotation_inverse, parent_scale_inverse = ob.matrix_parent_inverse.decompose()


        # Convert rotation quaternion to euler
        parent_rotation_inverse = parent_rotation_inverse.to_euler()


        # Apply the parent inverse matrix to the object's transforms
        ob.matrix_basis = ob.matrix_parent_inverse * ob.matrix_basis


        # Clear the parent inverse matrix
        ob.matrix_parent_inverse.identity()


        print() # Blank lines for legibility
        print()
        print()
        print()
        print ("Location Inverse: " + str(list(parent_location_inverse)))
        print ("Rotation Inverse: " + str(list(parent_rotation_inverse)))
        print ("Scale Inverse: " + str(list(parent_scale_inverse)))


        if ob.animation_data: # Check if object is animated
            action = ob.animation_data.action


            for fcu in action.fcurves: # Iterate through every f-curve


                print()
                print("Animated Property: " + fcu.data_path)
                print("Channel: " + str(fcu.array_index))
                # array_index of the f-curve is used as the index of the inverse that relates to the key frame
                # For example: if the data_path is location and the array_index is 1
                # then the proper parent inverse to apply is parent_location_inverse[1]


                
                for i in range(len(fcu.keyframe_points)): # Iterate through all key frames


                    print("Frame: " + str(fcu.keyframe_points[i].co.x))
                    print("Original Value:" + str(fcu.keyframe_points[i].co.y))


                    # Apply the parent inverse into the key frame values
                    # Different operations for location, rotation, and scale
                    if fcu.data_path == 'location':
                        print("Specific Inverse: " + str(parent_location_inverse[fcu.array_index]))
                        fcu.keyframe_points[i].co.y = fcu.keyframe_points[i].co.y + parent_location_inverse[fcu.array_index]
                        
                    elif fcu.data_path == 'rotation_euler':
                        print("Specific Inverse: " + str(parent_rotation_inverse[fcu.array_index]))
                        fcu.keyframe_points[i].co.y = parent_rotation_inverse[fcu.array_index] * fcu.keyframe_points[i].co.y


                    elif fcu.data_path == 'scale':
                        print("Specific Inverse: " + str(parent_scale_inverse[fcu.array_index]))
                        fcu.keyframe_points[i].co.y = fcu.keyframe_points[i].co.y * parent_scale_inverse[fcu.array_index]


                    print("New Value: " + str(fcu.keyframe_points[i].co.y))

To test this, make two objects and transform them so they don’t have the default values for location, rotation, and scale. Parent one to the other. On the child, set a key frame for the transform properties. Advance to another frame, alter the properties, and add another key frame. Run the script with the child selected.

You will see that the child shifts and does not maintain the transforms as they had been ultimately set in the world. Here is where I need your help. My problem is with properly converting the parent inverse matrix to be applied to the key frames and dealing with the various math classes (matrix, vector, quaternion, euler). I am having difficulty wrapping my mind around how they relate to each other to accomplish each necessary step. Also, I am a novice to python (but with some experience in other languages). So, your help with my code errors would be greatly appreciated.

Have a look at this script.

Seems i encountered a bug and reported it.

It looks like that bug was fixed and your script does the trick perfectly. It’s a little over my head so I’ll have to take some time to pick it apart, but thanks for sharing! I really appreciate it.

Edit: Actually the bug persists in 2.76. Since there was a fix committed for the bug report, I assumed it was resolved.

Besides i think there is a flaw in this script. All new keyframes should probably be temporarily stored in a list so the old ones are not overwritten immediately. Otherwise it will incorrectly interpolate between an old and a new keyframe.

This does not matter if for each keyframe all channels are present.