From 0cd5459815856329ded22651d21c25a84587149a Mon Sep 17 00:00:00 2001 From: Menithal Date: Thu, 1 Nov 2018 00:25:57 +0200 Subject: [PATCH] Major Fix for Custom Bones --- hifi_tools/armature/panel.py | 64 +++++++++++++++++-- hifi_tools/files/fst/operator.py | 6 +- hifi_tools/files/fst/writer.py | 9 +-- hifi_tools/utils/bones.py | 80 +++++++++++++++--------- hifi_tools/utils/custom.py | 102 +++++++++++++++++++++---------- 5 files changed, 186 insertions(+), 75 deletions(-) diff --git a/hifi_tools/armature/panel.py b/hifi_tools/armature/panel.py index d774d04..31afe91 100644 --- a/hifi_tools/armature/panel.py +++ b/hifi_tools/armature/panel.py @@ -28,7 +28,10 @@ import hifi_tools import webbrowser -from hifi_tools.utils.bones import combine_bones, build_skeleton, retarget_armature, correct_scale_rotation, set_selected_bones_physical, remove_selected_bones_physical +from hifi_tools import default_gateway_server + +from hifi_tools.gateway import client as GatewayClient +from hifi_tools.utils.bones import combine_bones, build_skeleton, retarget_armature, correct_scale_rotation, set_selected_bones_physical, remove_selected_bones_physical, bone_connection from hifi_tools.armature.skeleton import structure as base_armature from hifi_tools.utils.mmd import convert_mmd_avatar_hifi from hifi_tools.utils.mixamo import convert_mixamo_avatar_hifi @@ -36,11 +39,8 @@ from hifi_tools.utils.materials import make_materials_fullbright, make_materials_shadeless, convert_to_png, convert_images_to_mask, remove_materials_metallic from hifi_tools.utils.custom import HifiCustomAvatarBinderOperator -from hifi_tools.gateway import client as GatewayClient from bpy.props import StringProperty -from hifi_tools import default_gateway_server - # TODO: Move somewhere more sensible, this contains alot of other UI stuff not just armature @@ -85,6 +85,9 @@ def draw(self, context): layout.operator(HifiSetBonePhysicalOperator.bl_idname) layout.operator(HifiRemoveBonePhysicalOperator.bl_idname) layout.operator(HifiCombineBonesOperator.bl_idname) + layout.operator(HifiCombineBonesNonConnectedOperator.bl_idname) + layout.operator(HifiConnectBones.bl_idname) + layout.operator(HifiUnconnectBones.bl_idname) return None @@ -297,6 +300,54 @@ def execute(self, context): return {'FINISHED'} +class HifiConnectBones(bpy.types.Operator): + bl_idname = "bones_connect_selected.hifi" + bl_label = "Connect Selected " + + bl_space_type = "VIEW_3D" + bl_region_type = "TOOLS" + bl_category = "High Fidelity" + + def execute(self, context): + bone_connection(context.selected_editable_bones, True) + return {'FINISHED'} + +class HifiUnconnectBones(bpy.types.Operator): + bl_idname = "bones_deconnect_selected.hifi" + bl_label = "Deconnect Selected " + + bl_space_type = "VIEW_3D" + bl_region_type = "TOOLS" + bl_category = "High Fidelity" + + def execute(self, context): + bone_connection(context.selected_editable_bones, False) + return {'FINISHED'} + + + +class HifiCombineBonesNonConnectedOperator(bpy.types.Operator): + bl_idname = "bone_combine_detached.hifi" + bl_label = "Combine Bones Detached" + + bl_space_type = "VIEW_3D" + bl_region_type = "TOOLS" + bl_category = "High Fidelity" + + @classmethod + def poll(self, context): + return len(context.selected_bones) > 1 + + def execute(self, context): + + use_mirror_x = bpy.context.object.data.use_mirror_x + bpy.context.object.data.use_mirror_x = False + combine_bones(list(context.selected_bones), + context.active_bone, context.active_object, False) + bpy.context.object.data.use_mirror_x = use_mirror_x + return {'FINISHED'} + + class HifiCustomAvatarOperator(bpy.types.Operator): bl_idname = "armature_toolset_fix_custom_avatar.hifi" bl_label = "Custom Avatar" @@ -449,6 +500,7 @@ def draw(self, context): row = layout.row() row.label(self.bl_label) + class HifiForumOperator(bpy.types.Operator): bl_idname = "forum.hifi" bl_label = "Forum Thread / Bug Reports" @@ -467,7 +519,6 @@ def execute(self, context): return {'FINISHED'} - classes = [ HifiArmaturePanel, HifiMaterialsPanel, @@ -491,7 +542,8 @@ def execute(self, context): HifiCustomAvatarOperator, HifiFixScaleOperator, HifiForumOperator, - HifiCustomAvatarBinderOperator + HifiCustomAvatarBinderOperator, + HifiCombineBonesNonConnectedOperator ] diff --git a/hifi_tools/files/fst/operator.py b/hifi_tools/files/fst/operator.py index b6469f6..f2b206d 100644 --- a/hifi_tools/files/fst/operator.py +++ b/hifi_tools/files/fst/operator.py @@ -162,8 +162,8 @@ class FSTWriterOperator(bpy.types.Operator, ExportHelper): selected_only = BoolProperty( default=False, name="Selected Only", description="Selected Only") - anim_graph_url = StringProperty(default="", name="Animation JSON Url", - description="Avatar Animation JSON absolute url path") + #anim_graph_url = StringProperty(default="", name="Animation JSON Url", + # description="Avatar Animation JSON absolute url path") script = StringProperty(default="", name="Avatar Script Path", description="Avatar Script absolute url path, Script that is run on avatar") @@ -188,7 +188,7 @@ def draw(self, context): oven_tool = context.user_preferences.addons[hifi_tools.__name__].preferences.oventool - layout.prop(self, "anim_graph_url") + #layout.prop(self, "anim_graph_url") layout.prop(self, "script") enabled_ipfs = len( diff --git a/hifi_tools/files/fst/writer.py b/hifi_tools/files/fst/writer.py index e2b9c31..46327f4 100644 --- a/hifi_tools/files/fst/writer.py +++ b/hifi_tools/files/fst/writer.py @@ -22,6 +22,7 @@ import os import uuid import hifi_tools +import datetime import os.path as ntpath import shutil @@ -65,7 +66,7 @@ prefix_free_joint = "freeJoint = $\n" prefix_script = "script = $\n" -prefix_anim_graph_url = "animGraphUrl = $\n" +#prefix_anim_graph_url = "animGraphUrl = $\n" def default_blend_shape(selected): @@ -82,7 +83,7 @@ def fst_export(context, selected): preferences = bpy.context.user_preferences.addons[hifi_tools.__name__].preferences # file = open - uuid_gen = uuid.uuid5(uuid.NAMESPACE_DNS, context.filepath) + uuid_gen = uuid.uuid5(uuid.NAMESPACE_DNS, context.filepath + '?' + str(datetime.datetime.now()).replace(" ", "")) scene_id = str(uuid_gen) print("Exporting file to filepath", context.filepath) @@ -128,8 +129,8 @@ def fst_export(context, selected): if context.flow: print("Add Flow Script") - if len(context.anim_graph_url) > 0: - f.write(prefix_anim_graph_url.replace('$', context.anim_graph_url)) + #if len(context.anim_graph_url) > 0: + # f.write(prefix_anim_graph_url.replace('$', context.anim_graph_url)) # Writing these in separate loops because they need to done in order. for bone in armature.data.bones: diff --git a/hifi_tools/utils/bones.py b/hifi_tools/utils/bones.py index 08a5b4d..875cdc9 100644 --- a/hifi_tools/utils/bones.py +++ b/hifi_tools/utils/bones.py @@ -28,8 +28,8 @@ from hifi_tools.armature.skeleton import structure as base_armature corrected_axis = { - "GLOBAL_NEG_Z": ["Shoulder", "Arm", "Hand", "Thumb"], - "GLOBAL_NEG_Y": ["Spine", "Head", "Hips", "Leg", "Foot", "Toe", "Eye"] + "GLOBAL_NEG_Z": ["Shoulder", "Arm", "Hand", "Thumb", "Leg", "Foot", "Toe", "Head", "Hips"], + "GLOBAL_NEG_Y": ["Eye", "Spine", "Neck", "Head"] } bone_parent_structure = { @@ -65,13 +65,29 @@ number_text_re = re.compile(".+(\\d+).*") blender_copy_re = re.compile("\.001$") end_re = re.compile("_end$") +mixamorif_prefix = "Mixamorig:" +mixamo_prefix = "mixamo:" +def nuke_mixamo_prefix(edit_bones): + print("Show. No. Mercy to mixamo. Remove All as a prelim") -def combine_bones(selected_bones, active_bone, active_object): + found = False + for bone in edit_bones: + if "mixamo" in bone.name.lower(): + found = True + bone.name = bone.name.replace( + mixamorif_prefix, "").replace(mixamo_prefix, "") + + if found: + print("Mixamo Purge Complete") + + return found + + +def combine_bones(selected_bones, active_bone, active_object, use_connect=True): print("----------------------") print("Combining Bones", len(selected_bones), "-", active_bone, "-", active_object) - edit_bones = list(active_object.data.edit_bones) meshes = mesh.get_mesh_from(active_object.children) names_to_combine = [] active_bone_name = active_bone.name @@ -85,7 +101,7 @@ def combine_bones(selected_bones, active_bone, active_object): active_object.data.edit_bones.remove(bone) # TODO: Removal is broken :( for child in children: - child.use_connect = True + child.use_connect = use_connect print("Combining weights.", meshes) bpy.ops.object.mode_set(mode="OBJECT") @@ -110,6 +126,11 @@ def combine_bones(selected_bones, active_bone, active_object): print("Done") +def bone_connection(selected_bones, mode=False): + for bone in selected_bones: + bone.use_connect = mode + + def scale_helper(obj): if obj.dimensions.y > 2.4: print("Avatar too large > 2.4m, maybe incorrect? setting height to 1.9m. You can scale avatar inworld, instead") @@ -126,7 +147,7 @@ def scale_helper(obj): bpy.ops.pose.transforms_clear() bpy.ops.object.mode_set(mode='OBJECT') - + def remove_all_actions(): for action in bpy.data.actions: @@ -175,7 +196,8 @@ def __init__(self, side, mirror, name, mirror_name): def camel_case_split(name): s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) - return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1) + s2 = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1) + return re.sub('(.)(\d+)', r'\1_\2', s2) def get_bone_side_and_mirrored(bone_name): @@ -217,10 +239,9 @@ def get_bone_side_and_mirrored(bone_name): def clean_up_bone_name(bone_name, remove_clones=True): - - cleaned_bones = camel_case_split(bone_name) - - cleaned_bones = bone_name.replace(".", "_").replace(" ", "_") + + cleaned_bones = camel_case_split( + bone_name).replace(".", "_").replace(" ", "_") split = cleaned_bones.split("_") # Remove .001 blender suffic First remove every dot with _ to remove confusion @@ -239,25 +260,22 @@ def clean_up_bone_name(bone_name, remove_clones=True): new_bone_split.append(bone_name) else: for idx, val in enumerate(split): - print(idx, val) - if val is "r": - new_bone_split.append("Right") - elif val is "l": - new_bone_split.append("Left") + if val.lower() == "r" or val.lower() == "right": + new_bone_split.insert(0, "Right") + elif val.lower() == "l" or val.lower() == "left": + new_bone_split.insert(0, "Left") elif number_text_re.match(val): nr = number_text_re.match(val) group = nr.groups() if end: - last = str(int(group[0]) + 1) + last = str(int(group[0]) + 1).capitalize() else: - last = group[0] + last = group[0].capitalize() elif number_re.match(val): # value is a number, before the last - print("Idx", idx, length) if idx < length: - print("Storing") last = val.capitalize() else: - new_bone_split.append(val) + new_bone_split.append(val.capitalize()) if last is not None: if end: @@ -279,6 +297,7 @@ def remove_selected_bones_physical(bones): if physical_re.search(bone.name) is not None: bone.name = physical_re.sub("", bone.name) + def correct_bone(bone, bones): if bone.name == "Hips": bone.parent = None @@ -289,12 +308,15 @@ def correct_bone(bone, bones): if parent_bone is not None: bone.parent = parent_bone + def correct_bone_parents(bones): for bone in bones: correct_bone(bone, bones) def correct_bone_rotations(obj): + + bpy.ops.object.mode_set(mode="EDIT") name = obj.name if "Eye" in name: bone_head = Vector(obj.head) @@ -319,18 +341,17 @@ def correct_bone_rotations(obj): axises = corrected_axis.keys() correction = None found = False - for axis in axises: corrections = corrected_axis.get(axis) for correction in corrections: if correction in name: - print("Found correction", name, axis) - bpy.ops.object.mode_set(mode="EDIT") + print("Correcting Rolls,", name, axis) bpy.ops.armature.select_all(action="DESELECT") - + print(obj) obj.select = True - bpy.ops.armature.calculate_roll(type=axis) + print(obj) + bpy.ops.armature.select_all(action="DESELECT") found = True break @@ -433,7 +454,7 @@ def correct_scale_rotation(obj, rotation): bpy.ops.object.select_all(action="DESELECT") obj.select = True - + bpy.context.scene.objects.active = obj bpy.ops.object.origin_set(type="ORIGIN_CURSOR") bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) @@ -478,7 +499,6 @@ def navigate_armature(data, current_rest_node, world_matrix, parent, parent_node def retarget_armature(options, selected, selected_only=False): armature = find_armature(selected) - print("selected", selected, "armature", armature) if armature is not None: # Center Children First print(bpy.context.mode, armature) @@ -491,11 +511,13 @@ def retarget_armature(options, selected, selected_only=False): bpy.context.scene.objects.active = armature armature.select = True + bpy.context.object.data.pose_position = 'POSE' + # Make sure to reset the bones first. bpy.ops.object.transform_apply( location=False, rotation=True, scale=True) print("Selecting Bones") - + bpy.ops.object.mode_set(mode="POSE") bpy.ops.pose.select_all(action="SELECT") bpy.ops.pose.transforms_clear() diff --git a/hifi_tools/utils/custom.py b/hifi_tools/utils/custom.py index 999135b..40cdf2f 100644 --- a/hifi_tools/utils/custom.py +++ b/hifi_tools/utils/custom.py @@ -41,15 +41,15 @@ def get_bones(self, context): foot_name = "foot" toe_name = "toe" -hand_name = "hand" +hand_name = ["hand", "wrist"] -hand_re = re.compile("hand$") +hand_re = re.compile("(hand)|(wrist)$") -hand_thumb_name = "thumb1" -hand_index_name = "index1" -hand_middle_name = "middle1" -hand_ring_name = "ring1" -hand_pinky_name = "pinky1" +hand_thumb_re = re.compile("thumb(finger)?1") +hand_index_re = re.compile("index(finger)?1") +hand_middle_re = re.compile("middle(finger)?1") +hand_ring_re = re.compile("ring(finger)?1") +hand_pinky_re = re.compile("(pinky)|(little)(finger)?1") spine_name = "spine" spine1_name = "spine1" @@ -59,7 +59,7 @@ def get_bones(self, context): def automatic_bind_bones(self, avatar_bones): print('------') - + knee_check = False for bone in avatar_bones: cleaned_name = bones.clean_up_bone_name(bone.name).lower() @@ -78,7 +78,7 @@ def automatic_bind_bones(self, avatar_bones): if neck_bone in cleaned_name: self.neck = bone.name - if "lowerarm" in cleaned_name or "forearm" in cleaned_name: + if "lowerarm" in cleaned_name or "forearm" in cleaned_name or "elbow" in cleaned_name: self.fore_arm = bone.name elif "arm" in cleaned_name or "upperarm" in cleaned_name: self.arm = bone.name @@ -89,28 +89,35 @@ def automatic_bind_bones(self, avatar_bones): if "upleg" in cleaned_name or "thigh" in cleaned_name or "upleg" in cleaned_name: self.up_leg = bone.name - elif "calf" in cleaned_name or "leg" in cleaned_name: + elif "knee" in cleaned_name or "calf" in cleaned_name: self.leg = bone.name - if foot_name in cleaned_name: + if "knee" in cleaned_name: + knee_check = True + + # Counter: If Knee exists somewhere, it is most likely that Leg is the upper leg. + if knee_check and "leg" in cleaned_name: + self.up_leg = bone.name + + if foot_name in cleaned_name or "ankle" in cleaned_name: self.foot = bone.name if toe_name in cleaned_name: self.toe = bone.name - if hand_thumb_name in cleaned_name: + if hand_thumb_re.search(cleaned_name): self.hand_thumb = bone.name - elif hand_index_name in cleaned_name: + elif hand_index_re.search(cleaned_name): self.hand_index = bone.name - elif hand_middle_name in cleaned_name: + elif hand_middle_re.search(cleaned_name): self.hand_middle = bone.name - elif hand_ring_name in cleaned_name: + elif hand_ring_re.search(cleaned_name): self.hand_ring = bone.name - elif hand_pinky_name in cleaned_name: + elif hand_pinky_re.search(cleaned_name): self.hand_pinky = bone.name elif hand_re.search(cleaned_name) is not None: @@ -131,6 +138,7 @@ def update_bone_name(edit_bones, from_name, to_name): def update_bone_name_mirrored(edit_bones, from_name, to_name): + mirrored = bones.get_bone_side_and_mirrored(from_name) if mirrored is not None: update_bone_name(edit_bones, from_name, mirrored.side + to_name) @@ -140,7 +148,7 @@ def update_bone_name_mirrored(edit_bones, from_name, to_name): def update_bone_name_chained_mirrored(edit_bones, from_name, to_name): bone = bones.get_bone_side_and_mirrored(from_name) - if bone.index is not None: + if bone is not None and bone.index is not None: update_bone_name_mirrored(edit_bones, from_name, to_name + bone.index) ebone = edit_bones[bone.name] next_index = str(int(bone.index)+1) @@ -150,17 +158,16 @@ def update_bone_name_chained_mirrored(edit_bones, from_name, to_name): def rename_bones_and_fix_most_things(self, context): - print("Rename bones fix most things", self.armatures) - if len(self.armatures) < 1: + print("Rename bones fix most things", self.armature) + if len(self.armature) < 1: print("Armature Update cancelled") return {"CANCELLED"} # Naming Converted bpy.ops.object.mode_set(mode="EDIT") - print("Armatures") - armature = bpy.data.armatures[self.armatures] + armature = bpy.data.armatures[self.armature] ebones = armature.edit_bones - + print("--------") print("Updating Bone Names") update_bone_name(ebones, self.hips, "Hips") update_bone_name(ebones, self.spine, "Spine") @@ -169,6 +176,7 @@ def rename_bones_and_fix_most_things(self, context): update_bone_name(ebones, self.neck, "Neck") update_bone_name(ebones, self.head, "Head") + print("--------") print("Updating Bone Names Mirrored") update_bone_name_mirrored(ebones, self.eye, "Eye") update_bone_name_mirrored(ebones, self.shoulder, "Shoulder") @@ -181,6 +189,7 @@ def rename_bones_and_fix_most_things(self, context): update_bone_name_mirrored(ebones, self.foot, "Foot") update_bone_name_mirrored(ebones, self.toe, "Toe") + print("--------") print("Updating Bone Names Chained Mirrored") update_bone_name_chained_mirrored(ebones, self.hand_thumb, "HandThumb") update_bone_name_chained_mirrored(ebones, self.hand_index, "HandIndex") @@ -188,39 +197,50 @@ def rename_bones_and_fix_most_things(self, context): update_bone_name_chained_mirrored(ebones, self.hand_ring, "HandRing") update_bone_name_chained_mirrored(ebones, self.hand_pinky, "HandPinky") + print("--------") + print("Fix Rotations") # Fixing Rotations and Scales + # Now Refresh datablocks + bpy.ops.object.mode_set(mode="OBJECT") + armature = bpy.data.armatures[self.armature] + bpy.ops.object.mode_set(mode="EDIT") + + ebones = armature.edit_bones + for bone in ebones: + bone.hide = False + bone.name = bones.clean_up_bone_name(bone.name) bones.correct_bone_rotations(bone) bones.correct_bone(bone, ebones) - + bones.correct_bone_parents(armature.edit_bones) bpy.ops.object.mode_set(mode="OBJECT") - + bpy.ops.object.select_all(action="DESELECT") children = bpy.data.objects - for child in children: + for child in children: if child.type == "ARMATURE": - + child.select = True - + bones.correct_scale_rotation(child, True) - bones.correct_bone_rotations(child) + bpy.ops.object.mode_set(mode="POSE") bpy.ops.pose.select_all(action="SELECT") bpy.ops.pose.transforms_clear() bpy.ops.pose.select_all(action="DESELECT") bpy.ops.object.mode_set(mode="OBJECT") if child.type == "MESH": - # mesh.clean_unused_vertex_groups(child) + # mesh.clean_unused_vertex_groups(child) materials.clean_materials(child.material_slots) for material in bpy.data.materials: materials.flip_material_specular(material) bpy.ops.object.mode_set(mode="OBJECT") - + return {"FINISHED"} @@ -232,6 +252,8 @@ class HifiCustomAvatarBinderOperator(bpy.types.Operator): armatures = bpy.props.EnumProperty( name="Select Armature", items=get_armatures) + armature = bpy.props.StringProperty() + hips = bpy.props.StringProperty() spine = bpy.props.StringProperty() spine1 = bpy.props.StringProperty() @@ -265,21 +287,35 @@ def execute(self, context): def invoke(self, context, event): if self.armatures: - data = context.scene.objects[self.armatures].data + armature = context.scene.objects[self.armatures] + bpy.ops.object.mode_set(mode="OBJECT") + + bpy.ops.object.select_all(action='DESELECT') + armature.select = True + context.scene.objects.active = armature + + bpy.ops.object.mode_set(mode="EDIT") + data = armature.data + self.armature = data.name + + bones.nuke_mixamo_prefix(data.edit_bones) + + bpy.ops.object.mode_set(mode="OBJECT") automatic_bind_bones(self, data.bones) - # self.hips = "Hips" return context.window_manager.invoke_props_dialog(self, width=600) def draw(self, context): layout = self.layout - layout.label("This is an Experimental Feature. Please comment in the forums if there are any issues with rebinding.") + layout.label( + "This is an Experimental Feature. Please comment in the forums if there are any issues with rebinding.") layout.label("Everything is mirrored.") column = layout.column() # column.prop_search(scene, "custom_current_armature", bpy.data, "armatures", icon='ARMATURE_DATA', text="Select Armature") column.prop(self, "armatures") # context.scene.object[self.armatures] + # TODO: If avatar is not selected by default. if self.armatures is not "": data = context.scene.objects[self.armatures].data