diff options
author | Brendon Murphy <meta.androcto1@gmail.com> | 2011-08-24 15:45:59 +0400 |
---|---|---|
committer | Brendon Murphy <meta.androcto1@gmail.com> | 2011-08-24 15:45:59 +0400 |
commit | 8f5fe7f748f924d504f708b89a4f143de1b636cc (patch) | |
tree | 49dbf70f627de7e1ac5358c05a8cbe41534e2cc7 /mocap/__init__.py | |
parent | cf3881371a5c64d56f352cf09395f93072889a7c (diff) |
committing Motion Capture Add-on - Final deliverable for GSoC project 2011
thanks to Benjy Cook
well done!
Diffstat (limited to 'mocap/__init__.py')
-rw-r--r-- | mocap/__init__.py | 886 |
1 files changed, 886 insertions, 0 deletions
diff --git a/mocap/__init__.py b/mocap/__init__.py new file mode 100644 index 00000000..7a8547a9 --- /dev/null +++ b/mocap/__init__.py @@ -0,0 +1,886 @@ +# ##### 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": "Motion Capture Tools", + "author": "Benjy Cook", + "blender": (2, 5, 9), + "api": 39523, + "location": "Object UI -> Mocap tools", + "description": "Various tools for working with motion capture animation", + "warning": "", + "wiki_url": ("http://wiki.blender.org/index.php/User:Benjycook/GSOC/Manual"), + "tracker_url": "", + "support": 'OFFICIAL', + "category": "Animation"} + +if "bpy" in locals(): + import imp + if "mocap_constraints" in locals(): + imp.reload(mocap_constraints) + if "retarget" in locals(): + imp.reload(retarget) + if "mocap_tools" in locals(): + imp.reload(mocap_tools) +else: + import bpy + from bpy.props import * + from bpy import * + from . import mocap_constraints + from . import retarget + from . import mocap_tools + from .mocap_constraints import * + +# MocapConstraint class +# Defines MocapConstraint datatype, used to add and configute mocap constraints +# Attached to Armature data + + +class MocapConstraint(bpy.types.PropertyGroup): + name = bpy.props.StringProperty(name="Name", + default="Mocap Fix", + description="Name of Mocap Fix", + update=setConstraint) + constrained_bone = bpy.props.StringProperty(name="Bone", + default="", + description="Constrained Bone", + update=updateConstraintBoneType) + constrained_boneB = bpy.props.StringProperty(name="Bone (2)", + default="", + description="Other Constrained Bone (optional, depends on type)", + update=setConstraint) + s_frame = bpy.props.IntProperty(name="S", + default=0, + description="Start frame of Fix", + update=setConstraint) + e_frame = bpy.props.IntProperty(name="E", + default=100, + description="End frame of Fix", + update=setConstraint) + smooth_in = bpy.props.IntProperty(name="In", + default=10, + description="Amount of frames to smooth in", + update=setConstraint, + min=0) + smooth_out = bpy.props.IntProperty(name="Out", + default=10, + description="Amount of frames to smooth out", + update=setConstraint, + min=0) + targetMesh = bpy.props.StringProperty(name="Mesh", + default="", + description="Target of Fix - Mesh (optional, depends on type)", + update=setConstraint) + active = bpy.props.BoolProperty(name="Active", + default=True, + description="Fix is active", + update=setConstraint) + show_expanded = bpy.props.BoolProperty(name="Show Expanded", + default=True, + description="Fix is fully shown") + targetPoint = bpy.props.FloatVectorProperty(name="Point", size=3, + subtype="XYZ", default=(0.0, 0.0, 0.0), + description="Target of Fix - Point", + update=setConstraint) + targetDist = bpy.props.FloatProperty(name="Offset", + default=0.0, + description="Distance and Floor Fixes - Desired offset", + update=setConstraint) + targetSpace = bpy.props.EnumProperty( + items=[("WORLD", "World Space", "Evaluate target in global space"), + ("LOCAL", "Object space", "Evaluate target in object space"), + ("constrained_boneB", "Other Bone Space", "Evaluate target in specified other bone space")], + name="Space", + description="In which space should Point type target be evaluated", + update=setConstraint) + type = bpy.props.EnumProperty(name="Type of constraint", + items=[("point", "Maintain Position", "Bone is at a specific point"), + ("freeze", "Maintain Position at frame", "Bone does not move from location specified in target frame"), + ("floor", "Stay above", "Bone does not cross specified mesh object eg floor"), + ("distance", "Maintain distance", "Target bones maintained specified distance")], + description="Type of Fix", + update=updateConstraintBoneType) + real_constraint = bpy.props.StringProperty() + real_constraint_bone = bpy.props.StringProperty() + + + +# Animation Stitch Settings, used for animation stitching of 2 retargeted animations. +class AnimationStitchSettings(bpy.types.PropertyGroup): + first_action = bpy.props.StringProperty(name="Action 1", + description="First action in stitch") + second_action = bpy.props.StringProperty(name="Action 2", + description="Second action in stitch") + blend_frame = bpy.props.IntProperty(name="Stitch frame", + description="Frame to locate stitch on") + blend_amount = bpy.props.IntProperty(name="Blend amount", + description="Size of blending transitiion, on both sides of the stitch", + default=10) + second_offset = bpy.props.IntProperty(name="Second offset", + description="Frame offset for 2nd animation, where it should start", + default=10) + stick_bone = bpy.props.StringProperty(name="Stick Bone", + description="Bone to freeze during transition", + default="") + + + +# MocapNLA Tracks. Stores which tracks/actions are associated with each retargeted animation. +class MocapNLATracks(bpy.types.PropertyGroup): + name = bpy.props.StringProperty() + base_track = bpy.props.StringProperty() + auto_fix_track = bpy.props.StringProperty() + manual_fix_track = bpy.props.StringProperty() + stride_action = bpy.props.StringProperty() + + + + +#Update function for Advanced Retarget boolean variable. +def advancedRetargetToggle(self, context): + enduser_obj = context.active_object + performer_obj = [obj for obj in context.selected_objects if obj != enduser_obj] + if enduser_obj is None or len(performer_obj) != 1: + print("Need active and selected armatures") + return + else: + performer_obj = performer_obj[0] + if self.advancedRetarget: + retarget.preAdvancedRetargeting(performer_obj, enduser_obj) + else: + retarget.cleanTempConstraints(enduser_obj) + + + +def toggleIKBone(self, context): + #Update function for IK functionality. Is called when IK prop checkboxes are toggled. + if self.IKRetarget: + if not self.is_in_ik_chain: + print(self.name + " IK toggled ON!") + ik = self.constraints.new('IK') + #ik the whole chain up to the root, excluding + chainLen = 0 + for parent_bone in self.parent_recursive: + chainLen += 1 + if hasIKConstraint(parent_bone): + break + deformer_children = [child for child in parent_bone.children if child.bone.use_deform] + #~ if len(deformer_children) > 1: + #~ break + ik.chain_count = chainLen + for bone in self.parent_recursive: + if bone.is_in_ik_chain: + bone.IKRetarget = True + else: + print(self.name + " IK toggled OFF!") + cnstrn_bones = [] + newChainLength = [] + if hasIKConstraint(self): + cnstrn_bones = [self] + elif self.is_in_ik_chain: + cnstrn_bones = [child for child in self.children_recursive if hasIKConstraint(child)] + for cnstrn_bone in cnstrn_bones: + newChainLength.append(cnstrn_bone.parent_recursive.index(self) + 1) + if cnstrn_bones: + # remove constraint, and update IK retarget for all parents of cnstrn_bone up to chain_len + for i, cnstrn_bone in enumerate(cnstrn_bones): + print(cnstrn_bone.name) + if newChainLength: + ik = hasIKConstraint(cnstrn_bone) + ik.chain_count = newChainLength[i] + else: + ik = hasIKConstraint(cnstrn_bone) + cnstrn_bone.constraints.remove(ik) + cnstrn_bone.IKRetarget = False + for bone in cnstrn_bone.parent_recursive: + if not bone.is_in_ik_chain: + bone.IKRetarget = False + + +#MocapMap class for storing mapping on enduser performer, +# where a bone may be linked to more than one on the performer +class MocapMapping(bpy.types.PropertyGroup): + name = bpy.props.StringProperty() + + + +def updateIKRetarget(): + # ensures that Blender constraints and IK properties are in sync + # currently runs when module is loaded, should run when scene is loaded + # or user adds a constraint to armature. Will be corrected in the future, + # once python callbacks are implemented + for obj in bpy.data.objects: + if obj.pose: + bones = obj.pose.bones + for pose_bone in bones: + if pose_bone.is_in_ik_chain or hasIKConstraint(pose_bone): + pose_bone.IKRetarget = True + else: + pose_bone.IKRetarget = False + +updateIKRetarget() + +def hasIKConstraint(pose_bone): + #utility function / predicate, returns True if given bone has IK constraint + ik = [constraint for constraint in pose_bone.constraints if constraint.type == "IK"] + if ik: + return ik[0] + else: + return False + +class MocapPanel(bpy.types.Panel): + # Motion capture retargeting panel + bl_label = "Mocap tools" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "object" + + def draw(self, context): + self.layout.label("Preprocessing") + row = self.layout.row(align=True) + row.alignment = 'EXPAND' + row.operator("mocap.denoise", text='Clean noise') + row.operator("mocap.rotate_fix", text='Fix BVH Axis Orientation') + row.operator("mocap.scale_fix", text='Auto scale Performer') + row2 = self.layout.row(align=True) + row2.operator("mocap.looper", text='Loop animation') + row2.operator("mocap.limitdof", text='Constrain Rig') + row2.operator("mocap.removelimitdof", text='Unconstrain Rig') + self.layout.label("Retargeting") + enduser_obj = bpy.context.active_object + performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj] + if enduser_obj is None or len(performer_obj) != 1: + self.layout.label("Select performer rig and target rig (as active)") + else: + self.layout.operator("mocap.guessmapping", text="Guess Hiearchy Mapping") + labelRow = self.layout.row(align=True) + labelRow.label("Performer Rig") + labelRow.label("End user Rig") + performer_obj = performer_obj[0] + if performer_obj.data and enduser_obj.data: + if performer_obj.data.name in bpy.data.armatures and enduser_obj.data.name in bpy.data.armatures: + perf = performer_obj.data + enduser_arm = enduser_obj.data + perf_pose_bones = enduser_obj.pose.bones + MappingRow = self.layout.row(align=True) + footCol = MappingRow.column(align=True) + nameCol =MappingRow.column(align=True) + nameCol.scale_x = 2 + mapCol = MappingRow.column(align=True) + mapCol.scale_x = 2 + selectCol = MappingRow.column(align=True) + twistCol = MappingRow.column(align=True) + twistCol.scale_x = 0.1 + IKCol = MappingRow.column(align=True) + IKCol.scale_x = 0.3 + IKLabel = MappingRow.column(align=True) + IKLabel.scale_x = 0.2 + for bone in perf.bones: + footCol.prop(data=bone, property='foot', text='', icon='POSE_DATA') + nameCol.label(bone.name) + mapCol.prop_search(bone, "map", enduser_arm, "bones") + selectCol.operator("mocap.selectmap", text='', icon='CURSOR').perf_bone = bone.name + label_mod = "FK" + if bone.map: + pose_bone = perf_pose_bones[bone.map] + if pose_bone.is_in_ik_chain: + label_mod = "ik chain" + if hasIKConstraint(pose_bone): + label_mod = "ik end" + end_bone = enduser_obj.data.bones[bone.map] + twistCol.prop(data=end_bone, property='twistFix', text='', icon='RNA') + IKCol.prop(pose_bone, 'IKRetarget') + IKLabel.label(label_mod) + else: + twistCol.label(" ") + IKCol.label(" ") + IKLabel.label(" ") + mapRow = self.layout.row() + mapRow.operator("mocap.savemapping", text='Save mapping') + mapRow.operator("mocap.loadmapping", text='Load mapping') + extraSettings = self.layout.box() + if performer_obj.animation_data: + extraSettings.prop(data=performer_obj.animation_data.action, property='name', text='Action Name') + extraSettings.prop(enduser_arm, "frameStep") + extraSettings.prop(enduser_arm, "advancedRetarget", text='Advanced Retarget') + self.layout.operator("mocap.retarget", text='RETARGET!') + + +class MocapConstraintsPanel(bpy.types.Panel): + #Motion capture constraints panel + bl_label = "Mocap Fixes" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "object" + + def draw(self, context): + layout = self.layout + if context.active_object: + if context.active_object.data: + if context.active_object.data.name in bpy.data.armatures: + enduser_obj = context.active_object + enduser_arm = enduser_obj.data + layout.operator_menu_enum("mocap.addmocapfix", "type") + layout.operator("mocap.updateconstraints", text='Update Fixes') + bakeRow = layout.row() + bakeRow.operator("mocap.bakeconstraints", text='Bake Fixes') + bakeRow.operator("mocap.unbakeconstraints", text='Unbake Fixes') + layout.separator() + for i, m_constraint in enumerate(enduser_arm.mocap_constraints): + box = layout.box() + headerRow = box.row() + headerRow.prop(m_constraint, 'show_expanded', text='', icon='TRIA_DOWN' if m_constraint.show_expanded else 'TRIA_RIGHT', emboss=False) + headerRow.prop(m_constraint, 'type', text='') + headerRow.prop(m_constraint, 'name', text='') + headerRow.prop(m_constraint, 'active', icon='MUTE_IPO_ON' if not m_constraint.active else'MUTE_IPO_OFF', text='', emboss=False) + headerRow.operator("mocap.removeconstraint", text="", icon='X', emboss=False).constraint = i + if m_constraint.show_expanded: + box.separator() + box.prop_search(m_constraint, 'constrained_bone', enduser_obj.pose, "bones", icon='BONE_DATA') + if m_constraint.type == "distance" or m_constraint.type == "point": + box.prop_search(m_constraint, 'constrained_boneB', enduser_obj.pose, "bones", icon='CONSTRAINT_BONE') + frameRow = box.row() + frameRow.label("Frame Range:") + frameRow.prop(m_constraint, 's_frame') + frameRow.prop(m_constraint, 'e_frame') + smoothRow = box.row() + smoothRow.label("Smoothing:") + smoothRow.prop(m_constraint, 'smooth_in') + smoothRow.prop(m_constraint, 'smooth_out') + targetRow = box.row() + targetLabelCol = targetRow.column() + targetLabelCol.label("Target settings:") + targetPropCol = targetRow.column() + if m_constraint.type == "floor": + targetPropCol.prop_search(m_constraint, 'targetMesh', bpy.data, "objects") + if m_constraint.type == "point" or m_constraint.type == "freeze": + box.prop(m_constraint, 'targetSpace') + if m_constraint.type == "point": + targetPropCol.prop(m_constraint, 'targetPoint') + if m_constraint.type == "distance" or m_constraint.type == "floor": + targetPropCol.prop(m_constraint, 'targetDist') + layout.separator() + + +class ExtraToolsPanel(bpy.types.Panel): + # Motion capture retargeting panel + bl_label = "Extra Mocap Tools" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "object" + + def draw(self, context): + layout = self.layout + layout.operator("mocap.samples", text='Samples to Beziers') + layout.operator('mocap.pathediting', text="Follow Path") + activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature) + if activeIsArmature: + enduser_arm = context.active_object.data + selectBox = layout.box() + selectRetargets = selectBox.row() + selectRetargets.label("Retargeted Animations:") + selectRetargets.prop_search(enduser_arm, "active_mocap", enduser_arm, "mocapNLATracks") + stitchBox = layout.box() + stitchBox.label("Animation Stitching") + settings = enduser_arm.stitch_settings + stitchBox.prop_search(settings, "first_action", enduser_arm, "mocapNLATracks") + stitchBox.prop_search(settings, "second_action", enduser_arm, "mocapNLATracks") + stitchSettings = stitchBox.row() + stitchSettings.prop(settings, "blend_frame") + stitchSettings.prop(settings, "blend_amount") + stitchSettings.prop(settings, "second_offset") + stitchBox.prop_search(settings, "stick_bone", context.active_object.pose, "bones") + stitchBox.operator('mocap.animstitchguess', text="Guess Settings") + stitchBox.operator('mocap.animstitch', text="Stitch Animations") + + +class OBJECT_OT_RetargetButton(bpy.types.Operator): + #Retargeting operator. Assumes selected and active armatures, where the performer (the selected one) + # has an action for retargeting + '''Retarget animation from selected armature to active armature ''' + bl_idname = "mocap.retarget" + bl_label = "Retargets active action from Performer to Enduser" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + scene = context.scene + s_frame = scene.frame_start + e_frame = scene.frame_end + enduser_obj = context.active_object + performer_obj = [obj for obj in context.selected_objects if obj != enduser_obj] + if enduser_obj is None or len(performer_obj) != 1: + print("Need active and selected armatures") + else: + performer_obj = performer_obj[0] + s_frame, e_frame = performer_obj.animation_data.action.frame_range + s_frame = int(s_frame) + e_frame = int(e_frame) + if retarget.isRigAdvanced(enduser_obj) and not enduser_obj.data.advancedRetarget: + print("Recommended to use Advanced Retargeting method") + enduser_obj.data.advancedRetarget = True + else: + retarget.totalRetarget(performer_obj, enduser_obj, scene, s_frame, e_frame) + return {"FINISHED"} + + @classmethod + def poll(cls, context): + if context.active_object: + activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature) + performer_obj = [obj for obj in context.selected_objects if obj != context.active_object] + if performer_obj: + return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature) and performer_obj[0].animation_data + else: + return False + + +class OBJECT_OT_SaveMappingButton(bpy.types.Operator): + #Operator for saving mapping to enduser armature + '''Save mapping to active armature (for future retargets) ''' + bl_idname = "mocap.savemapping" + bl_label = "Saves user generated mapping from Performer to Enduser" + + def execute(self, context): + enduser_obj = bpy.context.active_object + performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj][0] + retarget.createDictionary(performer_obj.data, enduser_obj.data) + return {"FINISHED"} + + @classmethod + def poll(cls, context): + if context.active_object: + activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature) + performer_obj = [obj for obj in context.selected_objects if obj != context.active_object] + if performer_obj: + return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature) + else: + return False + + +class OBJECT_OT_LoadMappingButton(bpy.types.Operator): + '''Load saved mapping from active armature''' + #Operator for loading mapping to enduser armature + bl_idname = "mocap.loadmapping" + bl_label = "Loads user generated mapping from Performer to Enduser" + + def execute(self, context): + enduser_obj = bpy.context.active_object + performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj][0] + retarget.loadMapping(performer_obj.data, enduser_obj.data) + return {"FINISHED"} + + @classmethod + def poll(cls, context): + if context.active_object: + activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature) + performer_obj = [obj for obj in context.selected_objects if obj != context.active_object] + if performer_obj: + return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature) + else: + return False + + +class OBJECT_OT_SelectMapBoneButton(bpy.types.Operator): + #Operator for setting selected bone in enduser armature to the performer mapping + '''Select a bone for faster mapping''' + bl_idname = "mocap.selectmap" + bl_label = "Select a bone for faster mapping" + perf_bone = bpy.props.StringProperty() + + def execute(self, context): + enduser_obj = bpy.context.active_object + performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj][0] + selectedBone = "" + for bone in enduser_obj.data.bones: + boneVis = bone.layers + for i in range(32): + if boneVis[i] and enduser_obj.data.layers[i]: + if bone.select: + selectedBone = bone.name + break + performer_obj.data.bones[self.perf_bone].map = selectedBone + return {"FINISHED"} + + @classmethod + def poll(cls, context): + if context.active_object: + activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature) + performer_obj = [obj for obj in context.selected_objects if obj != context.active_object] + if performer_obj: + return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature) + else: + return False + + +class OBJECT_OT_ConvertSamplesButton(bpy.types.Operator): + #Operator to convert samples to beziers on the selected object + '''Convert active armature's sampled keyframed to beziers''' + bl_idname = "mocap.samples" + bl_label = "Converts samples / simplifies keyframes to beziers" + + def execute(self, context): + mocap_tools.fcurves_simplify(context, context.active_object) + return {"FINISHED"} + + @classmethod + def poll(cls, context): + return context.active_object.animation_data + + +class OBJECT_OT_LooperButton(bpy.types.Operator): + #Operator to trim fcurves which contain a few loops to a single one on the selected object + '''Trim active armature's animation to a single cycle, given a cyclic animation (such as a walk cycle)''' + bl_idname = "mocap.looper" + bl_label = "loops animation / sampled mocap data" + + def execute(self, context): + mocap_tools.autoloop_anim() + return {"FINISHED"} + + @classmethod + def poll(cls, context): + return context.active_object.animation_data + + +class OBJECT_OT_DenoiseButton(bpy.types.Operator): + #Operator to denoise impluse noise on the active object's fcurves + '''Denoise active armature's animation. Good for dealing with 'bad' frames inherent in mocap animation''' + bl_idname = "mocap.denoise" + bl_label = "Denoises sampled mocap data " + + def execute(self, context): + mocap_tools.denoise_median() + return {"FINISHED"} + + @classmethod + def poll(cls, context): + return context.active_object + + @classmethod + def poll(cls, context): + return context.active_object.animation_data + + +class OBJECT_OT_LimitDOFButton(bpy.types.Operator): + #Operator to analyze performer armature and apply rotation constraints on the enduser armature + '''Create limit constraints on the active armature from the selected armature's animation's range of motion''' + bl_idname = "mocap.limitdof" + bl_label = "Analyzes animations Max/Min DOF and adds hard/soft constraints" + + def execute(self, context): + performer_obj = [obj for obj in context.selected_objects if obj != context.active_object][0] + mocap_tools.limit_dof(context, performer_obj, context.active_object) + return {"FINISHED"} + + @classmethod + def poll(cls, context): + if context.active_object: + activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature) + performer_obj = [obj for obj in context.selected_objects if obj != context.active_object] + if performer_obj: + return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature) + else: + return False + + +class OBJECT_OT_RemoveLimitDOFButton(bpy.types.Operator): + #Removes constraints created by above operator + '''Removes previously created limit constraints on the active armature''' + bl_idname = "mocap.removelimitdof" + bl_label = "Removes previously created limit constraints on the active armature" + + def execute(self, context): + mocap_tools.limit_dof_toggle_off(context, context.active_object) + return {"FINISHED"} + + @classmethod + def poll(cls, context): + activeIsArmature = False + if context.active_object: + activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature) + return activeIsArmature + + +class OBJECT_OT_RotateFixArmature(bpy.types.Operator): + #Operator to fix common imported Mocap data issue of wrong axis system on active object + '''Realign the active armature's axis system to match Blender (Commonly needed after bvh import)''' + bl_idname = "mocap.rotate_fix" + bl_label = "Rotates selected armature 90 degrees (fix for bvh import)" + + def execute(self, context): + mocap_tools.rotate_fix_armature(context.active_object.data) + return {"FINISHED"} + + @classmethod + def poll(cls, context): + if context.active_object: + return isinstance(context.active_object.data, bpy.types.Armature) + + +class OBJECT_OT_ScaleFixArmature(bpy.types.Operator): + #Operator to scale down the selected armature to match the active one + '''Rescale selected armature to match the active animation, for convienence''' + bl_idname = "mocap.scale_fix" + bl_label = "Scales performer armature to match target armature" + + def execute(self, context): + enduser_obj = bpy.context.active_object + performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj][0] + mocap_tools.scale_fix_armature(performer_obj, enduser_obj) + return {"FINISHED"} + + @classmethod + def poll(cls, context): + if context.active_object: + activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature) + performer_obj = [obj for obj in context.selected_objects if obj != context.active_object] + if performer_obj: + return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature) + else: + return False + + +class MOCAP_OT_AddMocapFix(bpy.types.Operator): + #Operator to add a post-retarget fix + '''Add a post-retarget fix - useful for fixing certain artifacts following the retarget''' + bl_idname = "mocap.addmocapfix" + bl_label = "Add Mocap Fix to target armature" + type = bpy.props.EnumProperty(name="Type of Fix", + items=[("point", "Maintain Position", "Bone is at a specific point"), + ("freeze", "Maintain Position at frame", "Bone does not move from location specified in target frame"), + ("floor", "Stay above", "Bone does not cross specified mesh object eg floor"), + ("distance", "Maintain distance", "Target bones maintained specified distance")], + description="Type of fix") + + def execute(self, context): + enduser_obj = bpy.context.active_object + enduser_arm = enduser_obj.data + new_mcon = enduser_arm.mocap_constraints.add() + new_mcon.type = self.type + return {"FINISHED"} + + @classmethod + def poll(cls, context): + if context.active_object: + return isinstance(context.active_object.data, bpy.types.Armature) + + +class OBJECT_OT_RemoveMocapConstraint(bpy.types.Operator): + #Operator to remove a post-retarget fix + '''Remove this post-retarget fix''' + bl_idname = "mocap.removeconstraint" + bl_label = "Removes fixes from target armature" + constraint = bpy.props.IntProperty() + + def execute(self, context): + enduser_obj = bpy.context.active_object + enduser_arm = enduser_obj.data + m_constraints = enduser_arm.mocap_constraints + m_constraint = m_constraints[self.constraint] + if m_constraint.real_constraint: + bone = enduser_obj.pose.bones[m_constraint.real_constraint_bone] + cons_obj = getConsObj(bone) + removeConstraint(m_constraint, cons_obj) + m_constraints.remove(self.constraint) + return {"FINISHED"} + + @classmethod + def poll(cls, context): + if context.active_object: + return isinstance(context.active_object.data, bpy.types.Armature) + + +class OBJECT_OT_BakeMocapConstraints(bpy.types.Operator): + #Operator to bake all post-retarget fixes + '''Bake all post-retarget fixes to the Retarget Fixes NLA Track''' + bl_idname = "mocap.bakeconstraints" + bl_label = "Bake all fixes to target armature" + + def execute(self, context): + bakeConstraints(context) + return {"FINISHED"} + + @classmethod + def poll(cls, context): + if context.active_object: + return isinstance(context.active_object.data, bpy.types.Armature) + + +class OBJECT_OT_UnbakeMocapConstraints(bpy.types.Operator): + #Operator to unbake all post-retarget fixes + '''Unbake all post-retarget fixes - removes the baked data from the Retarget Fixes NLA Track''' + bl_idname = "mocap.unbakeconstraints" + bl_label = "Unbake all fixes to target armature" + + def execute(self, context): + unbakeConstraints(context) + return {"FINISHED"} + + @classmethod + def poll(cls, context): + if context.active_object: + return isinstance(context.active_object.data, bpy.types.Armature) + + +class OBJECT_OT_UpdateMocapConstraints(bpy.types.Operator): + #Operator to update all post-retarget fixes, similar to update dependencies on drivers + #Needed because python properties lack certain callbacks and some fixes take a while to recalculate. + '''Updates all post-retarget fixes - needed after changes to armature object or pose''' + bl_idname = "mocap.updateconstraints" + bl_label = "Updates all fixes to target armature - neccesary to take under consideration changes to armature object or pose" + + def execute(self, context): + updateConstraints(context.active_object, context) + return {"FINISHED"} + + @classmethod + def poll(cls, context): + if context.active_object: + return isinstance(context.active_object.data, bpy.types.Armature) + + +class OBJECT_OT_GuessHierachyMapping(bpy.types.Operator): + #Operator which calls heurisitic function to guess mapping between 2 armatures + '''Attemps to auto figure out hierarchy mapping''' + bl_idname = "mocap.guessmapping" + bl_label = "Attemps to auto figure out hierarchy mapping" + + def execute(self, context): + enduser_obj = bpy.context.active_object + performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj][0] + mocap_tools.guessMapping(performer_obj, enduser_obj) + return {"FINISHED"} + + @classmethod + def poll(cls, context): + if context.active_object: + activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature) + performer_obj = [obj for obj in context.selected_objects if obj != context.active_object] + if performer_obj: + return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature) + else: + return False + + +class OBJECT_OT_PathEditing(bpy.types.Operator): + #Operator which calls path editing function, making active object follow the selected curve. + '''Sets active object (stride object) to follow the selected curve''' + bl_idname = "mocap.pathediting" + bl_label = "Sets active object (stride object) to follow the selected curve" + + def execute(self, context): + path = [obj for obj in context.selected_objects if obj != context.active_object][0] + mocap_tools.path_editing(context, context.active_object, path) + return {"FINISHED"} + + @classmethod + def poll(cls, context): + if context.active_object: + selected_objs = [obj for obj in context.selected_objects if obj != context.active_object and isinstance(obj.data, bpy.types.Curve)] + return selected_objs + else: + return False + + +class OBJECT_OT_AnimationStitchingButton(bpy.types.Operator): + #Operator which calls stitching function, combining 2 animations onto the NLA. + '''Stitches two defined animations into a single one via alignment of NLA Tracks''' + bl_idname = "mocap.animstitch" + bl_label = "Stitches two defined animations into a single one via alignment of NLA Tracks" + + def execute(self, context): + mocap_tools.anim_stitch(context, context.active_object) + return {"FINISHED"} + + @classmethod + def poll(cls, context): + activeIsArmature = False + if context.active_object: + activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature) + if activeIsArmature: + stitch_settings = context.active_object.data.stitch_settings + return (stitch_settings.first_action and stitch_settings.second_action) + return False + + +class OBJECT_OT_GuessAnimationStitchingButton(bpy.types.Operator): + #Operator which calls stitching function heuristic, setting good values for above operator. + '''Guesses the stitch frame and second offset for animation stitch''' + bl_idname = "mocap.animstitchguess" + bl_label = "Guesses the stitch frame and second offset for animation stitch" + + def execute(self, context): + mocap_tools.guess_anim_stitch(context, context.active_object) + return {"FINISHED"} + + @classmethod + def poll(cls, context): + activeIsArmature = False + if context.active_object: + activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature) + if activeIsArmature: + stitch_settings = context.active_object.data.stitch_settings + return (stitch_settings.first_action and stitch_settings.second_action) + return False + + +def register(): + bpy.utils.register_class(MocapConstraint) + bpy.types.Armature.mocap_constraints = bpy.props.CollectionProperty(type=MocapConstraint) + bpy.utils.register_class(MocapMapping) + #string property for storing performer->enduser mapping + bpy.types.Bone.map = bpy.props.StringProperty() + #Collection Property for storing enduser->performer mapping + bpy.types.Bone.reverseMap = bpy.props.CollectionProperty(type=MocapMapping) + #Boolean property for storing foot bone toggle + bpy.types.Bone.foot = bpy.props.BoolProperty(name="Foot", + description="Marks this bone as a 'foot', which determines retargeted animation's translation", + default=False) + #Boolean property for storing if this bone is twisted along the y axis, + # which can happen due to various sources of performers + bpy.types.Bone.twistFix = bpy.props.BoolProperty(name="Twist Fix", + description="Fix Twist on this bone", + default=False) + #Boolean property for toggling ik retargeting for this bone + bpy.types.PoseBone.IKRetarget = bpy.props.BoolProperty(name="IK", + description="Toggles IK Retargeting method for given bone", + update=toggleIKBone, default=False) + bpy.utils.register_class(AnimationStitchSettings) + bpy.utils.register_class(MocapNLATracks) + #Animation Stitch Settings Property + bpy.types.Armature.stitch_settings = bpy.props.PointerProperty(type=AnimationStitchSettings) + #Current/Active retargeted animation on the armature + bpy.types.Armature.active_mocap = bpy.props.StringProperty(update=retarget.NLASystemInitialize) + #Collection of retargeted animations and their NLA Tracks on the armature + bpy.types.Armature.mocapNLATracks = bpy.props.CollectionProperty(type=MocapNLATracks) + #Advanced retargeting boolean property + bpy.types.Armature.advancedRetarget = bpy.props.BoolProperty(default=False, update=advancedRetargetToggle) + #frame step - frequency of frames to retarget. Skipping is useful for previewing, faster work etc. + bpy.types.Armature.frameStep = smooth_out = bpy.props.IntProperty(name="Frame Skip", + default=1, + description="Amount of frames to skip - for previewing retargets quickly. 1 is fully sampled", + min=1) + bpy.utils.register_module(__name__) + + +def unregister(): + bpy.utils.unregister_module(__name__) + +if __name__ == "__main__": + register() |