Changed name of Mocap constraints to mocap fixes, for user clarity.
[blender.git] / release / scripts / startup / ui_mocap.py
index 737f3fcfa56ec922eb52d74edc10f94cb7dada24..044e13e81f58f4d5123743cc180f04a63e40e6dd 100644 (file)
@@ -40,9 +40,9 @@ from mocap_constraints import *
 
 class MocapConstraint(bpy.types.PropertyGroup):
     name = bpy.props.StringProperty(name="Name",
-        default="Mocap Constraint",
-        description="Name of Mocap Constraint",
-        update=updateConstraint)
+        default="Mocap Fix",
+        description="Name of Mocap Fix",
+        update=setConstraint)
     constrained_bone = bpy.props.StringProperty(name="Bone",
         default="",
         description="Constrained Bone",
@@ -50,58 +50,57 @@ class MocapConstraint(bpy.types.PropertyGroup):
     constrained_boneB = bpy.props.StringProperty(name="Bone (2)",
         default="",
         description="Other Constrained Bone (optional, depends on type)",
-        update=updateConstraint)
+        update=setConstraint)
     s_frame = bpy.props.IntProperty(name="S",
         default=1,
-        description="Start frame of constraint",
-        update=updateConstraint)
+        description="Start frame of Fix",
+        update=setConstraint)
     e_frame = bpy.props.IntProperty(name="E",
         default=500,
-        description="End frame of constrain",
-        update=updateConstraint)
+        description="End frame of Fix",
+        update=setConstraint)
     smooth_in = bpy.props.IntProperty(name="In",
         default=10,
         description="Amount of frames to smooth in",
-        update=updateConstraint,
+        update=setConstraint,
         min=0)
     smooth_out = bpy.props.IntProperty(name="Out",
         default=10,
         description="Amount of frames to smooth out",
-        update=updateConstraint,
+        update=setConstraint,
         min=0)
     targetMesh = bpy.props.StringProperty(name="Mesh",
         default="",
-        description="Target of Constraint - Mesh (optional, depends on type)",
-        update=updateConstraint)
+        description="Target of Fix - Mesh (optional, depends on type)",
+        update=setConstraint)
     active = bpy.props.BoolProperty(name="Active",
         default=True,
-        description="Constraint is active",
-        update=updateConstraint)
-    baked = bpy.props.BoolProperty(name="Baked / Applied",
-        default=False,
-        description="Constraint has been baked to NLA layer",
-        update=updateConstraint)
+        description="Fix is active",
+        update=setConstraint)
+    show_expanded = bpy.props.BoolProperty(name="Show Expanded",
+        default=True,
+        description="Fix is fully shown")
     targetPoint = bpy.props.FloatVectorProperty(name="Point", size=3,
         subtype="XYZ", default=(0.0, 0.0, 0.0),
-        description="Target of Constraint - Point",
-        update=updateConstraint)
-    targetDist = bpy.props.FloatProperty(name="Dist",
-        default=1,
-        description="Distance Constraint - Desired distance",
-        update=updateConstraint)
+        description="Target of Fix - Point",
+        update=setConstraint)
+    targetDist = bpy.props.FloatProperty(name="Offset",
+        default=0.0,
+        description="Distance and Floor Fixes - Desired offset",
+        update=setConstraint)
     targetSpace = bpy.props.EnumProperty(
         items=[("WORLD", "World Space", "Evaluate target in global space"),
             ("LOCAL", "Object space", "Evaluate target in object space"),
             ("constrained_boneB", "Other Bone Space", "Evaluate target in specified other bone space")],
         name="Space",
         description="In which space should Point type target be evaluated",
-        update=updateConstraint)
+        update=setConstraint)
     type = bpy.props.EnumProperty(name="Type of constraint",
         items=[("point", "Maintain Position", "Bone is at a specific point"),
             ("freeze", "Maintain Position at frame", "Bone does not move from location specified in target frame"),
             ("floor", "Stay above", "Bone does not cross specified mesh object eg floor"),
             ("distance", "Maintain distance", "Target bones maintained specified distance")],
-        description="Type of constraint",
+        description="Type of Fix",
         update=updateConstraintBoneType)
     real_constraint = bpy.props.StringProperty()
     real_constraint_bone = bpy.props.StringProperty()
