Getting the actual rotation value for a pose bone when bone rotation is altered by a constraint

I have a pose bone whose rotation is affected by a Damped Track bone constraint. I would like to determine the exact rotation of that bone (via Python) when it is affected by the constraint.

In other words, I want to:

  1. Get the posed rotation of the bone in quaternions on a particular frame.
  2. De-activate or remove the Damped Track bone constraint.
  3. Put the quaternion rotation values in for that bone, and have it rotated at the exact same angle as it was when the Damped Track bone constraint was active during that frame.

I’ve found several code examples of how to do this. However, when I implement them, the quaternion values don’t turn out correctly. After converting the final matrix back to a quaternion, usually I get quaternions that look like (1,0,0,0), (0,1,0,0), (0,0,1,0), and (0,0,0,1).

I’m using Blender 2.93.1. I’m wondering if perhaps if something has changed in the API with respect to what the matrix values return, so the code examples I’ve been using are no longer valid. Below are links to some of the approaches I’ve tried.

Any ideas?

http://graemehill.ca/exporting-armature-animation-with-the-blender-25-python-api/

it’s the same as it has ever been. I’m not really sure what you’re trying to do- but if I constrain a bone to another bone I can get its constrained absolute rotation without any issues.

import bpy

def update_callback():
    bone = bpy.data.objects['ConstrainedBone']
    print(bone.matrix_world.to_quaternion())
    return 1.0
    
bpy.app.timers.register(update_callback) 

and the output, while moving the constraint target around the scene:

<Quaternion (w=0.6021, x=-0.1130, y=-0.0000, z=0.7904)>
<Quaternion (w=0.6598, x=0.1005, y=0.0000, z=0.7447)>
<Quaternion (w=0.6918, x=0.4271, y=0.0000, z=0.5823)>
<Quaternion (w=0.5953, x=0.6616, y=0.0000, z=-0.4560)>
<Quaternion (w=0.5480, x=-0.2655, y=0.0000, z=-0.7932)>
<Quaternion (w=0.5486, x=-0.2725, y=0.0000, z=-0.7904)>

etc

If I copy the quaternion to a temp variable, then remove the constraint and set the rotation of the matrix manually (decomposing the world matrix and then recomposing it with the cached rotation), it points exactly where it was before I disabled the constraint. I’m guessing that you’ve got some issues with your matrix math- post some code and somebody here might be able to step you through what you’re doing wrong.

1 Like

Thanks for the response testure, but I’m looking to work with pose bones, not edit bones. AFAIK there is no matrix_world property on a pose bone.

I’ve uploaded an example file here: sundriftproductions.com/blenderartists/bone_rotation_example.blend

The code from the file is below. I’ve made a little more progress since I originally posted, but it’s still not 100% working. It seems that I’m missing some matrix in my equations (the rest matrix, I think?) and so instead of the arm swinging around in a downward direction, it appears to be inverted. See the line that begins with matrix_final = for what I mean.

from console_python import get_console
from contextlib import redirect_stdout
import bpy
from mathutils import Matrix, Vector
from math import *


# Make sure the armature is selected and is in Pose Mode before running this script.
# Also make sure that the DampedTrack bone constraint for upper_arm_fk.R is active.
# Once you've run the script, turn off the DampedTrack bone constraint for upper_arm_fk.R to see the results.

armature_name = 'rig' # This needs to be an armature.
bone_name = 'upper_arm_fk.R' # This is a bone on the armature whose rotation is being altered by a Damped Track constraint. 

frame_start = 1
frame_end = 45

############################

bpy.ops.ed.undo_push()  # Manually record that when we do an undo, we want to go back to this exact state.

# Remember our original state for various setting we can restore them at the end.
original_use_keyframe_insert_auto = bpy.context.scene.tool_settings.use_keyframe_insert_auto
original_current_frame = bpy.context.scene.frame_current
original_mode = bpy.context.active_object.mode

arm = bpy.data.objects[armature_name]

allRotations = []

# Go through the animation and store our rotations in the allRotations collection.
# We do this first because we'll get crazy jerky animation if we immediately try to write the new keyframe rotation values to the bone.
for frame in range(0, frame_end - frame_start + 1):
    bpy.context.scene.frame_current = frame
    bpy.context.view_layer.update()
    bone = arm.pose.bones[bone_name]    
    matrix_final = arm.matrix_world @ bone.matrix # Am I missing something here? Does something need to be inverted? Do I need to get a rest matrix and multiply it somehow?
    real_rotation = matrix_final.to_quaternion().copy()                     
    print('frame ' + str(frame) + '\t' + str(real_rotation))
    allRotations.append(real_rotation)
        
