Changed name of Mocap constraints to mocap fixes, for user clarity.
[blender.git] / release / scripts / startup / ui_mocap.py
1 # ##### BEGIN GPL LICENSE BLOCK #####
2 #
3 #  This program is free software; you can redistribute it and/or
4 #  modify it under the terms of the GNU General Public License
5 #  as published by the Free Software Foundation; either version 2
6 #  of the License, or (at your option) any later version.
7 #
8 #  This program is distributed in the hope that it will be useful,
9 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
10 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 #  GNU General Public License for more details.
12 #
13 #  You should have received a copy of the GNU General Public License
14 #  along with this program; if not, write to the Free Software Foundation,
15 #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 #
17 # ##### END GPL LICENSE BLOCK #####
18
19 # <pep8 compliant>
20
21 import bpy
22
23 from bpy.props import *
24 from bpy import *
25 import mocap_constraints
26 import retarget
27 import mocap_tools
28 ### reloads modules (for testing purposes only)
29 from imp import reload
30 reload(mocap_constraints)
31 reload(retarget)
32 reload(mocap_tools)
33
34 from mocap_constraints import *
35
36 # MocapConstraint class
37 # Defines MocapConstraint datatype, used to add and configute mocap constraints
38 # Attached to Armature data
39
40
41 class MocapConstraint(bpy.types.PropertyGroup):
42     name = bpy.props.StringProperty(name="Name",
43         default="Mocap Fix",
44         description="Name of Mocap Fix",
45         update=setConstraint)
46     constrained_bone = bpy.props.StringProperty(name="Bone",
47         default="",
48         description="Constrained Bone",
49         update=updateConstraintBoneType)
50     constrained_boneB = bpy.props.StringProperty(name="Bone (2)",
51         default="",
52         description="Other Constrained Bone (optional, depends on type)",
53         update=setConstraint)
54     s_frame = bpy.props.IntProperty(name="S",
55         default=1,
56         description="Start frame of Fix",
57         update=setConstraint)
58     e_frame = bpy.props.IntProperty(name="E",
59         default=500,
60         description="End frame of Fix",
61         update=setConstraint)
62     smooth_in = bpy.props.IntProperty(name="In",
63         default=10,
64         description="Amount of frames to smooth in",
65         update=setConstraint,
66         min=0)
67     smooth_out = bpy.props.IntProperty(name="Out",
68         default=10,
69         description="Amount of frames to smooth out",
70         update=setConstraint,
71         min=0)
72     targetMesh = bpy.props.StringProperty(name="Mesh",
73         default="",
74         description="Target of Fix - Mesh (optional, depends on type)",
75         update=setConstraint)
76     active = bpy.props.BoolProperty(name="Active",
77         default=True,
78         description="Fix is active",
79         update=setConstraint)
80     show_expanded = bpy.props.BoolProperty(name="Show Expanded",
81         default=True,
82         description="Fix is fully shown")
83     targetPoint = bpy.props.FloatVectorProperty(name="Point", size=3,
84         subtype="XYZ", default=(0.0, 0.0, 0.0),
85         description="Target of Fix - Point",
86         update=setConstraint)
87     targetDist = bpy.props.FloatProperty(name="Offset",
88         default=0.0,
89         description="Distance and Floor Fixes - Desired offset",
90         update=setConstraint)
91     targetSpace = bpy.props.EnumProperty(
92         items=[("WORLD", "World Space", "Evaluate target in global space"),
93             ("LOCAL", "Object space", "Evaluate target in object space"),
94             ("constrained_boneB", "Other Bone Space", "Evaluate target in specified other bone space")],
95         name="Space",
96         description="In which space should Point type target be evaluated",
97         update=setConstraint)
98     type = bpy.props.EnumProperty(name="Type of constraint",
99         items=[("point", "Maintain Position", "Bone is at a specific point"),
100             ("freeze", "Maintain Position at frame", "Bone does not move from location specified in target frame"),
101             ("floor", "Stay above", "Bone does not cross specified mesh object eg floor"),
102             ("distance", "Maintain distance", "Target bones maintained specified distance")],
103         description="Type of Fix",
104         update=updateConstraintBoneType)
105     real_constraint = bpy.props.StringProperty()
106     real_constraint_bone = bpy.props.StringProperty()
107
108
109 bpy.utils.register_class(MocapConstraint)
110
111 bpy.types.Armature.mocap_constraints = bpy.props.CollectionProperty(type=MocapConstraint)
112
113 #Update function for IK functionality. Is called when IK prop checkboxes are toggled.
114
115
116 def toggleIKBone(self, context):
117     if self.IKRetarget:
118         if not self.is_in_ik_chain:
119             print(self.name + " IK toggled ON!")
120             ik = self.constraints.new('IK')
121             #ik the whole chain up to the root, excluding
122             chainLen = 0
123             for parent_bone in self.parent_recursive:
124                 chainLen += 1
125                 if hasIKConstraint(parent_bone):
126                     break
127                 deformer_children = [child for child in parent_bone.children if child.bone.use_deform]
128                 if len(deformer_children) > 1:
129                     break
130             ik.chain_count = chainLen
131             for bone in self.parent_recursive:
132                 if bone.is_in_ik_chain:
133                     bone.IKRetarget = True
134     else:
135         print(self.name + " IK toggled OFF!")
136         cnstrn_bone = False
137         if hasIKConstraint(self):
138             cnstrn_bone = self
139         elif self.is_in_ik_chain:
140             cnstrn_bone = [child for child in self.children_recursive if hasIKConstraint(child)][0]
141         if cnstrn_bone:
142             # remove constraint, and update IK retarget for all parents of cnstrn_bone up to chain_len
143             ik = [constraint for constraint in cnstrn_bone.constraints if constraint.type == "IK"][0]
144             cnstrn_bone.constraints.remove(ik)
145             cnstrn_bone.IKRetarget = False
146             for bone in cnstrn_bone.parent_recursive:
147                 if not bone.is_in_ik_chain:
148                     bone.IKRetarget = False
149
150
151 class MocapMapping(bpy.types.PropertyGroup):
152     name = bpy.props.StringProperty()
153
154 bpy.utils.register_class(MocapMapping)
155
156 bpy.types.Bone.map = bpy.props.StringProperty()
157 bpy.types.Bone.reverseMap = bpy.props.CollectionProperty(type=MocapMapping)
158 bpy.types.Bone.foot = bpy.props.BoolProperty(name="Foot",
159     description="Marks this bone as a 'foot', which determines retargeted animation's translation",
160     default=False)
161 bpy.types.PoseBone.IKRetarget = bpy.props.BoolProperty(name="IK",
162     description="Toggles IK Retargeting method for given bone",
163     update=toggleIKBone, default=False)
164
165
166 def updateIKRetarget():
167     # ensures that Blender constraints and IK properties are in sync
168     # currently runs when module is loaded, should run when scene is loaded
169     # or user adds a constraint to armature. Will be corrected in the future,
170     # once python callbacks are implemented
171     for obj in bpy.data.objects:
172         if obj.pose:
173             bones = obj.pose.bones
174             for pose_bone in bones:
175                 if pose_bone.is_in_ik_chain or hasIKConstraint(pose_bone):
176                     pose_bone.IKRetarget = True
177                 else:
178                     pose_bone.IKRetarget = False
179
180 updateIKRetarget()
181
182
183 class MocapPanel(bpy.types.Panel):
184     # Motion capture retargeting panel
185     bl_label = "Mocap tools"
186     bl_space_type = "PROPERTIES"
187     bl_region_type = "WINDOW"
188     bl_context = "object"
189
190     def draw(self, context):
191         self.layout.label("Preprocessing")
192         row = self.layout.row(align=True)
193         row.alignment = 'EXPAND'
194         row.operator("mocap.samples", text='Samples to Beziers')
195         row.operator("mocap.denoise", text='Clean noise')
196         row.operator("mocap.rotate_fix", text='Fix BVH Axis Orientation')
197         row.operator("mocap.scale_fix", text='Auto scale Performer')
198         row2 = self.layout.row(align=True)
199         row2.operator("mocap.looper", text='Loop animation')
200         row2.operator("mocap.limitdof", text='Constrain Rig')
201         self.layout.label("Retargeting")
202         enduser_obj = bpy.context.active_object
203         performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj]
204         if enduser_obj is None or len(performer_obj) != 1:
205             self.layout.label("Select performer rig and target rig (as active)")
206         else:
207             self.layout.operator("mocap.guessmapping", text="Guess Hiearchy Mapping")
208             row3 = self.layout.row(align=True)
209             column1 = row3.column(align=True)
210             column1.label("Performer Rig")
211             column2 = row3.column(align=True)
212             column2.label("Enduser Rig")
213             performer_obj = performer_obj[0]
214             if performer_obj.data and enduser_obj.data:
215                 if performer_obj.data.name in bpy.data.armatures and enduser_obj.data.name in bpy.data.armatures:
216                     perf = performer_obj.data
217                     enduser_arm = enduser_obj.data
218                     perf_pose_bones = enduser_obj.pose.bones
219                     for bone in perf.bones:
220                         row = self.layout.row()
221                         row.prop(data=bone, property='foot', text='', icon='POSE_DATA')
222                         row.label(bone.name)
223                         row.prop_search(bone, "map", enduser_arm, "bones")
224                         label_mod = "FK"
225                         if bone.map:
226                             pose_bone = perf_pose_bones[bone.map]
227                             if pose_bone.is_in_ik_chain:
228                                 label_mod = "ik chain"
229                             if hasIKConstraint(pose_bone):
230                                 label_mod = "ik end"
231                             row.prop(pose_bone, 'IKRetarget')
232                             row.label(label_mod)
233                         else:
234                             row.label(" ")
235                             row.label(" ")
236                     mapRow = self.layout.row()
237                     mapRow.operator("mocap.savemapping", text='Save mapping')
238                     mapRow.operator("mocap.loadmapping", text='Load mapping')
239                     self.layout.operator("mocap.retarget", text='RETARGET!')
240
241
242 class MocapConstraintsPanel(bpy.types.Panel):
243     #Motion capture constraints panel
244     bl_label = "Mocap Fixes"
245     bl_space_type = "PROPERTIES"
246     bl_region_type = "WINDOW"
247     bl_context = "object"
248
249     def draw(self, context):
250         layout = self.layout
251         if context.active_object:
252             if context.active_object.data:
253                 if context.active_object.data.name in bpy.data.armatures:
254                     enduser_obj = context.active_object
255                     enduser_arm = enduser_obj.data
256                     layout.operator_menu_enum("mocap.addmocapfix", "type")
257                     bakeRow = layout.row()
258                     bakeRow.operator("mocap.bakeconstraints")
259                     bakeRow.operator("mocap.unbakeconstraints")
260                     layout.separator()
261                     for i, m_constraint in enumerate(enduser_arm.mocap_constraints):
262                         box = layout.box()
263                         headerRow = box.row()
264                         headerRow.prop(m_constraint, 'show_expanded', text='', icon='TRIA_DOWN' if m_constraint.show_expanded else 'TRIA_RIGHT', emboss=False)
265                         headerRow.prop(m_constraint, 'type', text='')
266                         headerRow.prop(m_constraint, 'name', text='')
267                         headerRow.prop(m_constraint, 'active', icon='MUTE_IPO_ON' if m_constraint.active else'MUTE_IPO_OFF', text='', emboss=False)
268                         headerRow.operator("mocap.removeconstraint", text="", icon='X', emboss=False).constraint = i
269                         if m_constraint.show_expanded:
270                             box.separator()
271                             box.prop_search(m_constraint, 'constrained_bone', enduser_obj.pose, "bones", icon='BONE_DATA')
272                             if m_constraint.type == "distance" or m_constraint.type == "point":
273                                 box.prop_search(m_constraint, 'constrained_boneB', enduser_obj.pose, "bones", icon='CONSTRAINT_BONE')
274                             frameRow = box.row()
275                             frameRow.label("Frame Range:")
276                             frameRow.prop(m_constraint, 's_frame')
277                             frameRow.prop(m_constraint, 'e_frame')
278                             smoothRow = box.row()
279                             smoothRow.label("Smoothing:")
280                             smoothRow.prop(m_constraint, 'smooth_in')
281                             smoothRow.prop(m_constraint, 'smooth_out')
282                             targetRow = box.row()
283                             targetLabelCol = targetRow.column()
284                             targetLabelCol.label("Target settings:")
285                             targetPropCol = targetRow.column()
286                             if m_constraint.type == "floor":
287                                 targetPropCol.prop_search(m_constraint, 'targetMesh', bpy.data, "objects")
288                             if m_constraint.type == "point" or m_constraint.type == "freeze":
289                                 box.prop(m_constraint, 'targetSpace')
290                             if m_constraint.type == "point":
291                                 targetPropCol.prop(m_constraint, 'targetPoint')
292                             if m_constraint.type == "distance" or m_constraint.type == "floor":
293                                 targetPropCol.prop(m_constraint, 'targetDist')
294                             layout.separator()
295
296
297 class OBJECT_OT_RetargetButton(bpy.types.Operator):
298     '''Retarget animation from selected armature to active armature '''
299     bl_idname = "mocap.retarget"
300     bl_label = "Retargets active action from Performer to Enduser"
301
302     def execute(self, context):
303         enduser_obj = context.active_object
304         performer_obj = [obj for obj in context.selected_objects if obj != enduser_obj]
305         if enduser_obj is None or len(performer_obj) != 1:
306             print("Need active and selected armatures")
307         else:
308             performer_obj = performer_obj[0]
309         scene = context.scene
310         s_frame = scene.frame_start
311         e_frame = scene.frame_end
312         retarget.totalRetarget(performer_obj, enduser_obj, scene, s_frame, e_frame)
313         return {"FINISHED"}
314
315     @classmethod
316     def poll(cls, context):
317         if context.active_object:
318             activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature)
319         performer_obj = [obj for obj in context.selected_objects if obj != context.active_object]
320         if performer_obj:
321             return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature)
322         else:
323             return False
324
325
326 class OBJECT_OT_SaveMappingButton(bpy.types.Operator):
327     '''Save mapping to active armature (for future retargets) '''
328     bl_idname = "mocap.savemapping"
329     bl_label = "Saves user generated mapping from Performer to Enduser"
330
331     def execute(self, context):
332         enduser_obj = bpy.context.active_object
333         performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj][0]
334         retarget.createDictionary(performer_obj.data, enduser_obj.data)
335         return {"FINISHED"}
336
337     @classmethod
338     def poll(cls, context):
339         if context.active_object:
340             activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature)
341         performer_obj = [obj for obj in context.selected_objects if obj != context.active_object]
342         if performer_obj:
343             return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature)
344         else:
345             return False
346
347
348 class OBJECT_OT_LoadMappingButton(bpy.types.Operator):
349     '''Load saved mapping from active armature'''
350     bl_idname = "mocap.loadmapping"
351     bl_label = "Loads user generated mapping from Performer to Enduser"
352
353     def execute(self, context):
354         enduser_obj = bpy.context.active_object
355         performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj][0]
356         retarget.loadMapping(performer_obj.data, enduser_obj.data)
357         return {"FINISHED"}
358
359     @classmethod
360     def poll(cls, context):
361         if context.active_object:
362             activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature)
363         performer_obj = [obj for obj in context.selected_objects if obj != context.active_object]
364         if performer_obj:
365             return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature)
366         else:
367             return False
368
369
370 class OBJECT_OT_ConvertSamplesButton(bpy.types.Operator):
371     '''Convert active armature's sampled keyframed to beziers'''
372     bl_idname = "mocap.samples"
373     bl_label = "Converts samples / simplifies keyframes to beziers"
374
375     def execute(self, context):
376         mocap_tools.fcurves_simplify()
377         return {"FINISHED"}
378
379     @classmethod
380     def poll(cls, context):
381         return context.active_object.animation_data
382
383
384 class OBJECT_OT_LooperButton(bpy.types.Operator):
385     '''Trim active armature's animation to a single cycle, given a cyclic animation (such as a walk cycle)'''
386     bl_idname = "mocap.looper"
387     bl_label = "loops animation / sampled mocap data"
388
389     def execute(self, context):
390         mocap_tools.autoloop_anim()
391         return {"FINISHED"}
392
393     @classmethod
394     def poll(cls, context):
395         return context.active_object.animation_data
396
397
398 class OBJECT_OT_DenoiseButton(bpy.types.Operator):
399     '''Denoise active armature's animation. Good for dealing with 'bad' frames inherent in mocap animation'''
400     bl_idname = "mocap.denoise"
401     bl_label = "Denoises sampled mocap data "
402
403     def execute(self, context):
404         mocap_tools.denoise_median()
405         return {"FINISHED"}
406
407     @classmethod
408     def poll(cls, context):
409         return context.active_object
410
411     @classmethod
412     def poll(cls, context):
413         return context.active_object.animation_data
414
415
416 class OBJECT_OT_LimitDOFButton(bpy.types.Operator):
417     '''UNIMPLEMENTED: Create limit constraints on the active armature from the selected armature's animation's range of motion'''
418     bl_idname = "mocap.limitdof"
419     bl_label = "Analyzes animations Max/Min DOF and adds hard/soft constraints"
420
421     def execute(self, context):
422         return {"FINISHED"}
423
424     @classmethod
425     def poll(cls, context):
426         if context.active_object:
427             activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature)
428         performer_obj = [obj for obj in context.selected_objects if obj != context.active_object]
429         if performer_obj:
430             return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature)
431         else:
432             return False
433
434
435 class OBJECT_OT_RotateFixArmature(bpy.types.Operator):
436     '''Realign the active armature's axis system to match Blender (Commonly needed after bvh import)'''
437     bl_idname = "mocap.rotate_fix"
438     bl_label = "Rotates selected armature 90 degrees (fix for bvh import)"
439
440     def execute(self, context):
441         mocap_tools.rotate_fix_armature(context.active_object.data)
442         return {"FINISHED"}
443
444     @classmethod
445     def poll(cls, context):
446         if context.active_object:
447             return isinstance(context.active_object.data, bpy.types.Armature)
448
449
450 class OBJECT_OT_ScaleFixArmature(bpy.types.Operator):
451     '''Rescale selected armature to match the active animation, for convienence'''
452     bl_idname = "mocap.scale_fix"
453     bl_label = "Scales performer armature to match target armature"
454
455     def execute(self, context):
456         enduser_obj = bpy.context.active_object
457         performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj][0]
458         mocap_tools.scale_fix_armature(performer_obj, enduser_obj)
459         return {"FINISHED"}
460
461     @classmethod
462     def poll(cls, context):
463         if context.active_object:
464             activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature)
465         performer_obj = [obj for obj in context.selected_objects if obj != context.active_object]
466         if performer_obj:
467             return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature)
468         else:
469             return False
470
471
472 class MOCAP_OT_AddMocapFix(bpy.types.Operator):
473     '''Add a post-retarget fix - useful for fixing certain artifacts following the retarget'''
474     bl_idname = "mocap.addmocapfix"
475     bl_label = "Add Mocap Fix to target armature"
476     type = bpy.props.EnumProperty(name="Type of Fix",
477     items=[("point", "Maintain Position", "Bone is at a specific point"),
478         ("freeze", "Maintain Position at frame", "Bone does not move from location specified in target frame"),
479         ("floor", "Stay above", "Bone does not cross specified mesh object eg floor"),
480         ("distance", "Maintain distance", "Target bones maintained specified distance")],
481     description="Type of fix")
482
483     def execute(self, context):
484         enduser_obj = bpy.context.active_object
485         enduser_arm = enduser_obj.data
486         new_mcon = enduser_arm.mocap_constraints.add()
487         new_mcon.type = self.type
488         return {"FINISHED"}
489
490     @classmethod
491     def poll(cls, context):
492         if context.active_object:
493             return isinstance(context.active_object.data, bpy.types.Armature)
494
495
496 class OBJECT_OT_RemoveMocapConstraint(bpy.types.Operator):
497     '''Remove this post-retarget fix'''
498     bl_idname = "mocap.removeconstraint"
499     bl_label = "Removes fixes from target armature"
500     constraint = bpy.props.IntProperty()
501
502     def execute(self, context):
503         enduser_obj = bpy.context.active_object
504         enduser_arm = enduser_obj.data
505         m_constraints = enduser_arm.mocap_constraints
506         m_constraint = m_constraints[self.constraint]
507         if m_constraint.real_constraint:
508             bone = enduser_obj.pose.bones[m_constraint.real_constraint_bone]
509             cons_obj = getConsObj(bone)
510             removeConstraint(m_constraint, cons_obj)
511         m_constraints.remove(self.constraint)
512         return {"FINISHED"}
513
514     @classmethod
515     def poll(cls, context):
516         if context.active_object:
517             return isinstance(context.active_object.data, bpy.types.Armature)
518
519
520 class OBJECT_OT_BakeMocapConstraints(bpy.types.Operator):
521     '''Bake all post-retarget fixes to the Retarget Fixes NLA Track'''
522     bl_idname = "mocap.bakeconstraints"
523     bl_label = "Bake all fixes to target armature"
524
525     def execute(self, context):
526         bakeConstraints(context)
527         return {"FINISHED"}
528
529     @classmethod
530     def poll(cls, context):
531         if context.active_object:
532             return isinstance(context.active_object.data, bpy.types.Armature)
533
534
535 class OBJECT_OT_UnbakeMocapConstraints(bpy.types.Operator):
536     '''Unbake all post-retarget fixes - removes the baked data from the Retarget Fixes NLA Track'''
537     bl_idname = "mocap.unbakeconstraints"
538     bl_label = "Unbake all fixes to target armature"
539
540     def execute(self, context):
541         unbakeConstraints(context)
542         return {"FINISHED"}
543
544     @classmethod
545     def poll(cls, context):
546         if context.active_object:
547             return isinstance(context.active_object.data, bpy.types.Armature)
548
549
550 class OBJECT_OT_GuessHierachyMapping(bpy.types.Operator):
551     '''Attemps to auto figure out hierarchy mapping'''
552     bl_idname = "mocap.guessmapping"
553     bl_label = "Attemps to auto figure out hierarchy mapping"
554
555     def execute(self, context):
556         enduser_obj = bpy.context.active_object
557         performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj][0]
558         mocap_tools.guessMapping(performer_obj, enduser_obj)
559         return {"FINISHED"}
560
561     @classmethod
562     def poll(cls, context):
563         if context.active_object:
564             activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature)
565         performer_obj = [obj for obj in context.selected_objects if obj != context.active_object]
566         if performer_obj:
567             return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature)
568         else:
569             return False
570
571
572 def register():
573     bpy.utils.register_module(__name__)
574
575
576 def unregister():
577     bpy.utils.unregister_module(__name__)
578
579 if __name__ == "__main__":
580     register()