@@ -148,7 +147,14 @@ def toggleIKBone(self, context):
                 if not bone.is_in_ik_chain:
                     bone.IKRetarget = False
 
+
+class MocapMapping(bpy.types.PropertyGroup):
+    name = bpy.props.StringProperty()
+
+bpy.utils.register_class(MocapMapping)
+
 bpy.types.Bone.map = bpy.props.StringProperty()
+bpy.types.Bone.reverseMap = bpy.props.CollectionProperty(type=MocapMapping)
 bpy.types.Bone.foot = bpy.props.BoolProperty(name="Foot",
     description="Marks this bone as a 'foot', which determines retargeted animation's translation",
     default=False)
@@ -157,11 +163,6 @@ bpy.types.PoseBone.IKRetarget = bpy.props.BoolProperty(name="IK",
     update=toggleIKBone, default=False)
 
 
-def hasIKConstraint(pose_bone):
-    #utility function / predicate, returns True if given bone has IK constraint
-    return ("IK" in [constraint.type for constraint in pose_bone.constraints])
-
-
 def updateIKRetarget():
     # ensures that Blender constraints and IK properties are in sync
     # currently runs when module is loaded, should run when scene is loaded
@@ -193,20 +194,22 @@ class MocapPanel(bpy.types.Panel):
         row.operator("mocap.samples", text='Samples to Beziers')
         row.operator("mocap.denoise", text='Clean noise')
         row.operator("mocap.rotate_fix", text='Fix BVH Axis Orientation')
+        row.operator("mocap.scale_fix", text='Auto scale Performer')
         row2 = self.layout.row(align=True)
         row2.operator("mocap.looper", text='Loop animation')
         row2.operator("mocap.limitdof", text='Constrain Rig')
         self.layout.label("Retargeting")
-        row3 = self.layout.row(align=True)
-        column1 = row3.column(align=True)
-        column1.label("Performer Rig")
-        column2 = row3.column(align=True)
-        column2.label("Enduser Rig")
         enduser_obj = bpy.context.active_object
         performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj]
         if enduser_obj is None or len(performer_obj) != 1:
             self.layout.label("Select performer rig and target rig (as active)")
         else:
