BVH Export (almost working)

Hi, Iv been trying to get BVH export working for 2.44 release, and its almost working!!!

Iv checked other areas of the code and have narrowed the problem down to the joint rotation that dosnt work properly…

Rotating X/Y or Z axis works but when combind it gives slightly wrong results.
Iv tried applying the rotation 6 possible ways but that dosnt help.

Look for the word FIXME to see teh area that dosnt work.

also, changing the order the rotation is exported makes a difference.
you must change the channel order also “Yposition Zposition Xrotation”

If anyone has some spare time, or a vested interest in this working, take a look and see if you can fix.


#!BPY
"""
 Name: 'Motion Capture *.BVH...'
 Blender: 242
 Group: 'Export'
 Tooltip: 'Active Armature to BVH'
"""

import Blender
import bvh_import # for the class
from Blender import *
TranslationMatrix = Mathutils.TranslationMatrix
Quaternion = Mathutils.Quaternion

import BPyMessages
RotationMatrix = Blender.Mathutils.RotationMatrix
Matrix = Blender.Mathutils.Matrix
# Change the order rotation is applied.
MATRIX_IDENTITY_3x3 = Matrix([1,0,0],[0,1,0],[0,0,1])
MATRIX_IDENTITY_4x4 = Matrix([1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1])

def eulerRotate(x,y,z, rot_order):
    # Clamp all values between 0 and 360, values outside this raise an error.
    mats=[RotationMatrix(x,3,'x'), RotationMatrix(y,3,'y'), RotationMatrix(z,3,'z')]
    # print rot_order
    # Standard BVH multiplication order, apply the rotation in the order Z,X,Y
    return (mats[rot_order[2]]*(mats[rot_order[1]]* (mats[rot_order[0]]* MATRIX_IDENTITY_3x3))).toEuler()
    #return (mats[rot_order[0]]*(mats[rot_order[1]]* (mats[rot_order[2]]* MATRIX_IDENTITY_3x3))).toEuler()


