Visual transform helper functions for 2.5

While working on the ik/fk snapping for Rigify, I developed some handy functions for managing the transforms of pose bones. Below is the code. I’ve also included a simple operator that snaps all selected bones to the active bone, as a simple example of what you can do with it.

“get_pose_matrix_in_other_space()” is really the workhorse function here. You feed it a transform matrix in armature space, and you feed it a pose bone, and it returns the matrix but transformed into the pose bone’s local transform space. It takes into account several corner cases (bones having parents, hinge bones, local location disabled, etc.).

import bpy
from mathutils import Matrix, Vector
from math import acos


#########################################
## "Visual Transform" helper functions ##
#########################################

def get_pose_matrix_in_other_space(mat, pose_bone):
    """ Returns the transform matrix relative to pose_bone's current
        transform space.  In other words, presuming that mat is in
        armature space, slapping the returned matrix onto pose_bone
        should give it the armature-space transforms of mat.
        TODO: try to handle cases with axis-scaled parents better.
    """
    rest = pose_bone.bone.matrix_local.copy()
    rest_inv = rest.inverted()
    if pose_bone.parent:
        par_mat = pose_bone.parent.matrix.copy()
        par_inv = pose_bone.parent.matrix.inverted()
        par_rest = pose_bone.parent.bone.matrix_local.copy()
    else:
        par_mat = Matrix()
        par_inv = Matrix()
        par_rest = Matrix()

    # Get matrix in bone's current transform space
    smat = rest_inv * (par_rest * (par_inv * mat))

    # Compensate for non-inherited rotation/scale
    if not pose_bone.bone.use_inherit_rotation:
        loc = mat.to_translation()
        loc -= (par_mat*(par_rest.inverted() * rest)).to_translation()
        loc *= rest.inverted().to_quaternion()
        if pose_bone.bone.use_inherit_scale:
            t = par_mat.to_scale()
            par_scale = Matrix().Scale(t[0], 4, Vector((1,0,0)))
            par_scale *= Matrix().Scale(t[1], 4, Vector((0,1,0)))
            par_scale *= Matrix().Scale(t[2], 4, Vector((0,0,1)))
        else:
            par_scale = Matrix()

        smat = rest_inv * mat * par_scale.inverted()
        smat[3][0] = loc[0]
        smat[3][1] = loc[1]
        smat[3][2] = loc[2]
    elif not pose_bone.bone.use_inherit_scale:
        loc = smat.to_translation()
        rot = smat.to_quaternion()
        scl = mat.to_scale()

        smat = Matrix().Scale(scl[0], 4, Vector((1,0,0)))
        smat *= Matrix().Scale(scl[1], 4, Vector((0,1,0)))
        smat *= Matrix().Scale(scl[2], 4, Vector((0,0,1)))
        smat *= Matrix.Rotation(rot.angle, 4, rot.axis)
        smat[3][0] = loc[0]
        smat[3][1] = loc[1]
        smat[3][2] = loc[2]

    # Compensate for non-local location
    if not pose_bone.bone.use_local_location:
        loc = smat.to_translation() * (par_rest.inverted() * rest).to_quaternion()
        smat[3][0] = loc[0]
        smat[3][1] = loc[1]
        smat[3][2] = loc[2]

    return smat


def get_local_pose_matrix(pose_bone):
    """ Returns the local transform matrix of the given pose bone.
    """
    return get_pose_matrix_in_other_space(pose_bone.matrix, pose_bone)


def set_pose_translation(pose_bone, mat):
    """ Sets the pose bone's translation to the same translation as the given matrix.
        Matrix should be given in bone's local space.
    """
    pose_bone.location = mat.to_translation()


def set_pose_rotation(pose_bone, mat):
    """ Sets the pose bone's rotation to the same rotation as the given matrix.
        Matrix should be given in bone's local space.
    """
    q = mat.to_quaternion()

    if pose_bone.rotation_mode == 'QUATERNION':
        pose_bone.rotation_quaternion = q
    elif pose_bone.rotation_mode == 'AXIS_ANGLE':
        pose_bone.rotation_axis_angle[0] = q.angle
        pose_bone.rotation_axis_angle[1] = q.axis[0]
        pose_bone.rotation_axis_angle[2] = q.axis[1]
        pose_bone.rotation_axis_angle[3] = q.axis[2]
    else:
        pose_bone.rotation_euler = q.to_euler(pose_bone.rotation_mode)


def set_pose_scale(pose_bone, mat):
    """ Sets the pose bone's scale to the same scale as the given matrix.
        Matrix should be given in bone's local space.
    """
    pose_bone.scale = mat.to_scale()


def match_pose_translation(pose_bone, target_bone):
    """ Matches pose_bone's visual translation to target_bone's visual
        translation.
        This function assumes you are in pose mode on the relevant armature.
    """
    mat = get_pose_matrix_in_other_space(target_bone.matrix, pose_bone)
    set_pose_translation(pose_bone, mat)
    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.ops.object.mode_set(mode='POSE')


def match_pose_rotation(pose_bone, target_bone):
    """ Matches pose_bone's visual rotation to target_bone's visual
        rotation.
        This function assumes you are in pose mode on the relevant armature.
    """
    mat = get_pose_matrix_in_other_space(target_bone.matrix, pose_bone)
    set_pose_rotation(pose_bone, mat)
    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.ops.object.mode_set(mode='POSE')


def match_pose_scale(pose_bone, target_bone):
    """ Matches pose_bone's visual scale to target_bone's visual
        scale.
        This function assumes you are in pose mode on the relevant armature.
    """
    mat = get_pose_matrix_in_other_space(target_bone.matrix, pose_bone)
    set_pose_scale(pose_bone, mat)
    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.ops.object.mode_set(mode='POSE')


##############
## Operator ##
##############

class SnapPoseboneVisual(bpy.types.Operator):
    """ Snaps selected bones to the visual transforms of the active bone.
    """
    bl_idname = "pose.snap_bones"
    bl_label = "Snap Bones to Bone"
    bl_options = {'UNDO'}

    @classmethod
    def poll(cls, context):
        return (context.active_object != None and context.mode == 'POSE')

    def execute(self, context):
        use_global_undo = context.user_preferences.edit.use_global_undo
        context.user_preferences.edit.use_global_undo = False
        try:
            apb = context.active_pose_bone
            bones_sorted = []
            for bone in context.selected_pose_bones:
                bones_sorted += [bone]
            bones_sorted.sort(key=lambda bone: len(bone.parent_recursive))
            for pb in context.selected_pose_bones:
                if pb != apb:
                    match_pose_translation(pb, apb)
                    match_pose_rotation(pb, apb)
                    match_pose_scale(pb, apb)
        finally:
            context.user_preferences.edit.use_global_undo = use_global_undo
        return {'FINISHED'}


bpy.utils.register_class(SnapPoseboneVisual)

Hi, thanks for sharing.

Thinking this could have application for changing the rest pose of a bvh … import bvh, duplicate rig, change the rest pose, then snap the new pose into place using the original’s pose bone locations…

Just now seeing this… just wondering if this works well for using with IK/FK snapping?