From 4808d288e287a72b8e6d33207946ca36f3c9d38c Mon Sep 17 00:00:00 2001 From: 989onan Date: Thu, 4 Jul 2024 16:38:31 -0400 Subject: [PATCH] Added Resonite Tools - Added Resonite translation with automatic marking. If this doesn't work, please improve the dictionary, or hook it along with others into a better translation layer to deal with identifying body parts - Added Resonite export, which simply exports the model with the proper settings as a GLB/GLTF for more graceful importing and editing within the game. --- .vscode/settings.json | 0 core/common.py | 18 +++++ core/dictionaries.py | 112 +++++++++++++++++++++++++++++++ core/export_resonite.py | 35 ++++++++++ functions/resonite_functions.py | 114 ++++++++++++++++++++++++++++++++ ui/quick_access.py | 26 ++++---- ui/tools.py | 22 ++++++ 7 files changed, 315 insertions(+), 12 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 core/dictionaries.py create mode 100644 core/export_resonite.py create mode 100644 functions/resonite_functions.py create mode 100644 ui/tools.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e69de29 diff --git a/core/common.py b/core/common.py index b052b2c..6e08f93 100644 --- a/core/common.py +++ b/core/common.py @@ -1,6 +1,8 @@ import bpy import numpy as np +from .dictionaries import bone_names +from typing import List, Optional from bpy.types import Object, ShapeKey, Mesh, Context from functools import lru_cache @@ -39,3 +41,19 @@ def has_shapekeys(mesh_obj: Object) -> bool: @lru_cache(maxsize=None) def _get_shape_key_co(shape_key: ShapeKey) -> np.ndarray: return np.array([v.co for v in shape_key.data]) + +def simplify_bonename(n): + return n.lower().translate(dict.fromkeys(map(ord, u" _."))) + +def get_armature(context, armature_name=None) -> Optional[Object]: + if armature_name: + obj = bpy.data.objects[armature_name] + if obj.type == "ARMATURE": + return obj + else: + return None + if context.view_layer.objects.active: + obj = context.view_layer.objects.active + if obj.type == "ARMATURE": + return obj + return next((obj for obj in context.view_layer.objects if obj.type == 'ARMATURE'), None) diff --git a/core/dictionaries.py b/core/dictionaries.py new file mode 100644 index 0000000..cafc84a --- /dev/null +++ b/core/dictionaries.py @@ -0,0 +1,112 @@ +# GPL Licence + + +# Bone names from https://github.com/triazo/immersive_scaler/ +# Note from @989onan: Please make sure to make your names are lowercase in this array. I banged my head metaphorically till I figured that out... +# Taken from Tuxedo/Cats +bone_names = { + "right_shoulder": ["rightshoulder", "shoulderr", "rshoulder"], + "right_arm": ["rightarm", "armr", "rarm", "upperarmr", "rupperarm", "rightupperarm", "upperarmright", "uparmr", "ruparm"], + "right_elbow": ["rightelbow", "elbowr", "relbow", "lowerarmr", "rightlowerarm", "lowerarmr","rlowerarm", "lowerarmright", "lowarmr", "rlowarm", "forearmr","rforearm"], + "right_wrist": ["rightwrist", "wristr", "rwrist", "handr", "righthand", "rhand"], + + #hand l fingers + "pinkie_0_r": ["littlefinger0r","pinkie0r","rpinkie0","pinkiemetacarpalr"], + "pinkie_1_r": ["littlefinger1r","pinkie1r","rpinkie1","pinkieproximalr"], + "pinkie_2_r": ["littlefinger2r","pinkie2r","rpinkie2","pinkieintermediater"], + "pinkie_3_r": ["littlefinger3r","pinkie3r","rpinkie3","pinkiedistalr"], + + "ring_0_r": ["ringfinger0r","ring0r","rring0","ringmetacarpalr"], + "ring_1_r": ["ringfinger1r","ring1r","rring1","ringproximalr"], + "ring_2_r": ["ringfinger2r","ring2r","rring2","ringintermediater"], + "ring_3_r": ["ringfinger3r","ring3r","rring3","ringdistalr"], + + "middle_0_r": ["middlefinger0r","middle0r","rmiddle0","middlemetacarpalr"], + "middle_1_r": ["middlefinger1r","middle1r","rmiddle1","middleproximalr"], + "middle_2_r": ["middlefinger2r","middle2r","rmiddle2","middleintermediater"], + "middle_3_r": ["middlefinger3r","middle3r","rmiddle3","middledistalr"], + + "index_0_r": ["indexfinger0r","index0r","rindex0","indexmetacarpalr"], + "index_1_r": ["indexfinger1r","index1r","rindex1","indexproximalr"], + "index_2_r": ["indexfinger2r","index2r","rindex2","indexintermediater"], + "index_3_r": ["indexfinger3r","index3r","rindex3","indexdistalr"], + + "thumb_0_r": ["thumb0r","rthumb0","thumbmetacarpalr"], + "thumb_1_r": ['thumb1r',"rthumb1","thumbproximalr"], + "thumb_2_r": ['thumb2r',"rthumb2","thumbintermediater"], + "thumb_3_r": ['thumb3r',"rthumb3","thumbdistalr"], + + "right_leg": ["rightleg", "legr", "rleg", "upperlegr", "rupperleg", "thighr", "rightupperleg", "upperlegright", "uplegr", "rupleg"], + "right_knee": ["rightknee", "kneer", "rknee", "lowerlegr", "calfr", "rlowerleg", "rcalf", "rightlowerleg", "lowerlegright", "lowlegr", "rlowleg"], + "right_ankle": ["rightankle", "ankler", "rankle", "footright", "footr", "rfoot", "rightfoot", "rightfeet", "feetright", "rfeet", "feetr"], + "right_toe": ["righttoe", "toeright", "toer", "rtoe", "toesr", "rtoes"], + + "left_shoulder": ["leftshoulder", "shoulderl", "lshoulder"], + "left_arm": ["leftarm", "arml", "rarm", "upperarml", "lupperarm", "leftupperarm", "upperarmleft", "uparml", "luparm"], + "left_elbow": ["leftelbow", "elbowl", "lelbow", "lowerarml", "leftlowerarm", "lowerarmleft", "lowerarml", "llowerarm", "lowarml", "llowarm", "forearml","lforearm"], + "left_wrist": ["leftwrist", "wristl", "lwrist", "handl", "lefthand", "lhand"], + + #hand l fingers + + "pinkie_0_l": ["pinkiefinger0l","pinkie0l","lpinkie0","pinkiemetacarpall"], + "pinkie_1_l": ["littlefinger1l","pinkie1l","lpinkie1","pinkieproximall"], + "pinkie_2_l": ["littlefinger2l","pinkie2l","lpinkie2","pinkieintermediatel"], + "pinkie_3_l": ["littlefinger3l","pinkie3l","lpinkie3","pinkiedistall"], + + "ring_0_l": ["ringfinger0l","ring0l","lring0","ringmetacarpall"], + "ring_1_l": ["ringfinger1l","ring1l","lring1","ringproximall"], + "ring_2_l": ["ringfinger2l","ring2l","lring2","ringintermediatel"], + "ring_3_l": ["ringfinger3l","ring3l","lring3","ringdistall"], + + "middle_0_l": ["middlefinger0l","middle_0l","lmiddle0","middlemetacarpall"], + "middle_1_l": ["middlefinger1l","middle_1l","lmiddle1","middleproximall"], + "middle_2_l": ["middlefinger2l","middle_2l","lmiddle2","middleintermediatel"], + "middle_3_l": ["middlefinger3l","middle_3l","lmiddle3","middledistall"], + + "index_0_l": ["indexfinger0l","index0l","lindex0","indexmetacarpall"], + "index_1_l": ["indexfinger1l","index1l","lindex1","indexproximall"], + "index_2_l": ["indexfinger2l","index2l","lindex2","indexintermediatel"], + "index_3_l": ["indexfinger3l","index3l","lindex3","indexdistall"], + + "thumb_0_l": ["thumb0l","lthumb0","thumbmetacarpall"], + "thumb_1_l": ['thumb1l',"lthumb1","thumbproximall"], + "thumb_2_l": ['thumb2l',"lthumb2","thumbintermediatel"], + "thumb_3_l": ['thumb3l',"lthumb3","thumbdistall"], + + "left_leg": ["leftleg", "legl", "lleg", "upperlegl", "lupperleg", "thighl", "leftupperleg", "upperlegleft", "uplegl", "lupleg"], + "left_knee": ["leftknee", "kneel", "lknee", "lowerlegl", "llowerleg", "calfl", "lcalf", "leftlowerleg", "lowerlegleft", 'lowlegl', 'llowleg'], + "left_ankle": ["leftankle", "anklel", "rankle", "footleft", "footl", "lfoot", "leftfoot", "leftfeet", "feetleft", "lfeet", "feetl"], + "left_toe": ["lefttoe", "toeleft", "toel", "ltoe", "toesl", "ltoes"], + + "hips": ["pelvis", "hips", "hip"], + "spine": ["torso", "spine"], + "chest": ["chest"], + "upper_chest": ["upperchest", "chestupper"], + "neck": ["neck"], + "head": ["head", "cabeza"], + "left_eye": ["eyeleft", "lefteye", "eyel", "leye"], + "right_eye": ["eyeright", "righteye", "eyer", "reye"], +} + +# array taken from cats +dont_delete_these_main_bones = [ + 'Hips', 'Spine', 'Chest', 'Upper Chest', 'Neck', 'Head', + 'Left leg', 'Left knee', 'Left ankle', 'Left toe', + 'Right leg', 'Right knee', 'Right ankle', 'Right toe', + 'Left shoulder', 'Left arm', 'Left elbow', 'Left wrist', + 'Right shoulder', 'Right arm', 'Right elbow', 'Right wrist', + 'LeftEye', 'RightEye', 'Eye_L', 'Eye_R', + 'Left leg 2', 'Right leg 2', + + 'Thumb0_L', 'Thumb1_L', 'Thumb2_L', + 'IndexFinger1_L', 'IndexFinger2_L', 'IndexFinger3_L', + 'MiddleFinger1_L', 'MiddleFinger2_L', 'MiddleFinger3_L', + 'RingFinger1_L', 'RingFinger2_L', 'RingFinger3_L', + 'LittleFinger1_L', 'LittleFinger2_L', 'LittleFinger3_L', + + 'Thumb0_R', 'Thumb1_R', 'Thumb2_R', + 'IndexFinger1_R', 'IndexFinger2_R', 'IndexFinger3_R', + 'MiddleFinger1_R', 'MiddleFinger2_R', 'MiddleFinger3_R', + 'RingFinger1_R', 'RingFinger2_R', 'RingFinger3_R', + 'LittleFinger1_R', 'LittleFinger2_R', 'LittleFinger3_R', +] \ No newline at end of file diff --git a/core/export_resonite.py b/core/export_resonite.py new file mode 100644 index 0000000..5603773 --- /dev/null +++ b/core/export_resonite.py @@ -0,0 +1,35 @@ +import bpy + +from typing import List, Optional +from .common import get_armature +from bpy.types import Object, ShapeKey, Mesh, Context, Operator +from functools import lru_cache +from ..core.register import register_wrap + + +@register_wrap +class ExportResonite(Operator): + bl_idname = 'avatar_toolkit.export_resonite' + bl_label = "Export to Resonite" + bl_description = "Export a GLB with all animations and materials. For animation data see: " + bl_options = {'REGISTER', 'UNDO'} + filepath: bpy.props.StringProperty() + + + @classmethod + def poll(cls, context: Context): + if get_armature(context) is None: + return False + return True + + def execute(self, context: Context): + #settings stolen from cats. + bpy.ops.export_scene.gltf('INVOKE_AREA', + export_image_format = 'WEBP', + export_image_quality = 75, + export_materials = 'EXPORT', + export_animations = True, + export_animation_mode = 'ACTIONS', + export_nla_strips_merged_animation_name = 'Animation', + export_nla_strips = True) + return {'FINISHED'} \ No newline at end of file diff --git a/functions/resonite_functions.py b/functions/resonite_functions.py new file mode 100644 index 0000000..adaeead --- /dev/null +++ b/functions/resonite_functions.py @@ -0,0 +1,114 @@ +import bpy +from ..core.register import register_wrap +from typing import List, Optional +import re +from bpy.types import Operator, Context, Object +from ..core.dictionaries import bone_names +from ..core.common import get_armature, simplify_bonename + +@register_wrap +class ConvertToResonite(Operator): + bl_idname = 'avatar_toolkit.convert_to_resonite' + bl_label = "Convert to Resonite" #t('Tools.convert_to_resonite.label') + bl_description = "Converts bone names on a model to names compatable with Resonite" #t('Tools.convert_to_resonite.desc') + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context: Context) -> bool: + if not get_armature(context): + return False + return True + + def execute(self, context: Context) -> set: + armature = get_armature(context) + + translate_bone_fails = 0 + untranslated_bones = set() + + reverse_bone_lookup = dict() + for (preferred_name, name_list) in bone_names.items(): + for name in name_list: + reverse_bone_lookup[name] = preferred_name + + resonite_translations = { + 'hips': "Hips", + 'spine': "Spine", + 'chest': "Chest", + 'neck': "Neck", + 'head': "Head", + 'left_eye': "Eye.L", + 'right_eye': "Eye.R", + 'right_leg': "UpperLeg.R", + 'right_knee': "Calf.R", + 'right_ankle': "Foot.R", + 'right_toe': 'Toes.R', + 'right_shoulder': "Shoulder.R", + 'right_arm': "UpperArm.R", + 'right_elbow': "ForeArm.R", + 'right_wrist': "Hand.R", + 'left_leg': "UpperLeg.L", + 'left_knee': "Calf.L", + 'left_ankle': "Foot.L", + 'left_toe': "Toes.L", + 'left_shoulder': "Shoulder.L", + 'left_arm': "UpperArm.L", + 'left_elbow': "ForeArm.L", + 'left_wrist': "Hand.R", + + 'pinkie_1_l': "pinkie1.L", + 'pinkie_2_l': "pinkie2.L", + 'pinkie_3_l': "pinkie3.L", + 'ring_1_l': "ring1.L", + 'ring_2_l': "ring2.L", + 'ring_3_l': "ring3.L", + 'middle_1_l': "middle1.L", + 'middle_2_l': "middle2.L", + 'middle_3_l': "middle3.L", + 'index_1_l': "index1.L", + 'index_2_l': "index2.L", + 'index_3_l': "index3.L", + 'thumb_1_l': "thumb1.L", + 'thumb_2_l': "thumb2.L", + 'thumb_3_l': "thumb3.L", + + 'pinkie_1_r': "pinkie1.R", + 'pinkie_2_r': "pinkie2.R", + 'pinkie_3_r': "pinkie3.R", + 'ring_1_r': "ring1.R", + 'ring_2_r': "ring2.R", + 'ring_3_r': "ring3.R", + 'middle_1_r': "middle1.R", + 'middle_2_r': "middle2.R", + 'middle_3_r': "middle3.R", + 'index_1_r': "index1.R", + 'index_2_r': "index2.R", + 'index_3_r': "index3.R", + 'thumb_1_r': "thumb1.R", + 'thumb_2_r': "thumb2.R", + 'thumb_3_r': "thumb3.R" + } + + + + context.view_layer.objects.active = armature + bpy.ops.object.mode_set(mode='EDIT') + + bpy.ops.object.mode_set(mode='OBJECT') + for bone in armature.data.bones: + if simplify_bonename(bone.name) in reverse_bone_lookup and reverse_bone_lookup[simplify_bonename(bone.name)] in resonite_translations: + bone.name = resonite_translations[reverse_bone_lookup[simplify_bonename(bone.name)]] + else: + untranslated_bones.add(bone.name) + + bone.name = re.compile(re.escape(""), re.IGNORECASE).sub("",bone.name)+"" + translate_bone_fails += 1 + + bpy.ops.object.mode_set(mode='OBJECT') + + if translate_bone_fails > 0: + self.report({'INFO'}, "Failed to translate {translate_bone_fails} bones to humanoid names. Adding \"\" to their names.".format(translate_bone_fails=translate_bone_fails)) + else: + self.report({'INFO'}, "Successfully translated all bones to humanoid names") + + + return {'FINISHED'} \ No newline at end of file diff --git a/ui/quick_access.py b/ui/quick_access.py index 80562b1..1d93c25 100644 --- a/ui/quick_access.py +++ b/ui/quick_access.py @@ -1,6 +1,7 @@ import bpy from ..core.register import register_wrap from .panel import AvatarToolkitPanel +from bpy.types import Context from ..core.import_pmx import import_pmx from ..core.import_pmd import import_pmd @@ -14,7 +15,7 @@ class AvatarToolkitQuickAccessPanel(bpy.types.Panel): bl_category = "Avatar Toolkit" bl_parent_id = "OBJECT_PT_avatar_toolkit" - def draw(self, context): + def draw(self, context: Context): layout = self.layout layout.label(text="Quick Access Options") @@ -33,14 +34,14 @@ class AVATAR_TOOLKIT_OT_import_menu(bpy.types.Operator): bl_idname = "avatar_toolkit.import_menu" bl_label = "Import Menu" - def execute(self, context): + def execute(self, context: Context): return {'FINISHED'} - def invoke(self, context, event): + def invoke(self, context: Context, event): wm = context.window_manager return wm.invoke_popup(self, width=200) - def draw(self, context): + def draw(self, context: Context): layout = self.layout layout.label(text="Select Import Method") layout.operator("avatar_toolkit.import_pmx", text="Import PMX") @@ -51,16 +52,17 @@ class AVATAR_TOOLKIT_OT_export_menu(bpy.types.Operator): bl_idname = "avatar_toolkit.export_menu" bl_label = "Export Menu" - def execute(self, context): + def execute(self, context: Context): return {'FINISHED'} - def invoke(self, context, event): + def invoke(self, context: Context, event): wm = context.window_manager return wm.invoke_popup(self, width=200) - def draw(self, context): + def draw(self, context: Context): layout = self.layout - layout.label(text="Export options will go here") + layout.label(text="Select Export Method") + layout.operator("avatar_toolkit.export_resonite", text="Export Resonite") @register_wrap class AVATAR_TOOLKIT_OT_import_pmx(bpy.types.Operator): @@ -69,11 +71,11 @@ class AVATAR_TOOLKIT_OT_import_pmx(bpy.types.Operator): filepath: bpy.props.StringProperty(subtype="FILE_PATH") - def execute(self, context): + def execute(self, context: Context): import_pmx(self.filepath) return {'FINISHED'} - def invoke(self, context, event): + def invoke(self, context: Context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} @@ -84,10 +86,10 @@ class AVATAR_TOOLKIT_OT_import_pmd(bpy.types.Operator): filepath: bpy.props.StringProperty(subtype="FILE_PATH") - def execute(self, context): + def execute(self, context: Context): import_pmd(self.filepath) return {'FINISHED'} - def invoke(self, context, event): + def invoke(self, context: Context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} diff --git a/ui/tools.py b/ui/tools.py new file mode 100644 index 0000000..408a95d --- /dev/null +++ b/ui/tools.py @@ -0,0 +1,22 @@ +import bpy +from ..core.register import register_wrap +from .panel import AvatarToolkitPanel +from bpy.types import Context + +@register_wrap +class AvatarToolkitToolsPanel(bpy.types.Panel): + bl_label = "Tools" + bl_idname = "OBJECT_PT_avatar_toolkit_tools" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = "Avatar Toolkit" + bl_parent_id = "OBJECT_PT_avatar_toolkit" + + def draw(self, context: Context): + layout = self.layout + layout.label(text="Tools") + layout.separator(factor=0.5) + + row = layout.row(align=True) + row.scale_y = 1.5 + row.operator("avatar_toolkit.convert_to_resonite", text="Translate to Resonite") \ No newline at end of file