Here's the current version, I've got animation export working as well:

https://drive.google.com/file/d/0B442Qu ... sp=sharingImportant functions for BFB import; the bone matrixes are stored in bone space in a BFB, just like in a NIF IIRC.

- Code: Select all
`def vec_roll_to_mat3(vec, roll):`

#from http://gamedev.stackexchange.com/questions/32529/calculating-the-correct-roll-from-a-bone-transform-matrix

#from http://blenderartists.org/forum/showthread.php?260602-transform-matrix-to-bone-%28head-tail-roll%29-bug

#not altered at all

target = mathutils.Vector((0,1,0))

nor = vec.normalized()

axis = target.cross(nor)

if axis.dot(axis) > 0.0000000001: # this seems to be the problem for some bones, no idea how to fix

axis.normalize()

theta = target.angle(nor)

bMatrix = mathutils.Matrix.Rotation(theta, 3, axis)

else:

updown = 1 if target.dot(nor) > 0 else -1

bMatrix = mathutils.Matrix.Scale(updown, 3)

bMatrix[2][2] = 1.0

rMatrix = mathutils.Matrix.Rotation(roll, 3, nor)

mat = rMatrix * bMatrix

return mat

def mat3_to_vec_roll(mat):

#from http://gamedev.stackexchange.com/questions/32529/calculating-the-correct-roll-from-a-bone-transform-matrix

#from http://blenderartists.org/forum/showthread.php?260602-transform-matrix-to-bone-%28head-tail-roll%29-bug

#this was edited so it rotates the axis and roll as if the matrix was rotated by 90° about the bone's Z axis

#it would make more sense to keep this function in its original form and instead use a negative rotate_matrix_90Z() found in export

#used to be: vec = mat.col[1]

vec = mat.col[0]

#used to be: vecmat = vec_roll_to_mat3(mat.col[1], 0)

vecmat = vec_roll_to_mat3(mat.col[0], 0)

vecmatinv = vecmat.inverted()

rollmat = vecmatinv * mat

roll = math.atan2(rollmat[0][2], rollmat[2][2])

return vec, roll

def create_armature(bonesdict):

global armature

# Create armature and object

bpy.ops.object.add(type = 'ARMATURE', enter_editmode = True, location = mathutils.Vector((0,0,0)))

armature = bpy.context.object

armature.show_x_ray = True

armature.name = "DUMMY Scene Root"

armData = armature.data

armData.name = armature.name+'Amt'

armData.show_axes = True

armData.draw_type = 'STICK'

bpy.ops.object.mode_set(mode = 'EDIT')

# Create bones

for boneid in bonesdict:

parentid,bonename,bonematrix = bonesdict[boneid]

# create a new bone

bone = armData.edit_bones.new(bonename)

#iterate over all parents and multiply their matrices to create the matrix we need

parentid = bonesdict[boneid][0]

while parentid>0:

parentmatrix = bonesdict[parentid][2]

bonematrix = bonematrix*parentmatrix

parentid = bonesdict[parentid][0]

pos = bonematrix.transposed().to_translation()

#this function is changed to output axis and roll as if the matrix was rotated by 90° about Z

#it would probably make more sense to keep this function as it was and instead feed the matrix through a rotation like on export

axis, roll = mat3_to_vec_roll(bonematrix.transposed().to_3x3())

bone.head = pos

bone.tail = pos + axis

bone.roll = roll

#set the parent

parentid = bonesdict[boneid][0]

if parentid>0:

parentname = bonesdict[parentid][1]

parentbone = armData.edit_bones[parentname]

bone.parent = parentbone

#fix the bone length

for bone in armData.edit_bones:

#don't change Bip01

if bone.parent:

childheads = mathutils.Vector()

children = 0

for child in armData.edit_bones:

if child.parent == bone:

childheads+= child.head

children+= 1

#nub node: no children, end of a chain

if children == 0:

bonelength = 0.5

else:

bonelength = (bone.head-childheads/children).length

if bonelength == 0:

bonelength = 0.25

bone.length = bonelength

bpy.ops.object.mode_set(mode = 'OBJECT')

return armature

Then on BFB export, the rotation we made on import is simply countered to retrieve the original matrix. As mentioned in the code, the import should also be done with the rotate matrix function, just to keep it coherent.

- Code: Select all

def rotate_matrix_90Z(m):

#we want to get a rotation about the matrix' local Z axis, by 90 °

#this affects both the axis and the bone's roll

n = m.to_3x3()

#when transposed from blender, use row

#if working with raw blender data use col

local_z = n[2]

r = mathutils.Matrix.Rotation(math.radians(-90.0),3,local_z)

n *= r

n.resize_4x4()

#add the translation components back in

#test if it could be also done completely as a 4x4 matrix, or if that affects the tranlations

n.col[3] = m.col[3]

n[3] = m[3]

return n

def get_rest_matrix(bone):

#multiply with inverse parent matrix

if bone.parent:

return rotate_matrix_90Z(bone.matrix_local.transposed()) * rotate_matrix_90Z(bone.parent.matrix_local.transposed()).inverted()

#else it has no parent

return rotate_matrix_90Z(bone.matrix_local.transposed())

Animation import and export also use get_rest_matrix(bone) to get the rest matrix. The quats and translations are stored just like in NIFs.

A few calculations are done to make them work in blender, making the keys relative to the bones or whatever. That was lifted from the original NIF scripts, but any trace of extra matrices is now gone.

The real trick is a simple swizzle - on import quat.wxyz becomes quat.w-yxz and loc.xyz becomes loc.y-xz, and vice versa on export. That swizzling equals the ominous 90° rotation again and makes sure the rotations are aligned properly to the fixed roll and axis of the bones.

The code is much more simple than it used to be when I used extra matrices!

So with that technique, you have good looking skeletons AND fully functional animation support WITHOUT the need for correction matrices. It's so much easier to create custom skeletons now, because there's no more need to worry about the extra matrices, as your blender bones directly determine your end result matrices.