def bvh_export(filepath, ob, PREF_STARTFRAME, PREF_ENDFRAME, PREF_SCALE= 10.0):

    Window.EditMode(0)
    Blender.Window.WaitCursor(1)

    file= open(filepath, 'w')

    # bvh_nodes= {}
    arm_data= ob.data
    bones= arm_data.bones.values()

    bone_locs= {}
    bone_locs_rest_offset= {}

    # Build a dictionary of bone children.
    # None is for parentless bones
    bone_children= {None:[]}

    # initialize with blank lists
    for bone in bones:
        bone_children[bone.name] = []

    for bone in bones:
        parent= bone.parent
        bone_name= bone.name
        bone_locs[bone_name] = bone.head['ARMATURESPACE']
        if parent:
            bone_children[parent.name].append( bone_name )
        else: # root node
            bone_children[None].append( bone_name )

    # sort the children, not needed but may as well
    for children_list in bone_children.itervalues():
        children_list.sort()
        children_list.reverse()

    # build a (name:bone) mapping dict
    bone_dict= {}
    for bone in bones:    bone_dict[bone.name] = bone

    # bone name list in the order that the bones are written
    bones_serialized_names= []

    file.write('HIERARCHY
')

    def write_bones_recursive(bone_name, indent):

        my_bone_children= bone_children[bone_name]

        indent_str=  '	'*indent # cache?

        bone= bone_dict[bone_name]
        loc = bone_locs[bone_name]

        # make relative if we can
        if bone.parent:
            #loc= loc - bone_locs[bone.parent.name]
            loc= loc - bone_locs[bone.parent.name]

        bone_locs_rest_offset[bone_name] = loc

        bones_serialized_names.append(bone_name)
        if indent:
            file.write('%sJOINT %s
' % (indent_str, bone_name))
        else:
            file.write('%sROOT %s
' % (indent_str, bone_name))

        file.write('%s{
' % indent_str)
        file.write('%s	OFFSET %.6f %.6f %.6f
' % (indent_str, loc.x * PREF_SCALE, loc.y * PREF_SCALE, loc.z * PREF_SCALE))
        file.write('%s	CHANNELS 6 Xposition Yposition Zposition Zrotation Xrotation Yrotation
' % indent_str) # Seems best?

        if my_bone_children:
            # Write children
            for child_bone in my_bone_children:
                write_bones_recursive(child_bone, indent+1)
        else:
            # Write the bone end.
            file.write('%s	End Site
' % indent_str)
            file.write('%s	{
' % indent_str)
            loc= bone.tail['ARMATURESPACE'] - bone_locs[bone_name]
            file.write('%s		OFFSET %.6f %.6f %.6f
' % (indent_str, loc.x * PREF_SCALE, loc.y * PREF_SCALE, loc.z * PREF_SCALE))
            file.write('%s	}
' % indent_str)

        file.write('%s}
' % indent_str)

    if len(bone_children[None])==1:
        key= bone_children[None][0]

        indent= 0
        write_bones_recursive(key, indent)

    else:
        # Write a dummy parent node
        file.write('ROOT %s
' % key)
        file.write('{
')
        file.write('	OFFSET 0.0 0.0 0.0
')
        file.write('	CHANNELS 0
')
        key= None
        indent= 1
        write_bones_recursive(key, indent)

        file.write('}
')


    # redefine bones as sorted by bones_serialized_names
    # se we can write motion
    pose_dict= ob.getPose().bones

    class decorated_bone(object):
        __slots__=(\
        'name',# bone name, used as key in many places
        'parent',# decorated bone parent, set in a later loop
        'rest_bone',# blender armature bone
        'pose_bone',# blender pose bone
        'pose_mat',# blender pose matrix
        'rest_arm_mat',# blender rest matrix (armature space)
        'rest_local_mat',# blender rest batrix (local space)
        'pose_imat',# pose_mat inverted
        'rest_arm_imat',# rest_arm_mat inverted
        'rest_local_imat', # rest_local_mat inverted
        'rest_head_offset') # offset from parent in armature space

        def __init__(self, bone_name):
            self.name= bone_name
            self.rest_bone= bone_dict[bone_name]
            self.pose_bone= pose_dict[bone_name]

            self.pose_mat= self.pose_bone.poseMatrix

            mat= self.rest_bone.matrix
            self.rest_arm_mat= mat['ARMATURESPACE'].copy()
            self.rest_local_mat= mat['BONESPACE'].copy().resize4x4()

            # inverted mats
            self.pose_imat= self.pose_mat.copy().invert()
            self.rest_arm_imat= self.rest_arm_mat.copy().invert()
            self.rest_local_imat= self.rest_local_mat.copy().invert()

            self.rest_head_offset = bone_locs_rest_offset[bone_name]

            self.parent= None

        def update_posedata(self):
            self.pose_mat= self.pose_bone.poseMatrix
            self.pose_imat= self.pose_mat.copy().invert()

        def __repr__(self):
            if self.parent:
                return '["%s" child on "%s"]
' % (self.name, self.parent.name)
            else:
                return '["%s" root bone]
' % (self.name)

    bones_decorated= [ decorated_bone(bone_name) for bone_name in  bones_serialized_names]

    # Assign parents
    bones_decorated_dict= {}
    for db in bones_decorated:
        bones_decorated_dict[db.name] = db

    for db in bones_decorated:
        parent= db.rest_bone.parent
        if parent:
            db.parent = bones_decorated_dict[parent.name]
    del bones_decorated_dict
    # finish assigning parents
    print bones_decorated

    #print len(bones), len(pose_bones)

    file.write('MOTION
')
    file.write('Frames: %d
' % 100)
    file.write('Frame Time: %.6f
' % 0.03)

    triple= '%.6f %.6f %.6f '
    for frame in xrange(PREF_STARTFRAME, PREF_ENDFRAME+1):
        Blender.Set('curframe', frame)
        Blender.Window.RedrawAll()
        for dbone in bones_decorated:

            dbone.update_posedata()

            # Calculate the bone matrix, this is correct.
            matrix= dbone.pose_mat
            rest_matrix = dbone.rest_arm_mat
            if  dbone.parent:
                matrix=            matrix * dbone.parent.pose_imat
                rest_matrix=    rest_matrix * dbone.parent.rest_arm_imat
            matrix= matrix * rest_matrix.copy().invert()
            # Done,

            # location and rotation to write, this is correct also
            loc= (dbone.rest_head_offset + matrix.translationPart()) * PREF_SCALE
            file.write(triple % tuple(loc))

            # Rotation, Almost Correct!!!!
            # FIXME
            rot= matrix.toEuler()

            # Why do we need to flip the X axis???
            rot = rot[2], -rot[0], rot[1] # looks almost right
            rot = eulerRotate(rot[0], rot[1], rot[2], [1,2,0]) # LOOKS ALMOST RIGHT
            file.write(triple % tuple(rot))

        file.write('
')

    file.close()

    # print'BVH Exported: %s frames:%d
'% (filepath, numframes-1)  
    Blender.Window.WaitCursor(0)


def bvh_export_ui(filepath):
    # Dont overwrite
    if not BPyMessages.Warning_SaveOver(filepath):
        return

    scn= Scene.GetCurrent()
    ob_act= scn.objects.active
    if not ob_act or ob_act.type != 'Armature':
        BPyMessages.Error_NoArmatureActive()

    arm_ob= scn.objects.active

    if not arm_ob or arm_ob.type!='Armature':
        Blender.Draw.PupMenu('No Armature object selected.')
        return

    ctx = scn.getRenderingContext()
    orig_frame = Blender.Get('curframe')
    PREF_STARTFRAME= Blender.Draw.Create(int(ctx.startFrame()))
    PREF_ENDFRAME= Blender.Draw.Create(int(ctx.endFrame()))

    block = [\
    ("Start Frame: ", PREF_STARTFRAME, 1, 30000, "Start Bake from what frame?: Default 1"),\
    ("End Frame: ", PREF_ENDFRAME, 1, 30000, "End Bake on what Frame?"),\
    ]

    if not Blender.Draw.PupBlock("Export MDD", block):
        return

    PREF_STARTFRAME, PREF_ENDFRAME=\
        min(PREF_STARTFRAME.val, PREF_ENDFRAME.val),\
        max(PREF_STARTFRAME.val, PREF_ENDFRAME.val)

    bvh_export(filepath, ob_act, PREF_STARTFRAME, PREF_ENDFRAME)
    Blender.Set('curframe', orig_frame)

if __name__=='__main__':
    Blender.Window.FileSelector(bvh_export_ui, 'EXPORT BVH', sys.makename(ext='.bvh'))

Well, it’s not actually working for another objects !
I’m trying with a camera

This is for armatures, for objects you could export lightwave MOT format

well, i think i will write my own export script, because i need, in my work to export bvh for objects

I came across this. I think it will already be known to posters in this thread, but if not i’m hoping it will prod this quest back into action.
http://www.mail-archive.com/[email protected]/msg00647.html
http://www.mail-archive.com/[email protected]/msg00647/blender2cal3d.py

Hello,

I am very much in need of such a tool, so I was happy to stumble onto your development of the BVH exporter script. I am still fairly new to scripting python and the Blender API. Not sure if I discovered anything new, but here are my results. I imported the exported BVH file numerous times from different views. They always imported identically. So I them compared the imported file. The two did not lock up. I was curious if it might be just an offset issue, so I made an “empty” object and parented the armature to the empty file. The offset was close to RotX: 96.083, RotY: -0.464 and RotZ: 9.013 for the first frame. Upon playback, the two diverge and have separate ending locations. I went ahead and keyframed the end position so that they would lock up again. This time the offset was RotX: 75.685, RotY: 2.687 and RotZ 16.985. When the animation curves interpolation is set to linear, they somewhat closely marry, but not perfectly. Not sure if that will be any benefit to you, but I thought that I would give a crack at it.

Best Regards,

Stephen

Some more general observations:
1)The section of the code that sorts the children, most notably this line:

children_list.reverse()

seems to break something… to be honest I have forgotten what now, its been a while.
2) The output data always seems to have bits of the wrong limbs moving, as though the output sequence is wrong.
For exaample; I animate a single limb on a human heirarchy (import the standard SecondLife “SL_Avatar_Tpose.bvh” using bvh_import.py version = “1.90 06/08/01” ) , do a simple IK solver assisted raising of one arm, export using the above script,
and open in BVHacker to preview… sections of both the arm, and the LEG have animation.

A solution appears:


http://jacek.meratalk.com/2008/05/19/blender-to-sl-bvh-animation-exporter/

Great news, Ill see if we can get this included in blender and am happy to maintain.
Not sure if we accept CC licensed scripts but should be ok.

Excellent. Contact the author via her web page if you need to negotiate a different license. I think she simply aims to be flexible while retaining some credit.
She is responsive to this topic inworld (in SL) if you need me to do any conveyancing.

Also; I was pointing out to Jacek that her handling of rotations may be useful for making the (Ideasman’s) armature only version functional as well. I’m a newb at python, but it looks to me like the same or similar logic could be applied to some of the FIX ME sections.