+            self.layout.operator("mocap.guessmapping", text="Guess Hiearchy Mapping")
+            row3 = self.layout.row(align=True)
+            column1 = row3.column(align=True)
+            column1.label("Performer Rig")
+            column2 = row3.column(align=True)
+            column2.label("Enduser 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:
@@ -230,14 +233,15 @@ class MocapPanel(bpy.types.Panel):
                         else:
                             row.label(" ")
                             row.label(" ")
-                        
-
+                    mapRow = self.layout.row()
+                    mapRow.operator("mocap.savemapping", text='Save mapping')
+                    mapRow.operator("mocap.loadmapping", text='Load mapping')
                     self.layout.operator("mocap.retarget", text='RETARGET!')
 
 
 class MocapConstraintsPanel(bpy.types.Panel):
     #Motion capture constraints panel
-    bl_label = "Mocap constraints"
+    bl_label = "Mocap Fixes"
     bl_space_type = "PROPERTIES"
     bl_region_type = "WINDOW"
     bl_context = "object"
@@ -249,52 +253,122 @@ class MocapConstraintsPanel(bpy.types.Panel):
                 if context.active_object.data.name in bpy.data.armatures:
                     enduser_obj = context.active_object
                     enduser_arm = enduser_obj.data
-                    layout.operator("mocap.addconstraint")
+                    layout.operator_menu_enum("mocap.addmocapfix", "type")
+                    bakeRow = layout.row()
+                    bakeRow.operator("mocap.bakeconstraints")
+                    bakeRow.operator("mocap.unbakeconstraints")
                     layout.separator()
                     for i, m_constraint in enumerate(enduser_arm.mocap_constraints):
                         box = layout.box()
-                        box.prop(m_constraint, 'name')
-                        box.prop(m_constraint, 'type')
-                        box.prop_search(m_constraint, 'constrained_bone', enduser_obj.pose, "bones")
-                        if m_constraint.type == "distance" or m_constraint.type == "point":
-                            box.prop_search(m_constraint, 'constrained_boneB', enduser_obj.pose, "bones")
-                        frameRow = box.row()
-                        frameRow.label("Frame Range:")
-                        frameRow.prop(m_constraint, 's_frame')
-                        frameRow.prop(m_constraint, 'e_frame')
-                        smoothRow = box.row()
-                        smoothRow.label("Smoothing:")
-                        smoothRow.prop(m_constraint, 'smooth_in')
-                        smoothRow.prop(m_constraint, 'smooth_out')
-                        targetRow = box.row()
-                        targetLabelCol = targetRow.column()
-                        targetLabelCol.label("Target settings:")
-                        targetPropCol = targetRow.column()
-                        if m_constraint.type == "floor":
-                            targetPropCol.prop_search(m_constraint, 'targetMesh', bpy.data, "objects")
-                        if m_constraint.type == "point" or m_constraint.type == "freeze":
-                            box.prop(m_constraint, 'targetSpace')
-                        if m_constraint.type == "point":
-                            targetPropCol.prop(m_constraint, 'targetPoint')
-                        if m_constraint.type == "distance":
-                            targetPropCol.prop(m_constraint, 'targetDist')
-                        checkRow = box.row()
-                        checkRow.prop(m_constraint, 'active')
-                        checkRow.prop(m_constraint, 'baked')
-                        layout.operator("mocap.removeconstraint", text="Remove constraint").constraint = i
-                        layout.separator()
+                        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 m_constraint.active else'MUTE_IPO_OFF', text='', emboss=False)
+                        headerRow.operator("mocap.removeconstraint", text="", icon='X', emboss=False).constraint = i
+                        if m_constraint.show_expanded:
+                            box.separator()
+                            box.prop_search(m_constraint, 'constrained_bone', enduser_obj.pose, "bones", icon='BONE_DATA')
+                            if m_constraint.type == "distance" or m_constraint.type == "point":
+                                box.prop_search(m_constraint, 'constrained_boneB', enduser_obj.pose, "bones", icon='CONSTRAINT_BONE')
+                            frameRow = box.row()
+                            frameRow.label("Frame Range:")
+                            frameRow.prop(m_constraint, 's_frame')
+                            frameRow.prop(m_constraint, 'e_frame')
+                            smoothRow = box.row()
+                            smoothRow.label("Smoothing:")
+                            smoothRow.prop(m_constraint, 'smooth_in')
+                            smoothRow.prop(m_constraint, 'smooth_out')
+                            targetRow = box.row()
+                            targetLabelCol = targetRow.column()
+                            targetLabelCol.label("Target settings:")
+                            targetPropCol = targetRow.column()
+                            if m_constraint.type == "floor":
+                                targetPropCol.prop_search(m_constraint, 'targetMesh', bpy.data, "objects")
+                            if m_constraint.type == "point" or m_constraint.type == "freeze":
+                                box.prop(m_constraint, 'targetSpace')
+                            if m_constraint.type == "point":
+                                targetPropCol.prop(m_constraint, 'targetPoint')
+                            if m_constraint.type == "distance" or m_constraint.type == "floor":
+                                targetPropCol.prop(m_constraint, 'targetDist')
+                            layout.separator()
 
 
 class OBJECT_OT_RetargetButton(bpy.types.Operator):
+    '''Retarget animation from selected armature to active armature '''
     bl_idname = "mocap.retarget"
     bl_label = "Retargets active action from Performer to Enduser"
 
     def execute(self, context):
