Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel M. Basso <danielmbasso@gmail.com>2011-11-03 03:35:42 +0400
committerDaniel M. Basso <danielmbasso@gmail.com>2011-11-03 03:35:42 +0400
commitbd9769cdc374f604159b0e3a79bc35164a8985ab (patch)
treeae353cfd1984ec868ec6aa3eef009ef12d1a7e1f
parent49910302c9a195816377e0469d1d1c8f2638c101 (diff)
Added the first public version of Acclaim importer addon.
-rw-r--r--io_anim_acclaim/__init__.py449
1 files changed, 449 insertions, 0 deletions
diff --git a/io_anim_acclaim/__init__.py b/io_anim_acclaim/__init__.py
new file mode 100644
index 00000000..f345511a
--- /dev/null
+++ b/io_anim_acclaim/__init__.py
@@ -0,0 +1,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()