-
Notifications
You must be signed in to change notification settings - Fork 5
/
metsrig.py
1327 lines (1101 loc) · 49.7 KB
/
metsrig.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
"Version: 2.1"
"Date: 02/05/19"
import bpy
from bpy.props import *
from mathutils import Vector
import time
import webbrowser
# Witcher3 Hairs
# We'll probably want to rename hairs, eg. from "Ciri_Default" to "Hair_Long_03" or whatever, since hairs aren't unique to a single character.
# Optimization
# Could try limiting update_meshes() and update_node_values() to only worry about changed properties.
# This would be done by finding a list of the changed properties in pre_depsgraph_update() and then passing those in as params.
# This would enable users to manually enable/disable things, then interact with the rig, and not lose their manual modifications.
# There would need to be a button to refresh the entire rig though (which would probably happen by not passing the optional param from pre_depsgraph_update() into update_meshes() ).
# Animation
# Currently when values are animated, outfits don't update, since just because something is animated it doesn't call a depsgraph update when animation is played or timeline is scrubbed. We might need to run pre_depsgraph_uipdate() on frame change instead of depsgraph update, which would probably affect performance.
# I think even properties' update callbacks only run when the user changes them, not when animation changes them. Isn't that a bug?
# In any case, for cloth swapping to work with animation, we might need to check for changes on both ID and other properties on frame change. Yikes.
# Linking/Appending
# The rig should detect when itself is a proxy and be aware of what rig it is a proxy of. Use that rig's children instead of the proxy armature's(presumably non-existent) children.
# It should also detect when multiple copies of itself exist. Currently the only thing that needs to be done for two rigs to work without messing each other up is to change the 'material_controller' to the correct one.
def sombra_skinID(skinset, skin):
return skinset + (skin-1 if skinset==1 else 19) + (skin-1 if skinset==2 else skinset>2) + (skin-1 if skinset==3 else skinset>3) - 0.5
def get_children_recursive(obj, ret=[]):
# Return all the children and children of children of obj in a flat list.
for c in obj.children:
if(c not in ret):
ret.append(c)
ret = get_children_recursive(c, ret)
return ret
class MetsRig_BoolProperties(bpy.types.PropertyGroup):
""" Store a BoolProperty referencing an outfit/character property whose min==0 and max==1.
This BoolProperty will be used to display the property as a toggle button in the UI.
"""
def update_id_prop(self, context):
""" Callback function to update the ID property when this BoolProperty's value is changed.
"""
if( self.rig == None ): return
metsrig_props = self.rig.data.metsrig_properties
outfit_bone = self.rig.pose.bones.get("Properties_Outfit_"+metsrig_props.metsrig_outfits)
char_bone = self.rig.pose.bones.get("Properties_Character_"+metsrig_props.metsrig_chars)
for prop_owner in [outfit_bone, char_bone]:
if(prop_owner != None):
if(self.name in prop_owner):
prop_owner[self.name] = self.value
rig: PointerProperty(type=bpy.types.Object)
value: BoolProperty(
name='Boolean Value',
description='',
update=update_id_prop
)
class MetsRig_Properties(bpy.types.PropertyGroup):
# PropertyGroup for storing MetsRig related properties in.
# Character and Outfit specific properties will still be stored in their relevant Properties bones (eg. Properties_Outfit_Ciri_Default).
@staticmethod
def get_rigs():
""" Find all MetsRigs in the current view layer.
"""
ret = []
armatures = [o for o in bpy.context.view_layer.objects if o.type=='ARMATURE']
for o in armatures:
if("metsrig") in o.data:
ret.append(o)
return ret
def get_rig(self):
""" Find the rig that is using this instance.
Why not just check context.object? Callback functions get called even when the rig is not selected.
"""
for rig in self.get_rigs():
if(rig.data.metsrig_properties == self):
return rig
@classmethod
def pre_depsgraph_update(cls, scene):
""" Runs before every depsgraph update. Is used to handle user input by detecting changes in the rig properties.
"""
for rig in cls.get_rigs():
# Grabbing relevant data
mets_props = rig.data.metsrig_properties
character_bone = rig.pose.bones.get("Properties_Character_"+mets_props.metsrig_chars)
outfit_bone = rig.pose.bones.get("Properties_Outfit_"+mets_props.metsrig_outfits)
if('update' not in rig.data):
rig.data['update'] = 0
if('prev_props' not in rig.data):
rig.data['prev_props'] = ""
# Saving those properties into a list of dictionaries.
current_props = [{}, {}, {}]
saved_types = [int, float] # Types of properties that we save for user input checks.
def save_props(prop_owner, list_id):
for p in prop_owner.keys():
if(p=='_RNA_UI' or p=='prev_props'): continue # TODO this check would be redundant if we didn't save strings, and the 2nd part is already redundant due to next line.
if(type(prop_owner[p]) not in saved_types): continue
if(p=="prop_hierarchy"): continue
current_props[list_id][p] = prop_owner[p]
if(character_bone):
save_props(character_bone, 0)
else:
print("Warning: Character bone for " + mets_props.metsrig_chars + " not found. It needs to be named 'Properties_Character_CharName'.")
if(outfit_bone):
save_props(outfit_bone, 1)
else:
print("Warning: Outfit bone for " + mets_props.metsrig_outfits + " not found. It should be named 'Properties_Outfit_OutfitName' and its parent should be the character bone.")
save_props(rig.data, 2)
# Retrieving the list of dictionaries from the ID Property - have to use to_dict() on each dictionary due to the way ID properties... are.
prev_props = []
if(rig.data['prev_props'] != ""):
prev_props = [
rig.data['prev_props'][0].to_dict(),
rig.data['prev_props'][1].to_dict(),
rig.data['prev_props'][2].to_dict(),
]
# Finally, we compare the current and previous properties.
# If they do not match, that means there was user input, and it's time to update stuff.
if( current_props != prev_props ):
# Materials need to update before the depsgraph update, otherwise they will not update even in rendered view.
rig.data.metsrig_properties.update_node_values(bpy.context)
rig.data['prev_props'] = [current_props[0], current_props[1], current_props[2]]
# However, updating meshes before depsgraph update will cause an infinite loop, so we use a flag to let post_depsgraph_update know that it should update meshes.
rig.data['update'] = 1
@classmethod
def post_depsgraph_update(cls, scene):
"""Runs after every depsgraph update. If any user input to the rig properties was detected by pre_depsgraph_update(), this will call update_meshes().
"""
for rig in cls.get_rigs():
if(rig.data['update'] == 1):
rig.data.metsrig_properties.update_meshes(bpy.context)
rig.data['update'] = 0
def determine_visibility_by_expression(self, o):
""" Determine whether an object should be visible based on its Expression custom property.
Expressions will recognize any character or outfit property names as variables.
eg. "Outfit=='Ciri_Default' * Hood==1"
o: object to determine stuff on.
"""
rig = self.get_rig()
outfit_bone = rig.pose.bones.get('Properties_Outfit_' + self.metsrig_outfits)
character_bone = rig.pose.bones.get('Properties_Character_' + self.metsrig_chars)
# Replacing the variable names in the expression with the corresponding variables' values.
expression = o['Expression']
if( ('Outfit' not in expression) and ('Character' not in expression) and ('Hair' not in expression) ):
print("Warning: invalid expression - no 'Outfit', 'Character' or 'Hair' found. " + o.name)
return None
expression = expression.replace('Outfit', "'" + self.metsrig_outfits + "'")
expression = expression.replace('Character', "'" + self.metsrig_chars + "'")
expression = expression.replace('Hair', "'" + self.metsrig_hairs + "'")
bones = [outfit_bone, character_bone]
found_anything=False
for bone in bones:
for prop_name in bone.keys():
if(prop_name in expression):
found_anything = True
expression = expression.replace(prop_name, str(bone[prop_name]))
#if(not found_anything):
# return False
try:
ret = eval(expression)
return ret
except NameError: # When the variable name in an expression doesn't get replaced by its value because the object doesn't belong to the active outfit.
return False
def determine_visibility_by_properties(self, o):
""" Determine whether an object should be visible by matching its custom properties to the active character and outfit properties.
"""
rig = self.get_rig()
char_bone = rig.pose.bones.get("Properties_Character_"+self.metsrig_chars)
outfit_bone = rig.pose.bones.get("Properties_Outfit_"+self.metsrig_outfits)
if("Hair" in o):
if( self.metsrig_hairs != o['Hair'] ):
return False
if("Character" in o):
chars = o['Character'].split(", ")
if(self.metsrig_chars not in chars):
return False
if("Outfit" in o):
outfits = o['Outfit'].split(", ")
if(self.metsrig_outfits not in outfits):
return False
for prop_bone in [char_bone, outfit_bone]:
for prop_name in o.keys():
if( (prop_name == '_RNA_UI') or (prop_name not in prop_bone) ): continue
prop_value = prop_bone[prop_name] # Value of the property on the properties bone. Changed by the user via the UI.
requirement = o[prop_name] # Value of the property on the object. This defines what the property's value must be in order for this object to be visible.
# Checking if the property value fits the requirement...
if(type(requirement)==int):
if(not prop_value == requirement):
return False
elif('IDPropertyArray' in str(type(requirement))):
if(not prop_value in requirement.to_list()):
return False
elif(type(requirement)==str):
if('#' not in requirement): continue
if(not eval(requirement.replace("#", str(prop_value)))):
return False
else:
print("Error: Unsupported property type: " + str(type(requirement)) + " of property: " + p + " on object: " + o.name)
# If we got this far without returning False, then all the properties matched and this object should be visible.
return True
def determine_object_visibility(self, o):
""" Determine if an object should be visible based on its properties and the rig's current state.
"""
if('Expression' in o):
if( ('Hair' in o) or ('Outfit' in o) or ('Character' in o) ):
if( self.determine_visibility_by_properties(o) ):
return self.determine_visibility_by_expression(o)
else:
# This lets us combine expressions and outfits, which is mainly useful to let us debug expressions.
# This way, even if there is an expression, we check for hair/outfit/character first, and if those don't match, we don't have to run the expression.
return False
else:
return self.determine_visibility_by_expression(o)
elif( ('Hair' in o) or ('Outfit' in o) or ('Character' in o) ):
return self.determine_visibility_by_properties(o)
else:
return None
def determine_visibility_by_name(self, m, obj=None):
""" Determine whether the passed vertex group or shape key should be enabled, based on its name and the properties of the currently active outfit.
Naming convention example: M:Ciri_Default:Corset==1*Top==1
m: The vertex group or shape key (or anything with a "name").
"""
if("M:" not in m.name):
return None
# Gathering data
rig = self.get_rig()
active_outfit = self.metsrig_outfits
outfit_bone = rig.pose.bones.get('Properties_Outfit_' + active_outfit)
active_character = self.metsrig_chars
character_bone = rig.pose.bones.get('Properties_Character_' + active_character)
if(outfit_bone==None):
return None
parts = m.name.split(":") # The 3 parts: "M" to indicate it's a mask, the outfit/character names, and the expression.
prop_owners = parts[1].split(",") # outfit/characters are divided by ",".
bone = None
expression = ""
if(len(parts) == 3):
expression = parts[2]
found_owner = False
if(active_outfit in prop_owners):
bone = outfit_bone
found_owner=True
elif(active_character in prop_owners):
bone = character_bone
found_owner=True
else:
return False
if(found_owner):
if(expression in ["True", "False"]): # Sigh.
return eval(expression)
elif(len(parts) == 2):
bone = outfit_bone
expression = parts[1]
else:
assert False, "Vertex group or shape key name is invalid: " + m.name + " In object: " + obj.name
found_anything = False
for prop_name in bone.keys():
if(prop_name in expression):
found_anything = True
expression = expression.replace(prop_name, str(bone[prop_name]))
if(not found_anything):
return None
try:
return eval(expression)
except:
print("WARNING: Invalid Expression: " + expression + " from object: " + obj.name + " This thing: " + m.name)
def activate_vertex_groups_and_shape_keys(self, obj):
""" Combines vertex groups with the "Mask" vertex group on all objects belonging to the rig.
Whether a vertex group is active or not is decided based on its name, using determine_visibility_by_name().
"""
if(obj.type!='MESH'): return
mask_vertex_groups = [vg for vg in obj.vertex_groups if self.determine_visibility_by_name(vg, obj)]
final_mask_vg = obj.vertex_groups.get('Mask')
if(final_mask_vg):
for v in obj.data.vertices:
final_mask_vg.add([v.index], 0, 'REPLACE')
for mvg in mask_vertex_groups:
try:
if(mvg.weight(v.index) > 0):
final_mask_vg.add([v.index], 1, 'REPLACE')
break
except:
pass
# Toggling shape keys using the same naming convention as the VertexWeightMix modifiers.
if(obj.data.shape_keys!=None):
#shapekeys = [sk for sk in obj.data.shape_keys.key_blocks if "M-" in sk.name]
shapekeys = obj.data.shape_keys.key_blocks
for sk in shapekeys:
visible = self.determine_visibility_by_name(sk, obj)
if(visible != None):
sk.value = visible
def update_node_values(self, context):
""" In all materials belonging to this rig, update nodes that correspond to an outfit, character, rig data, or metsrig property.
eg. when Ciri's "Face" property changes, ensure that the "Face" value node in her material updates.
Also update the active texture of materials for better feedback while in Solid View.
"""
# Gathering relevant data
rig = self.get_rig()
outfit_bone = rig.pose.bones.get('Properties_Outfit_'+self.metsrig_outfits)
char_bone = rig.pose.bones.get('Properties_Character_'+self.metsrig_chars)
# Gathering all the keys and values from outfit, character, main properties and witcher3_properties(self).
big_dict = {}
for e in [outfit_bone, char_bone, rig.data, self]:
if( e==None ): continue
for k in e.keys():
if(k=='_RNA_UI'): continue
value = e[k]
if( type(value) in [int, float, str] ):
big_dict[k] = value
elif('IDPropertyArray' in str(type(value))):
big_dict[k] = value.to_list()
### Looking through every node of every material of every visible object. Trying to keep this optimized.
objs = list(filter(lambda o: type(o) == bpy.types.Object, get_children_recursive(rig)))
#objs = [o for o in children if o.hide_viewport==False] # Errors if objects were deleted.
# Gathering a list of the materials that the visible objects use. Also node groups.
node_trees = []
for o in objs:
try: # This try block is here because apparently objs contains objects that have potentially been deleted by the user, in which case it throws an error.
if(o.hide_viewport): continue
except:
continue
for ms in o.material_slots:
if(ms.material==None): continue
if(ms.material.node_tree not in node_trees):
node_trees.append(ms.material.node_tree)
self.update_material_controller(char_bone)
self.update_material_controller(outfit_bone)
def handle_group_node(group_node, active_texture=False):
""" For finding a node value connected even through reroute nodes, then muting unused texture nodes, and optionally changing the active node to this group_node's active texture. """
node = group_node
socket = None
# Find next connected non-reroute node.
while True:
if(len(node.inputs[0].links) > 0):
next_node = node.inputs[0].links[0].from_node
if(next_node.type!='REROUTE'):
socket = node.inputs[0].links[0].from_socket
node = next_node
break
node = next_node
else:
break
selector_value = int(socket.default_value)
# Setting active node for the sake of visual feedback when in Solid view.
if(len(group_node.inputs[selector_value].links) > 0 ):
if(active_texture):
nodes.active = group_node.inputs[selector_value].links[0].from_node
elif(len(group_node.inputs[1].links) > 0 ):
if(active_texture):
nodes.active = group_node.inputs[1].links[0].from_node
selector_value = 1
nodes.active.mute=False
# Muting input texture nodes that we don't need, to help avoid hitting Eevee texture node limits.
for i in range(1, len(group_node.inputs)):
chosen_one = i==selector_value
if(len(group_node.inputs[i].links) > 0 ):
node = group_node.inputs[i].links[0].from_node
if(node.type=='TEX_IMAGE'):
node.mute = i!=selector_value
# Update Value nodes that match the name of a property.
for nt in node_trees:
nodes = nt.nodes
for prop_name in big_dict.keys():
value = big_dict[prop_name]
# Checking for expressions (If a property's value is a string starting with "=", it will be evaluated as an expression)
if(type(value)==str and value.startswith("=")):
expression = value[1:]
for var_name in big_dict.keys():
if(var_name in expression):
expression = expression.replace(var_name, str(big_dict[var_name]))
value = eval(expression)
# Fixing constants (values that shouldn't be exposed to the user but still affect materials should be prefixed with "_".)
if(prop_name.startswith("_")):
prop_name = prop_name[1:]
if(prop_name in nodes):
#if(prop_name.startswith("_")): continue # Why did I have this line?
n = nodes[prop_name]
# Setting the value of the node to the value of the corresponding property.
if(type(value) in [int, float]):
n.outputs[0].default_value = value
else:
n.inputs[0].default_value = value[0]
n.inputs[1].default_value = value[1]
n.inputs[2].default_value = value[2]
### Updating active texture for the sake of visual feedback when in Solid view.
active_color_group = nodes.get('ACTIVE_COLOR_GROUP')
if(active_color_group):
handle_group_node(active_color_group, True)
for n in nodes:
if("SELECTOR_GROUP" in n.name):
handle_group_node(n)
def update_meshes(self, context):
""" Executes the cloth swapping system by updating object visibilities, mask vertex groups and shape key values.
"""
# TODO: This is kind of in a weird place here. I'm only calling it from here because post_depsgraph_update calls update_meshes.
# TODO: The only reason it's weird is because it's weird that we're giving special treatment to specifically body shape keys. Maybe at some point, when we need to, we can allow for any kind of shape key selector to be thrown into the UI, or generic shape keys to be assigned to characters/outfits, or something like that...
self.update_body_type(context)
def do_child(rig, obj, hide=None):
# Recursive function to control item visibilities.
# The object hierarchy assumes that if an object is disabled, all its children should be disabled, that's why this is done recursively.
visible = None
if(hide!=None):
visible = not hide
else:
visible = self.determine_object_visibility(obj)
if(visible!=None):
obj.hide_viewport = not visible
obj.hide_render = not visible
if(obj.hide_viewport == False):
self.activate_vertex_groups_and_shape_keys(obj)
else:
hide = True
# Recursion
for child in obj.children:
do_child(rig, child, hide)
# Gathering relevant data
hide = False if self.show_all_meshes else None
rig = self.get_rig()
# Recursively determining object visibilities
for child in rig.children:
do_child(rig, child, hide)
def update_bone_location(self, context):
""" Custom bone locations for each character can be specified in any bone in the rig, by adding a custom property to the bone named "BoneName_CharacterName".
The value of the custom property has to be a list of 3 floats containing edit_bone coordinates.
This function will move such bones to the specified coordinates in edit mode.
"""
rig = self.get_rig()
orig_mode = rig.mode
bpy.ops.object.mode_set(mode='EDIT')
active_character = self.metsrig_chars
# Moving the bones
for b in rig.pose.bones:
# The custom properties' names that store the bone locations are prefixed with the bone's name, eg. "Eye.L_Yennefer"
# This is necessary because of this bug: https://developer.blender.org/T61386
# If it ever gets fixed, next line can be changed (along with the custom property names)
chars = [p.replace(b.name + "_", "") for p in b.keys()]
if(active_character not in chars): continue
edit_bone = rig.data.edit_bones.get(b.name)
char_vector = b[b.name + "_" + active_character].to_list()
tail_relative = edit_bone.tail - edit_bone.head
edit_bone.head = char_vector
edit_bone.tail = edit_bone.head + tail_relative
bpy.ops.object.mode_set(mode=orig_mode)
def update_bone_layers(self, context):
""" Makes sure that outfit/hair bones that aren't being used are on a designated trash layer.
For this to work, all outfit and hair bones need to be organized into bonegroups named after the hair/outfit itself, prefixed with "Hair_" or "Outfit_" respectively.
"""
# Gathering info
rig = self.get_rig()
outfit_bone = rig.pose.bones.get('Properties_Outfit_'+self.metsrig_outfits)
character_bone = rig.pose.bones.get('Properties_Character_'+self.metsrig_chars)
if(character_bone == None): return # Every character should have a character bone which should contain at least a "Hair" property.
active_hair = self.metsrig_hairs
# Moving bones to designated layers.
for b in rig.data.bones:
bg = rig.pose.bones[b.name].bone_group
if(bg==None): continue
if('Hair' in bg.name):
hairs = bg.name.replace("Hair_", "").split(",")
if(active_hair in hairs ):
b.layers[10] = True
else:
b.layers[10] = False
elif('Outfit' in bg.name):
outfits = bg.name.replace("Outfit_", "").split(",")
if(self.metsrig_outfits in outfits):
b.layers[9] = True
else:
b.layers[9] = False
elif('Character' in bg.name):
characters = bg.name.replace("Character_", "").split(",")
if(self.metsrig_chars in characters):
b.layers[8] = True
else:
b.layers[8] = False
def update_bool_properties(self, context):
""" Create BoolProperties out of those outfit/character properties whose min==0 and max==1.
These BoolProperties are necessarry because only BoolProperties can be displayed in the UI as toggle buttons.
"""
rig = self.get_rig()
bool_props = rig.data.metsrig_boolproperties
bool_props.clear() # Nuke all the bool properties
outfit_bone = rig.pose.bones.get("Properties_Outfit_" + self.metsrig_outfits)
character_bone = rig.pose.bones.get("Properties_Character_" + self.metsrig_chars)
for prop_owner in [outfit_bone, character_bone]:
if(prop_owner==None): continue
for p in prop_owner.keys():
if( type(prop_owner[p]) != int or p.startswith("_") ): continue
min = prop_owner['_RNA_UI'].to_dict()[p]['min']
max = prop_owner['_RNA_UI'].to_dict()[p]['max']
if(min==0 and max==1):
new_bool = bool_props.add()
new_bool.name = p
new_bool.value = prop_owner[p]
new_bool.rig = rig
def outfits(self, context):
""" Callback function for finding the list of available outfits for the metsrig_outfits enum.
"""
rig = self.get_rig()
chars = [self.metsrig_chars]
if(self.metsrig_sets == 'Generic'):
chars = ['Generic']
elif(self.metsrig_sets == 'All'):
chars = [b.name.replace("Properties_Character_", "") for b in rig.pose.bones if "Properties_Character_" in b.name]
outfits = []
for char in chars:
char_bone = rig.pose.bones.get("Properties_Character_"+char)
if(not char_bone): continue
outfits.extend([c.name.replace("Properties_Outfit_", "") for c in char_bone.children if "Properties_Outfit_" in c.name])
items = []
for i, outfit in enumerate(outfits):
items.append((outfit, outfit, outfit, i)) # Identifier, name, description, can all be the character name.
return items
def chars(self, context):
""" Callback function for finding the list of available chars for the metsrig_chars enum.
"""
items = []
chars = [b.name.replace("Properties_Character_", "") for b in self.get_rig().pose.bones if "Properties_Character_" in b.name]
for char in chars:
if(char=='Generic'): continue
items.append((char, char, char))
return items
def hairs(self, context):
""" Callback function for finding the list of available hairs for the metsrig_hairs enum. """
rig = self.get_rig()
hairs = []
char_bones = [b for b in rig.pose.bones if "Properties_Character_" in b.name]
outfit_bones = [b for b in rig.pose.bones if "Properties_Outfit_" in b.name]
for bone in char_bones+outfit_bones:
if("Hair" not in bone): continue
bone_hairs = bone['Hair'].split(", ")
for bh in bone_hairs:
if(bh not in hairs):
hairs.append(bh)
items = [('None', 'None', 'None')]
for hair in hairs:
items.append((hair, hair, hair))
return items
def update_body_type(self, context):
""" Currently used to update body shape keys. """
# TODO: come up with a flexible solution for adding shape keys to the UI.
rig = self.get_rig()
char_bone = rig.pose.bones.get("Properties_Character_"+self.metsrig_chars)
outfit_bone = rig.pose.bones.get("Properties_Outfit_"+self.metsrig_outfits)
bodies = [o for o in rig.children if "Witcher3_Female_Body" in o.name] # TODO: Make this not Witcher specific.
for body in bodies:
body_shapekeys = [sk for sk in body.data.shape_keys.key_blocks if sk.name.startswith("body_")]
for bsk in body_shapekeys:
if( bsk.name == "body_" + str(rig.data['body']) ):
bsk.value = 1
else:
bsk.value = 0
def update_material_controller(self, bone):
# Updating material controller with this character's values.
rig = self.get_rig()
if('material_controller' in rig.data):
controller_nodegroup = bpy.data.node_groups.get(rig.data['material_controller'])
if(controller_nodegroup):
# To update the material controller, we set the nodegroup's input default values to the desired values.
# A nodegroup's input and output default_values are normally ONLY used by Blender to set said values when a new instance of the node is created.
# Which is to say, changing the input/output default_values won't have any effect on existing nodes.
# To overcome this, value nodes inside the controller material copy the values of the input/output default_values via drivers.
# Why not just add the Value nodes themselves to the UI? Because they don't have min/max values, but nodegroup inputs/outputs do.
# Why hook up the input rather than the output? It makes no difference. It just made more sense to me this way.
# Why not both?
for io in [controller_nodegroup.inputs, controller_nodegroup.outputs]:
for i in io:
prop_value = None
if("_" + i.name in bone):
prop_value = bone["_" + i.name]
elif(i.name in bone):
prop_value = bone[i.name]
else:
continue
if( type(prop_value) in [int, float] ):
i.default_value = prop_value
else:
color = prop_value.to_list()
color.append(1)
i.default_value = color
def change_outfit(self, context):
""" Update callback of metsrig_outfits enum. """
rig = self.get_rig()
char_bone = rig.pose.bones.get("Properties_Character_"+self.metsrig_chars)
if( (self.metsrig_outfits == '') ):
self.metsrig_outfits = self.outfits(context)[0][0]
outfit_bone = rig.pose.bones.get("Properties_Outfit_"+self.metsrig_outfits)
if('_body' in outfit_bone):
rig.data['body'] = outfit_bone['_body']
elif('_body' in char_bone):
rig.data['body'] = char_bone['_body']
else:
pass
#rig.data['body'] = 0 #TODO make this work better...
if('Hair' in outfit_bone):
self.metsrig_hairs = outfit_bone['Hair']
else:
self.update_meshes(context)
self.update_bone_layers(context)
#self.update_node_values(context)
self.update_bool_properties(context)
def change_characters(self, context):
""" Update callback of metsrig_chars enum.
"""
rig = self.get_rig()
char_bone = rig.pose.bones.get("Properties_Character_"+self.metsrig_chars)
if('Hair' in char_bone):
self.metsrig_hairs = char_bone['Hair'].split(", ")[0]
self.update_bone_location(context)
self.change_outfit(context) # Just to make sure the active outfit isn't None.
# TODO: Currently the only character with a unique nude body is Ciri. If another character has a unique body, we'll code this properly.
if(self.metsrig_chars == 'Ciri'):
rig.data['body_id'] = 2
else:
rig.data['body_id'] = 1
def change_hair(self, context):
self.update_meshes(context)
self.update_bone_layers(context)
metsrig_chars: EnumProperty(
name="Character",
items=chars,
update=change_characters)
metsrig_sets: EnumProperty(
name="Outfit Set",
description = "Set of outfits to choose from",
items={
('Character', "Canon Outfits", "Outfits of the selected character", 1),
('Generic', "Generic Outfits", "Outfits that don't belong to a character", 2),
('All', "All Outfits", "All outfits, including those of other characters", 3)
},
default='Character',
update=change_outfit)
metsrig_outfits: EnumProperty(
name="Outfit",
items=outfits,
update=change_outfit)
metsrig_hairs: EnumProperty(
name="Hairstyle",
items=hairs,
update=change_hair)
def update_physics(self, context):
""" Handle input to the Physics settings found under MetsRig Extras. """
# Saving and resetting speed multiplier
speed_mult = 0
if(self.physics_speed_multiplier != ""): # This is important to avoid inifnite looping the callback function.
try:
speed_mult = float(eval(self.physics_speed_multiplier))
except ValueError:
pass
self.physics_speed_multiplier = ""
rig = self.get_rig()
colors = {
'CLOTH' : (0, 1, 0, 1),
'COLLISION' : (1, 0, 0, 1),
}
for o in bpy.context.view_layer.objects:
if(o.parent != rig): continue
# Toggling all physics modifiers
for m in o.modifiers:
if( m.type=='CLOTH' or
m.type=='COLLISION' or (
"phys" in m.name.lower() and (
( m.type=='MESH_DEFORM' ) or
( m.type=='SURFACE_DEFORM')
)
)
):
# Change object color and save original
if(self.physics_toggle and m.type in colors ):
o['org_color'] = o.color
o.color = colors[m.type]
elif('org_color' in o):
o.color = o['org_color'].to_list()
del(o['org_color'])
m.show_viewport = self.physics_toggle
m.show_render = self.physics_toggle
if(m.type!='CLOTH'): continue
# Applying Speed multiplier
if(speed_mult != 0):
m.settings.time_scale = m.settings.time_scale * speed_mult
# Setting Start/End frame (with nudge)
if(self.physics_cache_start != m.point_cache.frame_start and
self.physics_cache_start > self.physics_cache_end):
self.physics_cache_end = self.physics_cache_start + 1
elif(self.physics_cache_end != m.point_cache.frame_end and
self.physics_cache_end < self.physics_cache_start):
self.physics_cache_start = self.physics_cache_end - 1
m.point_cache.frame_start = self.physics_cache_start
m.point_cache.frame_end = self.physics_cache_end
# Toggling Physics bone constraints
for b in rig.pose.bones:
for c in b.constraints:
if("phys" in c.name.lower()):
c.mute = not self.physics_toggle
# Toggling Physics collection(s)...
for collection in get_children_recursive(bpy.context.scene.collection):
if(type(collection) != bpy.types.Collection): continue
if(rig in collection.objects[:]):
for rig_collection in collection.children:
if( 'phys' in rig_collection.name.lower() ):
rig_collection.hide_viewport = not self.physics_toggle
rig_collection.hide_render = not self.physics_toggle
break
break
physics_toggle: BoolProperty(
name='Physics',
description='Toggle Physics systems (Enables Physics collection and Cloth, Mesh Deform, Surface Deform modifiers, etc)',
update=update_physics)
physics_speed_multiplier: StringProperty(
name='Apply multiplier to physics speed',
description = 'Apply entered multiplier to physics speed. The default physics setups are made for 60FPS. If you are animating at 30FPS, enter 2 here once. If you entered 2, you have to enter 0.5 to get back to the original physics speed',
default="",
update=update_physics)
physics_cache_start: IntProperty(
name='Physics Frame Start',
default=1,
update=update_physics,
min=0, max=1048573)
physics_cache_end: IntProperty(
name='Physics Frame End',
default=1,
update=update_physics,
min=1, max=1048574)
def update_shrinkwrap_targets(self, context):
""" Update the target object of any Shrinkwrap constraints named "Shrinkwrap_Anus" or "Shrinkwrap_Vagina", to match the object selected in the UI.
"""
rig = self.get_rig()
target_anus = self.shrinkwrap_target_anus
target_vagina = self.shrinkwrap_target_vagina
# Dictionary to map the expected constraint name on a bone to the shrinkwrap target.
# eg. Anus bones are expected to have a constraint called "shrinkwrap_anus".
targets = {"shrinkwrap_anus" : target_anus,
"shrinkwrap_vagina" : target_vagina }
for b in rig.pose.bones:
for c in b.constraints:
if(c.type=='SHRINKWRAP'):
for target_string in targets.keys():
if( c.name.lower() == target_string ):
c.target = targets[target_string]
shrinkwrap_target_anus: PointerProperty(
name='Sticky Anus Target',
description = 'Select object to which the anus should stick to. Warning: Can cause serious hit to performance',
type=bpy.types.Object,
update=update_shrinkwrap_targets)
shrinkwrap_target_vagina: PointerProperty(
name='Sticky Vagina Target',
description = 'Select object to which the vagina should stick to. Warning: Can cause serious hit to performance',
type=bpy.types.Object,
update=update_shrinkwrap_targets)
def update_render_modifiers(self, context):
""" Callback function for render_modifiers. Toggles SubSurf, Solidify, Bevel modifiers according to input from the UI.
"""
for o in get_children_recursive(self.get_rig()):
try: # Try block is here because for some reason if an object is deleted by user, this throws an error.
if( o.type != 'MESH' ): continue
except:
continue
for m in o.modifiers:
if(m.type in ['SOLIDIFY', 'BEVEL']):
m.show_viewport = self.render_modifiers
if(m.type == 'SUBSURF'):
m.show_viewport = True
m.levels = m.render_levels * self.render_modifiers
render_modifiers: BoolProperty(
name='render_modifiers',
description='Enable SubSurf, Solidify, Bevel, etc. modifiers in the viewport',
update=update_render_modifiers)
show_all_meshes: BoolProperty(
name='show_all_meshes',
description='Enable all child meshes of this armature',
update=update_meshes)
def update_ik(self, context):
""" Callback function for FK/IK switch values.
For this to work:
IK constraints are expected to be named according to the 'ik_prop_names' list.
Finger bones are expected to be named according to the 'finger_names' list.
"""
rig = self.get_rig()
ik_prop_names = ["ik_spine", "ik_arm_left", "ik_arm_right", "ik_leg_left", "ik_leg_right", "ik_fingers_left", "ik_fingers_right"]
finger_names = ["thumb", "index", "middle", "ring", "pinky"]
for b in rig.pose.bones:
for c in b.constraints:
name = c.name.lower()
if(not self.ik_per_finger):
for fn in finger_names:
if(fn in name):
if("_ik.l" in name):
name = "ik_fingers_left"
elif("_ik.r" in name):
name = "ik_fingers_right"
#continue # TODO is this break right?
break
if(name in ik_prop_names):
c.influence = getattr(self, name)
### FK/IK Properties
ik_spine: FloatProperty(
name='FK/IK Spine',
default=0,
update=update_ik,
min=0, max=1 )
ik_leg_left: FloatProperty(
name='FK/IK Left Leg',
default=0,
update=update_ik,
min=0, max=1 )
ik_leg_right: FloatProperty(
name='FK/IK Right Leg',
default=0,
update=update_ik,
min=0, max=1 )
ik_arm_left: FloatProperty(
name='FK/IK Left Arm',
default=0,
update=update_ik,
min=0, max=1 )
ik_arm_right: FloatProperty(
name='FK/IK Right Arm',
default=0,
update=update_ik,
min=0, max=1 )
ik_fingers_right: FloatProperty(
name='Right Fingers',
default=0,
update=update_ik,
min=0, max=1 )
ik_fingers_left: FloatProperty(
name='Left Fingers',
default=0,
update=update_ik,
min=0, max=1 )
ik_per_finger: BoolProperty(
name='Per Finger',
description='Control Ik/FK on individual fingers',
update=update_ik)
class MetsRigUI(bpy.types.Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'MetsRig'
@classmethod
def poll(cls, context):
if(context.object != None and
context.object in MetsRig_Properties.get_rigs()):
return True
@staticmethod
def safe_prop(ui, data, property, text="", text_ctxt="", translate=True, icon='NONE', expand=False, slider=False, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1, icon_value=0):
""" I often want to call layout.prop() without raising an exception if the property doesn't exist.
"""
# TODO: Do I still?
if(hasattr(data, property) or property[2:-2] in data):
# TODO: Isn't this redundant? can't we just replace empty strings with None?