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