-        retarget.totalRetarget()
+        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]
+        scene = context.scene
+        s_frame = scene.frame_start
+        e_frame = scene.frame_end
+        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)
+        else:
+            return False
+
+
+class OBJECT_OT_SaveMappingButton(bpy.types.Operator):
+    '''Save mapping to active armature (for future retargets) '''
+    bl_idname = "mocap.savemapping"
+    bl_label = "Saves user generated mapping from Performer to Enduser"
+
+    def execute(self, context):
+        enduser_obj = bpy.context.active_object
+        performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj][0]
+        retarget.createDictionary(performer_obj.data, enduser_obj.data)
+        return {"FINISHED"}
+
+    @classmethod
+    def poll(cls, context):
+        if context.active_object:
+            activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature)
+        performer_obj = [obj for obj in context.selected_objects if obj != context.active_object]
+        if performer_obj:
+            return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature)
+        else:
+            return False
+
+
+class OBJECT_OT_LoadMappingButton(bpy.types.Operator):
+    '''Load saved mapping from active armature'''
+    bl_idname = "mocap.loadmapping"
+    bl_label = "Loads user generated mapping from Performer to Enduser"
+
+    def execute(self, context):
+        enduser_obj = bpy.context.active_object
+        performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj][0]
+        retarget.loadMapping(performer_obj.data, enduser_obj.data)
+        return {"FINISHED"}
+
+    @classmethod
+    def poll(cls, context):
+        if context.active_object:
+            activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature)
+        performer_obj = [obj for obj in context.selected_objects if obj != context.active_object]
+        if performer_obj:
+            return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature)
+        else:
+            return False
+
 
 class OBJECT_OT_ConvertSamplesButton(bpy.types.Operator):
+    '''Convert active armature's sampled keyframed to beziers'''
     bl_idname = "mocap.samples"
     bl_label = "Converts samples / simplifies keyframes to beziers"
 
@@ -302,8 +376,13 @@ class OBJECT_OT_ConvertSamplesButton(bpy.types.Operator):
         mocap_tools.fcurves_simplify()
         return {"FINISHED"}
 
+    @classmethod
+    def poll(cls, context):
+        return context.active_object.animation_data
+
 
 class OBJECT_OT_LooperButton(bpy.types.Operator):
+    '''Trim active armature's animation to a single cycle, given a cyclic animation (such as a walk cycle)'''
     bl_idname = "mocap.looper"
     bl_label = "loops animation / sampled mocap data"
 
@@ -311,8 +390,13 @@ class OBJECT_OT_LooperButton(bpy.types.Operator):
         mocap_tools.autoloop_anim()
         return {"FINISHED"}
 
+    @classmethod
+    def poll(cls, context):
+        return context.active_object.animation_data
+
 
 class OBJECT_OT_DenoiseButton(bpy.types.Operator):
+    '''Denoise active armature's animation. Good for dealing with 'bad' frames inherent in mocap animation'''
     bl_idname = "mocap.denoise"
     bl_label = "Denoises sampled mocap data "
 
@@ -320,40 +404,99 @@ class OBJECT_OT_DenoiseButton(bpy.types.Operator):
         mocap_tools.denoise_median()
         return {"FINISHED"}
 
+    @classmethod
+    def poll(cls, context):
+        return context.active_object
+
+    @classmethod
+    def poll(cls, context):
+        return context.active_object.animation_data
+
 
 class OBJECT_OT_LimitDOFButton(bpy.types.Operator):
+    '''UNIMPLEMENTED: Create limit constraints on the active armature from the selected armature's animation's range of motion'''
     bl_idname = "mocap.limitdof"
     bl_label = "Analyzes animations Max/Min DOF and adds hard/soft constraints"
 
     def execute(self, context):
         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_RotateFixArmature(bpy.types.Operator):
+    '''Realign the active armature's axis system to match Blender (Commonly needed after bvh import)'''
     bl_idname = "mocap.rotate_fix"
     bl_label = "Rotates selected armature 90 degrees (fix for bvh import)"
 
     def execute(self, context):
         mocap_tools.rotate_fix_armature(context.active_object.data)
         return {"FINISHED"}
