io_anim_acclaim: move to contrib: T63750

This commit is contained in:
Brendon Murphy 2019-05-24 11:00:44 +10:00
parent c983a24728
commit d88d2ac99d
1 changed files with 0 additions and 546 deletions

View File

@ -1,546 +0,0 @@
# ##### 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": (2013, 1, 26, 1),
"blender": (2, 65, 9),
"location": "File > Import-Export",
"description": "Imports Acclaim Skeleton and Motion Capture Files",
"wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
"Scripts/Import-Export/Acclaim_Importer",
"category": "Import-Export",
}
import re
import bpy
from mathutils import Vector, Matrix
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()
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)
amc.close()
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 = 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.view_layer.objects.active = None
bpy.ops.object.add(type='ARMATURE', enter_editmode=True)
self.object = bpy.context.view_layer.objects.active
self.armature = self.object.data
self.object.name = self.name
self.armature.name = self.name
self.armature.display_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(),
)
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):
#
"Load an Acclaim Skeleton File"
#
bl_idname = "import_anim.asf"
bl_label = "Import ASF"
filepath: StringProperty(
subtype='FILE_PATH',
)
armature_name: StringProperty(
name="Armature Name", maxlen=63,
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.create_armature()
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':
self.cancel(context)
return {'CANCELLED'}
if event.type == 'TIMER':
if not self.sb.apply_next_frame():
self.cancel(context)
return {'FINISHED'}
return {'PASS_THROUGH'}
def execute(self, context):
self.timer = context.window_manager.\
event_timer_add(0.001, context.window)
context.window_manager.modal_handler_add(self)
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')
class AmcImporter(bpy.types.Operator):
#
"Load an Acclaim Motion Capture"
#
bl_idname = "import_anim.amc"
bl_label = "Import AMC"
filepath: StringProperty(
subtype='FILE_PATH',
)
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'}
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)")
def menu_func_mi(self, context):
self.layout.operator(AmcImporter.bl_idname,
text="Acclaim Motion Capture (.amc)")
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.TOPBAR_MT_file_import.append(menu_func_s)
bpy.types.TOPBAR_MT_file_import.append(menu_func_mi)
bpy.types.TOPBAR_MT_file_export.append(menu_func_me)
def unregister():
bpy.utils.unregister_module(__name__)
bpy.types.TOPBAR_MT_file_import.remove(menu_func_s)
bpy.types.TOPBAR_MT_file_import.remove(menu_func_mi)
bpy.types.TOPBAR_MT_file_export.remove(menu_func_me)
if __name__ == "__main__":
register()