diff options
Diffstat (limited to 'io_anim_bvh')
-rw-r--r-- | io_anim_bvh/__init__.py | 154 | ||||
-rw-r--r-- | io_anim_bvh/export_bvh.py | 281 | ||||
-rw-r--r-- | io_anim_bvh/import_bvh.py | 552 |
3 files changed, 987 insertions, 0 deletions
diff --git a/io_anim_bvh/__init__.py b/io_anim_bvh/__init__.py new file mode 100644 index 00000000..94d048f6 --- /dev/null +++ b/io_anim_bvh/__init__.py @@ -0,0 +1,154 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + +bl_info = { + "name": "BioVision Motion Capture (BVH) format", + "author": "Campbell Barton", + "blender": (2, 5, 7), + "api": 35622, + "location": "File > Import-Export", + "description": "Import-Export BVH from armature objects", + "warning": "", + "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.5/Py/"\ + "Scripts/Import-Export/MotionCapture_BVH", + "tracker_url": "", + "support": 'OFFICIAL', + "category": "Import-Export"} + +# To support reload properly, try to access a package var, if it's there, reload everything +if "bpy" in locals(): + import imp + if "import_bvh" in locals(): + imp.reload(import_bvh) + if "export_bvh" in locals(): + imp.reload(export_bvh) + +import bpy +from bpy.props import StringProperty, FloatProperty, IntProperty, BoolProperty, EnumProperty +from bpy_extras.io_utils import ImportHelper, ExportHelper + + +class ImportBVH(bpy.types.Operator, ImportHelper): + '''Load a BVH motion capture file''' + bl_idname = "import_anim.bvh" + bl_label = "Import BVH" + bl_options = {'REGISTER', 'UNDO'} + + filename_ext = ".bvh" + filter_glob = StringProperty(default="*.bvh", options={'HIDDEN'}) + + target = EnumProperty(items=( + ('ARMATURE', "Armature", ""), + ('OBJECT', "Object", ""), + ), + name="Target", + description="Import target type.", + default='ARMATURE') + + global_scale = FloatProperty(name="Scale", description="Scale the BVH by this value", min=0.0001, max=1000000.0, soft_min=0.001, soft_max=100.0, default=1.0) + frame_start = IntProperty(name="Start Frame", description="Starting frame for the animation", default=1) + use_cyclic = BoolProperty(name="Loop", description="Loop the animation playback", default=False) + rotate_mode = EnumProperty(items=( + ('QUATERNION', "Quaternion", "Convert rotations to quaternions"), + ('NATIVE', "Euler (Native)", "Use the rotation order defined in the BVH file"), + ('XYZ', "Euler (XYZ)", "Convert rotations to euler XYZ"), + ('XZY', "Euler (XZY)", "Convert rotations to euler XZY"), + ('YXZ', "Euler (YXZ)", "Convert rotations to euler YXZ"), + ('YZX', "Euler (YZX)", "Convert rotations to euler YZX"), + ('ZXY', "Euler (ZXY)", "Convert rotations to euler ZXY"), + ('ZYX', "Euler (ZYX)", "Convert rotations to euler ZYX"), + ), + name="Rotation", + description="Rotation conversion.", + default='NATIVE') + + def execute(self, context): + from . import import_bvh + return import_bvh.load(self, context, **self.as_keywords(ignore=("filter_glob",))) + + +class ExportBVH(bpy.types.Operator, ExportHelper): + '''Save a BVH motion capture file from an armature''' + bl_idname = "export_anim.bvh" + bl_label = "Export BVH" + + filename_ext = ".bvh" + filter_glob = StringProperty(default="*.bvh", options={'HIDDEN'}) + + global_scale = FloatProperty(name="Scale", description="Scale the BVH by this value", min=0.0001, max=1000000.0, soft_min=0.001, soft_max=100.0, default=1.0) + frame_start = IntProperty(name="Start Frame", description="Starting frame to export", default=0) + frame_end = IntProperty(name="End Frame", description="End frame to export", default=0) + + rotate_mode = EnumProperty(items=( + ('NATIVE', "Euler (Native)", "Use the rotation order defined in the BVH file"), + ('XYZ', "Euler (XYZ)", "Convert rotations to euler XYZ"), + ('XZY', "Euler (XZY)", "Convert rotations to euler XZY"), + ('YXZ', "Euler (YXZ)", "Convert rotations to euler YXZ"), + ('YZX', "Euler (YZX)", "Convert rotations to euler YZX"), + ('ZXY', "Euler (ZXY)", "Convert rotations to euler ZXY"), + ('ZYX', "Euler (ZYX)", "Convert rotations to euler ZYX"), + ), + name="Rotation", + description="Rotation conversion.", + default='NATIVE') + + @classmethod + def poll(cls, context): + obj = context.object + return obj and obj.type == 'ARMATURE' + + def invoke(self, context, event): + self.frame_start = context.scene.frame_start + self.frame_end = context.scene.frame_end + + return super().invoke(context, event) + + def execute(self, context): + if self.frame_start == 0 and self.frame_end == 0: + self.frame_start = context.scene.frame_start + self.frame_end = context.scene.frame_end + + from . import export_bvh + return export_bvh.save(self, context, **self.as_keywords(ignore=("check_existing", "filter_glob"))) + + +def menu_func_import(self, context): + self.layout.operator(ImportBVH.bl_idname, text="Motion Capture (.bvh)") + + +def menu_func_export(self, context): + self.layout.operator(ExportBVH.bl_idname, text="Motion Capture (.bvh)") + + +def register(): + bpy.utils.register_module(__name__) + + bpy.types.INFO_MT_file_import.append(menu_func_import) + bpy.types.INFO_MT_file_export.append(menu_func_export) + + +def unregister(): + bpy.utils.unregister_module(__name__) + + bpy.types.INFO_MT_file_import.remove(menu_func_import) + bpy.types.INFO_MT_file_export.remove(menu_func_export) + +if __name__ == "__main__": + register() diff --git a/io_anim_bvh/export_bvh.py b/io_anim_bvh/export_bvh.py new file mode 100644 index 00000000..851eb1cc --- /dev/null +++ b/io_anim_bvh/export_bvh.py @@ -0,0 +1,281 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + +# Script copyright (C) Campbell Barton +# fixes from Andrea Rugliancich + +import bpy + + +def write_armature(context, + filepath, + frame_start, + frame_end, + global_scale=1.0, + rotate_mode='NATIVE', + ): + + def ensure_rot_order(rot_order_str): + if set(rot_order_str) != {'X', 'Y', 'Z'}: + rot_order_str = "XYZ" + return rot_order_str + + from mathutils import Matrix, Vector, Euler + from math import degrees + + file = open(filepath, "w", encoding="utf8", newline="\n") + + obj = context.object + arm = obj.data + + # Build a dictionary of children. + # None for parentless + children = {None: []} + + # initialize with blank lists + for bone in arm.bones: + children[bone.name] = [] + + for bone in arm.bones: + children[getattr(bone.parent, "name", None)].append(bone.name) + + # sort the children + for children_list in children.values(): + children_list.sort() + + # bone name list in the order that the bones are written + serialized_names = [] + + node_locations = {} + + file.write("HIERARCHY\n") + + def write_recursive_nodes(bone_name, indent): + my_children = children[bone_name] + + indent_str = "\t" * indent + + bone = arm.bones[bone_name] + pose_bone = obj.pose.bones[bone_name] + loc = bone.head_local + node_locations[bone_name] = loc + + if rotate_mode == "NATIVE": + rot_order_str = ensure_rot_order(pose_bone.rotation_mode) + else: + rot_order_str = rotate_mode + + # make relative if we can + if bone.parent: + loc = loc - node_locations[bone.parent.name] + + if indent: + file.write("%sJOINT %s\n" % (indent_str, bone_name)) + else: + file.write("%sROOT %s\n" % (indent_str, bone_name)) + + file.write("%s{\n" % indent_str) + file.write("%s\tOFFSET %.6f %.6f %.6f\n" % (indent_str, loc.x * global_scale, loc.y * global_scale, loc.z * global_scale)) + if bone.use_connect and bone.parent: + file.write("%s\tCHANNELS 3 %srotation %srotation %srotation\n" % (indent_str, rot_order_str[0], rot_order_str[1], rot_order_str[2])) + else: + file.write("%s\tCHANNELS 6 Xposition Yposition Zposition %srotation %srotation %srotation\n" % (indent_str, rot_order_str[0], rot_order_str[1], rot_order_str[2])) + + if my_children: + # store the location for the children + # to het their relative offset + + # Write children + for child_bone in my_children: + serialized_names.append(child_bone) + write_recursive_nodes(child_bone, indent + 1) + + else: + # Write the bone end. + file.write("%s\tEnd Site\n" % indent_str) + file.write("%s\t{\n" % indent_str) + loc = bone.tail_local - node_locations[bone_name] + file.write("%s\t\tOFFSET %.6f %.6f %.6f\n" % (indent_str, loc.x * global_scale, loc.y * global_scale, loc.z * global_scale)) + file.write("%s\t}\n" % indent_str) + + file.write("%s}\n" % indent_str) + + if len(children[None]) == 1: + key = children[None][0] + serialized_names.append(key) + indent = 0 + + write_recursive_nodes(key, indent) + + else: + # Write a dummy parent node + file.write("ROOT %s\n" % key) + file.write("{\n") + file.write("\tOFFSET 0.0 0.0 0.0\n") + file.write("\tCHANNELS 0\n") # Xposition Yposition Zposition Xrotation Yrotation Zrotation + key = None + indent = 1 + + write_recursive_nodes(key, indent) + + file.write("}\n") + + # redefine bones as sorted by serialized_names + # so we can write motion + + 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 + "prev_euler", # last used euler to preserve euler compability in between keyframes + "connected", # is the bone connected to the parent bone? + "rot_order", + "rot_order_str", + ) + + _eul_order_lookup = { + 'XYZ': (0, 1, 2), + 'XZY': (0, 2, 1), + 'YXZ': (1, 0, 2), + 'YZX': (1, 2, 0), + 'ZXY': (2, 0, 1), + 'ZYX': (2, 1, 0)} + + def __init__(self, bone_name): + self.name = bone_name + self.rest_bone = arm.bones[bone_name] + self.pose_bone = obj.pose.bones[bone_name] + + if rotate_mode == "NATIVE": + self.rot_order_str = ensure_rot_order(self.pose_bone.rotation_mode) + else: + self.rot_order_str = rotate_mode + + self.rot_order = __class__._eul_order_lookup[self.rot_order_str] + + self.pose_mat = self.pose_bone.matrix + + mat = self.rest_bone.matrix + self.rest_arm_mat = self.rest_bone.matrix_local + self.rest_local_mat = self.rest_bone.matrix + + # inverted mats + self.pose_imat = self.pose_mat.inverted() + self.rest_arm_imat = self.rest_arm_mat.inverted() + self.rest_local_imat = self.rest_local_mat.inverted() + + self.parent = None + self.prev_euler = Euler((0.0, 0.0, 0.0), self.rot_order_str) + self.connected = (self.rest_bone.use_connect and self.rest_bone.parent) + + def update_posedata(self): + self.pose_mat = self.pose_bone.matrix + self.pose_imat = self.pose_mat.inverted() + + def __repr__(self): + if self.parent: + return "[\"%s\" child on \"%s\"]\n" % (self.name, self.parent.name) + else: + return "[\"%s\" root bone]\n" % (self.name) + + bones_decorated = [decorated_bone(bone_name) for bone_name in serialized_names] + + # Assign parents + bones_decorated_dict = {} + for dbone in bones_decorated: + bones_decorated_dict[dbone.name] = dbone + + for dbone in bones_decorated: + parent = dbone.rest_bone.parent + if parent: + dbone.parent = bones_decorated_dict[parent.name] + del bones_decorated_dict + # finish assigning parents + + scene = bpy.context.scene + + file.write("MOTION\n") + file.write("Frames: %d\n" % (frame_end - frame_start + 1)) + file.write("Frame Time: %.6f\n" % (1.0 / (scene.render.fps / scene.render.fps_base))) + + for frame in range(frame_start, frame_end + 1): + scene.frame_set(frame) + + for dbone in bones_decorated: + dbone.update_posedata() + + for dbone in bones_decorated: + trans = Matrix.Translation(dbone.rest_bone.head_local) + itrans = Matrix.Translation(-dbone.rest_bone.head_local) + + if dbone.parent: + mat_final = dbone.parent.rest_arm_mat * dbone.parent.pose_imat * dbone.pose_mat * dbone.rest_arm_imat + mat_final = itrans * mat_final * trans + loc = mat_final.to_translation() + (dbone.rest_bone.head_local - dbone.parent.rest_bone.head_local) + else: + mat_final = dbone.pose_mat * dbone.rest_arm_imat + mat_final = itrans * mat_final * trans + loc = mat_final.to_translation() + dbone.rest_bone.head + + # keep eulers compatible, no jumping on interpolation. + rot = mat_final.to_3x3().inverted().to_euler(dbone.rot_order_str, dbone.prev_euler) + + if not dbone.connected: + file.write("%.6f %.6f %.6f " % (loc * global_scale)[:]) + + file.write("%.6f %.6f %.6f " % (-degrees(rot[dbone.rot_order[0]]), -degrees(rot[dbone.rot_order[1]]), -degrees(rot[dbone.rot_order[2]]))) + + dbone.prev_euler = rot + + file.write("\n") + + file.close() + + print("BVH Exported: %s frames:%d\n" % (filepath, frame_end - frame_start + 1)) + + +def save(operator, context, filepath="", + frame_start=-1, + frame_end=-1, + global_scale=1.0, + rotate_mode="NATIVE", + ): + + write_armature(context, filepath, + frame_start=frame_start, + frame_end=frame_end, + global_scale=global_scale, + rotate_mode=rotate_mode, + ) + + return {'FINISHED'} + + +if __name__ == "__main__": + scene = bpy.context.scene + _read(bpy.data.filepath.rstrip(".blend") + ".bvh", bpy.context.object, scene.frame_start, scene.frame_end, 1.0) diff --git a/io_anim_bvh/import_bvh.py b/io_anim_bvh/import_bvh.py new file mode 100644 index 00000000..4f1d19ff --- /dev/null +++ b/io_anim_bvh/import_bvh.py @@ -0,0 +1,552 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + +# Script copyright (C) Campbell Barton + +import math +from math import radians + +import bpy +import mathutils +from mathutils import Vector, Euler, Matrix + + +class bvh_node_class(object): + __slots__ = ( + 'name', # bvh joint name + 'parent', # bvh_node_class type or None for no parent + 'children', # a list of children of this type. + 'rest_head_world', # worldspace rest location for the head of this node + 'rest_head_local', # localspace rest location for the head of this node + 'rest_tail_world', # worldspace rest location for the tail of this node + 'rest_tail_local', # worldspace rest location for the tail of this node + 'channels', # list of 6 ints, -1 for an unused channel, otherwise an index for the BVH motion data lines, lock triple then rot triple + 'rot_order', # a triple of indices as to the order rotation is applied. [0,1,2] is x/y/z - [None, None, None] if no rotation. + 'rot_order_str', # same as above but a string 'XYZ' format. + 'anim_data', # a list one tuple's one for each frame. (locx, locy, locz, rotx, roty, rotz), euler rotation ALWAYS stored xyz order, even when native used. + 'has_loc', # Conveinience function, bool, same as (channels[0]!=-1 or channels[1]!=-1 channels[2]!=-1) + 'has_rot', # Conveinience function, bool, same as (channels[3]!=-1 or channels[4]!=-1 channels[5]!=-1) + 'temp') # use this for whatever you want + + _eul_order_lookup = { + (0, 1, 2): 'XYZ', + (0, 2, 1): 'XZY', + (1, 0, 2): 'YXZ', + (1, 2, 0): 'YZX', + (2, 0, 1): 'ZXY', + (2, 1, 0): 'ZYX'} + + def __init__(self, name, rest_head_world, rest_head_local, parent, channels, rot_order): + self.name = name + self.rest_head_world = rest_head_world + self.rest_head_local = rest_head_local + self.rest_tail_world = None + self.rest_tail_local = None + self.parent = parent + self.channels = channels + self.rot_order = tuple(rot_order) + self.rot_order_str = __class__._eul_order_lookup[self.rot_order] + + # convenience functions + self.has_loc = channels[0] != -1 or channels[1] != -1 or channels[2] != -1 + self.has_rot = channels[3] != -1 or channels[4] != -1 or channels[5] != -1 + + self.children = [] + + # list of 6 length tuples: (lx,ly,lz, rx,ry,rz) + # even if the channels arnt used they will just be zero + # + self.anim_data = [(0, 0, 0, 0, 0, 0)] + + def __repr__(self): + return 'BVH name:"%s", rest_loc:(%.3f,%.3f,%.3f), rest_tail:(%.3f,%.3f,%.3f)' %\ + (self.name,\ + self.rest_head_world.x, self.rest_head_world.y, self.rest_head_world.z,\ + self.rest_head_world.x, self.rest_head_world.y, self.rest_head_world.z) + + +def read_bvh(context, file_path, rotate_mode='XYZ', global_scale=1.0): + # File loading stuff + # Open the file for importing + file = open(file_path, 'rU') + + # Seperate into a list of lists, each line a list of words. + file_lines = file.readlines() + # Non standard carrage returns? + if len(file_lines) == 1: + file_lines = file_lines[0].split('\r') + + # Split by whitespace. + file_lines = [ll for ll in [l.split() for l in file_lines] if ll] + + # Create Hirachy as empties + if file_lines[0][0].lower() == 'hierarchy': + #print 'Importing the BVH Hierarchy for:', file_path + pass + else: + raise 'ERROR: This is not a BVH file' + + bvh_nodes = {None: None} + bvh_nodes_serial = [None] + + channelIndex = -1 + + lineIdx = 0 # An index for the file. + while lineIdx < len(file_lines) - 1: + #... + if file_lines[lineIdx][0].lower() == 'root' or file_lines[lineIdx][0].lower() == 'joint': + + # Join spaces into 1 word with underscores joining it. + if len(file_lines[lineIdx]) > 2: + file_lines[lineIdx][1] = '_'.join(file_lines[lineIdx][1:]) + file_lines[lineIdx] = file_lines[lineIdx][:2] + + # MAY NEED TO SUPPORT MULTIPLE ROOT's HERE!!!, Still unsure weather multiple roots are possible.?? + + # Make sure the names are unique- Object names will match joint names exactly and both will be unique. + name = file_lines[lineIdx][1] + + #print '%snode: %s, parent: %s' % (len(bvh_nodes_serial) * ' ', name, bvh_nodes_serial[-1]) + + lineIdx += 2 # Increment to the next line (Offset) + rest_head_local = Vector((float(file_lines[lineIdx][1]), float(file_lines[lineIdx][2]), float(file_lines[lineIdx][3]))) * global_scale + lineIdx += 1 # Increment to the next line (Channels) + + # newChannel[Xposition, Yposition, Zposition, Xrotation, Yrotation, Zrotation] + # newChannel references indecies to the motiondata, + # if not assigned then -1 refers to the last value that will be added on loading at a value of zero, this is appended + # We'll add a zero value onto the end of the MotionDATA so this is always refers to a value. + my_channel = [-1, -1, -1, -1, -1, -1] + my_rot_order = [None, None, None] + rot_count = 0 + for channel in file_lines[lineIdx][2:]: + channel = channel.lower() + channelIndex += 1 # So the index points to the right channel + if channel == 'xposition': + my_channel[0] = channelIndex + elif channel == 'yposition': + my_channel[1] = channelIndex + elif channel == 'zposition': + my_channel[2] = channelIndex + + elif channel == 'xrotation': + my_channel[3] = channelIndex + my_rot_order[rot_count] = 0 + rot_count += 1 + elif channel == 'yrotation': + my_channel[4] = channelIndex + my_rot_order[rot_count] = 1 + rot_count += 1 + elif channel == 'zrotation': + my_channel[5] = channelIndex + my_rot_order[rot_count] = 2 + rot_count += 1 + + channels = file_lines[lineIdx][2:] + + my_parent = bvh_nodes_serial[-1] # account for none + + # Apply the parents offset accumulatively + if my_parent is None: + rest_head_world = Vector(rest_head_local) + else: + rest_head_world = my_parent.rest_head_world + rest_head_local + + bvh_node = bvh_nodes[name] = bvh_node_class(name, rest_head_world, rest_head_local, my_parent, my_channel, my_rot_order) + + # If we have another child then we can call ourselves a parent, else + bvh_nodes_serial.append(bvh_node) + + # Account for an end node + if file_lines[lineIdx][0].lower() == 'end' and file_lines[lineIdx][1].lower() == 'site': # There is sometimes a name after 'End Site' but we will ignore it. + lineIdx += 2 # Increment to the next line (Offset) + rest_tail = Vector((float(file_lines[lineIdx][1]), float(file_lines[lineIdx][2]), float(file_lines[lineIdx][3]))) * global_scale + + bvh_nodes_serial[-1].rest_tail_world = bvh_nodes_serial[-1].rest_head_world + rest_tail + bvh_nodes_serial[-1].rest_tail_local = bvh_nodes_serial[-1].rest_head_local + rest_tail + + # Just so we can remove the Parents in a uniform way- End has kids + # so this is a placeholder + bvh_nodes_serial.append(None) + + if len(file_lines[lineIdx]) == 1 and file_lines[lineIdx][0] == '}': # == ['}'] + bvh_nodes_serial.pop() # Remove the last item + + if len(file_lines[lineIdx]) == 1 and file_lines[lineIdx][0].lower() == 'motion': + #print '\nImporting motion data' + lineIdx += 3 # Set the cursor to the first frame + break + + lineIdx += 1 + + # Remove the None value used for easy parent reference + del bvh_nodes[None] + # Dont use anymore + del bvh_nodes_serial + + bvh_nodes_list = bvh_nodes.values() + + while lineIdx < len(file_lines): + line = file_lines[lineIdx] + for bvh_node in bvh_nodes_list: + #for bvh_node in bvh_nodes_serial: + lx = ly = lz = rx = ry = rz = 0.0 + channels = bvh_node.channels + anim_data = bvh_node.anim_data + if channels[0] != -1: + lx = global_scale * float(line[channels[0]]) + + if channels[1] != -1: + ly = global_scale * float(line[channels[1]]) + + if channels[2] != -1: + lz = global_scale * float(line[channels[2]]) + + if channels[3] != -1 or channels[4] != -1 or channels[5] != -1: + + rx = radians(float(line[channels[3]])) + ry = radians(float(line[channels[4]])) + rz = radians(float(line[channels[5]])) + + # Done importing motion data # + anim_data.append((lx, ly, lz, rx, ry, rz)) + lineIdx += 1 + + # Assign children + for bvh_node in bvh_nodes.values(): + bvh_node_parent = bvh_node.parent + if bvh_node_parent: + bvh_node_parent.children.append(bvh_node) + + # Now set the tip of each bvh_node + for bvh_node in bvh_nodes.values(): + + if not bvh_node.rest_tail_world: + if len(bvh_node.children) == 0: + # could just fail here, but rare BVH files have childless nodes + bvh_node.rest_tail_world = Vector(bvh_node.rest_head_world) + bvh_node.rest_tail_local = Vector(bvh_node.rest_head_local) + elif len(bvh_node.children) == 1: + bvh_node.rest_tail_world = Vector(bvh_node.children[0].rest_head_world) + bvh_node.rest_tail_local = bvh_node.rest_head_local + bvh_node.children[0].rest_head_local + else: + # allow this, see above + #if not bvh_node.children: + # raise 'error, bvh node has no end and no children. bad file' + + # Removed temp for now + rest_tail_world = Vector((0.0, 0.0, 0.0)) + rest_tail_local = Vector((0.0, 0.0, 0.0)) + for bvh_node_child in bvh_node.children: + rest_tail_world += bvh_node_child.rest_head_world + rest_tail_local += bvh_node_child.rest_head_local + + bvh_node.rest_tail_world = rest_tail_world * (1.0 / len(bvh_node.children)) + bvh_node.rest_tail_local = rest_tail_local * (1.0 / len(bvh_node.children)) + + # Make sure tail isnt the same location as the head. + if (bvh_node.rest_tail_local - bvh_node.rest_head_local).length <= 0.001 * global_scale: + print("\tzero length node found:", bvh_node.name) + bvh_node.rest_tail_local.y = bvh_node.rest_tail_local.y + global_scale / 10 + bvh_node.rest_tail_world.y = bvh_node.rest_tail_world.y + global_scale / 10 + + return bvh_nodes + + +def bvh_node_dict2objects(context, bvh_name, bvh_nodes, rotate_mode='NATIVE', frame_start=1, IMPORT_LOOP=False): + + if frame_start < 1: + frame_start = 1 + + scene = context.scene + for obj in scene.objects: + obj.select = False + + objects = [] + + def add_ob(name): + obj = bpy.data.objects.new(name, None) + scene.objects.link(obj) + objects.append(obj) + obj.select = True + + # nicer drawing. + obj.empty_draw_type = 'CUBE' + obj.empty_draw_size = 0.1 + + return obj + + # Add objects + for name, bvh_node in bvh_nodes.items(): + bvh_node.temp = add_ob(name) + bvh_node.temp.rotation_mode = bvh_node.rot_order_str[::-1] + + # Parent the objects + for bvh_node in bvh_nodes.values(): + for bvh_node_child in bvh_node.children: + bvh_node_child.temp.parent = bvh_node.temp + + # Offset + for bvh_node in bvh_nodes.values(): + # Make relative to parents offset + bvh_node.temp.location = bvh_node.rest_head_local + + # Add tail objects + for name, bvh_node in bvh_nodes.items(): + if not bvh_node.children: + ob_end = add_ob(name + '_end') + ob_end.parent = bvh_node.temp + ob_end.location = bvh_node.rest_tail_world - bvh_node.rest_head_world + + for name, bvh_node in bvh_nodes.items(): + obj = bvh_node.temp + + for frame_current in range(len(bvh_node.anim_data)): + + lx, ly, lz, rx, ry, rz = bvh_node.anim_data[frame_current] + + if bvh_node.has_loc: + obj.delta_location = Vector((lx, ly, lz)) - bvh_node.rest_head_world + obj.keyframe_insert("delta_location", index=-1, frame=frame_start + frame_current) + + if bvh_node.has_rot: + obj.delta_rotation_euler = rx, ry, rz + obj.keyframe_insert("delta_rotation_euler", index=-1, frame=frame_start + frame_current) + + return objects + + +def bvh_node_dict2armature(context, bvh_name, bvh_nodes, rotate_mode='XYZ', frame_start=1, IMPORT_LOOP=False): + + if frame_start < 1: + frame_start = 1 + + # Add the new armature, + scene = context.scene + for obj in scene.objects: + obj.select = False + + arm_data = bpy.data.armatures.new(bvh_name) + arm_ob = bpy.data.objects.new(bvh_name, arm_data) + + scene.objects.link(arm_ob) + + arm_ob.select = True + scene.objects.active = arm_ob + + bpy.ops.object.mode_set(mode='OBJECT', toggle=False) + bpy.ops.object.mode_set(mode='EDIT', toggle=False) + + # Get the average bone length for zero length bones, we may not use this. + average_bone_length = 0.0 + nonzero_count = 0 + for bvh_node in bvh_nodes.values(): + l = (bvh_node.rest_head_local - bvh_node.rest_tail_local).length + if l: + average_bone_length += l + nonzero_count += 1 + + # Very rare cases all bones couldbe zero length??? + if not average_bone_length: + average_bone_length = 0.1 + else: + # Normal operation + average_bone_length = average_bone_length / nonzero_count + + # XXX, annoying, remove bone. + while arm_data.edit_bones: + arm_ob.edit_bones.remove(arm_data.edit_bones[-1]) + + ZERO_AREA_BONES = [] + for name, bvh_node in bvh_nodes.items(): + # New editbone + bone = bvh_node.temp = arm_data.edit_bones.new(name) + + bone.head = bvh_node.rest_head_world + bone.tail = bvh_node.rest_tail_world + + # ZERO AREA BONES. + if (bone.head - bone.tail).length < 0.001: + print("\tzero length bone found:", bone.name) + if bvh_node.parent: + ofs = bvh_node.parent.rest_head_local - bvh_node.parent.rest_tail_local + if ofs.length: # is our parent zero length also?? unlikely + bone.tail = bone.tail - ofs + else: + bone.tail.y = bone.tail.y + average_bone_length + else: + bone.tail.y = bone.tail.y + average_bone_length + + ZERO_AREA_BONES.append(bone.name) + + for bvh_node in bvh_nodes.values(): + if bvh_node.parent: + # bvh_node.temp is the Editbone + + # Set the bone parent + bvh_node.temp.parent = bvh_node.parent.temp + + # Set the connection state + if not bvh_node.has_loc and\ + bvh_node.parent and\ + bvh_node.parent.temp.name not in ZERO_AREA_BONES and\ + bvh_node.parent.rest_tail_local == bvh_node.rest_head_local: + bvh_node.temp.use_connect = True + + # Replace the editbone with the editbone name, + # to avoid memory errors accessing the editbone outside editmode + for bvh_node in bvh_nodes.values(): + bvh_node.temp = bvh_node.temp.name + + # Now Apply the animation to the armature + + # Get armature animation data + bpy.ops.object.mode_set(mode='OBJECT', toggle=False) + + pose = arm_ob.pose + pose_bones = pose.bones + + if rotate_mode == 'NATIVE': + for bvh_node in bvh_nodes.values(): + bone_name = bvh_node.temp # may not be the same name as the bvh_node, could have been shortened. + pose_bone = pose_bones[bone_name] + pose_bone.rotation_mode = bvh_node.rot_order_str + + elif rotate_mode != 'QUATERNION': + for pose_bone in pose_bones: + pose_bone.rotation_mode = rotate_mode + else: + # Quats default + pass + + context.scene.update() + + arm_ob.animation_data_create() + action = bpy.data.actions.new(name=bvh_name) + arm_ob.animation_data.action = action + + # Replace the bvh_node.temp (currently an editbone) + # With a tuple (pose_bone, armature_bone, bone_rest_matrix, bone_rest_matrix_inv) + for bvh_node in bvh_nodes.values(): + bone_name = bvh_node.temp # may not be the same name as the bvh_node, could have been shortened. + pose_bone = pose_bones[bone_name] + rest_bone = arm_data.bones[bone_name] + bone_rest_matrix = rest_bone.matrix_local.to_3x3() + + bone_rest_matrix_inv = Matrix(bone_rest_matrix) + bone_rest_matrix_inv.invert() + + bone_rest_matrix_inv.resize_4x4() + bone_rest_matrix.resize_4x4() + bvh_node.temp = (pose_bone, bone, bone_rest_matrix, bone_rest_matrix_inv) + + # Make a dict for fast access without rebuilding a list all the time. + + # KEYFRAME METHOD, SLOW, USE IPOS DIRECT + # TODO: use f-point samples instead (Aligorith) + if rotate_mode != 'QUATERNION': + prev_euler = [Euler() for i in range(len(bvh_nodes))] + + # Animate the data, the last used bvh_node will do since they all have the same number of frames + for frame_current in range(len(bvh_node.anim_data) - 1): # skip the first frame (rest frame) + # print frame_current + + # if frame_current==40: # debugging + # break + + scene.frame_set(frame_start + frame_current) + + # Dont neet to set the current frame + for i, bvh_node in enumerate(bvh_nodes.values()): + pose_bone, bone, bone_rest_matrix, bone_rest_matrix_inv = bvh_node.temp + lx, ly, lz, rx, ry, rz = bvh_node.anim_data[frame_current + 1] + + if bvh_node.has_rot: + # apply rotation order and convert to XYZ + # note that the rot_order_str is reversed. + bone_rotation_matrix = Euler((rx, ry, rz), bvh_node.rot_order_str[::-1]).to_matrix().to_4x4() + bone_rotation_matrix = bone_rest_matrix_inv * bone_rotation_matrix * bone_rest_matrix + + if rotate_mode == 'QUATERNION': + pose_bone.rotation_quaternion = bone_rotation_matrix.to_quaternion() + else: + euler = bone_rotation_matrix.to_euler(pose_bone.rotation_mode, prev_euler[i]) + pose_bone.rotation_euler = euler + prev_euler[i] = euler + + if bvh_node.has_loc: + pose_bone.location = (bone_rest_matrix_inv * Matrix.Translation(Vector((lx, ly, lz)) - bvh_node.rest_head_local)).to_translation() + + if bvh_node.has_loc: + pose_bone.keyframe_insert("location") + if bvh_node.has_rot: + if rotate_mode == 'QUATERNION': + pose_bone.keyframe_insert("rotation_quaternion") + else: + pose_bone.keyframe_insert("rotation_euler") + + for cu in action.fcurves: + if IMPORT_LOOP: + pass # 2.5 doenst have cyclic now? + + for bez in cu.keyframe_points: + bez.interpolation = 'LINEAR' + + return arm_ob + + +def load(operator, context, filepath="", target='ARMATURE', rotate_mode='NATIVE', global_scale=1.0, use_cyclic=False, frame_start=1): + import time + t1 = time.time() + print('\tparsing bvh %r...' % filepath, end="") + + bvh_nodes = read_bvh(context, filepath, + rotate_mode=rotate_mode, + global_scale=global_scale) + + print('%.4f' % (time.time() - t1)) + + frame_orig = context.scene.frame_current + + t1 = time.time() + print('\timporting to blender...', end="") + + bvh_name = bpy.path.display_name_from_filepath(filepath) + + if target == 'ARMATURE': + bvh_node_dict2armature(context, bvh_name, bvh_nodes, + rotate_mode=rotate_mode, + frame_start=frame_start, + IMPORT_LOOP=use_cyclic) + + elif target == 'OBJECT': + bvh_node_dict2objects(context, bvh_name, bvh_nodes, + rotate_mode=rotate_mode, + frame_start=frame_start, + IMPORT_LOOP=use_cyclic) + + else: + raise Exception("invalid type") + + print('Done in %.4f\n' % (time.time() - t1)) + + context.scene.frame_set(frame_orig) + + return {'FINISHED'} |