-    
-    #def poll(self, context):
-      #  return context.active_object.data in bpy.data.armatures
+
+    @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):
+    '''Rescale selected armature to match the active animation, for convienence'''
+    bl_idname = "mocap.scale_fix"
+    bl_label = "Scales performer armature to match target armature"
+
+    def execute(self, context):
+        enduser_obj = bpy.context.active_object
+        performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj][0]
+        mocap_tools.scale_fix_armature(performer_obj, enduser_obj)
+        return {"FINISHED"}
+
+    @classmethod
+    def poll(cls, context):
+        if context.active_object:
+            activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature)
+        performer_obj = [obj for obj in context.selected_objects if obj != context.active_object]
+        if performer_obj:
+            return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature)
+        else:
+            return False
 
 
-class OBJECT_OT_AddMocapConstraint(bpy.types.Operator):
-    bl_idname = "mocap.addconstraint"
-    bl_label = "Add constraint to target armature"
+class MOCAP_OT_AddMocapFix(bpy.types.Operator):
+    '''Add a post-retarget fix - useful for fixing certain artifacts following the retarget'''
+    bl_idname = "mocap.addmocapfix"
+    bl_label = "Add Mocap Fix to target armature"
+    type = bpy.props.EnumProperty(name="Type of Fix",
+    items=[("point", "Maintain Position", "Bone is at a specific point"),
+        ("freeze", "Maintain Position at frame", "Bone does not move from location specified in target frame"),
+        ("floor", "Stay above", "Bone does not cross specified mesh object eg floor"),
+        ("distance", "Maintain distance", "Target bones maintained specified distance")],
+    description="Type of fix")
 
     def execute(self, context):
         enduser_obj = bpy.context.active_object
         enduser_arm = enduser_obj.data
         new_mcon = enduser_arm.mocap_constraints.add()
+        new_mcon.type = self.type
         return {"FINISHED"}
 
+    @classmethod
+    def poll(cls, context):
+        if context.active_object:
+            return isinstance(context.active_object.data, bpy.types.Armature)
+
 
 class OBJECT_OT_RemoveMocapConstraint(bpy.types.Operator):
+    '''Remove this post-retarget fix'''
     bl_idname = "mocap.removeconstraint"
-    bl_label = "Removes constraints from target armature"
+    bl_label = "Removes fixes from target armature"
     constraint = bpy.props.IntProperty()
 
     def execute(self, context):
@@ -368,6 +511,63 @@ class OBJECT_OT_RemoveMocapConstraint(bpy.types.Operator):
         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):
+    '''Bake all post-retarget fixes to the Retarget Fixes NLA Track'''
+    bl_idname = "mocap.bakeconstraints"
+    bl_label = "Bake all fixes to target armature"
+
+    def execute(self, context):
+        bakeConstraints(context)
+        return {"FINISHED"}
+
+    @classmethod
+    def poll(cls, context):
+        if context.active_object:
+            return isinstance(context.active_object.data, bpy.types.Armature)
+
+
+class OBJECT_OT_UnbakeMocapConstraints(bpy.types.Operator):
+    '''Unbake all post-retarget fixes - removes the baked data from the Retarget Fixes NLA Track'''
+    bl_idname = "mocap.unbakeconstraints"
+    bl_label = "Unbake all fixes to target armature"
+
+    def execute(self, context):
+        unbakeConstraints(context)
+        return {"FINISHED"}
+
+    @classmethod
+    def poll(cls, context):
+        if context.active_object:
+            return isinstance(context.active_object.data, bpy.types.Armature)
+
+
+class OBJECT_OT_GuessHierachyMapping(bpy.types.Operator):
+    '''Attemps to auto figure out hierarchy mapping'''
+    bl_idname = "mocap.guessmapping"
+    bl_label = "Attemps to auto figure out hierarchy mapping"
+
+    def execute(self, context):
+        enduser_obj = bpy.context.active_object
+        performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj][0]
+        mocap_tools.guessMapping(performer_obj, enduser_obj)
+        return {"FINISHED"}
+
+    @classmethod
+    def poll(cls, context):
+        if context.active_object:
+            activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature)
+        performer_obj = [obj for obj in context.selected_objects if obj != context.active_object]
+        if performer_obj:
+            return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature)
+        else:
+            return False
+
 
 def register():
     bpy.utils.register_module(__name__)