Skip to content
Snippets Groups Projects
__init__.py 16.5 KiB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449
# ##### 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>",
    'version': (2011, 11, 2, 1),
    'blender': (2, 6, 0),
    'api': 41226,
    'location': "File > Import",
    '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
from math import radians as rad, 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).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(rad(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)
        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()
                for dof, ang in zip(self.bones[b[0]]['dof'], vs):
                    rot = Matrix.Rotation(rad(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
        local = parent.inverted() * world
        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 = (rad(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, useFrameNo=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())
        self.fno = 0
        self.useFrameNo = useFrameNo
        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.useFrameNo 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):
    """
        Load an Acclaim Skeleton File
    """
    bl_idname = "import_anim.asf"
    bl_label = "Import ASF"

    filepath = StringProperty(name="File Path", maxlen=1024, default="",
                              description="Path to the ASF file")
    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.,
                          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")
    rotX = BoolProperty(name="Rotate X 90 degrees", default=False,
                              description="Correct orientation")
    rotZ = 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.properties.from_inches else 1.)
        sb = StructureBuilder(
            self.properties.filepath,
            self.properties.armature_name,
            self.properties.scale * uscale)
        sb.create_armature()
        sb.build_structure(self.properties.use_limits)
        if self.properties.rotX:
            bpy.ops.transform.rotate(value=(rad(90.),), axis=(1, 0, 0))
        if self.properties.rotZ:
            bpy.ops.transform.rotate(value=(rad(90.),), 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):
    """
        Load an Acclaim Motion Capture
    """
    bl_idname = "import_anim.amc"
    bl_label = "Import AMC"

    filepath = StringProperty(name="File Path", maxlen=1024, default="",
                              description="Path to the AMC file")
    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)
    useFrameNo = 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
        try:
            return (ob and ob.type == 'ARMATURE' and ob['source_file_path'])
        except:
            return False

    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.properties.filepath,
                               self.properties.frame_skip,
                               self.properties.useFrameNo)
        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'}


def menu_func_s(self, context):
    self.layout.operator(AsfImporter.bl_idname,
                         text="Acclaim Skeleton File (.asf)")


def menu_func_m(self, context):
    self.layout.operator(AmcImporter.bl_idname,
                         text="Acclaim Motion Capture (.amc)")


def register():
    bpy.utils.register_module(__name__)
    bpy.types.INFO_MT_file_import.append(menu_func_s)
    bpy.types.INFO_MT_file_import.append(menu_func_m)


def unregister():
    bpy.utils.unregister_module(__name__)
    bpy.types.INFO_MT_file_import.remove(menu_func_s)
    bpy.types.INFO_MT_file_import.remove(menu_func_m)


if __name__ == "__main__":
    register()