diff options
author | meta-androcto <meta.androcto1@gmail.com> | 2019-07-04 13:22:11 +0300 |
---|---|---|
committer | meta-androcto <meta.androcto1@gmail.com> | 2019-07-04 13:22:11 +0300 |
commit | e8dfc682098eab13fa4a28ee3a7cc9e99a575ba0 (patch) | |
tree | fc975a3b32a89e60c16e93d2f1842abeead276af | |
parent | 9a6e192fcecd676dea09233b35f12b8988d853e3 (diff) |
mocap tools: move to contrib: T63750
-rw-r--r-- | mocap/__init__.py | 933 | ||||
-rw-r--r-- | mocap/mocap_constraints.py | 451 | ||||
-rw-r--r-- | mocap/mocap_tools.py | 939 | ||||
-rw-r--r-- | mocap/retarget.py | 571 |
4 files changed, 0 insertions, 2894 deletions
diff --git a/mocap/__init__.py b/mocap/__init__.py deleted file mode 100644 index f040e0b1..00000000 --- a/mocap/__init__.py +++ /dev/null @@ -1,933 +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 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, 73, 0), - "version": (1, 1, 1), - "location": "Active Armature > Object Properties > Mocap tools", - "description": "Various tools for working with motion capture animation", - "warning": "", - "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/" - "Scripts/Animation/Motion_Capture_Tools", - "support": 'OFFICIAL', - "category": "Animation", -} - -if "bpy" in locals(): - import importlib - if "mocap_constraints" in locals(): - importlib.reload(mocap_constraints) - if "retarget" in locals(): - importlib.reload(retarget) - if "mocap_tools" in locals(): - importlib.reload(mocap_tools) -else: - import bpy - from bpy.props import ( - BoolProperty, - CollectionProperty, - EnumProperty, - FloatProperty, - FloatVectorProperty, - IntProperty, - PointerProperty, - StringProperty, - ) - from . import ( - mocap_constraints, - retarget, - mocap_tools, - ) - - -# MocapConstraint class -# Defines MocapConstraint datatype, used to add and configute mocap constraints -# Attached to Armature data - -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 MocapConstraint(bpy.types.PropertyGroup): - name: StringProperty(name="Name", - default="Mocap Fix", - description="Name of Mocap Fix", - update=mocap_constraints.setConstraint) - constrained_bone: StringProperty(name="Bone", - default="", - description="Constrained Bone", - update=mocap_constraints.updateConstraintBoneType) - constrained_boneB: StringProperty(name="Bone (2)", - default="", - description="Other Constrained Bone (optional, depends on type)", - update=mocap_constraints.setConstraint) - s_frame: IntProperty(name="Start", - default=0, - description="Start frame of Fix", - update=mocap_constraints.setConstraint) - e_frame: IntProperty(name="End", - default=100, - description="End frame of Fix", - update=mocap_constraints.setConstraint) - smooth_in: IntProperty(name="In", - default=10, - description="Number of frames to smooth in", - update=mocap_constraints.setConstraint, - min=0) - smooth_out: IntProperty(name="Out", - default=10, - description="Number of frames to smooth out", - update=mocap_constraints.setConstraint, - min=0) - targetMesh: StringProperty(name="Mesh", - default="", - description="Target of Fix - Mesh (optional, depends on type)", - update=mocap_constraints.setConstraint) - active: BoolProperty(name="Active", - default=True, - description="Fix is active", - update=mocap_constraints.setConstraint) - show_expanded: BoolProperty(name="Show Expanded", - default=True, - description="Fix is fully shown") - targetPoint: FloatVectorProperty(name="Point", size=3, - subtype="XYZ", default=(0.0, 0.0, 0.0), - description="Target of Fix - Point", - update=mocap_constraints.setConstraint) - targetDist: FloatProperty(name="Offset", - default=0.0, - description="Distance and Floor Fixes - Desired offset", - update=mocap_constraints.setConstraint) - targetSpace: 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=mocap_constraints.setConstraint) - type: 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=mocap_constraints.updateConstraintBoneType) - real_constraint: StringProperty() - real_constraint_bone: StringProperty() - - -# Animation Stitch Settings, used for animation stitching of 2 retargeted animations. -class AnimationStitchSettings(bpy.types.PropertyGroup): - first_action: StringProperty(name="Action 1", - description="First action in stitch") - second_action: StringProperty(name="Action 2", - description="Second action in stitch") - blend_frame: IntProperty(name="Stitch frame", - description="Frame to locate stitch on") - blend_amount: IntProperty(name="Blend amount", - description="Size of blending transition, on both sides of the stitch", - default=10) - second_offset: IntProperty(name="Second offset", - description="Frame offset for 2nd animation, where it should start", - default=10) - stick_bone: 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: StringProperty() - base_track: StringProperty() - auto_fix_track: StringProperty() - manual_fix_track: StringProperty() - stride_action: 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: StringProperty() - -# Disabling for now [#28933] - campbell -''' -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" - - @classmethod - def poll(cls, context): - obj = context.object - return obj.type == 'ARMATURE' and context.active_object is not None and context.mode in {'EDIT_ARMATURE', - 'POSE', - 'OBJECT'} - - def draw(self, context): - layout = self.layout - - layout.label(text="Preprocessing:") - - row = layout.row(align=True) - 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') - - row = layout.row(align=True) - row.operator("mocap.looper", text='Loop animation') - row.operator("mocap.limitdof", text='Constrain Rig') - row.operator("mocap.removelimitdof", text='Unconstrain Rig') - - layout.label(text="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: - layout.label(text="Select performer rig and target rig (as active)") - else: - layout.operator("mocap.guessmapping", text="Guess Hierarchy Mapping") - labelRow = layout.row(align=True) - labelRow.label(text="Performer Rig") - labelRow.label(text="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 = 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) - 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", text='') - 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(text=" ") - IKCol.label(text=" ") - IKLabel.label(text=" ") - mapRow = 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 and performer_obj.animation_data.action: - 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') - 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" - bl_options = {'DEFAULT_CLOSED'} - - @classmethod - def poll(cls, context): - obj = context.object - return obj.type == 'ARMATURE' and context.active_object is not None and context.mode in {'EDIT_ARMATURE', - 'POSE', - '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', text="") - 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(text="Frame Range:") - frameRow.prop(m_constraint, 's_frame') - frameRow.prop(m_constraint, 'e_frame') - smoothRow = box.row() - smoothRow.label(text="Smoothing:") - smoothRow.prop(m_constraint, 'smooth_in') - smoothRow.prop(m_constraint, 'smooth_out') - targetRow = box.row() - targetLabelCol = targetRow.column() - targetLabelCol.label(text="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" - bl_options = {'DEFAULT_CLOSED'} - - @classmethod - def poll(cls, context): - obj = context.object - return obj.type == 'ARMATURE' and context.active_object is not None and context.mode in {'EDIT_ARMATURE', - 'POSE', - '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(text="Retargeted Animations:") - selectRetargets.prop_search(enduser_arm, "active_mocap", enduser_arm, "mocapNLATracks") - stitchBox = layout.box() - stitchBox.label(text="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 = "Retarget" - 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 = "Save 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] - 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 = "Load 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] - 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 Mapping Bone" - perf_bone: 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 = "Convert Samples" - - 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 = "Loop Mocap" - - 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 - """Removes spikes from all fcurves on the selected object""" - bl_idname = "mocap.denoise" - bl_label = "Denoise Mocap" - - def execute(self, context): - obj = context.active_object - mocap_tools.denoise(obj, obj.animation_data.action.fcurves) - return {'FINISHED'} - - @classmethod - def poll(cls, context): - obj = context.active_object - return obj and obj.animation_data and obj.animation_data.action - - -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 = "Set DOF 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 - """Remove previously created limit constraints on the active armature""" - bl_idname = "mocap.removelimitdof" - bl_label = "Remove DOF Constraints" - - 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 = "Rotate Fix" - - 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 convenience""" - bl_idname = "mocap.scale_fix" - bl_label = "Scale Fix" - - 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" - type: 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 = "Remove Mocap Fix" - constraint: 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 = mocap_constraints.getConsObj(bone) - mocap_constraints.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 Mocap Fixes" - - def execute(self, context): - mocap_constraints.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 Mocap Fixes" - - def execute(self, context): - mocap_constraints.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. - """Update all post-retarget fixes (necessary to take under """ \ - """consideration changes to armature object or pose)""" - bl_idname = "mocap.updateconstraints" - bl_label = "Update Mocap Fixes" - - def execute(self, context): - mocap_constraints.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 - """Attempt to auto figure out hierarchy mapping""" - bl_idname = "mocap.guessmapping" - bl_label = "Guess 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. - """Set active object (stride object) to follow the selected curve""" - bl_idname = "mocap.pathediting" - bl_label = "Set Path" - - 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. - """Stitch two defined animations into a single one via alignment of NLA Tracks""" - bl_idname = "mocap.animstitch" - bl_label = "Stitch Animations" - - 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. - """Guess the stitch frame and second offset for animation stitch""" - bl_idname = "mocap.animstitchguess" - bl_label = "Guess 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 = CollectionProperty(type=MocapConstraint) - bpy.utils.register_class(MocapMapping) - #string property for storing performer->enduser mapping - bpy.types.Bone.map = StringProperty() - #Collection Property for storing enduser->performer mapping - bpy.types.Bone.reverseMap = CollectionProperty(type=MocapMapping) - #Boolean property for storing foot bone toggle - bpy.types.Bone.foot = BoolProperty(name="Foot", - description="Mark 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 = 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 = BoolProperty(name="IK", - description="Toggle 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 = PointerProperty(type=AnimationStitchSettings) - #Current/Active retargeted animation on the armature - bpy.types.Armature.active_mocap = StringProperty(update=retarget.NLASystemInitialize) - #Collection of retargeted animations and their NLA Tracks on the armature - bpy.types.Armature.mocapNLATracks = CollectionProperty(type=MocapNLATracks) - #Advanced retargeting boolean property - bpy.types.Armature.advancedRetarget = BoolProperty(default=False, update=advancedRetargetToggle) - #frame step - frequency of frames to retarget. Skipping is useful for previewing, faster work etc. - bpy.types.Armature.frameStep = IntProperty(name="Frame Skip", - default=1, - description="Number 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() diff --git a/mocap/mocap_constraints.py b/mocap/mocap_constraints.py deleted file mode 100644 index 3d3e4a60..00000000 --- a/mocap/mocap_constraints.py +++ /dev/null @@ -1,451 +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 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> - -import bpy -from mathutils import Vector -from bpy_extras import anim_utils -from . import retarget - - -### Utility Functions - - -def getConsObj(bone): - #utility function - returns related IK target if bone has IK - ik = [constraint for constraint in bone.constraints if constraint.type == "IK"] - if ik: - ik = ik[0] - cons_obj = ik.target - if ik.subtarget: - cons_obj = ik.target.pose.bones[ik.subtarget] - else: - cons_obj = bone - return cons_obj - - -def consObjToBone(cons_obj): - #Utility function - returns related bone from ik object - if cons_obj.name[-3:] == "Org": - return cons_obj.name[:-3] - else: - return cons_obj.name - -### And and Remove Constraints (called from operators) - - -def addNewConstraint(m_constraint, cons_obj): - #Decide the correct Blender constraint according to the Mocap constraint type - if m_constraint.type == "point" or m_constraint.type == "freeze": - c_type = "LIMIT_LOCATION" - if m_constraint.type == "distance": - c_type = "LIMIT_DISTANCE" - if m_constraint.type == "floor": - c_type = "LIMIT_LOCATION" - #create and store the new constraint within m_constraint - real_constraint = cons_obj.constraints.new(c_type) - real_constraint.name = "Auto fixes " + str(len(cons_obj.constraints)) - m_constraint.real_constraint_bone = consObjToBone(cons_obj) - m_constraint.real_constraint = real_constraint.name - #set the rest of the constraint properties - setConstraint(m_constraint, bpy.context) - - -def removeConstraint(m_constraint, cons_obj): - #remove the influence fcurve and Blender constraint - oldConstraint = cons_obj.constraints[m_constraint.real_constraint] - removeFcurves(cons_obj, bpy.context.active_object, oldConstraint, m_constraint) - cons_obj.constraints.remove(oldConstraint) - -### Update functions. There are 3: UpdateType/Bone -### update framing (deals with changes in the desired frame range) -### And setConstraint which deals with the rest - - -def updateConstraintBoneType(m_constraint, context): - #If the constraint exists, we need to remove it - #from the old bone - obj = context.active_object - bones = obj.pose.bones - if m_constraint.real_constraint: - bone = bones[m_constraint.real_constraint_bone] - cons_obj = getConsObj(bone) - removeConstraint(m_constraint, cons_obj) - #Regardless, after that we create a new constraint - if m_constraint.constrained_bone: - bone = bones[m_constraint.constrained_bone] - cons_obj = getConsObj(bone) - addNewConstraint(m_constraint, cons_obj) - - -def setConstraintFraming(m_constraint, context): - obj = context.active_object - bones = obj.pose.bones - bone = bones[m_constraint.constrained_bone] - cons_obj = getConsObj(bone) - real_constraint = cons_obj.constraints[m_constraint.real_constraint] - #remove the old keyframes - removeFcurves(cons_obj, obj, real_constraint, m_constraint) - #set the new ones according to the m_constraint properties - s, e = m_constraint.s_frame, m_constraint.e_frame - s_in, s_out = m_constraint.smooth_in, m_constraint.smooth_out - real_constraint.influence = 1 - real_constraint.keyframe_insert(data_path="influence", frame=s) - real_constraint.keyframe_insert(data_path="influence", frame=e) - real_constraint.influence = 0 - real_constraint.keyframe_insert(data_path="influence", frame=s - s_in) - real_constraint.keyframe_insert(data_path="influence", frame=e + s_out) - - -def removeFcurves(cons_obj, obj, real_constraint, m_constraint): - #Determine if the constrained object is a bone or an empty - if isinstance(cons_obj, bpy.types.PoseBone): - fcurves = obj.animation_data.action.fcurves - else: - fcurves = cons_obj.animation_data.action.fcurves - #Find the RNA data path of the constraint's influence - RNA_paths = [] - RNA_paths.append(real_constraint.path_from_id("influence")) - if m_constraint.type == "floor" or m_constraint.type == "point": - RNA_paths += [real_constraint.path_from_id("max_x"), real_constraint.path_from_id("min_x")] - RNA_paths += [real_constraint.path_from_id("max_y"), real_constraint.path_from_id("min_y")] - RNA_paths += [real_constraint.path_from_id("max_z"), real_constraint.path_from_id("min_z")] - #Retrieve the correct fcurve via the RNA data path and remove it - fcurves_del = [fcurve for fcurve in fcurves if fcurve.data_path in RNA_paths] - #clear the fcurve and set the frames. - if fcurves_del: - for fcurve in fcurves_del: - fcurves.remove(fcurve) - #remove armature fcurves (if user keyframed m_constraint properties) - if obj.data.animation_data and m_constraint.type == "point": - if obj.data.animation_data.action: - path = m_constraint.path_from_id("targetPoint") - m_fcurves = [fcurve for fcurve in obj.data.animation_data.action.fcurves if fcurve.data_path == path] - for curve in m_fcurves: - obj.data.animation_data.action.fcurves.remove(curve) - -#Utility function for copying property fcurves over - - -def copyFCurve(newCurve, oldCurve): - for point in oldCurve.keyframe_points: - newCurve.keyframe_points.insert(frame=point.co.x, value=point.co.y) - -#Creates new fcurves for the constraint properties (for floor and point) - - -def createConstraintFCurves(cons_obj, obj, real_constraint): - if isinstance(cons_obj, bpy.types.PoseBone): - c_fcurves = obj.animation_data.action.fcurves - else: - c_fcurves = cons_obj.animation_data.action.fcurves - c_x_path = [real_constraint.path_from_id("max_x"), real_constraint.path_from_id("min_x")] - c_y_path = [real_constraint.path_from_id("max_y"), real_constraint.path_from_id("min_y")] - c_z_path = [real_constraint.path_from_id("max_z"), real_constraint.path_from_id("min_z")] - c_constraints_path = c_x_path + c_y_path + c_z_path - existing_curves = [fcurve for fcurve in c_fcurves if fcurve.data_path in c_constraints_path] - if existing_curves: - for curve in existing_curves: - c_fcurves.remove(curve) - xCurves, yCurves, zCurves = [], [], [] - for path in c_constraints_path: - newCurve = c_fcurves.new(path) - if path in c_x_path: - xCurves.append(newCurve) - elif path in c_y_path: - yCurves.append(newCurve) - else: - zCurves.append(newCurve) - return xCurves, yCurves, zCurves - - -# Function that copies all settings from m_constraint to the real Blender constraints -# Is only called when blender constraint already exists - - -def setConstraint(m_constraint, context): - if not m_constraint.constrained_bone: - return - obj = context.active_object - bones = obj.pose.bones - bone = bones[m_constraint.constrained_bone] - cons_obj = getConsObj(bone) - real_constraint = cons_obj.constraints[m_constraint.real_constraint] - NLATracks = obj.data.mocapNLATracks[obj.data.active_mocap] - obj.animation_data.action = bpy.data.actions[NLATracks.auto_fix_track] - - #frame changing section - setConstraintFraming(m_constraint, context) - s, e = m_constraint.s_frame, m_constraint.e_frame - s_in, s_out = m_constraint.smooth_in, m_constraint.smooth_out - s -= s_in - e += s_out - #Set the blender constraint parameters - if m_constraint.type == "point": - constraint_settings = False # are fix settings keyframed? - if not m_constraint.targetSpace == "constrained_boneB": - real_constraint.owner_space = m_constraint.targetSpace - else: - real_constraint.owner_space = "LOCAL" - if obj.data.animation_data: - if obj.data.animation_data.action: - path = m_constraint.path_from_id("targetPoint") - m_fcurves = [fcurve for fcurve in obj.data.animation_data.action.fcurves if fcurve.data_path == path] - if m_fcurves: - constraint_settings = True - xCurves, yCurves, zCurves = createConstraintFCurves(cons_obj, obj, real_constraint) - for curve in xCurves: - copyFCurve(curve, m_fcurves[0]) - for curve in yCurves: - copyFCurve(curve, m_fcurves[1]) - for curve in zCurves: - copyFCurve(curve, m_fcurves[2]) - if m_constraint.targetSpace == "constrained_boneB" and m_constraint.constrained_boneB: - c_frame = context.scene.frame_current - bakedPos = {} - src_bone = bones[m_constraint.constrained_boneB] - if not constraint_settings: - xCurves, yCurves, zCurves = createConstraintFCurves(cons_obj, obj, real_constraint) - print("please wait a moment, calculating fix") - for t in range(s, e): - context.scene.frame_set(t) - src_bone_pos = src_bone.matrix.to_translation() - bakedPos[t] = src_bone_pos + m_constraint.targetPoint # final position for constrained bone in object space - context.scene.frame_set(c_frame) - for frame in bakedPos.keys(): - pos = bakedPos[frame] - for xCurve in xCurves: - xCurve.keyframe_points.insert(frame=frame, value=pos.x) - for yCurve in yCurves: - yCurve.keyframe_points.insert(frame=frame, value=pos.y) - for zCurve in zCurves: - zCurve.keyframe_points.insert(frame=frame, value=pos.z) - - if not constraint_settings: - x, y, z = m_constraint.targetPoint - real_constraint.max_x = x - real_constraint.max_y = y - real_constraint.max_z = z - real_constraint.min_x = x - real_constraint.min_y = y - real_constraint.min_z = z - real_constraint.use_max_x = True - real_constraint.use_max_y = True - real_constraint.use_max_z = True - real_constraint.use_min_x = True - real_constraint.use_min_y = True - real_constraint.use_min_z = True - - if m_constraint.type == "freeze": - context.scene.frame_set(s) - real_constraint.owner_space = m_constraint.targetSpace - bpy.context.scene.frame_set(m_constraint.s_frame) - if isinstance(cons_obj, bpy.types.PoseBone): - vec = obj.matrix_world * (cons_obj.matrix.to_translation()) - #~ if obj.parent: - #~ vec = obj.parent.matrix_world * vec - x, y, z = vec - else: - x, y, z = cons_obj.matrix_world.to_translation() - - real_constraint.max_x = x - real_constraint.max_y = y - real_constraint.max_z = z - real_constraint.min_x = x - real_constraint.min_y = y - real_constraint.min_z = z - real_constraint.use_max_x = True - real_constraint.use_max_y = True - real_constraint.use_max_z = True - real_constraint.use_min_x = True - real_constraint.use_min_y = True - real_constraint.use_min_z = True - - if m_constraint.type == "distance" and m_constraint.constrained_boneB: - real_constraint.owner_space = "WORLD" - real_constraint.target = obj - real_constraint.subtarget = getConsObj(bones[m_constraint.constrained_boneB]).name - real_constraint.limit_mode = "LIMITDIST_ONSURFACE" - if m_constraint.targetDist < 0.01: - m_constraint.targetDist = 0.01 - real_constraint.distance = m_constraint.targetDist - - if m_constraint.type == "floor" and m_constraint.targetMesh: - real_constraint.mute = True - real_constraint.owner_space = "WORLD" - #calculate the positions throughout the range - s, e = m_constraint.s_frame, m_constraint.e_frame - s_in, s_out = m_constraint.smooth_in, m_constraint.smooth_out - s -= s_in - e += s_out - bakedPos = {} - floor = bpy.data.objects[m_constraint.targetMesh] - c_frame = context.scene.frame_current - print("please wait a moment, calculating fix") - for t in range(s, e): - context.scene.frame_set(t) - axis = obj.matrix_world.to_3x3() * Vector((0, 0, 1)) - offset = obj.matrix_world.to_3x3() * Vector((0, 0, m_constraint.targetDist)) - ray_origin = (cons_obj.matrix * obj.matrix_world).to_translation() - offset # world position of constrained bone - ray_target = ray_origin + axis - #convert ray points to floor's object space - ray_origin = floor.matrix_world.inverted() * ray_origin - ray_target = floor.matrix_world.inverted() * ray_target - ray_direction = ray_target - ray_origin - ok, hit, nor, ind = floor.ray_cast(ray_origin, ray_direction) - if ok: - bakedPos[t] = (floor.matrix_world * hit) - bakedPos[t] += Vector((0, 0, m_constraint.targetDist)) - else: - bakedPos[t] = (cons_obj.matrix * obj.matrix_world).to_translation() - context.scene.frame_set(c_frame) - #create keyframes for real constraint - xCurves, yCurves, zCurves = createConstraintFCurves(cons_obj, obj, real_constraint) - for frame in bakedPos.keys(): - pos = bakedPos[frame] - for xCurve in xCurves: - xCurve.keyframe_points.insert(frame=frame, value=pos.x) - for yCurve in yCurves: - yCurve.keyframe_points.insert(frame=frame, value=pos.y) - for zCurve in zCurves: - zCurve.keyframe_points.insert(frame=frame, value=pos.z) - real_constraint.use_max_x = True - real_constraint.use_max_y = True - real_constraint.use_max_z = True - real_constraint.use_min_x = True - real_constraint.use_min_y = True - real_constraint.use_min_z = True - - # active/baked check - real_constraint.mute = (not m_constraint.active) - - -def locBake(s_frame, e_frame, bones): - scene = bpy.context.scene - bakeDict = {} - for bone in bones: - bakeDict[bone.name] = {} - for t in range(s_frame, e_frame): - scene.frame_set(t) - for bone in bones: - bakeDict[bone.name][t] = bone.matrix.copy() - for t in range(s_frame, e_frame): - for bone in bones: - print(bone.bone.matrix_local.to_translation()) - bone.matrix = bakeDict[bone.name][t] - bone.keyframe_insert("location", frame=t) - - -# Baking function which bakes all bones effected by the constraint -def bakeAllConstraints(obj, s_frame, e_frame, bones): - for bone in bones: - bone.bone.select = False - selectedBones = [] # Marks bones that need a full bake - simpleBake = [] # Marks bones that need only a location bake - for end_bone in bones: - if end_bone.name in [m_constraint.real_constraint_bone for m_constraint in obj.data.mocap_constraints]: - #For all bones that have a constraint: - ik = retarget.hasIKConstraint(end_bone) - cons_obj = getConsObj(end_bone) - if ik: - #If it's an auto generated IK: - if ik.chain_count == 0: - selectedBones += bones # Chain len 0, bake everything - else: - selectedBones += [end_bone] + end_bone.parent_recursive[:ik.chain_count - 1] # Bake the chain - else: - #It's either an FK bone which we should just bake - #OR a user created IK target bone - simpleBake += [end_bone] - for bone in selectedBones: - bone.bone.select = True - NLATracks = obj.data.mocapNLATracks[obj.data.active_mocap] - obj.animation_data.action = bpy.data.actions[NLATracks.auto_fix_track] - constraintTrack = obj.animation_data.nla_tracks[NLATracks.auto_fix_track] - constraintStrip = constraintTrack.strips[0] - constraintStrip.action_frame_start = s_frame - constraintStrip.action_frame_end = e_frame - constraintStrip.frame_start = s_frame - constraintStrip.frame_end = e_frame - if selectedBones: - # Use bake function from NLA Bake Action operator - anim_utils.bake_action( - obj, - s_frame, - e_frame, - action=constraintStrip.action, - only_selected=True, - do_pose=True, - do_object=False, - ) - if simpleBake: - #Do a "simple" bake, location only, world space only. - locBake(s_frame, e_frame, simpleBake) - - -#Calls the baking function and decativates relevant constraints -def bakeConstraints(context): - obj = context.active_object - bones = obj.pose.bones - s_frame, e_frame = context.scene.frame_start, context.scene.frame_end - #Bake relevant bones - bakeAllConstraints(obj, s_frame, e_frame, bones) - for m_constraint in obj.data.mocap_constraints: - end_bone = bones[m_constraint.real_constraint_bone] - cons_obj = getConsObj(end_bone) - # It's a control empty: turn the ik off - if not isinstance(cons_obj, bpy.types.PoseBone): - ik_con = retarget.hasIKConstraint(end_bone) - if ik_con: - ik_con.mute = True - # Deactivate related Blender Constraint - m_constraint.active = False - - -#Deletes the baked fcurves and reactivates relevant constraints -def unbakeConstraints(context): - # to unbake constraints we delete the whole strip - obj = context.active_object - bones = obj.pose.bones - scene = bpy.context.scene - NLATracks = obj.data.mocapNLATracks[obj.data.active_mocap] - obj.animation_data.action = bpy.data.actions[NLATracks.auto_fix_track] - constraintTrack = obj.animation_data.nla_tracks[NLATracks.auto_fix_track] - constraintStrip = constraintTrack.strips[0] - action = constraintStrip.action - # delete the fcurves on the strip - for fcurve in action.fcurves: - action.fcurves.remove(fcurve) - # reactivate relevant constraints - for m_constraint in obj.data.mocap_constraints: - end_bone = bones[m_constraint.real_constraint_bone] - cons_obj = getConsObj(end_bone) - # It's a control empty: turn the ik back on - if not isinstance(cons_obj, bpy.types.PoseBone): - ik_con = retarget.hasIKConstraint(end_bone) - if ik_con: - ik_con.mute = False - m_constraint.active = True - - -def updateConstraints(obj, context): - fixes = obj.data.mocap_constraints - for fix in fixes: - fix.active = False - fix.active = True diff --git a/mocap/mocap_tools.py b/mocap/mocap_tools.py deleted file mode 100644 index 5f9c2a2d..00000000 --- a/mocap/mocap_tools.py +++ /dev/null @@ -1,939 +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 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> - -from math import sqrt, radians, floor, ceil -import bpy -import time -from mathutils import Vector, Matrix - - -# A Python implementation of n sized Vectors. -# Mathutils has a max size of 4, and we need at least 5 for Simplify Curves and even more for Cross Correlation. -# Vector utility functions -class NdVector: - vec = [] - - def __init__(self, vec): - self.vec = vec[:] - - def __len__(self): - return len(self.vec) - - def __mul__(self, otherMember): - # assume anything with list access is a vector - if isinstance(otherMember, NdVector): - a = self.vec - b = otherMember.vec - n = len(self) - return sum([a[i] * b[i] for i in range(n)]) - else: - # int/float - return NdVector([otherMember * x for x in self.vec]) - - def __sub__(self, otherVec): - a = self.vec - b = otherVec.vec - n = len(self) - return NdVector([a[i] - b[i] for i in range(n)]) - - def __add__(self, otherVec): - a = self.vec - b = otherVec.vec - n = len(self) - return NdVector([a[i] + b[i] for i in range(n)]) - - def __div__(self, scalar): - return NdVector([x / scalar for x in self.vec]) - - @property - def length(self): - return sqrt(self * self) - - @property - def lengthSq(self): - return (self * self) - - def normalize(self): - len = self.length - self.vec = [x / len for x in self.vec] - - def copy(self): - return NdVector(self.vec) - - def __getitem__(self, i): - return self.vec[i] - - @property - def x(self): - return self.vec[0] - - @property - def y(self): - return self.vec[1] - - def resize_2d(self): - return Vector((self.x, self.y)) - - -#Sampled Data Point class for Simplify Curves -class dataPoint: - index = 0 - # x,y1,y2,y3 coordinate of original point - co = NdVector((0, 0, 0, 0, 0)) - #position according to parametric view of original data, [0,1] range - u = 0 - #use this for anything - temp = 0 - - def __init__(self, index, co, u=0): - self.index = index - self.co = co - self.u = u - - -# Helper to convert from a sampled fcurve back to editable keyframes one. -def make_editable_fcurves(fcurves): - for fc in fcurves: - if fc.sampled_points: - fc.convert_to_keyframes(floor(fc.sampled_points[0].co[0]), ceil(fc.sampled_points[-1].co[0]) + 1) - - -#Cross Correlation Function -#http://en.wikipedia.org/wiki/Cross_correlation -#IN: curvesA, curvesB - bpy_collection/list of fcurves to analyze. Auto-Correlation is when they are the same. -# margin - When searching for the best "start" frame, how large a neighborhood of frames should we inspect (similar to epsilon in Calculus) -#OUT: startFrame, length of new anim, and curvesA -def crossCorrelationMatch(curvesA, curvesB, margin): - dataA = [] - dataB = [] - start = int(max(curvesA[0].range()[0], curvesB[0].range()[0])) - end = int(min(curvesA[0].range()[1], curvesB[0].range()[1])) - - #transfer all fcurves data on each frame to a single NdVector. - for i in range(1, end): - vec = [] - for fcurve in curvesA: - if fcurve.data_path in [otherFcurve.data_path for otherFcurve in curvesB]: - vec.append(fcurve.evaluate(i)) - dataA.append(NdVector(vec)) - vec = [] - for fcurve in curvesB: - if fcurve.data_path in [otherFcurve.data_path for otherFcurve in curvesA]: - vec.append(fcurve.evaluate(i)) - dataB.append(NdVector(vec)) - - #Comparator for Cross Correlation. "Classic" implementation uses dot product, as do we. - def comp(a, b): - return a * b - - #Create Rxy, which holds the Cross Correlation data. - N = len(dataA) - Rxy = [0.0] * N - for i in range(N): - for j in range(i, min(i + N, N)): - Rxy[i] += comp(dataA[j], dataB[j - i]) - for j in range(i): - Rxy[i] += comp(dataA[j], dataB[j - i + N]) - Rxy[i] /= float(N) - - #Find the Local maximums in the Cross Correlation data via numerical derivative. - def LocalMaximums(Rxy): - Rxyd = [Rxy[i] - Rxy[i - 1] for i in range(1, len(Rxy))] - maxs = [] - for i in range(1, len(Rxyd) - 1): - a = Rxyd[i - 1] - b = Rxyd[i] - #sign change (zerocrossing) at point i, denoting max point (only) - if (a >= 0 and b < 0) or (a < 0 and b >= 0): - maxs.append((i, max(Rxy[i], Rxy[i - 1]))) - return [x[0] for x in maxs] - #~ return max(maxs, key=lambda x: x[1])[0] - - #flms - the possible offsets of the first part of the animation. In Auto-Corr, this is the length of the loop. - flms = LocalMaximums(Rxy[0:int(len(Rxy))]) - ss = [] - - #for every local maximum, find the best one - i.e. also has the best start frame. - for flm in flms: - diff = [] - - for i in range(len(dataA) - flm): - diff.append((dataA[i] - dataB[i + flm]).lengthSq) - - def lowerErrorSlice(diff, e): - #index, error at index - bestSlice = (0, 100000) - for i in range(e, len(diff) - e): - errorSlice = sum(diff[i - e:i + e + 1]) - if errorSlice < bestSlice[1]: - bestSlice = (i, errorSlice, flm) - return bestSlice - - s = lowerErrorSlice(diff, margin) - ss.append(s) - - #Find the best result and return it. - ss.sort(key=lambda x: x[1]) - return ss[0][2], ss[0][0], dataA - - -#Uses auto correlation (cross correlation of the same set of curves) and trims the active_object's fcurves -#Except for location curves (which in mocap tend to be not cyclic, e.g. a walk cycle forward) -#Transfers the fcurve data to a list of NdVector (length of list is number of fcurves), and calls the cross correlation function. -#Then trims the fcurve accordingly. -#IN: Nothing, set the object you want as active and call. Assumes object has animation_data.action! -#OUT: Trims the object's fcurves (except location curves). -def autoloop_anim(): - context = bpy.context - obj = context.active_object - - def locCurve(x): - x.data_path == "location" - - fcurves = [x for x in obj.animation_data.action.fcurves if not locCurve(x)] - - margin = 10 - - flm, s, data = crossCorrelationMatch(fcurves, fcurves, margin) - loop = data[s:s + flm] - - #performs blending with a root falloff on the seam's neighborhood to ensure good tiling. - for i in range(1, margin + 1): - w1 = sqrt(float(i) / margin) - loop[-i] = (loop[-i] * w1) + (loop[0] * (1 - w1)) - - for curve in fcurves: - pts = curve.keyframe_points - for i in range(len(pts) - 1, -1, -1): - pts.remove(pts[i]) - - for c, curve in enumerate(fcurves): - pts = curve.keyframe_points - for i in range(len(loop)): - pts.insert(i + 2, loop[i][c]) - - context.scene.frame_end = flm - - -#simplifyCurves: performs the bulk of the samples to bezier conversion. -#IN: curveGroup - which can be a collection of singleFcurves, or grouped (via nested lists) . -# error - threshold of permittable error (max distance) of the new beziers to the original data -# reparaError - threshold of error where we should try to fix the parameterization rather than split the existing curve. > error, usually by a small constant factor for best performance. -# maxIterations - maximum number of iterations of reparameterizations we should attempt. (Newton-Rahpson is not guaranteed to converge, so this is needed). -# group_mode - boolean, indicating whether we should place bezier keyframes on the same x (frame), or optimize each individual curve. -#OUT: None. Deletes the existing curves and creates the new beziers. -def simplifyCurves(curveGroup, error, reparaError, maxIterations, group_mode): - #Calculates the unit tangent of point v - def unitTangent(v, data_pts): - tang = NdVector((0, 0, 0, 0, 0)) - if v != 0: - #If it's not the first point, we can calculate a leftside tangent - tang += data_pts[v].co - data_pts[v - 1].co - if v != len(data_pts) - 1: - #If it's not the last point, we can calculate a rightside tangent - tang += data_pts[v + 1].co - data_pts[v].co - tang.normalize() - return tang - - #assign parametric u value for each point in original data, via relative arc length - #http://en.wikipedia.org/wiki/Arc_length - def chordLength(data_pts, s, e): - totalLength = 0 - for pt in data_pts[s:e + 1]: - i = pt.index - if i == s: - chordLength = 0 - else: - chordLength = (data_pts[i].co - data_pts[i - 1].co).length - totalLength += chordLength - pt.temp = totalLength - for pt in data_pts[s:e + 1]: - if totalLength == 0: - print(s, e) - pt.u = (pt.temp / totalLength) - - # get binomial coefficient lookup table, this function/table is only called with args - # (3,0),(3,1),(3,2),(3,3),(2,0),(2,1),(2,2)! - binomDict = {(3, 0): 1, - (3, 1): 3, - (3, 2): 3, - (3, 3): 1, - (2, 0): 1, - (2, 1): 2, - (2, 2): 1, - } - - #value at pt t of a single bernstein Polynomial - def bernsteinPoly(n, i, t): - binomCoeff = binomDict[(n, i)] - return binomCoeff * pow(t, i) * pow(1 - t, n - i) - - # fit a single cubic to data points in range [s(tart),e(nd)]. - def fitSingleCubic(data_pts, s, e): - - # A - matrix used for calculating C matrices for fitting - def A(i, j, s, e, t1, t2): - if j == 1: - t = t1 - if j == 2: - t = t2 - u = data_pts[i].u - return t * bernsteinPoly(3, j, u) - - # X component, used for calculating X matrices for fitting - def xComponent(i, s, e): - di = data_pts[i].co - u = data_pts[i].u - v0 = data_pts[s].co - v3 = data_pts[e].co - a = v0 * bernsteinPoly(3, 0, u) - b = v0 * bernsteinPoly(3, 1, u) - c = v3 * bernsteinPoly(3, 2, u) - d = v3 * bernsteinPoly(3, 3, u) - return (di - (a + b + c + d)) - - t1 = unitTangent(s, data_pts) - t2 = unitTangent(e, data_pts) - c11 = sum([A(i, 1, s, e, t1, t2) * A(i, 1, s, e, t1, t2) for i in range(s, e + 1)]) - c12 = sum([A(i, 1, s, e, t1, t2) * A(i, 2, s, e, t1, t2) for i in range(s, e + 1)]) - c21 = c12 - c22 = sum([A(i, 2, s, e, t1, t2) * A(i, 2, s, e, t1, t2) for i in range(s, e + 1)]) - - x1 = sum([xComponent(i, s, e) * A(i, 1, s, e, t1, t2) for i in range(s, e + 1)]) - x2 = sum([xComponent(i, s, e) * A(i, 2, s, e, t1, t2) for i in range(s, e + 1)]) - - # calculate Determinate of the 3 matrices - det_cc = c11 * c22 - c21 * c12 - det_cx = c11 * x2 - c12 * x1 - det_xc = x1 * c22 - x2 * c12 - - # if matrix is not homogenous, fudge the data a bit - if det_cc == 0: - det_cc = 0.01 - - # alpha's are the correct offset for bezier handles - alpha0 = det_xc / det_cc # offset from right (first) point - alpha1 = det_cx / det_cc # offset from left (last) point - - sRightHandle = data_pts[s].co.copy() - sTangent = t1 * abs(alpha0) - sRightHandle += sTangent # position of first pt's handle - eLeftHandle = data_pts[e].co.copy() - eTangent = t2 * abs(alpha1) - eLeftHandle += eTangent # position of last pt's handle. - - # return a 4 member tuple representing the bezier - return (data_pts[s].co, - sRightHandle, - eLeftHandle, - data_pts[e].co) - - # convert 2 given data points into a cubic bezier. - # handles are offset along the tangent at - # a 3rd of the length between the points. - def fitSingleCubic2Pts(data_pts, s, e): - alpha0 = alpha1 = (data_pts[s].co - data_pts[e].co).length / 3 - - sRightHandle = data_pts[s].co.copy() - sTangent = unitTangent(s, data_pts) * abs(alpha0) - sRightHandle += sTangent # position of first pt's handle - eLeftHandle = data_pts[e].co.copy() - eTangent = unitTangent(e, data_pts) * abs(alpha1) - eLeftHandle += eTangent # position of last pt's handle. - - #return a 4 member tuple representing the bezier - return (data_pts[s].co, - sRightHandle, - eLeftHandle, - data_pts[e].co) - - #evaluate bezier, represented by a 4 member tuple (pts) at point t. - def bezierEval(pts, t): - sumVec = NdVector((0, 0, 0, 0, 0)) - for i in range(4): - sumVec += pts[i] * bernsteinPoly(3, i, t) - return sumVec - - #calculate the highest error between bezier and original data - #returns the distance and the index of the point where max error occurs. - def maxErrorAmount(data_pts, bez, s, e): - maxError = 0 - maxErrorPt = s - if e - s < 3: - return 0, None - for pt in data_pts[s:e + 1]: - bezVal = bezierEval(bez, pt.u) - normalize_error = pt.co.length - if normalize_error == 0: - normalize_error = 1 - tmpError = (pt.co - bezVal).length / normalize_error - if tmpError >= maxError: - maxError = tmpError - maxErrorPt = pt.index - return maxError, maxErrorPt - - #calculated bezier derivative at point t. - #That is, tangent of point t. - def getBezDerivative(bez, t): - n = len(bez) - 1 - sumVec = NdVector((0, 0, 0, 0, 0)) - for i in range(n - 1): - sumVec += (bez[i + 1] - bez[i]) * bernsteinPoly(n - 1, i, t) - return sumVec - - #use Newton-Raphson to find a better parameterization of datapoints, - #one that minimizes the distance (or error) - # between bezier and original data. - def newtonRaphson(data_pts, s, e, bez): - for pt in data_pts[s:e + 1]: - if pt.index == s: - pt.u = 0 - elif pt.index == e: - pt.u = 1 - else: - u = pt.u - qu = bezierEval(bez, pt.u) - qud = getBezDerivative(bez, u) - #we wish to minimize f(u), - #the squared distance between curve and data - fu = (qu - pt.co).length ** 2 - fud = (2 * (qu.x - pt.co.x) * (qud.x)) - (2 * (qu.y - pt.co.y) * (qud.y)) - if fud == 0: - fu = 0 - fud = 1 - pt.u = pt.u - (fu / fud) - - #Create data_pts, a list of dataPoint type, each is assigned index i, and an NdVector - def createDataPts(curveGroup, group_mode): - make_editable_fcurves(curveGroup if group_mode else (curveGroup,)) - - if group_mode: - print([x.data_path for x in curveGroup]) - comp_cos = (0,) * (4 - len(curveGroup)) # We need to add that number of null cos to get our 5D vector. - kframes = sorted(set(kf.co.x for fc in curveGroup for kf in fc.keyframe_points)) - data_pts = [dataPoint(i, NdVector((fra,) + tuple(fc.evaluate(fra) for fc in curveGroup) + comp_cos)) - for i, fra in enumerate(kframes)] - else: - data_pts = [dataPoint(i, NdVector((kf.co.x, kf.co.y, 0, 0, 0))) - for i, kf in enumerate(curveGroup.keyframe_points)] - return data_pts - - #Recursively fit cubic beziers to the data_pts between s and e - def fitCubic(data_pts, s, e): - # if there are less than 3 points, fit a single basic bezier - if e - s < 3: - bez = fitSingleCubic2Pts(data_pts, s, e) - else: - #if there are more, parameterize the points - # and fit a single cubic bezier - chordLength(data_pts, s, e) - bez = fitSingleCubic(data_pts, s, e) - - #calculate max error and point where it occurs - maxError, maxErrorPt = maxErrorAmount(data_pts, bez, s, e) - #if error is small enough, reparameterization might be enough - if maxError < reparaError and maxError > error: - for i in range(maxIterations): - newtonRaphson(data_pts, s, e, bez) - if e - s < 3: - bez = fitSingleCubic2Pts(data_pts, s, e) - else: - bez = fitSingleCubic(data_pts, s, e) - - #recalculate max error and point where it occurs - maxError, maxErrorPt = maxErrorAmount(data_pts, bez, s, e) - - #repara wasn't enough, we need 2 beziers for this range. - #Split the bezier at point of maximum error - if maxError > error: - fitCubic(data_pts, s, maxErrorPt) - fitCubic(data_pts, maxErrorPt, e) - else: - #error is small enough, return the beziers. - beziers.append(bez) - return - - # deletes the sampled points and creates beziers. - def createNewCurves(curveGroup, beziers, group_mode): - #remove all existing data points - if group_mode: - for fcurve in curveGroup: - for i in range(len(fcurve.keyframe_points) - 1, 0, -1): - fcurve.keyframe_points.remove(fcurve.keyframe_points[i], fast=True) - else: - fcurve = curveGroup - for i in range(len(fcurve.keyframe_points) - 1, 0, -1): - fcurve.keyframe_points.remove(fcurve.keyframe_points[i], fast=True) - - #insert the calculated beziers to blender data. - if group_mode: - for fullbez in beziers: - for i, fcurve in enumerate(curveGroup): - bez = [Vector((vec[0], vec[i + 1])) for vec in fullbez] - newKey = fcurve.keyframe_points.insert(frame=bez[0].x, value=bez[0].y, options={'FAST'}) - newKey.handle_right = (bez[1].x, bez[1].y) - - newKey = fcurve.keyframe_points.insert(frame=bez[3].x, value=bez[3].y, options={'FAST'}) - newKey.handle_left = (bez[2].x, bez[2].y) - else: - for bez in beziers: - for vec in bez: - vec.resize_2d() - newKey = fcurve.keyframe_points.insert(frame=bez[0].x, value=bez[0].y, options={'FAST'}) - newKey.handle_right = (bez[1].x, bez[1].y) - - newKey = fcurve.keyframe_points.insert(frame=bez[3].x, value=bez[3].y, options={'FAST'}) - newKey.handle_left = (bez[2].x, bez[2].y) - - # We used fast remove/insert, time to update the curves! - for fcurve in (curveGroup if group_mode else (curveGroup,)): - fcurve.update() - - # indices are detached from data point's frame (x) value and - # stored in the dataPoint object, represent a range - - data_pts = createDataPts(curveGroup, group_mode) - - if not data_pts: - return - - s = 0 # start - e = len(data_pts) - 1 # end - - beziers = [] - - #begin the recursive fitting algorithm. - fitCubic(data_pts, s, e) - - #remove old Fcurves and insert the new ones - createNewCurves(curveGroup, beziers, group_mode) - - -#Main function of simplification, which called by Operator -#IN: -# sel_opt- either "sel" (selected) or "all" for which curves to effect -# error- maximum error allowed, in fraction (20% = 0.0020, which is the default), -# i.e. divide by 10000 from percentage wanted. -# group_mode- boolean, to analyze each curve separately or in groups, -# where a group is all curves that effect the same property/RNA path -def fcurves_simplify(context, obj, sel_opt="all", error=0.002, group_mode=True): - # main vars - fcurves = obj.animation_data.action.fcurves - - if sel_opt == "sel": - sel_fcurves = [fcurve for fcurve in fcurves if fcurve.select] - else: - sel_fcurves = fcurves[:] - - #Error threshold for Newton Raphson reparamatizing - reparaError = error * 32 - maxIterations = 16 - - if group_mode: - fcurveDict = {} - #this loop sorts all the fcurves into groups of 3 or 4, - #based on their RNA Data path, which corresponds to - #which property they effect - for curve in sel_fcurves: - if curve.data_path in fcurveDict: # if this bone has been added, append the curve to its list - fcurveDict[curve.data_path].append(curve) - else: - fcurveDict[curve.data_path] = [curve] # new bone, add a new dict value with this first curve - fcurveGroups = fcurveDict.values() - else: - fcurveGroups = sel_fcurves - - if error > 0.00000: - #simplify every selected curve. - totalt = 0 - for i, fcurveGroup in enumerate(fcurveGroups): - print("Processing curve " + str(i + 1) + "/" + str(len(fcurveGroups))) - t = time.clock() - simplifyCurves(fcurveGroup, error, reparaError, maxIterations, group_mode) - t = time.clock() - t - print(str(t)[:5] + " seconds to process last curve") - totalt += t - print(str(totalt)[:5] + " seconds, total time elapsed") - - return - - -def detect_min_max(v): - """ - Converted from MATLAB script at http://billauer.co.il/peakdet.html - - Yields indices of peaks, i.e. local minima/maxima. - - % Eli Billauer, 3.4.05 (Explicitly not copyrighted). - % This function is released to the public domain; Any use is allowed. - """ - - min_val, max_val = float('inf'), -float('inf') - - check_max = True - - for i, val in enumerate(v): - if val > max_val: - max_val = val - if val < min_val: - min_val = val - - if check_max: - if val < max_val: - yield i - min_val = val - check_max = False - else: - if val > min_val: - yield i - max_val = val - check_max = True - - -def denoise(obj, fcurves): - """ - Implementation of non-linear blur filter. - Finds spikes in the fcurve, and replaces spikes that are too big with the average of the surrounding keyframes. - """ - make_editable_fcurves(fcurves) - - for fcurve in fcurves: - org_pts = fcurve.keyframe_points[:] - - for idx in detect_min_max(pt.co.y for pt in fcurve.keyframe_points[1:-1]): - # Find the neighbours - prev_pt = org_pts[idx - 1].co.y - next_pt = org_pts[idx + 1].co.y - this_pt = org_pts[idx] - - # Check the distance from the min/max to the average of the surrounding points. - avg = (prev_pt + next_pt) / 2 - is_peak = abs(this_pt.co.y - avg) > avg * 0.02 - - if is_peak: - diff = avg - fcurve.keyframe_points[idx].co.y - fcurve.keyframe_points[idx].co.y = avg - fcurve.keyframe_points[idx].handle_left.y += diff - fcurve.keyframe_points[idx].handle_right.y += diff - - # Important to update the curve after modifying it! - fcurve.update() - - -# Receives armature, and rotations all bones by 90 degrees along the X axis -# This fixes the common axis issue BVH files have when importing. -# IN: Armature (bpy.types.Armature) -def rotate_fix_armature(arm_data): - global_matrix = Matrix.Rotation(radians(90), 4, "X") - bpy.ops.object.mode_set(mode='EDIT', toggle=False) - #disconnect all bones for ease of global rotation - connectedBones = [] - for bone in arm_data.edit_bones: - if bone.use_connect: - connectedBones.append(bone.name) - bone.use_connect = False - - #rotate all the bones around their center - for bone in arm_data.edit_bones: - bone.transform(global_matrix) - - #reconnect the bones - for bone in connectedBones: - arm_data.edit_bones[bone].use_connect = True - bpy.ops.object.mode_set(mode='OBJECT', toggle=False) - - -#Roughly scales the performer armature to match the enduser armature -#IN: perfromer_obj, enduser_obj, Blender objects whose .data is an armature. -def scale_fix_armature(performer_obj, enduser_obj): - perf_bones = performer_obj.data.bones - end_bones = enduser_obj.data.bones - - def calculateBoundingRadius(bones): - # Calculate the average position of each bone - center = sum((bone.head_local for bone in bones), Vector()) - center /= len(bones) - - # The radius is defined as the max distance from the center. - radius = max((bone.head_local - center).length for bone in bones) - return radius - - perf_rad = calculateBoundingRadius(performer_obj.data.bones) - end_rad = calculateBoundingRadius(enduser_obj.data.bones) - - factor = end_rad / perf_rad - performer_obj.scale = factor * Vector((1, 1, 1)) - - -#Guess Mapping -#Given a performer and enduser armature, attempts to guess the hierarchy mapping -def guessMapping(performer_obj, enduser_obj): - perf_bones = performer_obj.data.bones - end_bones = enduser_obj.data.bones - - root = perf_bones[0] - - def findBoneSide(bone): - if "Left" in bone: - return "Left", bone.replace("Left", "").lower().replace(".", "") - if "Right" in bone: - return "Right", bone.replace("Right", "").lower().replace(".", "") - if "L" in bone: - return "Left", bone.replace("Left", "").lower().replace(".", "") - if "R" in bone: - return "Right", bone.replace("Right", "").lower().replace(".", "") - return "", bone - - def nameMatch(bone_a, bone_b): - # nameMatch - receives two strings, returns 2 if they are relatively the same, 1 if they are the same but R and L and 0 if no match at all - side_a, noside_a = findBoneSide(bone_a) - side_b, noside_b = findBoneSide(bone_b) - if side_a == side_b: - if noside_a in noside_b or noside_b in noside_a: - return 2 - else: - if noside_a in noside_b or noside_b in noside_a: - return 1 - return 0 - - def guessSingleMapping(perf_bone): - possible_bones = [end_bones[0]] - - while possible_bones: - for end_bone in possible_bones: - match = nameMatch(perf_bone.name, end_bone.name) - if match == 2 and not perf_bone.map: - perf_bone.map = end_bone.name - #~ elif match == 1 and not perf_bone.map: - #~ oppo = perf_bones[oppositeBone(perf_bone)].map - # if oppo: - # perf_bone = oppo - newPossibleBones = [] - for end_bone in possible_bones: - newPossibleBones += list(end_bone.children) - possible_bones = newPossibleBones - - for child in perf_bone.children: - guessSingleMapping(child) - - guessSingleMapping(root) - - -# Creates limit rotation constraints on the enduser armature based on range of motion (max min of fcurves) of the performer. -# IN: context (bpy.context, etc.), and 2 blender objects which are armatures -# OUT: creates the limit constraints. -def limit_dof(context, performer_obj, enduser_obj): - limitDict = {} - perf_bones = [bone for bone in performer_obj.pose.bones if bone.bone.map] - c_frame = context.scene.frame_current - for bone in perf_bones: - limitDict[bone.bone.map] = [1000, 1000, 1000, -1000, -1000, -1000] - for t in range(context.scene.frame_start, context.scene.frame_end): - context.scene.frame_set(t) - for bone in perf_bones: - end_bone = enduser_obj.pose.bones[bone.bone.map] - bake_matrix = bone.matrix - rest_matrix = end_bone.bone.matrix_local - - if end_bone.parent and end_bone.bone.use_inherit_rotation: - srcParent = bone.parent - parent_mat = srcParent.matrix - parent_rest = end_bone.parent.bone.matrix_local - parent_rest_inv = parent_rest.inverted() - parent_mat_inv = parent_mat.inverted() - bake_matrix = parent_mat_inv * bake_matrix - rest_matrix = parent_rest_inv * rest_matrix - - rest_matrix_inv = rest_matrix.inverted() - bake_matrix = rest_matrix_inv * bake_matrix - - mat = bake_matrix - euler = mat.to_euler() - limitDict[bone.bone.map][0] = min(limitDict[bone.bone.map][0], euler.x) - limitDict[bone.bone.map][1] = min(limitDict[bone.bone.map][1], euler.y) - limitDict[bone.bone.map][2] = min(limitDict[bone.bone.map][2], euler.z) - limitDict[bone.bone.map][3] = max(limitDict[bone.bone.map][3], euler.x) - limitDict[bone.bone.map][4] = max(limitDict[bone.bone.map][4], euler.y) - limitDict[bone.bone.map][5] = max(limitDict[bone.bone.map][5], euler.z) - for bone in enduser_obj.pose.bones: - existingConstraint = [constraint for constraint in bone.constraints if constraint.name == "DOF Limitation"] - if existingConstraint: - bone.constraints.remove(existingConstraint[0]) - end_bones = [bone for bone in enduser_obj.pose.bones if bone.name in limitDict.keys()] - for bone in end_bones: - #~ if not bone.is_in_ik_chain: - newCons = bone.constraints.new("LIMIT_ROTATION") - newCons.name = "DOF Limitation" - newCons.owner_space = "LOCAL" - newCons.min_x, newCons.min_y, newCons.min_z, newCons.max_x, newCons.max_y, newCons.max_z = limitDict[bone.name] - newCons.use_limit_x = True - newCons.use_limit_y = True - newCons.use_limit_z = True - context.scene.frame_set(c_frame) - - -# Removes the constraints that were added by limit_dof on the enduser_obj -def limit_dof_toggle_off(context, enduser_obj): - for bone in enduser_obj.pose.bones: - existingConstraint = [constraint for constraint in bone.constraints if constraint.name == "DOF Limitation"] - if existingConstraint: - bone.constraints.remove(existingConstraint[0]) - - -# Reparameterizes a blender path via keyframing it's eval_time to match a stride_object's forward velocity. -# IN: Context, stride object (blender object with location keyframes), path object. -def path_editing(context, stride_obj, path): - y_fcurve = [fcurve for fcurve in stride_obj.animation_data.action.fcurves if fcurve.data_path == "location"][1] - s, e = context.scene.frame_start, context.scene.frame_end # y_fcurve.range() - s = int(s) - e = int(e) - y_s = y_fcurve.evaluate(s) - y_e = y_fcurve.evaluate(e) - direction = (y_e - y_s) / abs(y_e - y_s) - existing_cons = [constraint for constraint in stride_obj.constraints if constraint.type == "FOLLOW_PATH"] - for cons in existing_cons: - stride_obj.constraints.remove(cons) - path_cons = stride_obj.constraints.new("FOLLOW_PATH") - if direction < 0: - path_cons.forward_axis = "TRACK_NEGATIVE_Y" - else: - path_cons.forward_axis = "FORWARD_Y" - path_cons.target = path - path_cons.use_curve_follow = True - path.data.path_duration = e - s - try: - path.data.animation_data.action.fcurves - except AttributeError: - path.data.keyframe_insert("eval_time", frame=0) - eval_time_fcurve = [fcurve for fcurve in path.data.animation_data.action.fcurves if fcurve.data_path == "eval_time"] - eval_time_fcurve = eval_time_fcurve[0] - totalLength = 0 - parameterization = {} - print("evaluating curve") - for t in range(s, e - 1): - if s == t: - chordLength = 0 - else: - chordLength = (y_fcurve.evaluate(t) - y_fcurve.evaluate(t + 1)) - totalLength += chordLength - parameterization[t] = totalLength - for t in range(s + 1, e - 1): - if totalLength == 0: - print("no forward motion") - parameterization[t] /= totalLength - parameterization[t] *= e - s - parameterization[e] = e - s - for t in parameterization.keys(): - eval_time_fcurve.keyframe_points.insert(frame=t, value=parameterization[t]) - y_fcurve.mute = True - print("finished path editing") - - -#Animation Stitching -#Stitches two retargeted animations together via NLA settings. -#IN: enduser_obj, a blender armature that has had two retargets applied. -def anim_stitch(context, enduser_obj): - stitch_settings = enduser_obj.data.stitch_settings - action_1 = stitch_settings.first_action - action_2 = stitch_settings.second_action - if stitch_settings.stick_bone != "": - selected_bone = enduser_obj.pose.bones[stitch_settings.stick_bone] - else: - selected_bone = enduser_obj.pose.bones[0] - scene = context.scene - TrackNamesA = enduser_obj.data.mocapNLATracks[action_1] - TrackNamesB = enduser_obj.data.mocapNLATracks[action_2] - enduser_obj.data.active_mocap = action_1 - anim_data = enduser_obj.animation_data - # add tracks for action 2 - mocapAction = bpy.data.actions[TrackNamesB.base_track] - mocapTrack = anim_data.nla_tracks.new() - mocapTrack.name = TrackNamesB.base_track - mocapStrip = mocapTrack.strips.new(TrackNamesB.base_track, stitch_settings.blend_frame, mocapAction) - mocapStrip.extrapolation = "HOLD_FORWARD" - mocapStrip.blend_in = stitch_settings.blend_amount - mocapStrip.action_frame_start += stitch_settings.second_offset - mocapStrip.action_frame_end += stitch_settings.second_offset - constraintTrack = anim_data.nla_tracks.new() - constraintTrack.name = TrackNamesB.auto_fix_track - constraintAction = bpy.data.actions[TrackNamesB.auto_fix_track] - constraintStrip = constraintTrack.strips.new(TrackNamesB.auto_fix_track, stitch_settings.blend_frame, constraintAction) - constraintStrip.extrapolation = "HOLD_FORWARD" - constraintStrip.blend_in = stitch_settings.blend_amount - userTrack = anim_data.nla_tracks.new() - userTrack.name = TrackNamesB.manual_fix_track - userAction = bpy.data.actions[TrackNamesB.manual_fix_track] - userStrip = userTrack.strips.new(TrackNamesB.manual_fix_track, stitch_settings.blend_frame, userAction) - userStrip.extrapolation = "HOLD_FORWARD" - userStrip.blend_in = stitch_settings.blend_amount - #stride bone - if enduser_obj.parent: - if enduser_obj.parent.name == "stride_bone": - stride_bone = enduser_obj.parent - stride_anim_data = stride_bone.animation_data - stride_anim_data.use_nla = True - stride_anim_data.action = None - for track in stride_anim_data.nla_tracks: - stride_anim_data.nla_tracks.remove(track) - actionATrack = stride_anim_data.nla_tracks.new() - actionATrack.name = TrackNamesA.stride_action - actionAStrip = actionATrack.strips.new(TrackNamesA.stride_action, 0, bpy.data.actions[TrackNamesA.stride_action]) - actionAStrip.extrapolation = "NOTHING" - actionBTrack = stride_anim_data.nla_tracks.new() - actionBTrack.name = TrackNamesB.stride_action - actionBStrip = actionBTrack.strips.new(TrackNamesB.stride_action, stitch_settings.blend_frame, bpy.data.actions[TrackNamesB.stride_action]) - actionBStrip.action_frame_start += stitch_settings.second_offset - actionBStrip.action_frame_end += stitch_settings.second_offset - actionBStrip.extrapolation = "NOTHING" - #we need to change the stride_bone's action to add the offset - aStrideCurves = [fcurve for fcurve in bpy.data.actions[TrackNamesA.stride_action].fcurves if fcurve.data_path == "location"] - bStrideCurves = [fcurve for fcurve in bpy.data.actions[TrackNamesB.stride_action].fcurves if fcurve.data_path == "location"] - scene.frame_set(stitch_settings.blend_frame - 1) - desired_pos = (enduser_obj.matrix_world * selected_bone.matrix.to_translation()) - scene.frame_set(stitch_settings.blend_frame) - actual_pos = (enduser_obj.matrix_world * selected_bone.matrix.to_translation()) - print(desired_pos, actual_pos) - offset = Vector(actual_pos) - Vector(desired_pos) - - for i, fcurve in enumerate(bStrideCurves): - print(offset[i], i, fcurve.array_index) - for pt in fcurve.keyframe_points: - pt.co.y -= offset[i] - pt.handle_left.y -= offset[i] - pt.handle_right.y -= offset[i] - - #actionBStrip.blend_in = stitch_settings.blend_amount - - -#Guesses setting for animation stitching via Cross Correlation -def guess_anim_stitch(context, enduser_obj): - stitch_settings = enduser_obj.data.stitch_settings - action_1 = stitch_settings.first_action - action_2 = stitch_settings.second_action - TrackNamesA = enduser_obj.data.mocapNLATracks[action_1] - TrackNamesB = enduser_obj.data.mocapNLATracks[action_2] - mocapA = bpy.data.actions[TrackNamesA.base_track] - mocapB = bpy.data.actions[TrackNamesB.base_track] - curvesA = mocapA.fcurves - curvesB = mocapB.fcurves - flm, s, data = crossCorrelationMatch(curvesA, curvesB, 10) - print("Guessed the following for start and offset: ", s, flm) - enduser_obj.data.stitch_settings.blend_frame = flm - enduser_obj.data.stitch_settings.second_offset = s diff --git a/mocap/retarget.py b/mocap/retarget.py deleted file mode 100644 index d5a17097..00000000 --- a/mocap/retarget.py +++ /dev/null @@ -1,571 +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 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> - -import bpy -from mathutils import Vector, Matrix -from math import radians -from bpy_extras.anim_utils import bake_action - - -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 - - -def createDictionary(perf_arm, end_arm): - # clear any old data - for end_bone in end_arm.bones: - for mapping in end_bone.reverseMap: - end_bone.reverseMap.remove(0) - - for perf_bone in perf_arm.bones: - #find its match and add perf_bone to the match's mapping - if perf_bone.map: - end_bone = end_arm.bones[perf_bone.map] - newMap = end_bone.reverseMap.add() - newMap.name = perf_bone.name - end_bone.foot = perf_bone.foot - - #root is the root of the enduser - root = end_arm.bones[0].name - feetBones = [bone.name for bone in perf_arm.bones if bone.foot] - return feetBones, root - - -def loadMapping(perf_arm, end_arm): - for end_bone in end_arm.bones: - #find its match and add perf_bone to the match's mapping - if end_bone.reverseMap: - for perf_bone in end_bone.reverseMap: - perf_arm.bones[perf_bone.name].map = end_bone.name - -#creation of intermediate armature -# the intermediate armature has the hierarchy of the end user, -# does not have rotation inheritance -# and bone roll is identical to the performer -# its purpose is to copy over the rotations -# easily while concentrating on the hierarchy changes - - -def createIntermediate(performer_obj, enduser_obj, root, s_frame, e_frame, scene, step): - #creates and keyframes an empty with its location - #the original position of the tail bone - #useful for storing the important data in the original motion - #i.e. using this empty to IK the chain to that pos / DEBUG - - #Simple 1to1 retarget of a bone - def singleBoneRetarget(inter_bone, perf_bone): - perf_world_rotation = perf_bone.matrix - inter_world_base_rotation = inter_bone.bone.matrix_local - inter_world_base_inv = inter_world_base_rotation.inverted() - bake_matrix = (inter_world_base_inv.to_3x3() * perf_world_rotation.to_3x3()) - return bake_matrix.to_4x4() - - #uses 1to1 and interpolation/averaging to match many to 1 retarget - def manyPerfToSingleInterRetarget(inter_bone, performer_bones_s): - retarget_matrices = [singleBoneRetarget(inter_bone, perf_bone) for perf_bone in performer_bones_s] - lerp_matrix = Matrix() - for i in range(len(retarget_matrices) - 1): - first_mat = retarget_matrices[i] - next_mat = retarget_matrices[i + 1] - lerp_matrix = first_mat.lerp(next_mat, 0.5) - return lerp_matrix - - #determines the type of hierarchy change needed and calls the - #right function - def retargetPerfToInter(inter_bone): - if inter_bone.bone.reverseMap: - perf_bone_name = inter_bone.bone.reverseMap - # 1 to many not supported yet - # then its either a many to 1 or 1 to 1 - if len(perf_bone_name) > 1: - performer_bones_s = [performer_bones[map.name] for map in perf_bone_name] - #we need to map several performance bone to a single - inter_bone.matrix_basis = manyPerfToSingleInterRetarget(inter_bone, performer_bones_s) - else: - perf_bone = performer_bones[perf_bone_name[0].name] - inter_bone.matrix_basis = singleBoneRetarget(inter_bone, perf_bone) - #Some bones have incorrect roll on the source armature, and need to be marked for fixing - if inter_bone.bone.twistFix: - inter_bone.matrix_basis *= Matrix.Rotation(radians(180), 4, "Y") - rot_mode = inter_bone.rotation_mode - if rot_mode == "QUATERNION": - inter_bone.keyframe_insert("rotation_quaternion") - elif rot_mode == "AXIS_ANGLE": - inter_bone.keyframe_insert("rotation_axis_angle") - else: - inter_bone.keyframe_insert("rotation_euler") - - #creates the intermediate armature object - inter_obj = enduser_obj.copy() - inter_obj.data = inter_obj.data.copy() # duplicate data - bpy.context.collection.objects.link(inter_obj) - inter_obj.name = "intermediate" - bpy.context.view_layer.objects.active = inter_obj - bpy.ops.object.mode_set(mode='EDIT') - #add some temporary connecting bones in case end user bones are not connected to their parents - rollDict = {} - print("creating temp bones") - for bone in inter_obj.data.edit_bones: - if not bone.use_connect and bone.parent: - if inter_obj.data.bones[bone.parent.name].reverseMap or inter_obj.data.bones[bone.name].reverseMap: - newBone = inter_obj.data.edit_bones.new("Temp") - newBone.head = bone.parent.tail - newBone.tail = bone.head - newBone.parent = bone.parent - bone.parent = newBone - bone.use_connect = True - newBone.use_connect = True - rollDict[bone.name] = bone.roll - bone.roll = 0 - #resets roll - print("retargeting to intermediate") - bpy.ops.object.mode_set(mode="OBJECT") - inter_obj.data.name = "inter_arm" - inter_arm = inter_obj.data - performer_bones = performer_obj.pose.bones - inter_bones = inter_obj.pose.bones - #clears inheritance - for inter_bone in inter_bones: - if inter_bone.bone.reverseMap: - inter_bone.bone.use_inherit_rotation = False - else: - inter_bone.bone.use_inherit_rotation = True - - for t in range(s_frame, e_frame, step): - if (t - s_frame) % 10 == 0: - print("First pass: retargeting frame {0}/{1}".format(t, e_frame - s_frame)) - scene.frame_set(t) - for bone in inter_bones: - retargetPerfToInter(bone) - - return inter_obj - -# this procedure copies the rotations over from the intermediate -# armature to the end user one. -# As the hierarchies are 1 to 1, this is a simple matter of -# copying the rotation, while keeping in mind bone roll, parenting, etc. -# TODO: Control Bones: If a certain bone is constrained in a way -# that its rotation is determined by another (a control bone) -# We should determine the right pos of the control bone. -# Scale: ? Should work but needs testing. - - -def retargetEnduser(inter_obj, enduser_obj, root, s_frame, e_frame, scene, step): - inter_bones = inter_obj.pose.bones - end_bones = enduser_obj.pose.bones - - #Basic "visual baking" function, for transferring rotations from intermediate to end user - def bakeTransform(end_bone): - src_bone = inter_bones[end_bone.name] - trg_bone = end_bone - bake_matrix = src_bone.matrix - rest_matrix = trg_bone.bone.matrix_local - - if trg_bone.parent and trg_bone.bone.use_inherit_rotation: - srcParent = src_bone.parent - if "Temp" in srcParent.name: - srcParent = srcParent.parent - parent_mat = srcParent.matrix - parent_rest = trg_bone.parent.bone.matrix_local - parent_rest_inv = parent_rest.inverted() - parent_mat_inv = parent_mat.inverted() - bake_matrix = parent_mat_inv * bake_matrix - rest_matrix = parent_rest_inv * rest_matrix - - rest_matrix_inv = rest_matrix.inverted() - bake_matrix = rest_matrix_inv * bake_matrix - end_bone.matrix_basis = bake_matrix - rot_mode = end_bone.rotation_mode - if rot_mode == "QUATERNION": - end_bone.keyframe_insert("rotation_quaternion") - elif rot_mode == "AXIS_ANGLE": - end_bone.keyframe_insert("rotation_axis_angle") - else: - end_bone.keyframe_insert("rotation_euler") - if not end_bone.bone.use_connect: - end_bone.keyframe_insert("location") - - for bone in end_bone.children: - bakeTransform(bone) - - for t in range(s_frame, e_frame, step): - if (t - s_frame) % 10 == 0: - print("Second pass: retargeting frame {0}/{1}".format(t, e_frame - s_frame)) - scene.frame_set(t) - end_bone = end_bones[root] - end_bone.location = Vector((0, 0, 0)) - end_bone.keyframe_insert("location") - bakeTransform(end_bone) - -#receives the performer feet bones as a variable -# by "feet" I mean those bones that have plants -# (they don't move, despite root moving) somewhere in the animation. - - -def copyTranslation(performer_obj, enduser_obj, perfFeet, root, s_frame, e_frame, scene, enduser_obj_mat): - - perf_bones = performer_obj.pose.bones - end_bones = enduser_obj.pose.bones - - perfRoot = perf_bones[0].name - endFeet = [perf_bones[perfBone].bone.map for perfBone in perfFeet] - locDictKeys = perfFeet + endFeet + [perfRoot] - - def tailLoc(bone): - return bone.center + (bone.vector / 2) - - #Step 1 - we create a dict that contains these keys: - #(Performer) Hips, Feet - #(End user) Feet - # where the values are their world position on each frame in range (s,e) - - locDict = {} - for key in locDictKeys: - locDict[key] = [] - - for t in range(scene.frame_start, scene.frame_end): - scene.frame_set(t) - for bone in perfFeet: - locDict[bone].append(tailLoc(perf_bones[bone])) - locDict[perfRoot].append(tailLoc(perf_bones[perfRoot])) - for bone in endFeet: - locDict[bone].append(tailLoc(end_bones[bone])) - - # now we take our locDict and analyze it. - # we need to derive all chains - - def locDeriv(key, t): - graph = locDict[key] - return graph[t + 1] - graph[t] - - # now find the plant frames, where perfFeet don't move much - - linearAvg = [] - for key in perfFeet: - for i in range(len(locDict[key]) - 1): - v = locDeriv(key, i) - if (v.length < 0.1): - hipV = locDeriv(perfRoot, i) - endV = locDeriv(perf_bones[key].bone.map, i) - #this is a plant frame. - #lets see what the original hip delta is, and the corresponding - #end bone's delta - if endV.length != 0: - linearAvg.append(hipV.length / endV.length) - - action_name = performer_obj.animation_data.action.name - #is there a stride_bone? - if "stride_bone" in bpy.data.objects: - stride_action = bpy.data.actions.new("Stride Bone " + action_name) - stride_action.use_fake_user = True - stride_bone = enduser_obj.parent - stride_bone.animation_data.action = stride_action - else: - bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.object.add() - stride_bone = bpy.context.active_object - stride_bone.name = "stride_bone" - stride_bone.location = enduser_obj_mat.to_translation() - if linearAvg: - #determine the average change in scale needed - avg = sum(linearAvg) / len(linearAvg) - else: - avg = 1 - scene.frame_set(s_frame) - initialPos = (tailLoc(perf_bones[perfRoot]) / avg) - for t in range(s_frame, e_frame): - scene.frame_set(t) - #calculate the new position, by dividing by the found ratio between performer and enduser - newTranslation = (tailLoc(perf_bones[perfRoot]) / avg) - stride_bone.location = enduser_obj_mat * (newTranslation - initialPos) - stride_bone.keyframe_insert("location") - stride_bone.animation_data.action.name = ("Stride Bone " + action_name) - - return stride_bone - - -def IKRetarget(performer_obj, enduser_obj, s_frame, e_frame, scene, step): - end_bones = enduser_obj.pose.bones - for pose_bone in end_bones: - ik_constraint = hasIKConstraint(pose_bone) - if ik_constraint: - target_is_bone = False - # set constraint target to corresponding empty if targetless, - # if not, keyframe current target to corresponding empty - perf_bone = pose_bone.bone.reverseMap[-1].name - bpy.ops.object.mode_set(mode='EDIT') - orgLocTrg = originalLocationTarget(pose_bone, enduser_obj) - bpy.ops.object.mode_set(mode='OBJECT') - if not ik_constraint.target: - ik_constraint.target = enduser_obj - ik_constraint.subtarget = pose_bone.name + "IK" - target = orgLocTrg - - # There is a target now - if ik_constraint.subtarget: - target = ik_constraint.target.pose.bones[ik_constraint.subtarget] - target.bone.use_local_location = False - target_is_bone = True - else: - target = ik_constraint.target - - # bake the correct locations for the ik target bones - for t in range(s_frame, e_frame, step): - scene.frame_set(t) - if target_is_bone: - final_loc = pose_bone.tail - target.bone.matrix_local.to_translation() - else: - final_loc = pose_bone.tail - target.location = final_loc - target.keyframe_insert("location") - ik_constraint.mute = False - scene.frame_set(s_frame) - bpy.ops.object.mode_set(mode='OBJECT') - - -def turnOffIK(enduser_obj): - end_bones = enduser_obj.pose.bones - for pose_bone in end_bones: - ik_constraint = hasIKConstraint(pose_bone) - if ik_constraint: - ik_constraint.mute = True - - -#copy the object matrixes and clear them (to be reinserted later) -def cleanAndStoreObjMat(performer_obj, enduser_obj): - perf_obj_mat = performer_obj.matrix_world.copy() - enduser_obj_mat = enduser_obj.matrix_world.copy() - zero_mat = Matrix() - performer_obj.matrix_world = zero_mat - enduser_obj.matrix_world = zero_mat - return perf_obj_mat, enduser_obj_mat - - -#restore the object matrixes after parenting the auto generated IK empties -def restoreObjMat(performer_obj, enduser_obj, perf_obj_mat, enduser_obj_mat, stride_bone, scene, s_frame): - pose_bones = enduser_obj.pose.bones - for pose_bone in pose_bones: - if pose_bone.name + "Org" in bpy.data.objects: - empty = bpy.data.objects[pose_bone.name + "Org"] - empty.parent = stride_bone - performer_obj.matrix_world = perf_obj_mat - enduser_obj.parent = stride_bone - scene.frame_set(s_frame) - enduser_obj_mat = enduser_obj_mat.to_3x3().to_4x4() * Matrix.Translation(stride_bone.matrix_world.to_translation()) - enduser_obj.matrix_world = enduser_obj_mat - - -#create (or return if exists) the related IK empty to the bone -def originalLocationTarget(end_bone, enduser_obj): - ik_bone = hasIKConstraint(end_bone).subtarget - if not ik_bone: - print("Adding IK bones for: " + end_bone.name) - newBone = enduser_obj.data.edit_bones.new(end_bone.name + "IK") - newBone.head = end_bone.tail - newBone.tail = end_bone.tail + Vector((0, 0.1, 0)) - else: - newBone = enduser_obj.pose.bones[ik_bone] - return newBone - - -#create the specified NLA setup for base animation, constraints and tweak layer. -def NLASystemInitialize(enduser_arm, context): - enduser_obj = context.active_object - NLATracks = enduser_arm.mocapNLATracks[enduser_obj.data.active_mocap] - name = NLATracks.name - anim_data = enduser_obj.animation_data - s_frame = 0 - if ("Base " + name) in bpy.data.actions: - mocapAction = bpy.data.actions[("Base " + name)] - else: - print("That retargeted anim has no base action") - anim_data.use_nla = True - for track in anim_data.nla_tracks: - anim_data.nla_tracks.remove(track) - mocapTrack = anim_data.nla_tracks.new() - mocapTrack.name = "Base " + name - NLATracks.base_track = mocapTrack.name - mocapStrip = mocapTrack.strips.new("Base " + name, s_frame, mocapAction) - constraintTrack = anim_data.nla_tracks.new() - constraintTrack.name = "Auto fixes " + name - NLATracks.auto_fix_track = constraintTrack.name - if ("Auto fixes " + name) in bpy.data.actions: - constraintAction = bpy.data.actions[("Auto fixes " + name)] - else: - constraintAction = bpy.data.actions.new("Auto fixes " + name) - constraintAction.use_fake_user = True - constraintStrip = constraintTrack.strips.new("Auto fixes " + name, s_frame, constraintAction) - constraintStrip.extrapolation = "NOTHING" - userTrack = anim_data.nla_tracks.new() - userTrack.name = "Manual fixes " + name - NLATracks.manual_fix_track = userTrack.name - if ("Manual fixes " + name) in bpy.data.actions: - userAction = bpy.data.actions[("Manual fixes " + name)] - else: - userAction = bpy.data.actions.new("Manual fixes " + name) - userAction.use_fake_user = True - userStrip = userTrack.strips.new("Manual fixes " + name, s_frame, userAction) - userStrip.extrapolation = "HOLD" - userStrip.blend_type = "ADD" - anim_data.nla_tracks.active = constraintTrack - anim_data.action_extrapolation = "NOTHING" - #set the stride_bone's action - if "stride_bone" in bpy.data.objects: - stride_bone = bpy.data.objects["stride_bone"] - if NLATracks.stride_action: - stride_bone.animation_data.action = bpy.data.actions[NLATracks.stride_action] - else: - NLATracks.stride_action = stride_bone.animation_data.action.name - stride_bone.animation_data.action.use_fake_user = True - anim_data.action = None - - -def preAdvancedRetargeting(performer_obj, enduser_obj): - createDictionary(performer_obj.data, enduser_obj.data) - bones = enduser_obj.pose.bones - map_bones = [bone for bone in bones if bone.bone.reverseMap] - perf_root = performer_obj.pose.bones[0].name - for bone in map_bones: - perf_bone = bone.bone.reverseMap[0].name - - cons = bone.constraints.new('COPY_ROTATION') - cons.name = "retargetTemp" - locks = bone.lock_rotation - cons.use_x = not locks[0] - cons.use_y = not locks[1] - cons.use_z = not locks[2] - cons.target = performer_obj - cons.subtarget = perf_bone - cons.target_space = 'WORLD' - cons.owner_space = 'WORLD' - - if (not bone.bone.use_connect) and (perf_bone != perf_root): - cons = bone.constraints.new('COPY_LOCATION') - cons.name = "retargetTemp" - cons.target = performer_obj - cons.subtarget = perf_bone - cons.use_x = True - cons.use_y = True - cons.use_z = True - cons.target_space = 'LOCAL' - cons.owner_space = 'LOCAL' - - -def prepareForBake(enduser_obj): - bones = enduser_obj.pose.bones - for bone in bones: - bone.bone.select = False - map_bones = [bone for bone in bones if bone.bone.reverseMap] - for bone in map_bones: - for cons in bone.constraints: - if "retargetTemp" in cons.name: - bone.bone.select = True - - -def cleanTempConstraints(enduser_obj): - bones = enduser_obj.pose.bones - map_bones = [bone for bone in bones if bone.bone.reverseMap] - for bone in map_bones: - for cons in bone.constraints: - if "retargetTemp" in cons.name: - bone.constraints.remove(cons) - - -#Main function that runs the retargeting sequence. -#If advanced == True, we assume constraint's were already created -def totalRetarget(performer_obj, enduser_obj, scene, s_frame, e_frame): - perf_arm = performer_obj.data - end_arm = enduser_obj.data - advanced = end_arm.advancedRetarget - step = end_arm.frameStep - enduser_obj.animation_data_create() - - try: - enduser_obj.animation_data.action = bpy.data.actions.new("temp") - enduser_obj.animation_data.action.use_fake_user = True - except: - print("no need to create new action") - - print("creating Dictionary") - feetBones, root = createDictionary(perf_arm, end_arm) - print("cleaning stuff up") - perf_obj_mat, enduser_obj_mat = cleanAndStoreObjMat(performer_obj, enduser_obj) - if not advanced: - turnOffIK(enduser_obj) - print("Creating intermediate armature (for first pass)") - inter_obj = createIntermediate(performer_obj, enduser_obj, root, s_frame, e_frame, scene, step) - print("First pass: retargeting from intermediate to end user") - retargetEnduser(inter_obj, enduser_obj, root, s_frame, e_frame, scene, step) - else: - prepareForBake(enduser_obj) - print("Retargeting pose (Advanced Retarget)") - bake_action( - bpy.context.object, - s_frame, e_frame, - action=enduser_obj.animation_data.action, - only_selected=True, - do_pose=True, - do_object=False, - frame_step=step, - ) - name = performer_obj.animation_data.action.name[:10] - #We trim the name down to 10 chars because of Action Name length maximum - enduser_obj.animation_data.action.name = "Base " + name - print("Second pass: retargeting root translation and clean up") - stride_bone = copyTranslation(performer_obj, enduser_obj, feetBones, root, s_frame, e_frame, scene, enduser_obj_mat) - if not advanced: - print("hry") - bpy.ops.object.select_all(action='DESELECT') - bpy.context.view_layer.objects.active = enduser_obj - bpy.ops.object.select_pattern(pattern=enduser_obj.name, extend=False) - IKRetarget(performer_obj, enduser_obj, s_frame, e_frame, scene, step) - bpy.ops.object.select_pattern(pattern=stride_bone.name, extend=False) - restoreObjMat(performer_obj, enduser_obj, perf_obj_mat, enduser_obj_mat, stride_bone, scene, s_frame) - bpy.ops.object.mode_set(mode='OBJECT') - if not advanced: - bpy.ops.object.select_pattern(pattern=inter_obj.name, extend=False) - bpy.ops.object.delete() - else: - cleanTempConstraints(enduser_obj) - bpy.ops.object.select_pattern(pattern=enduser_obj.name, extend=False) - - if not name in [tracks.name for tracks in end_arm.mocapNLATracks]: - NLATracks = end_arm.mocapNLATracks.add() - NLATracks.name = name - else: - NLATracks = end_arm.mocapNLATracks[name] - end_arm.active_mocap = name - print("retargeting done!") - - -def isRigAdvanced(enduser_obj): - bones = enduser_obj.pose.bones - for bone in bones: - for constraint in bone.constraints: - if constraint.type != "IK": - return True - if enduser_obj.data.animation_data: - if enduser_obj.data.animation_data.drivers: - return True |