calculate bone location/rotation from fcurve-animation-data

calculate bone location/rotation from fcurve-animation-data

I tried to understand the way the internal calculation works for the armature animation.
This is one first step,
blender-2.56 rev.34674
the calculation is not optimized, it is to compare the result with the internal blender calculation.
It only uses the fcurve-animation-data of the bones - no constraints etc.

pls. note every bug/wrong understanding and math.errors.

i tested it with an animation of bvh-data, thats why the name of the
test-armature is “BvhRig” in the example


# what i found in blenkernel/intern/armature.c
#/* **************** The new & simple (but OK!) armature evaluation ********* */ 
#
#/*  ****************** And how it works! ****************************************
#
#  This is the bone transformation trick; they're hierarchical so each bone(b)
#  is in the coord system of bone(b-1):
#
#  arm_mat(b)= arm_mat(b-1) * yoffs(b-1) * d_root(b) * bone_mat(b) 
#  
#  -> yoffs is just the y axis translation in parent's coord system
#  -> d_root is the translation of the bone root, also in parent's coord system
#
#  pose_mat(b)= pose_mat(b-1) * yoffs(b-1) * d_root(b) * bone_mat(b) * chan_mat(b)
#
#  we then - in init deform - store the deform in chan_mat, such that:
#
#  pose_mat(b)= arm_mat(b) * chan_mat(b)
#  
#  *************************************************************************** */
#

import bpy
from mathutils import *

def pretty_vector(v):
    s = "%02.3f %02.3f %02.3f"%(v[0], v[1], v[2])
    return(s)

def pretty(obj):
    s = ""
    for i in obj:
        s += "%+01.2f "%(i)
    return(s)

def pretty_m(m):
    s = "euler "
    s += pretty(m.to_euler())
    if m.col_size == 4:
        s += "  loc "
        s += pretty(m.to_translation())
    return(s)

# read the fcurves values dont works for frame-numbers .. 
# how to get interpolated values ... ? in blender-2.49 there was a way, still missing in blender-2.5x?
# found it, use fcurve.evaluate(frame-number)
def get_action_location(action, bonename, frame=1):
#    print("loc for:", bonename)
    loc = Vector()
    if action == None:
        return(loc)
    data_path = 'pose.bones["%s"].location'%(bonename)
    for fc in action.fcurves:
        if fc.data_path == data_path:
#            loc[fc.array_index] = fc.keyframe_points[frame-1].co[1]
            loc[fc.array_index] = fc.evaluate(frame)
    return(loc)
def get_action_rotation(action, bonename, frame=1):
#    print("rot for:", bonename)
    rot = Quaternion( (1, 0, 0, 0) )  #the default quat is not 0
    if action == None:
        return(rot)
    data_path = 'pose.bones["%s"].rotation_quaternion'%(bonename)
    for fc in action.fcurves:
        if fc.data_path == data_path: # and frame > 0 and frame-1 <= len(fc.keyframe_points):
#            print("rot", fc.array_index, fc.keyframe_points[frame-1].co[1])
#            rot[fc.array_index] = fc.keyframe_points[frame-1].co[1]
            rot[fc.array_index] = fc.evaluate(frame)
    return(rot)

# return the matrix for a bone for the fcurves animation and object - without scale or constraints ..
# armature-object, name of the bone, possible action, frame-number for action-keyframes, trace-values-output
def mybone(armature, bonename, action=None, frame=1, debug=-2):
    do_print = False
    if debug == -1: do_print = True  #print all for -1 .. or only for the bone-number in parent-chain
    parents=[]
    parents.append(bonename) # append the bone itself
    b = bonename
    while armature.pose.bones[b].parent: #is there a parent bone? loop till top-parent reached
        b = armature.pose.bones[b].parent.name #get parents-name
        parents.append(b) #and append the bone-name
    parents.reverse() # reverse to start with top-parent and work down to the child
    if do_print: print(parents)
    new_matrix = {}
    matrix_world = armature.matrix_world.copy() #use the armature-object matrix offset?
    if do_print: print("world:", pretty_m(matrix_world))
    for i,b in enumerate(parents): 
        if do_print or debug == i: print(i, b)
        posematrix = armature.pose.bones[b].matrix.copy() #use it for comparator check, should be no more used if it works
        if do_print or debug == i: print("matrix:____________", pretty_m(posematrix))
        m = get_action_rotation(action, b, frame) #read the fcurve-animation rotation
        m = m.to_matrix().to_4x4()
        l = get_action_location(action, b, frame) #read the fcurve-animation location
        if do_print or debug == i: print("from anim-data:", pretty_m(Matrix.Translation(l)*m) )
        local = armature.data.bones[b].matrix_local.copy() # get the armatures bone matrix_local - restpose
        if do_print or debug == i: print("local", pretty_m(local))
        if i > 0: # there is a parent, use the parents space coord-system
            # the parents current animation-setting
            pm = new_matrix[parents[i-1]] #parents current matrix
            # parents-pose-matrix subtracted with its local-matrix - to this bones local-matrix
            # undo/subtract a matrix operation ist multiplication with its inverted
            pm = pm * armature.data.bones[parents[i-1]].matrix_local.inverted() * local
            # add this bones animation-data ... 
            m = pm * m 
        else: #no parent, the only one i need is the local-matrix and the animation-data
            m = local * m  # i dont get, why this is different compared with child-bones-procedure
            l = l * local  # with root-bone rotation and translation, build the full matrix
            m = Matrix.Translation(l) * m # translate the rotation-matrix .. to the head position
        if do_print or debug == i: print("matrix:            ",pretty_m(m) )
        new_matrix[b] = m # store the current animation-setting .. for the child-bones
    return(matrix_world * m)
  
