Skip to content
Snippets Groups Projects
__init__.py 19.2 KiB
Newer Older
  • Learn to ignore specific revisions
  • # ##### 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 3
    #  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-80 compliant>
    
    # This script was developed with financial support from the Foundation for
    # Science and Technology of Portugal, under the grant SFRH/BD/66452/2009.
    
    
    bl_info = {
        'name': "Acclaim Motion Capture Files (.asf, .amc)",
        'author': "Daniel Monteiro Basso <daniel@basso.inf.br>",
    
    Daniel M. Basso's avatar
    Daniel M. Basso committed
        'version': (2012, 2, 29, 1),
        'blender': (2, 6, 2),
        'location': "File > Import-Export",
    
        'description': "Imports Acclaim Skeleton and Motion Capture Files",
    
        'wiki_url': "http://wiki.blender.org/index.php/Extensions:2.5/Py/"
    
                    "Scripts/Import-Export/Acclaim_Importer",
    
        'tracker_url': "http://projects.blender.org/tracker/index.php?"
    
                       "func=detail&aid=27127&group_id=153&atid=467",
        'category': 'Import-Export'}
    
    
    import re
    import bpy
    from mathutils import Vector, Matrix
    
    Daniel M. Basso's avatar
    Daniel M. Basso committed
    from math import radians, degrees
    
    from bpy.props import (StringProperty,
                           BoolProperty,
                           FloatProperty,
                           IntProperty,
                           )
    
    
    
    class DataStructure:
        """
            Parse the Skeleton and Motion Files to an internal data structure.
        """
        doc = re.compile(r"(?ms):(\w+)\s+([^:]+)")
        block = re.compile(r"(?ms)begin\s+(.*?)\s+end")
        bonedata = re.compile(r"(?ms)(name|direction|length|axis|dof)\s+(.*?)\s*$"
                              "|limits(\s.*)")
    
        def __init__(self, file_path, scale=1.):
            self.scale = scale
    
            source = open(file_path, encoding="utf-8", errors="replace").read()
    
            sections = dict(DataStructure.doc.findall(source))
            if not sections:
                raise ValueError("Wrong file structure.")
    
            if 'units' in sections:
                units = dict(u.strip().split()
                    for u in sections['units'].splitlines()
                    if u.strip())
                if 'length' in units:
                    self.scale /= float(units['length'])
    
            if 'bonedata' not in sections:
                raise ValueError("Bone data section not found.")
            bm = DataStructure.block.findall(sections['bonedata'])
            if not bm:
                raise ValueError("Bone data section malformed.")
            self.bones = {'root': {
                'dof': ['X', 'Y', 'Z'],
                'direction': Vector(),  # should be orientation of root sector
                'length': 1,
                'axis': Matrix(),
                'axis_inv': Matrix(),
                }}
            for b in bm:
                bd = dict((i[0] or 'limits', i[0] and i[1] or i[2])
                            for i in DataStructure.bonedata.findall(b))
                for k in bd:
                    s = [t for t in re.split(r"[^a-zA-Z0-9-+.]", bd[k]) if t]
                    if k == 'axis':
                        rot = Matrix()
                        for ang, basis in zip(s[:3], s[3].upper()):
    
                            rot = Matrix.Rotation(radians(float(ang)),
                                                  4, basis) * rot
    
                        bd['axis'] = rot
                    elif k == 'direction':
                        bd[k] = Vector([float(n) for n in s])
                    elif k == 'length':
                        bd[k] = float(s[0]) * self.scale
                    elif k == 'dof':
                        bd[k] = [a[1].upper() for a in s]  # only rotations
                    elif k == 'limits':
                        bd[k] = s
                if 'axis' in bd:
                    bd['axis_inv'] = bd['axis'].inverted()
                self.bones[bd['name']] = bd
    
            if 'hierarchy' not in sections:
                raise ValueError("Hierarchy section not found.")
            hm = DataStructure.block.search(sections['hierarchy'])
            if not hm:
                raise ValueError("Hierarchy section malformed.")
            self.hierarchy = {}
            for l in hm.group(1).splitlines():
                t = l.strip().split()
                self.hierarchy[t[0]] = t[1:]
    
        def scan_motion_capture(self, filename, skip=5):
            """
                Parse an Acclaim Motion Capture file and iterates over the data
            """
    
            amc = open(filename, encoding="utf-8", errors="replace")
    
            l = ' '
            while l and not l[0].isdigit():
                l = amc.readline().strip()
            while l:
                frame = int(l)
                bdefs = []
                while True:
                    l = amc.readline().strip()
                    if not l or l[0].isdigit():
                        break
                    bdefs.append(l.split())
                if (frame - 1) % skip != 0:
                    continue
                self.pose_def = {}
                for b in bdefs:
                    vs = [float(v) for v in b[1:]]
                    if b[0] == 'root':
                        loc = Vector(vs[:3]) * self.scale
                        vs = vs[3:]
                    rot = Matrix()
    
    Daniel M. Basso's avatar
    Daniel M. Basso committed
                    if 'dof' not in self.bones[b[0]]:
                        # If 'dof' isn't defined it probably means the AMC comes
                        # from a different origin than the ASF, such as the
                        # AMC exporter in this package. Assume XYZ order.
                        self.bones[b[0]]['dof'] = ['X', 'Y', 'Z']
    
                    for dof, ang in zip(self.bones[b[0]]['dof'], vs):
    
                        rot = Matrix.Rotation(radians(ang), 4, dof) * rot
    
                    self.pose_def[b[0]] = rot
                pose = self.calculate_pose(Matrix.Translation(loc))
                yield(frame / skip + 1, pose)
    
    
        def calculate_pose(self, parent, bone='root'):
            """
                Calculate each bone transform iteratively
            """
            bd = self.bones[bone]
            tail = Matrix.Translation(bd['direction'] * bd['length'])
            if bone in self.pose_def:
                tail = bd['axis'] * self.pose_def[bone] * bd['axis_inv'] * tail
            world = parent * tail
    
    Daniel M. Basso's avatar
    Daniel M. Basso committed
            local = tail
    
            yield(bone, world, local)
            if bone in self.hierarchy:
                for child in self.hierarchy[bone]:
                    for b, w, l in self.calculate_pose(world, child):
                        yield(b, w, l)
    
    
    class StructureBuilder(DataStructure):
        def __init__(self, file_path, name="Skel", scale=1.):
            """
                Setup instance data and load the skeleton
            """
            self.file_path = file_path
            self.name = name
            self.user_def_scale = scale
            DataStructure.__init__(self, file_path, scale)
    
        def create_armature(self):
            """
                Create the armature and leave it in edit mode
            """
            bpy.context.scene.objects.active = None
            bpy.ops.object.add(type='ARMATURE', enter_editmode=True)
            self.object = bpy.context.scene.objects.active
            self.armature = self.object.data
            self.object.name = self.name
            self.armature.name = self.name
            self.armature.draw_type = 'STICK'
            self.object['source_file_path'] = self.file_path
            self.object['source_scale'] = self.user_def_scale
            self.object['MhxArmature'] = 'Daz'
    
        def load_armature(self, obj):
            """
                Assign the armature object to be used for loading motion
            """
            self.object = obj
    
        def build_structure(self, use_limits=False):
            """
                Create the root bone and start the recursion, exit edit mode
            """
            self.use_limits = use_limits
            bpy.ops.armature.bone_primitive_add(name='root')
            root_dir = Vector((0, 0.1 * self.scale, 0))
            bpy.ops.transform.translate(value=root_dir + Vector((.0, .0, -1.0)))
            self.recursive_add_bones()
            bpy.ops.armature.select_all(action='DESELECT')
            bpy.ops.object.mode_set(mode='OBJECT')
    
        def recursive_add_bones(self, parent_name='root'):
            """
                Traverse the hierarchy creating bones and constraints
            """
            if  parent_name not in self.hierarchy:
                return
            for name in self.hierarchy[parent_name]:
                self.add_bone(name, parent_name)
                if self.use_limits:
                    self.add_limit_constraint(name)
                self.recursive_add_bones(name)
    
        def add_bone(self, name, parent_name):
            """
                Extrude a bone from the specified parent, and configure it
            """
            bone_def = self.bones[name]
            bpy.ops.armature.select_all(action='DESELECT')
            # select tail of parent bone
            self.armature.edit_bones[parent_name].select_tail = True
            # extrude and name the new bone
            bpy.ops.armature.extrude()
            self.armature.edit_bones[-1].name = name
            # translate the tail of the new bone
            tail = bone_def['direction'] * bone_def['length']
            bpy.ops.transform.translate(value=tail)
            # align the bone to the rotation axis
            axis = bone_def['axis'].to_3x3()
            vec = axis * Vector((.0, .0, -1.0))
            self.armature.edit_bones[-1].align_roll(vector=vec)
    
        def add_limit_constraint(self, name):
            """
                Create the limit rotation constraint of the specified bone
            """
            bpy.ops.object.mode_set(mode='POSE')
            bone_def = self.bones[name]
            dof = bone_def['dof'] if 'dof' in bone_def else ''
            pb = self.object.pose.bones[name]
            self.armature.bones.active = self.armature.bones[name]
            bpy.ops.pose.constraint_add(type='LIMIT_ROTATION')
            constr = pb.constraints[-1]
            constr.owner_space = 'LOCAL'
            constr.use_limit_x = True
            constr.use_limit_y = True
            constr.use_limit_z = True
            if dof:
    
                limits = (radians(float(v)) for v in bone_def['limits'])
    
                if 'X' in dof:
                    constr.min_x = next(limits)
                    constr.max_x = next(limits)
                if 'Y' in dof:
                    constr.max_z = -next(limits)
                    constr.min_z = -next(limits)
                if 'Z' in dof:
                    constr.min_y = next(limits)
                    constr.max_y = next(limits)
            bpy.ops.object.mode_set(mode='EDIT')
    
    
        def load_motion_capture(self, filename, frame_skip=5, use_frame_no=False):
    
            """
                Create the keyframes for a motion capture file
            """
            bpy.context.active_object.animation_data_clear()
            bpy.ops.object.mode_set(mode='POSE')
            bpy.ops.pose.select_all(action='SELECT')
            bpy.ops.pose.rot_clear()
            bpy.ops.pose.loc_clear()
            self.rest = {}
            for b in self.object.pose.bones:
    
                self.rest[b.name] = (b,
                                     b.matrix.to_3x3(),
                                     b.matrix.to_3x3().inverted(),
                                     )
    
    Daniel M. Basso's avatar
    Daniel M. Basso committed
            self.fno = 1  # default Blender scene start frame
    
            self.use_frame_no = use_frame_no
    
            self.motion = iter(self.scan_motion_capture(filename, frame_skip))
    
        def apply_next_frame(self):
            try:
                frame, bones = next(self.motion)
            except StopIteration:
                return False
    
            regframe = frame if self.use_frame_no else self.fno
    
            self.fno += 1
            for name, w, l in bones:
                b, P, Pi = self.rest[name]
                if name == 'root':
                    b.location = w.to_translation()
                    b.keyframe_insert('location', -1, regframe, name)
                T = Pi * l.to_3x3() * P
                b.rotation_quaternion = T.to_quaternion()
                b.keyframe_insert('rotation_quaternion', -1, regframe, name)
            return True
    
    
    class AsfImporter(bpy.types.Operator):
    
    Daniel M. Basso's avatar
    Daniel M. Basso committed
        #
        "Load an Acclaim Skeleton File"
        #
    
        bl_idname = "import_anim.asf"
        bl_label = "Import ASF"
    
    
        filepath = StringProperty(
    
                )
        armature_name = StringProperty(
                name="Armature Name", maxlen=32,
                default="Skeleton",
                description="Name of the new object",
                )
        use_limits = BoolProperty(
                name="Use Limits", default=False,
                description="Create bone constraints for limits",
                )
        scale = FloatProperty(
                name="Scale",
                default=1.0,
                description="Scale the armature by this value",
                min=0.0001, max=1000000.0,
                soft_min=0.001, soft_max=100.0,
                )
        from_inches = BoolProperty(
                name="Convert from inches to metric",
                default=False, description="Scale by 2.54/100",
                )
        use_rot_x = BoolProperty(
                name="Rotate X 90 degrees",
                default=False,
                description="Correct orientation",
                )
        use_rot_z = BoolProperty(
                name="Rotate Z 90 degrees",
                default=False,
                description="Correct orientation",
                )
    
    
        filter_glob = StringProperty(default="*.asf", options={'HIDDEN'})
    
        def execute(self, context):
    
            uscale = (0.0254 if self.from_inches else 1.0)
            sb = StructureBuilder(self.filepath,
                                  self.armature_name,
                                  self.scale * uscale,
                                  )
    
            sb.build_structure(self.use_limits)
            if self.use_rot_x:
                bpy.ops.transform.rotate(value=(radians(90.0),), axis=(1, 0, 0))
            if self.use_rot_z:
                bpy.ops.transform.rotate(value=(radians(90.0),), axis=(0, 0, 1))
    
            return {'FINISHED'}
    
        def invoke(self, context, event):
            wm = context.window_manager
            wm.fileselect_add(self)
            return {'RUNNING_MODAL'}
    
    
    class AmcAnimator(bpy.types.Operator):
        """
            Load an Acclaim Motion Capture
        """
        bl_idname = "import_anim.amc_animate"
        bl_label = "Animate AMC"
    
        sb = None
        timer = None
    
        def modal(self, context, event):
            if event.type == 'ESC':
                return self.cancel(context)
            if event.type == 'TIMER':
                if not self.sb.apply_next_frame():
                    return self.cancel(context)
            return {'PASS_THROUGH'}
    
        def execute(self, context):
            context.window_manager.modal_handler_add(self)
            self.timer = context.window_manager.\
                event_timer_add(0.001, context.window)
            return {'RUNNING_MODAL'}
    
        def cancel(self, context):
            bpy.context.scene.frame_set(bpy.context.scene.frame_current)
            context.window_manager.event_timer_remove(self.timer)
            bpy.ops.object.mode_set(mode='OBJECT')
            return {'CANCELLED'}
    
    
    class AmcImporter(bpy.types.Operator):
    
    Daniel M. Basso's avatar
    Daniel M. Basso committed
        #
        "Load an Acclaim Motion Capture"
        #
    
        bl_idname = "import_anim.amc"
        bl_label = "Import AMC"
    
    
                )
        frame_skip = IntProperty(
                name="Fps divisor",
                default=4,
                # usually the sample rate is 120, so the default 4 gives you 30fps
                description="Frame supersampling factor",
                min=1,
                )
        use_frame_no = BoolProperty(
                name="Use frame numbers",
                default=False,
                description="Offset start of animation according to the source",
                )
    
    
        filter_glob = StringProperty(default="*.amc", options={'HIDDEN'})
    
        @classmethod
        def poll(cls, context):
            ob = context.active_object
    
            return (ob and ob.type == 'ARMATURE' and 'source_file_path' in ob)
    
    
        def execute(self, context):
            ob = context.active_object
            sb = StructureBuilder(
                ob['source_file_path'],
                ob.name,
                ob['source_scale'])
            sb.load_armature(ob)
    
            sb.load_motion_capture(self.filepath,
                                   self.frame_skip,
                                   self.use_frame_no)
    
            AmcAnimator.sb = sb
            bpy.ops.import_anim.amc_animate()
            return {'FINISHED'}
    
        def invoke(self, context, event):
            ob = context.active_object
            import os
            if not os.path.exists(ob['source_file_path']):
                self.report({'ERROR'},
                    "Original Armature source file not found... was it moved?")
                return {'CANCELLED'}
            wm = context.window_manager
            wm.fileselect_add(self)
            return {'RUNNING_MODAL'}
    
    
    
    Daniel M. Basso's avatar
    Daniel M. Basso committed
    class AmcExporter(bpy.types.Operator):
        #
        "Save an animation in Acclaim format"
        #
        bl_idname = "export_anim.amc"
        bl_label = "Export AMC"
    
        filepath = StringProperty(
                subtype='FILE_PATH'
                )
        use_scale = BoolProperty(
                name="Use original armature scale",
                default=True,
                description="Scale movement to original scale if available",
                )
    
        filter_glob = StringProperty(default="*.amc", options={'HIDDEN'})
    
        @classmethod
        def poll(cls, context):
            ob = context.active_object
            return (ob and ob.type == 'ARMATURE' and 'source_file_path' in ob)
    
        def execute(self, context):
            ob = context.active_object
            scn = context.scene
            out = open(self.filepath, "w")
            out.write(":FULLY-SPECIFIED\n:DEGREES\n")
            ds = DataStructure(ob['source_file_path'], ob['source_scale'])
            scale = ds.scale if self.use_scale else 1
            for frame in range(scn.frame_start, scn.frame_end + 1):
                out.write("{}\n".format(frame))
                scn.frame_set(frame)
                for bone in ob.pose.bones:
                    out.write("{} ".format(bone.name))
                    if bone.name == "root":
                        loc = bone.location / scale
                        out.write(" ".join(str(v) for v in loc) + " ")
                        rot = bone.matrix.to_euler()
                    else:
                        A = ds.bones[bone.name]['axis'].to_3x3()
                        R = ob.data.bones[bone.name].matrix_local.to_3x3()
                        AiR = A.transposed() * R
                        AiR_i = AiR.inverted()
                        rot = (AiR * bone.matrix_basis.to_3x3() * AiR_i).to_euler()
                    out.write(" ".join(str(degrees(v)) for v in rot) + "\n")
            return {'FINISHED'}
    
        def invoke(self, context, event):
            wm = context.window_manager
            wm.fileselect_add(self)
            return {'RUNNING_MODAL'}
    
    
    
    def menu_func_s(self, context):
        self.layout.operator(AsfImporter.bl_idname,
                             text="Acclaim Skeleton File (.asf)")
    
    
    
    Daniel M. Basso's avatar
    Daniel M. Basso committed
    def menu_func_mi(self, context):
    
        self.layout.operator(AmcImporter.bl_idname,
                             text="Acclaim Motion Capture (.amc)")
    
    
    
    Daniel M. Basso's avatar
    Daniel M. Basso committed
    def menu_func_me(self, context):
        self.layout.operator(AmcExporter.bl_idname,
                             text="Acclaim Motion Capture (.amc)")
    
    
    
    def register():
        bpy.utils.register_module(__name__)
        bpy.types.INFO_MT_file_import.append(menu_func_s)
    
    Daniel M. Basso's avatar
    Daniel M. Basso committed
        bpy.types.INFO_MT_file_import.append(menu_func_mi)
        bpy.types.INFO_MT_file_export.append(menu_func_me)
    
    
    
    def unregister():
        bpy.utils.unregister_module(__name__)
        bpy.types.INFO_MT_file_import.remove(menu_func_s)
    
    Daniel M. Basso's avatar
    Daniel M. Basso committed
        bpy.types.INFO_MT_file_import.remove(menu_func_mi)
        bpy.types.INFO_MT_file_export.remove(menu_func_me)