-
Notifications
You must be signed in to change notification settings - Fork 1
/
face_tools.py
1313 lines (1079 loc) · 57.4 KB
/
face_tools.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env mayapy
# encoding: utf-8
"""
Moonbot Face Tools
Created by Chris Lesage on 2016-08.
Copyright (c) 2016 Moonbot Studios. All rights reserved.
A set of tools and scripts for generating a face rig.
The face skeleton and blendshapes are driven by poses which are
the combined delta positions of locators and attributes.
"""
#TODO: Outstanding Issues
'''
- I have to reconsider the joints vs. blendshape balance with the new Shape Editor tools. Blendshapes are way more powerful in 2016.5.
- A way to visualize how many joints, how many blendshapes and how many poses a rig has would be nice.
- A way to add/remove poses on the fly, after having built.
- Include scaling in the POSELOCs, but I'll have to make it multiplication instead of addition.
- When we upgrade past Maya 2016.5, the combinationShape node will become available.
- Create the face template via script. (LOW PRIORITY. It is fairly static, so importing it as an asset is totally feasible.)
- Connect the blendshapes via script. (This depends on them being named to match the poses in the map.)
- It would be great to have macros, so "wide mouth" was a combo of "left wide mouth" and "right wide mouth", rather than duplicating poses.
- Fix the issue where overriding a pose doesn't stop it from piping out a value to the blendshape.
- The solution is to connect it after the multiplyDivide node that is driven by the overrides, not directly from the pose.
- I have **Macros, Overrides and Blendshapes**. It seems like there might be some conceptual overlap here...
SYMMETRY:
- Connect the right side POSLOC's in symmetry. Then, if I want to break that, I can, if I need asymmetry.
- AND create macros, so that left+right = the full pose, which is also mappable and breakable if I need to offset.
- That way, you edit 3 poses, by editing one.
'''
#SCRIPT_DIR = os.path.dirname('C:/Users/clesage/dev/tech/maya/scripts/moonbot/butterfly/')
#FACE_CTLS_FILE = os.path.join(SCRIPT_DIR, 'ctrlsface.data')
#FACE_CTLS = envtools.load_dict(FACE_CTLS_FILE)
__version__ = '0.4'
import traceback
from PySide import QtCore
from PySide import QtGui
#TODO: Integrate this so the tool can be maintained more easily. C:\moonbot\tools\tech\maya\scripts\mayaUtils\qt.py
try:
from shiboken import wrapInstance
except:
# future proofing for Maya 2017.
from shiboken2 import wrapInstance
import pymel.core as pm
import pymel.core.datatypes as dt
from pymel.util.path import path
import maya.cmds as mc
import maya.OpenMayaUI as omui
import sys
import os
import envtools
import json
from functools import wraps
DEBUG = True
DRYRUN = False
##################################
###### PySide UI Functions #######
##################################
def undo(func):
""" Puts the wrapped `func` into a single Maya Undo action, then
undoes it when the function enters the finally: block
from schworer Github """
@wraps(func)
def _undofunc(*args, **kwargs):
try:
# start an undo chunk
mc.undoInfo(ock=True)
return func(*args, **kwargs)
finally:
# after calling the func, end the undo chunk
mc.undoInfo(cck=True)
return _undofunc
def maya_main_window():
''' Return the Maya main window widget as a Python object '''
main_window_ptr = omui.MQtUtil.mainWindow()
return wrapInstance(long(main_window_ptr), QtGui.QWidget)
class FaceTools(QtGui.QDialog):
def __init__(self, parent=maya_main_window()):
super(FaceTools, self).__init__(parent)
def create(self):
''' Create the UI '''
self.setWindowTitle('Face Tools v' + __version__)
self.setWindowFlags(QtCore.Qt.Tool)
self.posemapConnected = False
#TODO: Solve this path relatively
self.iconPath = 'C:/Users/clesage/dev/tech/maya/scripts/moonbot/butterfly/icons/faceTools/'
self.templates = 'C:/Users/clesage/dev/tech/maya/scripts/moonbot/butterfly/templates/'
self.create_controls()
self.create_layout()
def create_controls(self):
''' Create the widgets and signals for the dialog '''
def make_icon_button(label, pressFunction, iconName, iconWidth, width=120, bgCol='666'):
#TODO: Find a better way to get this path
button = QtGui.QPushButton(label)
button.setStyleSheet('padding:2px; background-color: #{}; color: #eee; text-align:left;'.format(bgCol))
button.clicked.connect(pressFunction)
button.setIcon( QtGui.QIcon(self.iconPath + iconName) )
button.setIconSize(QtCore.QSize(iconWidth,iconWidth))
button.setMinimumWidth(width*0.5)
button.setMaximumWidth(width+40)
return button
### Rigging & Skeleton Section ###
self.importSkeletonBtn = make_icon_button('Import Joint Template', self.importSkeletonBtn_pressed, 'importFile16.png', 16, 150)
self.importControlsBtn = make_icon_button('Import Face Controls', self.importControlsBtn_pressed, 'importFile16.png', 16, 150)
self.connectFaceMapBtn = make_icon_button('Connect Face Map', self.connectFaceMapBtn_pressed, 'connect.png', 16, 120)
self.faceMapPath = QtGui.QLineEdit('../path/to/character.posemap')
self.faceMapPath.editingFinished.connect(self.facemap_path_changed)
#self.faceMapPath.textChanged.connect(self.facemap_path_changed)
self.zoneList = QtGui.QListWidget()
#TODO: Get the zones from the loaded face map.
self.zoneList.addItems([
'Head',
'Brow',
'Eye',
'Nose',
'Cheeks',
'Mouth',
])
self.zoneList.setCurrentRow(0)
self.zoneList.setMaximumHeight(60)
self.zoneList.setMaximumHeight(90)
self.zoneList.currentItemChanged.connect(self.zone_selection_changed)
self.addToZoneBtn = QtGui.QPushButton('Add to Zone')
self.addToZoneBtn.clicked.connect(self.addToZoneBtn_pressed)
self.removeFromZoneBtn = QtGui.QPushButton('Remove from Zone')
self.removeFromZoneBtn.clicked.connect(self.removefromZoneBtn_pressed)
### Building Section ###
self.buildRigBtn = QtGui.QPushButton('Build Face Rig')
self.buildRigBtn.setFixedHeight(30)
self.buildRigBtn.clicked.connect(self.buildRigBtn_pressed)
self.connectBlendsBtn = QtGui.QPushButton('Connect Blendshapes')
self.connectBlendsBtn.setFixedHeight(30)
self.connectBlendsBtn.clicked.connect(self.connectBlendsBtn_pressed)
### Pose Editing Section ###
self.poseEditBtn1 = make_icon_button('Copy', self.poseEditBtn1_pressed, 'copyPose.png', 24, 120, '5d6a5d')
self.poseEditBtn2 = make_icon_button('Paste', self.poseEditBtn2_pressed, 'pastePose.png', 24, 120, '6a5d5d')
self.poseEditBtn3 = make_icon_button('Combine', self.poseEditBtn3_pressed, 'combinePoses.png', 24, 120)
self.poseEditBtn4 = make_icon_button('Mirror', self.poseEditBtn4_pressed, 'mirrorPose.png', 24, 120)
self.poseEditBtn5 = make_icon_button('Split Mirror', self.poseEditBtn5_pressed, 'splitMirrorPose.png', 24, 120)
self.poseEditBtn6 = make_icon_button('Reset', self.poseEditBtn6_pressed, 'resetPose.png', 24, 120)
self.poseEditBtn7 = make_icon_button('Import (Add)', self.poseEditBtn7_pressed, 'importPoses.png', 24, 120)
self.poseEditBtn8 = make_icon_button('Import (Replace)', self.poseEditBtn8_pressed, 'importPoses.png', 24, 120)
self.poseEditBtn9 = make_icon_button('Save All', self.poseEditBtn9_pressed, 'saveAllPoses.png', 24, 120)
### Cleaning Up Section ###
self.cleanUpBtn = QtGui.QPushButton('Clean Up Rig (destructive!)')
self.cleanUpBtn.clicked.connect(self.cleanUpBtn_pressed)
self.cleanUpBtn.setStyleSheet('padding:7px; text-align:center; background-color: #6a5d5d; color: #eee;')
self.cleanUpBtn.setFixedWidth(170)
### Merging to Body Section ###
self.mergeBodyBtn = QtGui.QPushButton('Create blendshape rigspecs')
self.mergeBodyBtn.clicked.connect(self.mergeBodyBtn_pressed)
def create_layout(self):
''' Create the layouts and add widgets '''
mainLayout = QtGui.QVBoxLayout()
mainLayout.setContentsMargins(*[6]*4)
flatStyle = False # False draws a border around the whole section
groupFont = QtGui.QFont('Helvetica Neue', 10, QtGui.QFont.Bold)
groupPadding = 4 # padding at top and bottom of each section
groupSpacing = 4 # the space between each section
rigGroup = QtGui.QGroupBox('Rigging and Skeleton')
rigGroup.setFlat(flatStyle)
rigGroup.setFont(groupFont)
riggingLayout = QtGui.QVBoxLayout()
riggingLayout.setContentsMargins(*[2]*4)
riggingLayout.addSpacing(groupPadding)
buttonRow = QtGui.QHBoxLayout()
buttonRow.setAlignment(QtCore.Qt.AlignLeft)
buttonRow.addWidget(self.importSkeletonBtn)
buttonRow.addWidget(self.importControlsBtn)
riggingLayout.addLayout(buttonRow)
buttonRow = QtGui.QHBoxLayout()
buttonRow.addWidget(self.connectFaceMapBtn)
buttonRow.addWidget(self.faceMapPath)
riggingLayout.addLayout(buttonRow)
riggingLayout.addSpacing(groupPadding)
rigGroup.setLayout(riggingLayout)
zoneGroup = QtGui.QGroupBox('Face Zones')
zoneGroup.setFlat(flatStyle)
zoneGroup.setFont(groupFont)
zoneLayout = QtGui.QVBoxLayout()
zoneLayout.setContentsMargins(*[2]*4)
zoneLayout.addSpacing(groupPadding)
zoneGroupLayout = QtGui.QHBoxLayout()
zoneGroupLayout.addWidget(self.zoneList)
zoneButtonLayout = QtGui.QVBoxLayout()
zoneButtonLayout.setAlignment(QtCore.Qt.AlignLeft)
zoneButtonLayout.setAlignment(QtCore.Qt.AlignBottom)
zoneButtonLayout.setContentsMargins(*[2]*4)
zoneButtonLayout.addWidget(self.addToZoneBtn)
zoneButtonLayout.addWidget(self.removeFromZoneBtn)
zoneGroupLayout.addLayout(zoneButtonLayout)
zoneLayout.addLayout(zoneGroupLayout)
zoneLayout.addSpacing(groupPadding)
zoneGroup.setLayout(zoneLayout)
buildGroup = QtGui.QGroupBox('Build')
buildGroup.setFlat(flatStyle)
buildGroup.setFont(groupFont)
buildingOptionsLayout = QtGui.QHBoxLayout()
buildingOptionsLayout.setContentsMargins(*[2]*4)
buildingLayout = QtGui.QVBoxLayout()
buildingLayout.setContentsMargins(*[2]*4)
buildingLayout.addSpacing(groupPadding)
buttonRow = QtGui.QHBoxLayout()
buttonRow.addWidget(self.buildRigBtn)
buttonRow.addWidget(self.connectBlendsBtn)
buildingLayout.addLayout(buildingOptionsLayout)
buildingLayout.addLayout(buttonRow)
buildingLayout.addSpacing(groupPadding)
buildGroup.setLayout(buildingLayout)
poseGroup = QtGui.QGroupBox('Edit Poses')
poseGroup.setFlat(flatStyle)
poseGroup.setFont(groupFont)
posingLayout = QtGui.QGridLayout()
posingLayout.addWidget(self.poseEditBtn1, 0, 0)
posingLayout.addWidget(self.poseEditBtn2, 0, 1)
posingLayout.addWidget(self.poseEditBtn3, 0, 2)
posingLayout.addWidget(self.poseEditBtn4)
posingLayout.addWidget(self.poseEditBtn5)
posingLayout.addWidget(self.poseEditBtn6)
posingLayout.addWidget(self.poseEditBtn7)
posingLayout.addWidget(self.poseEditBtn8)
posingLayout.addWidget(self.poseEditBtn9)
poseGroup.setLayout(posingLayout)
cleanGroup = QtGui.QGroupBox('Optimize Rig')
cleanGroup.setFlat(flatStyle)
cleanGroup.setFont(groupFont)
cleaningLayout = QtGui.QVBoxLayout()
cleaningLayout.setAlignment(QtCore.Qt.AlignCenter)
cleaningLayout.setContentsMargins(*[2]*4)
cleaningLayout.addSpacing(groupPadding)
cleaningLayout.addWidget(self.cleanUpBtn)
cleaningLayout.addSpacing(groupPadding)
cleanGroup.setLayout(cleaningLayout)
'''
# These buttons do nothing yet.
mergeGroup = QtGui.QGroupBox('Merge to Body')
mergeGroup.setFlat(flatStyle)
mergeGroup.setFont(groupFont)
mergingLayout = QtGui.QVBoxLayout()
mergingLayout.setContentsMargins(*[2]*4)
mergingLayout.addSpacing(groupPadding)
mergingLayout.addWidget(self.mergeBodyBtn)
mergingLayout.addSpacing(groupPadding)
mergeGroup.setLayout(mergingLayout)
'''
pixmap = QtGui.QPixmap(self.iconPath + 'face.png')
lbl = QtGui.QLabel()
lbl.setAlignment(QtCore.Qt.AlignRight)
lbl.setStyleSheet('padding-right:20px;')
lbl.setPixmap(pixmap)
mainLayout.addWidget(lbl)
mainLayout.addWidget(rigGroup)
mainLayout.addSpacing(groupSpacing)
mainLayout.addWidget(zoneGroup)
mainLayout.addSpacing(groupSpacing)
mainLayout.addWidget(buildGroup)
mainLayout.addSpacing(groupSpacing)
mainLayout.addWidget(poseGroup)
mainLayout.addSpacing(groupSpacing)
mainLayout.addWidget(cleanGroup)
mainLayout.addSpacing(groupSpacing)
# mainLayout.addWidget(mergeGroup)
mainLayout.addStretch()
self.setLayout(mainLayout)
#--------------------------------------------------------------------------
# SLOTS
#--------------------------------------------------------------------------
def connectFaceMapBtn_pressed(self):
sender = self.sender()
templatePath = self.templates
print('{} pressed. importing [{}]'.format(sender.text(), templatePath))
result = pm.fileDialog2(cc='Cancel', cap='Load a template Pose Map',
ff='JSON (*.json)',
fm=1, okc='Select', dir=templatePath)
if result is not None:
#TODO: Add some kind of validation that this is a posemap file
#TODO: IMPORTANT: Add this path to a metanode so it is stored in the scene.
self.faceMapPath.setText(result[0])
print('Face Map set to {}'.format(result[0]))
self.posemapConnected = True
self.connectFaceMapBtn.setIcon( QtGui.QIcon(self.iconPath + 'connected.png') )
def importControlsBtn_pressed(self):
sender = self.sender()
templatePath = self.templates
print('{} pressed. importing [{}]'.format(sender.text(), templatePath))
result = pm.fileDialog2(cc='Cancel', cap='Import face controls template.',
ff='Maya Scenes (*.ma);;Maya Scenes (*.mb)',
fm=1, okc='Import into scene', dir=templatePath)
if result is not None:
#TODO: Add some kind of validation that this is a template file
print('Imported {}'.format(result[0]))
for f in result:
pm.importFile(path(f), defaultNamespace=False)
def importSkeletonBtn_pressed(self):
sender = self.sender()
print('{} pressed.'.format(sender.text()))
result = pm.fileDialog2(cc='Cancel', cap='Import a template face skeleton',
ff='Maya Scenes (*.ma);;Maya Scenes (*.mb);;All Files (*.*)',
fm=1, okc='Select', dir=os.path.dirname(pm.sceneName()))
if result is not None:
#TODO: Add some kind of validation
print('Importing {}'.format(result[0]))
def addToZoneBtn_pressed(self):
sender = self.sender()
currentZone = self.zoneList.currentItem().text().lower()
print('Adding selection to [{}] zone'.format(currentZone))
add_to_zone(pm.selected(type='transform'), currentZone)
def removefromZoneBtn_pressed(self):
sender = self.sender()
currentZone = self.zoneList.currentItem().text().lower()
print('Removing selection from [{}] zone'.format(currentZone))
remove_from_zone(pm.selected(type='transform'), currentZone)
def buildRigBtn_pressed(self):
sender = self.sender()
#print('{} pressed'.format(sender.text()))
if self.posemapConnected:
poseMapFile = 'C:\\Users\\clesage\\dev\\tech\\maya\\scripts\\moonbot\\butterfly\\templates\\test_face_map.json'
poseMapFile = self.faceMapPath.text()
build_face(poseMapFile)
else:
print('The pose map must be connected before building.')
def connectBlendsBtn_pressed(self):
sender = self.sender()
#print('{} pressed'.format(sender.text()))
#TODO: Pass in blendshapes
auto_connect_blendshape(None)
def poseEditBtn1_pressed(self):
''' Copy Pose '''
sender = self.sender()
print('{} pressed'.format(sender.text()))
def poseEditBtn2_pressed(self):
''' Paste Pose '''
sender = self.sender()
print('{} pressed'.format(sender.text()))
def poseEditBtn3_pressed(self):
''' Combine Poses '''
sender = self.sender()
print('{} pressed'.format(sender.text()))
def poseEditBtn4_pressed(self):
''' Mirror Pose '''
sender = self.sender()
print('{} pressed'.format(sender.text()))
def poseEditBtn5_pressed(self):
''' Split Mirror Pose '''
sender = self.sender()
print('{} pressed'.format(sender.text()))
def poseEditBtn6_pressed(self):
''' Reset Pose to Neutral/Zero '''
sender = self.sender()
print('{} pressed'.format(sender.text()))
def poseEditBtn7_pressed(self):
''' Import Poses From File - ADD MODE - Replace only the poses in the file, and leave the rest alone. '''
sender = self.sender()
print('{} pressed. Importing poses from file.'.format(sender.text()))
#TODO: Use a scene-relative path to store these poses. Not in templates
templatePath = self.templates
print('{} pressed. importing [{}]'.format(sender.text(), templatePath))
result = pm.fileDialog2(cc='Cancel', cap='Import Poses',
ff='JSON (*.json)',
fm=1, okc='Choose', dir=templatePath)
if result is not None:
apply_poses_from_json(result[0])
def poseEditBtn8_pressed(self):
''' Import Poses From File - REPLACE MODE - Replace all the poses. If a pose isn't in the file, set to init. '''
sender = self.sender()
print('{} pressed. Importing poses from file.'.format(sender.text()))
#TODO: Use a scene-relative path to store these poses. Not in templates
templatePath = self.templates
print('{} pressed. importing [{}]'.format(sender.text(), templatePath))
result = pm.fileDialog2(cc='Cancel', cap='Import Poses',
ff='JSON (*.json)',
fm=1, okc='Choose', dir=templatePath)
if result is not None:
apply_poses_from_json(result[0])
def poseEditBtn9_pressed(self):
''' Save All Poses To File '''
sender = self.sender()
templatePath = self.templates
print('{} pressed. importing [{}]'.format(sender.text(), templatePath))
#TODO: Figure out how to save a file. This fileDialog looks for an existing file.
result = pm.fileDialog2(cc='Cancel', cap='Save all poses',
ff='JSON (*.json)',
fm=1, okc='Save', dir=templatePath)
if result is not None:
save_all_poses(result[0])
def cleanUpBtn_pressed(self):
sender = self.sender()
#print('{} pressed'.format(sender.text()))
result = pm.confirmDialog(
title='Optimize the face rig',
message='Are you ready to clean the face rig?\n\nThis removes extra utility nodes, and removes any empty face poses. You will lose the ability to edit those empty poses. The rig will speed up dramatically.',
button=['OK', 'Cancel'],
defaultButton='Cancel',
cancelButton='Cancel',
dismissString='Cancel')
if result == 'OK':
disconnect_poselocs()
clean_face_rig()
return True
else:
print 'cancelling. Face was not optimized.'
return False
def mergeBodyBtn_pressed(self):
sender = self.sender()
print('{} pressed'.format(sender.text()))
def facemap_path_changed(self):
sender = self.sender()
print('Text changed to "{}"'.format(sender.text()))
def zone_selection_changed(self, current, previous):
pass
#print('Zone changed from [{}] to [{}]'.format(previous.text(), current.text()))
##################################
##### Face Rigging Functions #####
##################################
def import_json_dict(jsonFile, validation):
''' open a json file. Look for a validation key in the dictionary to verify the correct type of data. '''
with open(jsonFile, 'r') as f:
facejson = json.load(f)
validationKey = facejson.get('validation', None)
if validationKey == validation:
return facejson
else:
pm.warning('This json does not appear to contain {} information.'.format(validation))
return False
def export_json_dict(jsonFile, dictData, validation):
''' save a json file. Write a validation key in the dictionary to verify the correct type of data. '''
with open(jsonFile, 'w') as f:
json.dump(dictData, f, sort_keys=False, indent=4)
print('Exported {} to {}'.format(validation, jsonFile))
#TODO: Consider how this functionality works. I haven't used it once.
# Pose Attributes can be used to drive arbitrary attributes based on the value of poses. So 2 poses could drive a wrinkle corrective for example.
poseattributes = [
'attribute1',
'attribute2',
'attribute3',
'attribute4',
]
def add_a_keyable_attribute(myObj, oDataType, oParamName, oMin=None, oMax=None, oDefault=0.0):
""" adds an attribute that shows up in the channel box; returns the newly created attribute """
oFullName = '.'.join( [str(myObj),oParamName] )
if pm.objExists(oFullName):
return pm.PyNode(oFullName)
else:
myObj.addAttr(oParamName, at=oDataType, keyable=True, dv=oDefault)
myAttr = pm.PyNode(myObj + '.' + oParamName)
if oMin != None:
myAttr.setMin(oMin)
if oMax != None:
myAttr.setMax(oMax)
pm.setAttr(myAttr, e=True, channelBox=True)
pm.setAttr(myAttr, e=True, keyable=True)
return myAttr
def add_meta_attribute(myObj, oParamName, oValue):
'''adds a string attribute into "extra" attributes. Useful for meta information'''
oFullName = '.'.join( [str(myObj),oParamName] )
if pm.objExists(oFullName):
pm.PyNode(str(FullName)).set(oValue) # if it exists, just set the value
return pm.PyNode(oFullName)
else:
myObj.addAttr(oParamName, dt='string')
oParam = pm.PyNode(str(oFullName))
oParam.set(oValue)
return oParam
def move_loc_position(target, source):
""" this function reads a world coordinate 'source'
and moves the localPosition of the target locator. """
# a temp dumb hack until I figure out how to translate world to local coordinates.
# your math is bad and you should feed bad
tempLoc = pm.spaceLocator(n='ZZZ_TEMP_LOCATOR_{}'.format(target.name()))
pm.parent(tempLoc, target)
tempLoc.setRotation([0,0,0])
tempLoc.setTranslation(source, space='world')
target.localPosition.set(tempLoc.getTranslation(space='object'))
pm.delete(tempLoc)
def connect_mirror_transform(oLeft, oRight):
""" this connects one PyNode transform to the other to create mirror behaviour
The nodes should have XYZ rotation order. """
nodeName = '{}_{}_symmetry_MLT'.format(oLeft.name(), oRight.name())
oMultT = pm.createNode('multiplyDivide', n='left' + str(i) + '_MLT')
oLeft.tx.connect(oMultT.input1X)
oMultT.input2X.set(-1) # translateX
oMultT.input2Y.set(-1) # rotateY
oMultT.input2Z.set(-1) # rotateZ
oMultT.outputX.connect(oRight.tx)
oMultT.outputY.connect(oRight.ry)
oMultT.outputZ.connect(oRight.rz)
# all other axes are 1:1 connected.
oLeft.ty.connect(oRight.ty)
oLeft.tz.connect(oRight.tz)
oLeft.rx.connect(oRight.rx)
# connect scale separately so user can disconnect one axis at a time if needed.
oLeft.sx.connect(oRight.sx)
oLeft.sy.connect(oRight.sy)
oLeft.sz.connect(oRight.sz)
def get_midpoint(vecA, vecB, weight=0.5):
"""Helper to get middle point between two vectors. Weight is 0.0 to 1.0 blend between the two.
So for example, 0.0 would return the position of oObject1. 1.0 would be oObject2. 0.5 is halfway."""
try:
vecA = dt.Vector(vecA) # just in case it isn't already cast as a vector
vecB = dt.Vector(vecB)
vecC = vecB-vecA
vecD = vecC * weight # 0.5 is default which finds the mid-point.
vecE = vecA + vecD
return vecE
except Exception, e:
# TODO: include some useful error checking
return False
def create_pose_node(pose, zone, inputHooks, masterScale, poseDriver, poseRange, poseOverrides, poseOverrideMaps):
#TODO: Make some logic that auto-splits "symmetry" into two poses, left and right.
poseName = pose.replace('symmetry','left')
try:
oZone = pm.PyNode('{}_zone'.format(zone))
faceJoints = oZone.members()
except:
# a zone may have no joints, but will still drive blendshapes.
faceJoints = []
# initialize the HOOKS and OVERRIDES nodes.
hookName = 'face_{}_HOOKS'.format(zone)
overName = 'face_{}_OVERRIDES'.format(zone)
oHook = pm.PyNode(hookName)
oOver = pm.PyNode(overName)
posesRoot = pm.PyNode('face_POSES')
#####################
poseLocators = {} # first loop creates the pose locators and puts them into hierarchy
oPoseNode = pm.group(em=True, n='{}_POSE'.format(poseName)) # eg. left_smile_POSE
pm.parent(oPoseNode, posesRoot)
# lock and hide all transform attrs
for each in oPoseNode.listAttr(keyable=True):
each.set(keyable=False, channelBox=False)
#TODO: Low priority: Create a proper meta info scheme instead of channel box attrs.
# Add an enum attribute with the name of the zone
pm.addAttr(oPoseNode, ln='zone', at='enum', en=zone)
myAttr = pm.PyNode(oPoseNode.name() + '.zone')
pm.setAttr(myAttr, e=True, channelBox=True)
pm.setAttr(myAttr, e=True, keyable=True)
myAttr.lock()
for each in faceJoints:
jKey = each.name()
pivotPosition = inputHooks['pivotpositions'][jKey]
pivotRotation = inputHooks['pivotrotations'][jKey]
jointPosition = inputHooks['jointpositions'][jKey]
jointRotation = inputHooks['jointrotations'][jKey]
jointName = each.name().rpartition('_')[0].replace('symmetry','left') # cut off the suffix
#NOTE: This name is long and cumbersome
poseLocName = '{}__{}__POSELOC'.format(jointName, poseName) # eg. left_cheek_puff__right_smile__POSELOC
oRoot = pm.group(em=True, n=poseLocName + '_grp')
oLoc = pm.spaceLocator(n=poseLocName)
oLoc.localScale.set([0.4 * masterScale]*3)
poseLocators[jKey] = oLoc # adding each locator to a dictionary whose key is the joint name
pm.parent(oLoc, oRoot)
pm.parent(oRoot, oPoseNode)
oRoot.setTranslation(pivotPosition, space='world')
oRoot.setRotation(pivotRotation, space='world')
move_loc_position(oLoc, jointPosition) # moves the localPosition of the locator to a world coordinate
#####################
# Populate the _POSE node with all of the attributes
oTitleAttr = add_a_keyable_attribute(oPoseNode, 'double', 'arbitraryAttributes')
oTitleAttr.lock()
attrDict = {}
for poseAttr in poseattributes: # The arbitrary attributes
oPoseAttr = add_a_keyable_attribute(oPoseNode, 'double', 'pose_{}'.format(poseAttr))
attrDict[poseAttr] = oPoseAttr
for poseAttr in ['tx', 'ty', 'tz', 'rx', 'ry', 'rz']:
attrDict[poseAttr] = []
for poseAttr in ['tx', 'ty', 'tz', 'rx', 'ry', 'rz']:
oTitleAttr = add_a_keyable_attribute(oPoseNode, 'double', '{}_POSE_{}'.format(poseName, poseAttr))
oTitleAttr.lock()
for each in faceJoints:
jKey = each.name()
jointName = each.name().rpartition('_')[0].replace('symmetry','left') # cut off the suffix
pmaDriven = inputHooks[jKey]['driven']
pmaTranslate = inputHooks[jKey]['translate']
pmaRotate = inputHooks[jKey]['rotate']
#pmaScale = inputHooks[jKey]['scale']
newAttrName = '{}_{}'.format(jointName, poseAttr)
oPoseAttr = add_a_keyable_attribute(oPoseNode, 'double', newAttrName)
poseLocator = poseLocators[jKey] # from dictionary
# eg. connect left_cheek_puff_POSELOC.tx to left_smile_POSE.left_cheek_puff_tx
pm.PyNode('{}.{}'.format(poseLocator, poseAttr)).connect(oPoseAttr)
attrDict[poseAttr].append(oPoseAttr)
# Now set up the hook connection to drive it
oHookAttr = add_a_keyable_attribute(oHook, 'double', poseName)
oOverAttr = add_a_keyable_attribute(oOver, 'double', poseName)
if poseDriver == 'controller.attribute':
pass
else:
poseMapper = pm.createNode('remapValue', n='{}_poseMapper'.format(poseName))
try:
pm.PyNode(poseDriver).connect(poseMapper.inputValue)
poseMapper.outValue.connect(oHookAttr)
poseMapper.inputMin.set(poseRange[0])
poseMapper.inputMax.set(poseRange[1])
poseMapper.outputMin.set(poseRange[2])
poseMapper.outputMax.set(poseRange[3])
except:
pm.warning('{} HOOK failed to connect properly'.format(poseName))
pass
#TODO: Create a scheme so any number of poses can drive the override. (Clamped from 0 to 1 using a remapValue)
if poseOverrides:
poseOverridePMA = pm.createNode('plusMinusAverage', n='{}_override_PMA'.format(poseName))
add_a_keyable_attribute(poseOverridePMA, 'long', 'poseCount')
poseOverridePMA.output1D.connect(oOverAttr)
poseOverridePMA.poseCount.set(0)
for eachOver, overRange in zip(poseOverrides, poseOverrideMaps):
overrideName = eachOver.replace('.','_')
overrideMapper = pm.createNode('remapValue', n='{}_{}_poseOverrideMapper'.format(poseName, overrideName))
try:
pm.PyNode(eachOver).connect(overrideMapper.inputValue)
overrideMapper.inputMin.set(overRange[0])
overrideMapper.inputMax.set(overRange[1])
overrideMapper.outputMin.set(overRange[2])
overrideMapper.outputMax.set(overRange[3])
pmaIndex = poseOverridePMA.poseCount.get()
overrideMapper.outValue.connect(poseOverridePMA.input1D[pmaIndex])
poseOverridePMA.poseCount.set(pmaIndex + 1)
except:
pm.warning('{} OVERRIDE failed to connect properly'.format(poseName))
pass
# create an Override for each pose (not for each joint)
# This override will mute the multiplyDivide for each joint
poseOverrideMAP = pm.createNode('remapValue', n='{}_override_MAP'.format(poseName))
poseOverrideMULT = pm.createNode('multiplyDivide', n='{}_override_MLT'.format(poseName))
poseOverrideMAP.outputMin.set(1.0) # this MAP node reverses so as the override turns on, the MLT multiplies off
poseOverrideMAP.outputMax.set(0.0)
oOverAttr.connect(poseOverrideMAP.inputValue)
oHookAttr.connect(poseOverrideMULT.input1X)
poseOverrideMAP.outValue.connect(poseOverrideMULT.input2X)
# Zip the corresponding pose locator attributes and face joint.
# The faceJoints are used as dictKeys in this section
### TRANSLATION ###
if faceJoints:
for attrX, attrY, attrZ, currentJoint in zip( attrDict['tx'], attrDict['ty'], attrDict['tz'], faceJoints ):
# Hook in the multiply for the translations
oMult = pm.createNode('multiplyDivide', n='{}_{}_translate_MLT'.format(poseName, currentJoint))
attrX.connect(oMult.input1X)
attrY.connect(oMult.input1Y)
attrZ.connect(oMult.input1Z)
# poseOverrideMULT only uses the X channel as a mute for all 3 oMult axes.
poseOverrideMULT.outputX.connect(oMult.input2X)
poseOverrideMULT.outputX.connect(oMult.input2Y)
poseOverrideMULT.outputX.connect(oMult.input2Z)
oPlus = inputHooks[currentJoint.name()]['translate']
pmaIndex = oPlus.poseCount.get()
oMult.output.connect(oPlus.input3D[pmaIndex].input3D)
oPlus.poseCount.set(pmaIndex + 1)
### ROTATION ###
for attrX, attrY, attrZ, currentJoint in zip( attrDict['rx'], attrDict['ry'], attrDict['rz'], faceJoints ):
# Hook in the multiply for the rotations
oMult = pm.createNode('multiplyDivide', n='{}_{}_rotate_MLT'.format(poseName, currentJoint))
attrX.connect(oMult.input1X)
attrY.connect(oMult.input1Y)
attrZ.connect(oMult.input1Z)
# poseOverrideMULT only uses the X channel as a mute for all 3 oMult axes.
poseOverrideMULT.outputX.connect(oMult.input2X)
poseOverrideMULT.outputX.connect(oMult.input2Y)
poseOverrideMULT.outputX.connect(oMult.input2Z)
oPlus = inputHooks[currentJoint.name()]['rotate']
pmaIndex = oPlus.poseCount.get()
oMult.output.connect(oPlus.input3D[pmaIndex].input3D)
oPlus.poseCount.set(pmaIndex + 1)
### CUSTOM ATTRIBUTES ###
#TODO: Hook in the custom attributes, and attach them to the override mute control
for poseAttr in poseattributes:
oAttrA = attrDict[poseAttr]
# Hook in the multiply for the arbitrary attributes
oMult = pm.createNode('multiplyDivide', n='{}_{}_attributes_MLT'.format(poseName, currentJoint))
oAttrA.connect(oMult.input1X)
# poseOverrideMULT only uses the X channel as a mute for all 3 oMult axes.
# and the arbitrary attributes only pass one channel.
poseOverrideMULT.outputX.connect(oMult.input2X)
oPlus = inputHooks['poseattributes'][poseAttr]
pmaIndex = oPlus.poseCount.get()
oMult.outputX.connect(oPlus.input1D[pmaIndex])
oPlus.poseCount.set(pmaIndex + 1)
def create_hooks(faceMap, masterScale):
# create the HOOKS and OVERRIDES nodes.
hookRoot = pm.group(em=True, n='face_HOOKS')
overRoot = pm.group(em=True, n='face_OVERRIDES')
posesRoot = pm.group(em=True, n='face_POSES')
add_meta_attribute(posesRoot, facemap, '') # stores a path to the pose map
oRigRoot = pm.PyNode('|face_RIG')
pm.parent(hookRoot, overRoot, posesRoot, oRigRoot)
# Get the zones from the face map file.
# I want the HOOKS to exist even if there are no joints in the zone set.
for zone in faceMap['faceposes'].keys():
hookName = 'face_{}_HOOKS'.format(zone)
overName = 'face_{}_OVERRIDES'.format(zone)
oHook = pm.spaceLocator(n=hookName)
oOver = pm.spaceLocator(n=overName)
oHook.localScale.set([0.5 * masterScale]*3)
oOver.localScale.set([0.5 * masterScale]*3)
for each in oHook.listAttr(keyable=True):
each.set(keyable=False, channelBox=False)
for each in oOver.listAttr(keyable=True):
each.set(keyable=False, channelBox=False)
pm.parent(oHook, hookRoot)
pm.parent(oOver, overRoot)
def add_to_zone(oColl, zone):
oldSel = pm.selected()
if not pm.objExists('{}_zone'.format(zone)):
oZone = pm.createNode('objectSet', n='{}_zone'.format(zone))
else:
oZone = pm.PyNode('{}_zone'.format(zone))
[oZone.add(x) for x in oColl]
pm.select(oldSel)
def remove_from_zone(oColl, zone):
oldSel = pm.selected()
if not pm.objExists('{}_zone'.format(zone)):
oZone = pm.createNode('objectSet', n='{}_zone'.format(zone))
else:
oZone = pm.PyNode('{}_zone'.format(zone))
for each in oColl:
if each in oZone:
oZone.remove(each)
pm.select(oldSel)
def create_joint_drivers(masterScale):
""" This function builds the skeleton hierarchy and the end-result nodes and pivots.
Returns a dictionary of the final plusMinusAverage nodes that all the poses drive """
# Here I get all the zones from the sets rather than from the pose map.
oRigRoot = pm.PyNode('|face_RIG')
oSkeletonRoot = pm.PyNode('|face_RIG|face_skeleton_RIG')
allZones = pm.ls('*_zone', type='objectSet')
allJoints = set(pm.ls([x.members() for x in allZones]))
inputHooks = {}
jointRoot = list(allJoints)[0].root() # grab one of the joints and find the skeleton root
pm.parent(jointRoot, oSkeletonRoot)
# 'poseCount' is an attribute to keep track of how many poses are fed into each plusMinusAverage node.
# This is so I can reliably append. I'm sure there is a better Maya way, but there are also index bugs.
inputHooks['poseattributes'] = {}
inputHooks['pivotpositions'] = {}
inputHooks['pivotrotations'] = {}
inputHooks['jointpositions'] = {}
inputHooks['jointrotations'] = {}
aaRoot = pm.group(em=True, n='attributeJnt_grp')
pm.parent(aaRoot, oRigRoot)
for i, poseAttr in enumerate(poseattributes): # The arbitrary attributes
# Generate the special arbitrary attribute joints in this loop
aaGroup = pm.group(em=True, n='{}_attrJnt_grp'.format(poseAttr))
pm.select(None)
aaJoint = pm.joint(n='{}_attr_jnt'.format(poseAttr))
aaJoint.radius.set(1.0 * masterScale)
pm.select(None)
pm.parent(aaJoint, aaGroup)
pm.parent(aaGroup, aaRoot)
aaGroup.tx.set( (i*0.4)+2.0 ) #TODO: This should be a part of the skeleton, and constrained by a driver rig (so it can bake)
# add all together with PMA, and clamp from 0-1 with remapValue
#TODO: I might also have to clamp the input. But HOOKS will usually have a sane driver amount... But right now, if you keep driving the pose and the Attribute is set to 0.1, when you get to 10 on the HOOK, the attribute will reach 1.
pmaAttr = pm.createNode('plusMinusAverage', n='custom_{}_PlusA_PMA'.format(poseAttr))
pmaRemap = pm.createNode('remapValue', n='custom_{}_RemapA_MAP'.format(poseAttr))
add_a_keyable_attribute(pmaAttr, 'long', 'poseCount')
pmaAttr.output1D.connect(pmaRemap.inputValue)
pmaRemap.outValue.connect(aaJoint.translateY)
#pmaAttr.input1D[0]
inputHooks['poseattributes'][poseAttr] = pmaAttr
for each in allJoints:
jKey = each.name()
jointBaseName = each.name().rpartition('_')[0]
# Add a plusMinusAverage for each driven joint and store it in a dictionary.
# This dict will be referenced by all the zones and poses.
# A dict, because here I am iterating on all joints. Later I'll be iterating on pose sets of joints.
inputHooks[jKey] = {}
offsetCtlName = jointBaseName + '_offset_ctrl'
offsetCtlZeroName = jointBaseName + '_offsetCtrl_zero'
drivenName = jointBaseName + '_driven'
zeroName = jointBaseName + '_zero'
##### Set up the pivot hierarchy
# The requirements right now are a _posepivot locator parented underneath the joint and placed at an arbitrary position.
jointPosition = each.getTranslation(space='world')
jointRotation = each.getRotation(space='world')
inputHooks['jointpositions'][jKey] = jointPosition
inputHooks['jointrotations'][jKey] = jointRotation
pivotPositionLoc = [x for x in each.getChildren() if 'posepivot' in x.name()]
if len(pivotPositionLoc) > 0:
pivotPosition = pivotPositionLoc[0].getTranslation(space='world')
pivotRotation = pivotPositionLoc[0].getRotation(space='world')
pm.delete(pivotPositionLoc)
inputHooks['pivotpositions'][jKey] = pivotPosition
inputHooks['pivotrotations'][jKey] = pivotRotation
else:
# If no _posepivot is found, use the joint's position instead.
pivotPosition = each.getTranslation(space='world')
pivotRotation = each.getRotation(space='world')
inputHooks['pivotpositions'][jKey] = pivotPosition
inputHooks['pivotrotations'][jKey] = pivotRotation
oOffsetCtl = pm.spaceLocator(n=offsetCtlName)
oOffsetCtlZero = pm.group(em=True, n=offsetCtlZeroName)
oDriven = pm.spaceLocator(n=drivenName)
oDrivenRoot = pm.group(em=True, n=zeroName)
oOffsetCtl.localScale.set([0.5 * masterScale]*3)
oDriven.localScale.set([0.5 * masterScale]*3)
oOffsetCtl.setTranslation( jointPosition, space='world')
oOffsetCtlZero.setTranslation( jointPosition, space='world')
oDriven.setTranslation( pivotPosition, space='world')
oDrivenRoot.setTranslation( pivotPosition, space='world')
oOffsetCtl.setRotation( pivotRotation, space='world')
oOffsetCtlZero.setRotation( pivotRotation, space='world')
oDriven.setRotation( pivotRotation, space='world')
oDrivenRoot.setRotation( pivotRotation, space='world')
try:
pm.parent(oDrivenRoot, each.getParent())
except:
pass
pm.parent(oDriven, oDrivenRoot)
pm.parent(oOffsetCtlZero, oDriven)
pm.parent(oOffsetCtl, oOffsetCtlZero)
pm.parent(each, oOffsetCtl) #TODO: Eventually abstract the skeleton and constrain it
pmaTranslate = pm.createNode('plusMinusAverage', n='{}_translate_PMA'.format(drivenName))
pmaRotate = pm.createNode('plusMinusAverage', n='{}_rotate_PMA'.format(drivenName))
#pmaScale = pm.createNode('plusMinusAverage', n='{}_scale_PMA'.format(drivenName))
add_a_keyable_attribute(pmaTranslate, 'long', 'poseCount')
add_a_keyable_attribute(pmaRotate, 'long', 'poseCount')
#add_a_keyable_attribute(pmaScale, 'long', 'poseCount')
pmaTranslate.output3D.connect(oDriven.translate)
pmaRotate.output3D.connect(oDriven.rotate)
#pmaScale.output3D.connect(oDriven.scale)
inputHooks[jKey]['driven'] = oDriven
inputHooks[jKey]['translate'] = pmaTranslate
inputHooks[jKey]['rotate'] = pmaRotate
#inputHooks[jKey]['scale'] = pmaScale
return inputHooks
def auto_connect_blendshape(oBlendshapes):
""" Pass in a list of blendshape nodes. This will connect them to the rig. """
if DRYRUN:
print('connecting blendshapes to rig - DRY RUN ONLY')
return False
hookNodes = pm.ls('*_HOOKS', type='transform')
blendShapeTransfer = []