# now run the test for this armature, its action and the bone-name at the framenumbers
armature="BvhRig"
bone="head"
dbg=-2
frame=30

for frame in range(20, 30, 2):
    bpy.context.scene.frame_set(frame)
    for bone in ("head", "rFoot"):
        m = mybone(bpy.data.objects[armature], bone, bpy.data.objects[armature].animation_data.action, frame, dbg)
    # this prints the pose-matrix of the bone without the armature-animation-offest
        print("original:",bone, pretty_m(bpy.data.objects[armature].pose.bones[bone].matrix))
    # this prints the python-calculated bone-animation with armature-offset
    # if the armature is not rotated or moved, then it should be the same (without constraints .. or scaling)
        print("calculat:", bone, pretty_m(m))
    

today the second part,
its based on the first part,
which i needed to understand and then to lookup
how to transpose a action to an armature with
a different restpose. This is only a restpose-change
in rotations, not locations or bone-sizes.
It might be of use for those recorded actions, where one
need to use a different restpose to attach to a already made mesh.
Imagine having a source with different actions done for different
restpose-settings (human-armature in start-pose and different relaxed ones)


#transfer action, same both actions must exist, to armature with different restpose (only rotation-differences)
def transfer_action(a1, a2, action_source, action_dest): 
#first source armature, second armature with different restpose only rotation!
#then the two actions, you have to make a copy before
    #first try only the rotations, what if some missing - i need all 4 of a quaternion
    print(action_source.name, "dest:", action_dest.name)
#for several tries allways copy the source-action-entries again
    for f1, f2 in zip(action_source.fcurves, action_dest.fcurves):
        for i in range(len (f1.keyframe_points)):
            #break
            f2.keyframe_points[i].co[1] = f1.keyframe_points[i].co[1]
    fcbones = {}
#collect the groups of action-fcurves
    for fc in action_dest.fcurves:
        data_path = fc.data_path
        if data_path in fcbones:
            fcbones[data_path][fc.array_index] = fc
        else:
            fcbones[data_path] = [-1, -1, -1, -1]
            fcbones[data_path][fc.array_index] = fc
#    print(fcbones)
    for k in (fcbones.keys()):
        if "quaternion" in k:
            bonename = k.split('"')[1] #should be 3 parts with bonename in ".."
            print(bonename)
            local_new = a2.data.bones[bonename].matrix_local.to_3x3()
            local_old = a1.data.bones[bonename].matrix_local.to_3x3()
            if a2.pose.bones[bonename].parent: #for a parent, use its changes too
                pname = a2.pose.bones[bonename].parent.name
                local_pnew = a2.data.bones[pname].matrix_local.to_3x3()
                local_pold = a1.data.bones[pname].matrix_local.to_3x3()
                ll = local_new.inverted() * local_pnew * local_pold.inverted() * local_old
            else:
                ll = local_new.inverted() * local_old
            #loop over all keyframes
            fcs = fcbones[k]
            for i in range( len (fcs[0].keyframe_points) ):
                q = Quaternion()
                for j in range(4):
                    q[j] = fcs[j].keyframe_points[i].co[1]
                qnew = (ll * q.to_matrix() ).to_quaternion()
                #print(pretty(q), pretty(qnew))
                for j in range(4):
                    fcs[j].keyframe_points[i].co[1] = qnew[j]
    
# now run the test for this armatures, and its actions
armature="BvhRig.001"  #source-armature
a2 = "BvhRig"  #destination-armature with same copy of action, but different restpose

transfer_action(bpy.data.objects[armature], bpy.data.objects[a2],
     bpy.data.objects[armature].animation_data.action, bpy.data.objects[a2].animation_data.action)

its strange, no one appreciated that code,
thanks, its amazing and very helpful