# Do a second pass through the animation -- pop the first item on the allRotations collection and store that rotation as part of the animation.
for frame in range(0, frame_end - frame_start + 1):
    bpy.context.scene.frame_current = frame
    bpy.context.view_layer.update()
    bone = arm.pose.bones[bone_name]        
    bone.rotation_quaternion = allRotations.pop(0)

    # Record the keyframe with the "real" rotation.
    bpy.ops.anim.keyframe_insert_menu(type='WholeCharacter', confirm_success=True)


# Restore the original settings.
bpy.context.scene.tool_settings.use_keyframe_insert_auto = original_use_keyframe_insert_auto
bpy.context.scene.frame_current = original_current_frame
bpy.context.view_layer.update()
bpy.ops.object.mode_set(mode=original_mode)  

Never mind – I figured it out.

It turns out that the old code that I had found in the links I listed in my first post were using an asterisk to multiply matrices. I’m not totally clear on what changed with Python or its libraries, but apparently the correct way to multiply matrices (using Blender’s implementation of Python, anyway) is with the @ symbol.

Anyway, I got everything working. Here’s an updated version of the code above, in case this is helpful to anyone looking to solve a similar problem in the future.

from console_python import get_console
from contextlib import redirect_stdout
import bpy
from mathutils import Matrix, Vector
from math import *



def quatRotation(poseBone):
    # poseBone.matrix is in object space - we need to convert it to local space 
    if poseBone.parent is not None:
        parentRefPoseMtx = poseBone.parent.bone.matrix_local
        boneRefPoseMtx = poseBone.bone.matrix_local
        parentPoseMtx = poseBone.parent.matrix
        bonePoseMtx = poseBone.matrix
        boneLocMtx = ( parentRefPoseMtx.inverted() @ boneRefPoseMtx ).inverted() @ ( parentPoseMtx.inverted() @ bonePoseMtx )
    else:
        boneRefPoseMtx = poseBone.bone.matrix_local
        bonePoseMtx = poseBone.matrix
        boneLocMtx = boneRefPoseMtx.inverted() @ bonePoseMtx

    loc, rot, scale = boneLocMtx.decompose()    
    return rot




# Make sure the armature is selected and is in Pose Mode before running this script.
# Also make sure that the DampedTrack bone constraint for upper_arm_fk.R is active.
# Once you've run the script, turn off the DampedTrack bone constraint for upper_arm_fk.R to see the results.

armature_name = 'rig' # This needs to be an armature.
bone_name = 'upper_arm_fk.R' # This is a bone on the armature whose rotation is being altered by a Damped Track constraint. 

frame_start = 1
frame_end = 110


############################

bpy.ops.ed.undo_push()  # Manually record that when we do an undo, we want to go back to this exact state.

# Remember our original state for various setting we can restore them at the end.
original_use_keyframe_insert_auto = bpy.context.scene.tool_settings.use_keyframe_insert_auto
original_current_frame = bpy.context.scene.frame_current
original_mode = bpy.context.active_object.mode

arm = bpy.data.objects[armature_name]

allRotations = []

# Go through the animation and store our rotations in the allRotations collection.
# We do this first because we'll get crazy jerky animation if we immediately try to write the new keyframe rotation values to the bone.
for frame in range(0, frame_end - frame_start + 1):
    bpy.context.scene.frame_current = frame
    bpy.context.view_layer.update()
    bone = arm.pose.bones[bone_name]        
    real_rotation = quatRotation(bone)      
    print('frame ' + str(frame) + '\t' + str(real_rotation))
    allRotations.append(real_rotation)
        
# Do a second pass through the animation -- pop the first item on the allRotations collection and store that rotation as part of the animation.
for frame in range(0, frame_end - frame_start + 1):
    bpy.context.scene.frame_current = frame
    bpy.context.view_layer.update()
    bone = arm.pose.bones[bone_name]        
    bone.rotation_quaternion = allRotations.pop(0)

    # Record the keyframe with the "real" rotation.
    bpy.ops.anim.keyframe_insert_menu(type='WholeCharacter', confirm_success=True)


# Restore the original settings.
bpy.context.scene.tool_settings.use_keyframe_insert_auto = original_use_keyframe_insert_auto
bpy.context.scene.frame_current = original_current_frame
bpy.context.view_layer.update()
bpy.ops.object.mode_set(mode=original_mode)