diff --git a/core/common.py b/core/common.py index 4b23aac..e5e817c 100644 --- a/core/common.py +++ b/core/common.py @@ -1,6 +1,6 @@ import bpy import numpy as np -from bpy.types import Context, Object, Modifier, EditBone +from bpy.types import Context, Object, Modifier, EditBone, Operator from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable from ..core.logging_setup import logger from ..core.translations import t @@ -485,3 +485,61 @@ def remove_unused_shapekeys(mesh_obj: Object, tolerance: float = 0.001) -> int: removed_count += 1 return removed_count + +def save_armature_state(armature: Object) -> Dict[str, Any]: + """Save current armature state for recovery""" + state = { + 'bones': {}, + 'pose': {}, + 'settings': {} + } + + # Save bone data + for bone in armature.data.bones: + state['bones'][bone.name] = { + 'head': bone.head_local.copy(), + 'tail': bone.tail_local.copy(), + 'roll': bone.roll, + 'parent': bone.parent.name if bone.parent else None + } + + # Save pose data if exists + if armature.pose: + for bone in armature.pose.bones: + state['pose'][bone.name] = { + 'location': bone.location.copy(), + 'rotation': bone.rotation_quaternion.copy(), + 'scale': bone.scale.copy() + } + + return state + +def restore_armature_state(armature: Object, state: Dict[str, Any]) -> None: + """Restore armature from saved state""" + bpy.ops.object.mode_set(mode='EDIT') + + # Restore bones + for name, data in state['bones'].items(): + if name in armature.data.edit_bones: + bone = armature.data.edit_bones[name] + bone.head = data['head'] + bone.tail = data['tail'] + bone.roll = data['roll'] + + # Restore parenting + for name, data in state['bones'].items(): + if data['parent'] and name in armature.data.edit_bones: + bone = armature.data.edit_bones[name] + if data['parent'] in armature.data.edit_bones: + bone.parent = armature.data.edit_bones[data['parent']] + + bpy.ops.object.mode_set(mode='POSE') + + # Restore pose if exists + if 'pose' in state: + for name, data in state['pose'].items(): + if name in armature.pose.bones: + bone = armature.pose.bones[name] + bone.location = data['location'] + bone.rotation_quaternion = data['rotation'] + bone.scale = data['scale'] diff --git a/core/dictionaries.py b/core/dictionaries.py index 3d5b8f2..4ec0b07 100644 --- a/core/dictionaries.py +++ b/core/dictionaries.py @@ -5,89 +5,97 @@ # 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", "valvebipedbip01rclavicle"], - "right_arm": ["rightarm", "armr", "rarm", "upperarmr", "rupperarm", "rightupperarm", "uparmr", "ruparm", "valvebipedbip01rupperarm"], - "right_elbow": ["rightelbow", "elbowr", "relbow", "lowerarmr", "rightlowerarm", "lowerarmr","rlowerarm", "lowarmr", "rlowarm", "forearmr","rforearm", "valvebipedbip01rforearm"], - "right_wrist": ["rightwrist", "wristr", "rwrist", "handr", "righthand", "rhand", "valvebipedbip01rhand"], - - #hand l fingers - "pinkie_0_r": ["littlefinger0r","pinkie0r","rpinkie0","pinkiemetacarpalr"], - "pinkie_1_r": ["littlefinger1r","pinkie1r","rpinkie1","pinkieproximalr", "valvebipedbip01rfinger4"], - "pinkie_2_r": ["littlefinger2r","pinkie2r","rpinkie2","pinkieintermediater", "valvebipedbip01rfinger41"], - "pinkie_3_r": ["littlefinger3r","pinkie3r","rpinkie3","pinkiedistalr", "valvebipedbip01rfinger42"], - - "ring_0_r": ["ringfinger0r","ring0r","rring0","ringmetacarpalr"], - "ring_1_r": ["ringfinger1r","ring1r","rring1","ringproximalr", "valvebipedbip01rfinger3"], - "ring_2_r": ["ringfinger2r","ring2r","rring2","ringintermediater", "valvebipedbip01rfinger31"], - "ring_3_r": ["ringfinger3r","ring3r","rring3","ringdistalr", "valvebipedbip01rfinger32"], - - "middle_0_r": ["middlefinger0r","middle0r","rmiddle0","middlemetacarpalr"], - "middle_1_r": ["middlefinger1r","middle1r","rmiddle1","middleproximalr", "valvebipedbip01rfinger2"], - "middle_2_r": ["middlefinger2r","middle2r","rmiddle2","middleintermediater", "valvebipedbip01rfinger21"], - "middle_3_r": ["middlefinger3r","middle3r","rmiddle3","middledistalr", "valvebipedbip01rfinger22"], - - "index_0_r": ["indexfinger0r","index0r","rindex0","indexmetacarpalr"], - "index_1_r": ["indexfinger1r","index1r","rindex1","indexproximalr", "valvebipedbip01rfinger1"], - "index_2_r": ["indexfinger2r","index2r","rindex2","indexintermediater", "valvebipedbip01rfinger11"], - "index_3_r": ["indexfinger3r","index3r","rindex3","indexdistalr", "valvebipedbip01rfinger12"], - - "thumb_0_r": ["thumb0r","rthumb0","thumbmetacarpalr"], - "thumb_1_r": ['thumb1r',"rthumb1","thumbproximalr", "valvebipedbip01rfinger0"], - "thumb_2_r": ['thumb2r',"rthumb2","thumbintermediater", "valvebipedbip01rfinger01"], - "thumb_3_r": ['thumb3r',"rthumb3","thumbdistalr", "valvebipedbip01rfinger02"], - - "right_leg": ["rightleg", "legr", "rleg", "upperlegr", "rupperleg", "thighr", "rightupperleg", "uplegr", "rupleg", "valvebipedbip01rthigh"], - "right_knee": ["rightknee", "kneer", "rknee", "lowerlegr", "calfr", "rlowerleg", "rcalf", "rightlowerleg", "lowlegr", "rlowleg", "valvebipedbip01rcalf"], - "right_ankle": ["rightankle", "ankler", "rankle", "rightfoot", "footr", "rfoot", "rightfoot", "rightfeet", "feetright", "rfeet", "feetr", "valvebipedbip01rfoot"], - "right_toe": ["righttoe", "toeright", "toer", "rtoe", "toesr", "rtoes", "valvebipedbip01rtoe0"], - - "left_shoulder": ["leftshoulder", "shoulderl", "lshoulder", "valvebipedbip01lclavicle"], - "left_arm": ["leftarm", "arml", "rarm", "upperarml", "lupperarm", "leftupperarm", "uparml", "luparm", "valvebipedbip01lupperarm"], - "left_elbow": ["leftelbow", "elbowl", "lelbow", "lowerarml", "leftlowerarm", "lowerarml", "llowerarm", "lowarml", "llowarm", "forearml","lforearm", "valvebipedbip01lforearm"], - "left_wrist": ["leftwrist", "wristl", "lwrist", "handl", "lefthand", "lhand", "valvebipedbip01lhand"], - - #hand l fingers - - "pinkie_0_l": ["pinkiefinger0l","pinkie0l","lpinkie0","pinkiemetacarpall"], - "pinkie_1_l": ["littlefinger1l","pinkie1l","lpinkie1","pinkieproximall", "valvebipedbip01lfinger4"], - "pinkie_2_l": ["littlefinger2l","pinkie2l","lpinkie2","pinkieintermediatel", "valvebipedbip01lfinger41"], - "pinkie_3_l": ["littlefinger3l","pinkie3l","lpinkie3","pinkiedistall", "valvebipedbip01lfinger42"], - - "ring_0_l": ["ringfinger0l","ring0l","lring0","ringmetacarpall"], - "ring_1_l": ["ringfinger1l","ring1l","lring1","ringproximall", "valvebipedbip01lfinger3"], - "ring_2_l": ["ringfinger2l","ring2l","lring2","ringintermediatel", "valvebipedbip01lfinger31"], - "ring_3_l": ["ringfinger3l","ring3l","lring3","ringdistall", "valvebipedbip01lfinger32"], - - "middle_0_l": ["middlefinger0l","middle_0l","lmiddle0","middlemetacarpall"], - "middle_1_l": ["middlefinger1l","middle_1l","lmiddle1","middleproximall", "valvebipedbip01lfinger2"], - "middle_2_l": ["middlefinger2l","middle_2l","lmiddle2","middleintermediatel", "valvebipedbip01lfinger21"], - "middle_3_l": ["middlefinger3l","middle_3l","lmiddle3","middledistall", "valvebipedbip01lfinger22"], - - "index_0_l": ["indexfinger0l","index0l","lindex0","indexmetacarpall"], - "index_1_l": ["indexfinger1l","index1l","lindex1","indexproximall", "valvebipedbip01lfinger1"], - "index_2_l": ["indexfinger2l","index2l","lindex2","indexintermediatel", "valvebipedbip01lfinger11"], - "index_3_l": ["indexfinger3l","index3l","lindex3","indexdistall", "valvebipedbip01lfinger12"], - - "thumb_0_l": ["thumb0l","lthumb0","thumbmetacarpall"], - "thumb_1_l": ['thumb1l',"lthumb1","thumbproximall", "valvebipedbip01lfinger0"], - "thumb_2_l": ['thumb2l',"lthumb2","thumbintermediatel", "valvebipedbip01lfinger01"], - "thumb_3_l": ['thumb3l',"lthumb3","thumbdistall", "valvebipedbip01lfinger02"], - - "left_leg": ["leftleg", "legl", "lleg", "upperlegl", "lupperleg", "thighl", "leftupperleg", "uplegl", "lupleg", "valvebipedbip01lthigh"], - "left_knee": ["leftknee", "kneel", "lknee", "lowerlegl", "llowerleg", "calfl", "lcalf", "leftlowerleg", 'lowlegl', 'llowleg', "valvebipedbip01lcalf"], - "left_ankle": ["leftankle", "anklel", "rankle", "leftfoot", "footl", "lfoot", "leftfoot", "leftfeet", "feetleft", "lfeet", "feetl", "valvebipedbip01lfoot"], - "left_toe": ["lefttoe", "toeleft", "toel", "ltoe", "toesl", "ltoes", "valvebipedbip01ltoe0"], - - "hips": ["pelvis", "hips", "hip", "valvebipedbip01pelvis"], - "spine": ["torso", "spine", "valvebipedbip01spine"], - "chest": ["chest", "valvebipedbip01spine1"], - "upper_chest": ["upperchest", "valvebipedbip01spine4"], - "neck": ["neck", "valvebipedbip01neck1"], - "head": ["head", "valvebipedbip01head1"], - "left_eye": ["eyeleft", "lefteye", "eyel", "leye"], - "right_eye": ["eyeright", "righteye", "eyer", "reye"], + "right_shoulder": ["rightshoulder", "shoulderr", "rshoulder", "valvebipedbip01rclavicle", "右肩"], + "right_arm": ["rightarm", "armr", "rarm", "upperarmr", "rupperarm", "rightupperarm", "uparmr", "ruparm", "valvebipedbip01rupperarm", "右腕"], + "right_elbow": ["rightelbow", "elbowr", "relbow", "lowerarmr", "rightlowerarm", "lowerarmr", "rlowerarm", "lowarmr", "rlowarm", "forearmr", "rforearm", "valvebipedbip01rforearm", "右ひじ"], + "right_wrist": ["rightwrist", "wristr", "rwrist", "handr", "righthand", "rhand", "valvebipedbip01rhand", "右手首"], + "pinkie_0_r": ["littlefinger0r", "pinkie0r", "rpinkie0", "pinkiemetacarpalr", "右小指0"], + "pinkie_1_r": ["littlefinger1r", "pinkie1r", "rpinkie1", "pinkieproximalr", "valvebipedbip01rfinger4", "右小指1"], + "pinkie_2_r": ["littlefinger2r", "pinkie2r", "rpinkie2", "pinkieintermediater", "valvebipedbip01rfinger41", "右小指2"], + "pinkie_3_r": ["littlefinger3r", "pinkie3r", "rpinkie3", "pinkiedistalr", "valvebipedbip01rfinger42", "右小指3"], + "ring_0_r": ["ringfinger0r", "ring0r", "rring0", "ringmetacarpalr", "右薬指0"], + "ring_1_r": ["ringfinger1r", "ring1r", "rring1", "ringproximalr", "valvebipedbip01rfinger3", "右薬指1"], + "ring_2_r": ["ringfinger2r", "ring2r", "rring2", "ringintermediater", "valvebipedbip01rfinger31", "右薬指2"], + "ring_3_r": ["ringfinger3r", "ring3r", "rring3", "ringdistalr", "valvebipedbip01rfinger32", "右薬指3"], + "middle_0_r": ["middlefinger0r", "middle0r", "rmiddle0", "middlemetacarpalr", "右中指0"], + "middle_1_r": ["middlefinger1r", "middle1r", "rmiddle1", "middleproximalr", "valvebipedbip01rfinger2", "右中指1"], + "middle_2_r": ["middlefinger2r", "middle2r", "rmiddle2", "middleintermediater", "valvebipedbip01rfinger21", "右中指2"], + "middle_3_r": ["middlefinger3r", "middle3r", "rmiddle3", "middledistalr", "valvebipedbip01rfinger22", "右中指3"], + "index_0_r": ["indexfinger0r", "index0r", "rindex0", "indexmetacarpalr", "右人差指0"], + "index_1_r": ["indexfinger1r", "index1r", "rindex1", "indexproximalr", "valvebipedbip01rfinger1", "右人差指1"], + "index_2_r": ["indexfinger2r", "index2r", "rindex2", "indexintermediater", "valvebipedbip01rfinger11", "右人差指2"], + "index_3_r": ["indexfinger3r", "index3r", "rindex3", "indexdistalr", "valvebipedbip01rfinger12", "右人差指3"], + "thumb_0_r": ["thumb0r", "rthumb0", "thumbmetacarpalr", "右親指0"], + "thumb_1_r": ["thumb1r", "rthumb1", "thumbproximalr", "valvebipedbip01rfinger0", "右親指1"], + "thumb_2_r": ["thumb2r", "rthumb2", "thumbintermediater", "valvebipedbip01rfinger01", "右親指2"], + "thumb_3_r": ["thumb3r", "rthumb3", "thumbdistalr", "valvebipedbip01rfinger02", "右親指3"], + "right_leg": ["rightleg", "legr", "rleg", "upperlegr", "rupperleg", "thighr", "rightupperleg", "uplegr", "rupleg", "valvebipedbip01rthigh", "右足"], + "right_knee": ["rightknee", "kneer", "rknee", "lowerlegr", "calfr", "rlowerleg", "rcalf", "rightlowerleg", "lowlegr", "rlowleg", "valvebipedbip01rcalf", "右ひざ"], + "right_ankle": ["rightankle", "ankler", "rankle", "rightfoot", "footr", "rfoot", "rightfoot", "rightfeet", "feetright", "rfeet", "feetr", "valvebipedbip01rfoot", "右足首"], + "right_toe": ["righttoe", "toeright", "toer", "rtoe", "toesr", "rtoes", "valvebipedbip01rtoe0", "右つま先"], + "left_shoulder": ["leftshoulder", "shoulderl", "lshoulder", "valvebipedbip01lclavicle", "左肩"], + "left_arm": ["leftarm", "arml", "larm", "upperarml", "lupperarm", "leftupperarm", "uparml", "luparm", "valvebipedbip01lupperarm", "左腕"], + "left_elbow": ["leftelbow", "elbowl", "lelbow", "lowerarml", "leftlowerarm", "lowerarml", "llowerarm", "lowarml", "llowarm", "forearml", "lforearm", "valvebipedbip01lforearm", "左ひじ"], + "left_wrist": ["leftwrist", "wristl", "lwrist", "handl", "lefthand", "lhand", "valvebipedbip01lhand", "左手首"], + "pinkie_0_l": ["pinkiefinger0l", "pinkie0l", "lpinkie0", "pinkiemetacarpall", "左小指0"], + "pinkie_1_l": ["littlefinger1l", "pinkie1l", "lpinkie1", "pinkieproximall", "valvebipedbip01lfinger4", "左小指1"], + "pinkie_2_l": ["littlefinger2l", "pinkie2l", "lpinkie2", "pinkieintermediatel", "valvebipedbip01lfinger41", "左小指2"], + "pinkie_3_l": ["littlefinger3l", "pinkie3l", "lpinkie3", "pinkiedistall", "valvebipedbip01lfinger42", "左小指3"], + "ring_0_l": ["ringfinger0l", "ring0l", "lring0", "ringmetacarpall", "左薬指0"], + "ring_1_l": ["ringfinger1l", "ring1l", "lring1", "ringproximall", "valvebipedbip01lfinger3", "左薬指1"], + "ring_2_l": ["ringfinger2l", "ring2l", "lring2", "ringintermediatel", "valvebipedbip01lfinger31", "左薬指2"], + "ring_3_l": ["ringfinger3l", "ring3l", "lring3", "ringdistall", "valvebipedbip01lfinger32", "左薬指3"], + "middle_0_l": ["middlefinger0l", "middle_0l", "lmiddle0", "middlemetacarpall", "左中指0"], + "middle_1_l": ["middlefinger1l", "middle_1l", "lmiddle1", "middleproximall", "valvebipedbip01lfinger2", "左中指1"], + "middle_2_l": ["middlefinger2l", "middle_2l", "lmiddle2", "middleintermediatel", "valvebipedbip01lfinger21", "左中指2"], + "middle_3_l": ["middlefinger3l", "middle_3l", "lmiddle3", "middledistall", "valvebipedbip01lfinger22", "左中指3"], + "index_0_l": ["indexfinger0l", "index0l", "lindex0", "indexmetacarpall", "左人差指0"], + "index_1_l": ["indexfinger1l", "index1l", "lindex1", "indexproximall", "valvebipedbip01lfinger1", "左人差指1"], + "index_2_l": ["indexfinger2l", "index2l", "lindex2", "indexintermediatel", "valvebipedbip01lfinger11", "左人差指2"], + "index_3_l": ["indexfinger3l", "index3l", "lindex3", "indexdistall", "valvebipedbip01lfinger12", "左人差指3"], + "thumb_0_l": ["thumb0l", "lthumb0", "thumbmetacarpall", "左親指0"], + "thumb_1_l": ["thumb1l", "lthumb1", "thumbproximall", "valvebipedbip01lfinger0", "左親指1"], + "thumb_2_l": ["thumb2l", "lthumb2", "thumbintermediatel", "valvebipedbip01lfinger01", "左親指2"], + "thumb_3_l": ["thumb3l", "lthumb3", "thumbdistall", "valvebipedbip01lfinger02", "左親指3"], + "left_leg": ["leftleg", "legl", "lleg", "upperlegl", "lupperleg", "thighl", "leftupperleg", "uplegl", "lupleg", "valvebipedbip01lthigh", "左足"], + "left_knee": ["leftknee", "kneel", "lknee", "lowerlegl", "llowerleg", "calfl", "lcalf", "leftlowerleg", "lowlegl", "llowleg", "valvebipedbip01lcalf", "左ひざ"], + "left_ankle": ["leftankle", "anklel", "lankle", "leftfoot", "footl", "lfoot", "leftfoot", "leftfeet", "feetleft", "lfeet", "feetl", "valvebipedbip01lfoot", "左足首"], + "left_toe": ["lefttoe", "toeleft", "toel", "ltoe", "toesl", "ltoes", "valvebipedbip01ltoe0", "左つま先"], + "hips": ["pelvis", "hips", "hip", "valvebipedbip01pelvis", "腰"], + "spine": ["torso", "spine", "valvebipedbip01spine", "脊椎"], + "chest": ["chest", "valvebipedbip01spine1", "胸"], + "upper_chest": ["upperchest", "valvebipedbip01spine4", "上胸"], + "neck": ["neck", "valvebipedbip01neck1", "首"], + "head": ["head", "valvebipedbip01head1", "頭"], + "left_eye": ["eyeleft", "lefteye", "eyel", "leye", "左目"], + "right_eye": ["eyeright", "righteye", "eyer", "reye", "右目"], } + +# Add VRM bone name variations +bone_names.update({ + 'hips': bone_names['hips'] + ['j_bip_c_hips', 'j_hips', 'vrm_hips'], + 'spine': bone_names['spine'] + ['j_bip_c_spine', 'j_spine', 'vrm_spine'], + 'chest': bone_names['chest'] + ['j_bip_c_chest', 'j_chest', 'vrm_chest'], + 'upper_chest': bone_names['upper_chest'] + ['j_bip_c_upper_chest', 'j_upper_chest', 'vrm_upperchest'], + 'neck': bone_names['neck'] + ['j_bip_c_neck', 'j_neck', 'vrm_neck'], + 'head': bone_names['head'] + ['j_bip_c_head', 'j_head', 'vrm_head'], + + # VRM specific finger naming + 'thumb_0_l': bone_names['thumb_0_l'] + ['thumb_metacarpal_l', 'j_thumb1_l'], + 'index_0_l': bone_names['index_0_l'] + ['index_metacarpal_l', 'j_index1_l'], + 'middle_0_l': bone_names['middle_0_l'] + ['middle_metacarpal_l', 'j_middle1_l'], + 'ring_0_l': bone_names['ring_0_l'] + ['ring_metacarpal_l', 'j_ring1_l'], + 'pinkie_0_l': bone_names['pinkie_0_l'] + ['little_metacarpal_l', 'j_little1_l'], + + # Mirror for right side + 'thumb_0_r': bone_names['thumb_0_r'] + ['thumb_metacarpal_r', 'j_thumb1_r'], + 'index_0_r': bone_names['index_0_r'] + ['index_metacarpal_r', 'j_index1_r'], + 'middle_0_r': bone_names['middle_0_r'] + ['middle_metacarpal_r', 'j_middle1_r'], + 'ring_0_r': bone_names['ring_0_r'] + ['ring_metacarpal_r', 'j_ring1_r'], + 'pinkie_0_r': bone_names['pinkie_0_r'] + ['little_metacarpal_r', 'j_little1_r'] +}) + # array taken from cats dont_delete_these_main_bones = [ 'Hips', 'Spine', 'Chest', 'Upper Chest', 'Neck', 'Head', @@ -166,79 +174,3 @@ resonite_translations = { 'thumb_2_r': "thumb2.R", 'thumb_3_r': "thumb3.R" } - -mmd_bone_renames = { - # Core body - "センター": "Center", - "グルーブ": "Groove", - "腰": "Waist", - "上半身": "Upper Body", - "上半身2": "Upper Body 2", - "下半身": "Lower Body", - - # Head - "首": "Neck", - "頭": "Head", - "両目": "Eyes", - "左目": "Eye_L", - "右目": "Eye_R", - - # Arms - "左肩": "Shoulder_L", - "左腕": "Arm_L", - "左ひじ": "Elbow_L", - "左手首": "Wrist_L", - "右肩": "Shoulder_R", - "右腕": "Arm_R", - "右ひじ": "Elbow_R", - "右手首": "Wrist_R", - - # Fingers - "左親指1": "Thumb1_L", - "左親指2": "Thumb2_L", - "左人指1": "Index1_L", - "左人指2": "Index2_L", - "左人指3": "Index3_L", - "左中指1": "Middle1_L", - "左中指2": "Middle2_L", - "左中指3": "Middle3_L", - "左薬指1": "Ring1_L", - "左薬指2": "Ring2_L", - "左薬指3": "Ring3_L", - "左小指1": "Pinky1_L", - "左小指2": "Pinky2_L", - "左小指3": "Pinky3_L", - - "右親指1": "Thumb1_R", - "右親指2": "Thumb2_R", - "右人指1": "Index1_R", - "右人指2": "Index2_R", - "右人指3": "Index3_R", - "右中指1": "Middle1_R", - "右中指2": "Middle2_R", - "右中指3": "Middle3_R", - "右薬指1": "Ring1_R", - "右薬指2": "Ring2_R", - "右薬指3": "Ring3_R", - "右小指1": "Pinky1_R", - "右小指2": "Pinky2_R", - "右小指3": "Pinky3_R", - - # Legs - "左足": "Leg_L", - "左ひざ": "Knee_L", - "左足首": "Ankle_L", - "右足": "Leg_R", - "右ひざ": "Knee_R", - "右足首": "Ankle_R", - - # Toes - "左つま先": "Toe_L", - "右つま先": "Toe_R", - - # IK bones - "左足IK": "Leg_IK_L", - "右足IK": "Leg_IK_R", - "左つま先IK": "Toe_IK_L", - "右つま先IK": "Toe_IK_R" -} diff --git a/core/properties.py b/core/properties.py index 9dbb70d..83be578 100644 --- a/core/properties.py +++ b/core/properties.py @@ -116,30 +116,22 @@ class AvatarToolkitSceneProperties(PropertyGroup): max=1.0 ) - mmd_keep_upper_chest: BoolProperty( - name=t("MMDTools.keep_upper_chest"), - description=t("MMDTools.keep_upper_chest_desc"), + mmd_process_twist_bones: BoolProperty( + name=t("MMD.process_twist_bones"), + description=t("MMD.process_twist_bones_desc"), + default=True + ) + + mmd_connect_bones: BoolProperty( + name=t("MMD.connect_bones"), + description=t("MMD.connect_bones_desc"), default=True ) - mmd_remove_unused_bones: BoolProperty( - name=t("MMDTools.remove_unused"), - description=t("MMDTools.remove_unused_desc"), - default=True - ) - - mmd_merge_distance: FloatProperty( - name=t("MMDTools.merge_distance"), - description=t("MMDTools.merge_distance_desc"), - default=0.001, - min=0.0001, - max=0.1 - ) - - mmd_cleanup_shapekeys: BoolProperty( - name=t("MMDTools.cleanup_shapekeys"), - description=t("MMDTools.cleanup_shapekeys_desc"), - default=True + save_backup_state: BoolProperty( + name="Save Backup State", + description="Save the initial state of the armature before standardizing bones", + default=False ) def register() -> None: diff --git a/functions/mmd_tools.py b/functions/mmd_tools.py index 93645fa..8a85799 100644 --- a/functions/mmd_tools.py +++ b/functions/mmd_tools.py @@ -1,498 +1,502 @@ import bpy -import numpy as np -from typing import Set, Dict, List, Optional, Tuple -from bpy.types import Operator, Context, Object, EditBone, Mesh +from typing import Tuple, Set, Dict +from bpy.types import Operator, Context, Object +from mathutils import Vector +from ..core.common import ( + ProgressTracker, + get_active_armature, + validate_meshes, + simplify_bonename, + duplicate_bone_chain, + save_armature_state, + restore_armature_state, + get_all_meshes, + validate_bone_hierarchy, + transfer_vertex_weights, + get_vertex_weights +) from ..core.logging_setup import logger from ..core.translations import t -from ..core.common import ( - get_active_armature, - validate_armature, - get_all_meshes, - ProgressTracker, - transfer_vertex_weights, - remove_unused_shapekeys -) -from ..core.dictionaries import bone_names, mmd_bone_renames +from ..core.dictionaries import bone_names -class AvatarToolkit_OT_FixBoneNames(Operator): - """Standardize and fix bone names""" - bl_idname = "avatar_toolkit.fix_bone_names" - bl_label = t("MMDTools.fix_bone_names") - bl_description = t("MMDTools.fix_bone_names_desc") +class AvatarToolkit_OT_StandardizeMMDBones(Operator): + bl_idname = "avatar_toolkit.mmd_standardize_bones" + bl_label = t("MMD.standardize_bones") bl_options = {'REGISTER', 'UNDO'} - @classmethod - def poll(cls, context: Context) -> bool: - armature = get_active_armature(context) - if not armature: - return False - valid, _ = validate_armature(armature) - return valid + def standardize_bone_names(self, armature: Object) -> None: + """Standardize bone names using MMD to Unity/VRChat conventions""" + for bone in armature.data.bones: + simplified_name = simplify_bonename(bone.name) + for standard_name, variations in bone_names.items(): + if simplified_name in variations: + bone.name = standard_name + break - def execute(self, context: Context) -> Set[str]: - armature = get_active_armature(context) - - with ProgressTracker(context, 3, "Fixing Bone Names") as progress: - bpy.ops.object.mode_set(mode='EDIT') - - # First pass - standardize names - for bone in armature.data.edit_bones: - bone.name = self.standardize_bone_name(bone.name) - progress.step("Standardized names") + def process_lr_bones(self, armature: Object) -> None: + """Process left/right bone pairs for consistency""" + for bone in armature.data.bones: + if bone.name.endswith(('_l', '_r', '.l', '.r', 'Left', 'Right')): + base_name = bone.name.rsplit('_', 1)[0] + side = '_l' if any(s in bone.name.lower() for s in ('left', '_l', '.l')) else '_r' + bone.name = f"{base_name}{side}" - # Second pass - apply MMD mappings - for bone in armature.data.edit_bones: - if bone.name in mmd_bone_renames: - bone.name = mmd_bone_renames[bone.name] - progress.step("Applied MMD mappings") + def resolve_name_conflicts(self, armature: Object) -> None: + """Handle duplicate bone names""" + used_names = set() + for bone in armature.data.bones: + base_name = bone.name + counter = 1 + while bone.name in used_names: + bone.name = f"{base_name}_{counter}" + counter += 1 + used_names.add(bone.name) - # Third pass - fix common names - for bone in armature.data.edit_bones: - self.fix_common_names(bone) - progress.step("Fixed common names") - - self.report({'INFO'}, t("MMDTools.bones_renamed")) - return {'FINISHED'} - - def standardize_bone_name(self, name: str) -> str: - """Standardize bone naming convention""" - prefixes = ['def-', 'def_', 'sk_', 'b_', 'bone_', 'mmd_'] - name_lower = name.lower() - - # Remove common prefixes - for prefix in prefixes: - if name_lower.startswith(prefix): - name = name[len(prefix):] - break - - # Fix side indicators - name = name.replace('_l', '_L').replace('_r', '_R') - name = name.replace('.l', '_L').replace('.r', '_R') - name = name.replace('左', '_L').replace('右', '_R') - - return name - - def fix_common_names(self, bone: EditBone) -> None: - """Fix common bone names to standard names""" - for standard_name, variations in bone_names.items(): - if bone.name.lower() in variations: - bone.name = standard_name - break - -class AvatarToolkit_OT_FixBoneHierarchy(Operator): - """Fix bone parenting and hierarchy""" - bl_idname = "avatar_toolkit.fix_bone_hierarchy" - bl_label = t("MMDTools.fix_hierarchy") - bl_description = t("MMDTools.fix_hierarchy_desc") - bl_options = {'REGISTER', 'UNDO'} - - @classmethod - def poll(cls, context: Context) -> bool: - armature = get_active_armature(context) - if not armature: - return False - valid, _ = validate_armature(armature) - return valid - - def execute(self, context: Context) -> Set[str]: - armature = get_active_armature(context) - - with ProgressTracker(context, 3, "Fixing Bone Hierarchy") as progress: - bpy.ops.object.mode_set(mode='EDIT') - - # Fix spine chain - self.fix_spine_chain(armature) - progress.step("Fixed spine chain") - - # Fix limb chains - self.fix_limb_chains(armature) - progress.step("Fixed limb chains") - - # Fix bone orientations - self.fix_bone_orientations(armature) - progress.step("Fixed bone orientations") - - self.report({'INFO'}, t("MMDTools.hierarchy_fixed")) - return {'FINISHED'} - - def fix_spine_chain(self, armature: Object) -> None: - """Fix the spine bone chain hierarchy""" + def process_spine_chain(self, armature: Object) -> None: + """Process spine bones for VRChat compatibility""" + bpy.ops.object.mode_set(mode='EDIT') edit_bones = armature.data.edit_bones - spine_chain = ['Hips', 'Spine', 'Chest', 'Neck', 'Head'] - previous = None - for bone_name in spine_chain: - if bone_name in edit_bones: - bone = edit_bones[bone_name] - if previous: - bone.parent = edit_bones[previous] - previous = bone_name - - def fix_limb_chains(self, armature: Object) -> None: - """Fix arm and leg bone chains""" - edit_bones = armature.data.edit_bones - limb_chains = { - 'Left': { - 'arm': ['Left shoulder', 'Left arm', 'Left elbow', 'Left wrist'], - 'leg': ['Left leg', 'Left knee', 'Left ankle', 'Left toe'] - }, - 'Right': { - 'arm': ['Right shoulder', 'Right arm', 'Right elbow', 'Right wrist'], - 'leg': ['Right leg', 'Right knee', 'Right ankle', 'Right toe'] - } + spine_bones = { + 'hips': None, + 'spine': None, + 'chest': None, + 'upper_chest': None, + 'neck': None, + 'head': None } - for side in limb_chains: - for chain in limb_chains[side].values(): - previous = None - for bone_name in chain: - if bone_name in edit_bones: - bone = edit_bones[bone_name] - if previous: - bone.parent = edit_bones[previous] - previous = bone_name + # Map existing spine bones + for bone in edit_bones: + simplified = simplify_bonename(bone.name) + for spine_name in spine_bones.keys(): + if simplified in bone_names[spine_name]: + spine_bones[spine_name] = bone + break + + # Create missing spine bones + if spine_bones['spine'] and not spine_bones['chest']: + chest = edit_bones.new('chest') + chest.head = spine_bones['spine'].tail + chest.tail = spine_bones['neck'].head if spine_bones['neck'] else spine_bones['head'].head + spine_bones['chest'] = chest + + # Set up spine hierarchy + if spine_bones['hips']: + for i, key in enumerate(['spine', 'chest', 'upper_chest', 'neck', 'head']): + if spine_bones[key]: + prev_key = list(spine_bones.keys())[i] + if spine_bones[prev_key]: + spine_bones[key].parent = spine_bones[prev_key] - def fix_bone_orientations(self, armature: Object) -> None: - """Fix bone roll and axis orientations""" + def correct_bone_orientations(self, armature: Object) -> None: + """Automatically correct bone orientations to align with Unity's axes""" + bpy.ops.object.mode_set(mode='EDIT') edit_bones = armature.data.edit_bones - # Fix spine chain orientations - spine_bones = ['Hips', 'Spine', 'Chest'] - for name in spine_bones: - if name in edit_bones: - bone = edit_bones[name] - bone.roll = 0 - bone.tail.y = bone.head.y - - # Fix arm orientations - arm_bones = ['Left arm', 'Right arm', 'Left elbow', 'Right elbow'] - for name in arm_bones: - if name in edit_bones: - bone = edit_bones[name] - bone.roll = 0 if 'Left' in name else np.pi - -class AvatarToolkit_OT_FixBoneWeights(Operator): - """Fix and clean up bone weights""" - bl_idname = "avatar_toolkit.fix_bone_weights" - bl_label = t("MMDTools.fix_weights") - bl_description = t("MMDTools.fix_weights_desc") - bl_options = {'REGISTER', 'UNDO'} + # Define standard orientations + orientations = { + 'spine': Vector((0, 0, 1)), # Points up + 'chest': Vector((0, 0, 1)), + 'neck': Vector((0, 0, 1)), + 'head': Vector((0, 0, 1)), + 'shoulder': Vector((1, 0, 0)), # Points outward + 'arm': Vector((0, -1, 0)), # Points down + 'elbow': Vector((0, -1, 0)), + 'leg': Vector((0, -1, 0)), + 'knee': Vector((0, -1, 0)), + 'foot': Vector((1, 0, 0)), # Points forward + } + + for bone in edit_bones: + simplified_name = simplify_bonename(bone.name) + for bone_type, direction in orientations.items(): + if bone_type in simplified_name: + # Calculate new tail position while maintaining length + length = (bone.tail - bone.head).length + bone.tail = bone.head + direction * length + break @classmethod def poll(cls, context: Context) -> bool: - armature = get_active_armature(context) - if not armature: - return False - valid, _ = validate_armature(armature) - return valid + """Check if there is an active armature in the scene""" + return get_active_armature(context) is not None def execute(self, context: Context) -> Set[str]: - armature = get_active_armature(context) - meshes = get_all_meshes(context) - - if not meshes: - self.report({'WARNING'}, t("MMDTools.no_meshes")) + try: + armature = get_active_armature(context) + + # Save initial state if enabled + if context.scene.avatar_toolkit.save_backup_state: + self.initial_state = save_armature_state(armature) + + with ProgressTracker(context, 6, "Standardizing Bones") as progress: + # Step 1: Standardize bone names + self.standardize_bone_names(armature) + progress.step("Standardized bone names") + + # Step 3: Process left/right bones + self.process_lr_bones(armature) + progress.step("Processed left/right bones") + + # Step 4: Handle name conflicts + self.resolve_name_conflicts(armature) + progress.step("Resolved naming conflicts") + + # Step 5: Process spine chain + self.process_spine_chain(armature) + progress.step("Processed spine chain") + + # Step 6: Correct bone orientations + self.correct_bone_orientations(armature) + progress.step("Corrected bone orientations") + + self.report({'INFO'}, t("MMD.bones_standardized")) + return {'FINISHED'} + + except Exception as e: + logger.error(f"Bone standardization failed: {str(e)}") + if hasattr(self, 'initial_state'): + restore_armature_state(armature, self.initial_state) + self.report({'ERROR'}, str(e)) return {'CANCELLED'} - - with ProgressTracker(context, len(meshes), "Fixing Bone Weights") as progress: - for mesh in meshes: - # Clean weights - self.clean_weights(mesh, context.scene.avatar_toolkit.clean_weights_threshold) - - # Handle twist bones - if context.scene.avatar_toolkit.merge_twist_bones: - self.process_twist_bones(mesh) - - # Remove empty groups - self.remove_empty_groups(mesh) - - # Normalize weights - self.normalize_weights(mesh) - - progress.step(f"Processed {mesh.name}") - self.report({'INFO'}, t("MMDTools.weights_fixed")) - return {'FINISHED'} - - def clean_weights(self, mesh: Object, threshold: float) -> None: - """Remove weights below threshold""" - for vertex_group in mesh.vertex_groups: - for vertex in mesh.data.vertices: - try: - weight = vertex_group.weight(vertex.index) - if weight < threshold: - vertex_group.remove([vertex.index]) - except RuntimeError: - continue - - def process_twist_bones(self, mesh: Object) -> None: - """Process and merge twist bone weights""" - twist_groups = [g for g in mesh.vertex_groups if 'twist' in g.name.lower()] - for group in twist_groups: - base_name = group.name.lower().replace('twist', '').strip('_') - for target in mesh.vertex_groups: - if target.name.lower() == base_name: - transfer_vertex_weights(mesh, group.name, target.name) - break - - def remove_empty_groups(self, mesh: Object) -> None: - """Remove vertex groups with no weights""" - empty_groups = [] - for group in mesh.vertex_groups: - has_weights = False - for vert in mesh.data.vertices: - for g in vert.groups: - if g.group == group.index and g.weight > 0: - has_weights = True - break - if has_weights: - break - if not has_weights: - empty_groups.append(group) - - for group in empty_groups: - mesh.vertex_groups.remove(group) - - def normalize_weights(self, mesh: Object) -> None: - """Normalize vertex weights""" - for vertex in mesh.data.vertices: - total_weight = sum(group.weight for group in vertex.groups) - if total_weight > 0: - for group in vertex.groups: - group.weight /= total_weight - -class AvatarToolkit_OT_FixMMDFeatures(Operator): - """Fix MMD-specific features and settings""" - bl_idname = "avatar_toolkit.fix_mmd_features" - bl_label = t("MMDTools.fix_mmd_features") - bl_description = t("MMDTools.fix_mmd_features_desc") +class AvatarToolkit_OT_ProcessMMDWeights(Operator): + bl_idname = "avatar_toolkit.mmd_process_weights" + bl_label = t("MMD.process_weights") bl_options = {'REGISTER', 'UNDO'} - @classmethod - def poll(cls, context: Context) -> bool: - armature = get_active_armature(context) - if not armature: - return False - valid, _ = validate_armature(armature) - return valid + def merge_bone_weights(self, context: Context, mesh: Object, source: str, target: str) -> None: + """Transfer weights from source bone to target bone""" + transfer_vertex_weights( + mesh, + source, + target, + context.scene.avatar_toolkit.merge_weights_threshold + ) - def execute(self, context: Context) -> Set[str]: - armature = get_active_armature(context) - meshes = get_all_meshes(context) + def process_eye_weights(self, context: Context, mesh: Object) -> None: + """Handle special cases for eye bone weights""" + eye_bones = { + 'eye_l': ['eyel', 'lefteye', 'eye.l'], + 'eye_r': ['eyer', 'righteye', 'eye.r'] + } - with ProgressTracker(context, 4, "Fixing MMD Features") as progress: - # Process shape keys - for mesh in meshes: - self.process_shape_keys(mesh) - progress.step("Processed shape keys") - - # Fix MMD shading - self.fix_mmd_shading(meshes) - progress.step("Fixed MMD shading") - - # Handle physics cleanup - self.cleanup_physics(armature) - progress.step("Cleaned up physics") - - # Remove unused data - self.cleanup_unused_data(context) - progress.step("Cleaned up unused data") - - return {'FINISHED'} + for target, sources in eye_bones.items(): + for source in sources: + if source in mesh.vertex_groups: + self.merge_bone_weights(context, mesh, source, target) - def process_shape_keys(self, mesh: Object) -> None: - """Process and clean up shape keys""" - if not mesh.data.shape_keys: + def process_twist_bones(self, context: Context, mesh: Object) -> None: + """Process and merge twist bone weights""" + if not context.scene.avatar_toolkit.mmd_process_twist_bones: return - # Clean unused shape keys - remove_unused_shapekeys(mesh) + twist_pairs = [ + ('arm_twist_l', 'left_arm'), + ('arm_twist_r', 'right_arm'), + ('forearm_twist_l', 'left_elbow'), + ('forearm_twist_r', 'right_elbow') + ] - # Sort and rename shape keys - shape_keys = mesh.data.shape_keys.key_blocks - for key in shape_keys: - # Handle Japanese prefixes - if key.name.startswith('防'): - key.name = key.name[1:] - # Handle common MMD prefixes - if key.name.startswith('表情'): - key.name = key.name[2:] + for twist, target in twist_pairs: + if twist in mesh.vertex_groups: + self.merge_bone_weights(context, mesh, twist, target) - def fix_mmd_shading(self, meshes: List[Object]) -> None: - """Fix MMD material shading settings""" - for mesh in meshes: - for material in mesh.data.materials: - if material: - material.use_backface_culling = True - material.blend_method = 'HASHED' - if material.node_tree: - for node in material.node_tree.nodes: - if node.type == 'BSDF_PRINCIPLED': - node.inputs['Alpha'].default_value = 1.0 - - def cleanup_physics(self, armature: Object) -> None: - """Clean up MMD physics objects""" - physics_objects = [obj for obj in bpy.data.objects - if obj.parent == armature and - (obj.rigid_body or obj.rigid_body_constraint)] + def cleanup_vertex_groups(self, context: Context, mesh: Object) -> None: + """Remove empty and unused vertex groups""" + threshold = context.scene.avatar_toolkit.clean_weights_threshold - for obj in physics_objects: - bpy.data.objects.remove(obj, do_unlink=True) - - def cleanup_unused_data(self, context: Context) -> None: - """Clean up unused MMD data""" - # Remove unused actions - for action in bpy.data.actions: - if not action.users: - bpy.data.actions.remove(action) + # Get list of used bones from armature + armature = mesh.find_armature() + if not armature: + return + + valid_bones = set(bone.name for bone in armature.data.bones) + + # Remove unused groups + for group in mesh.vertex_groups[:]: + if group.name not in valid_bones: + mesh.vertex_groups.remove(group) + continue - # Remove empty vertex groups - for mesh in get_all_meshes(context): - self.remove_empty_groups(mesh) - - def remove_empty_groups(self, mesh: Object) -> None: - """Remove empty vertex groups""" - empty_groups = [] - for group in mesh.vertex_groups: + # Check if group has any weights above threshold has_weights = False for vert in mesh.data.vertices: - for g in vert.groups: - if g.group == group.index and g.weight > 0: - has_weights = True - break + for group_element in vert.groups: + if group_element.group == group.index: + if group_element.weight > threshold: + has_weights = True + break if has_weights: break + if not has_weights: - empty_groups.append(group) - - for group in empty_groups: - mesh.vertex_groups.remove(group) + mesh.vertex_groups.remove(group) -class AvatarToolkit_OT_AdvancedBoneOps(Operator): - """Advanced bone operations and fixes""" - bl_idname = "avatar_toolkit.advanced_bone_ops" - bl_label = t("MMDTools.advanced_bone_ops") - bl_description = t("MMDTools.advanced_bone_ops_desc") - bl_options = {'REGISTER', 'UNDO'} + def merge_remaining_weights(self, context: Context, mesh: Object) -> None: + """Process remaining weight merging cases""" + # Common MMD weight merge pairs + merge_pairs = [ + # Finger weights + ('pinky', 'pinkie'), + ('thumb0', 'thumb_0'), + ('index0', 'index_0'), + ('middle0', 'middle_0'), + ('ring0', 'ring_0'), + + # Additional arm weights + ('upperarm', 'arm'), + ('lowerarm', 'elbow'), + ('wrist', 'hand'), + + # Leg weights + ('upperleg', 'leg'), + ('lowerleg', 'knee'), + ('ankle', 'foot'), + + # Spine weights + ('spine1', 'chest'), + ('spine2', 'upper_chest'), + ] + + for source, target in merge_pairs: + for suffix in ['_l', '_r', '.l', '.r']: + source_name = f"{source}{suffix}" + target_name = f"{target}{suffix}" + if source_name in mesh.vertex_groups: + self.merge_bone_weights(context, mesh, source_name, target_name) + + @classmethod + def poll(cls, context: Context) -> bool: + """Check if there is an active armature in the scene""" + return get_active_armature(context) is not None def execute(self, context: Context) -> Set[str]: - armature = get_active_armature(context) - - with ProgressTracker(context, 4, "Advanced Bone Operations") as progress: - # Fix zero length bones - self.fix_zero_length_bones(armature) - progress.step("Fixed zero length bones") + try: + meshes = get_all_meshes(context) - # Connect bones with children - self.connect_bone_chains(armature) - progress.step("Connected bone chains") + # Save initial state + if context.scene.avatar_toolkit.save_backup_state: + self.initial_states = {mesh: get_vertex_weights(mesh) for mesh in meshes} - # Handle bone roll values - self.fix_bone_rolls(armature) - progress.step("Fixed bone rolls") + with ProgressTracker(context, len(meshes) * 4, "Processing Weights") as progress: + for mesh in meshes: + # Step 1: Process eye weights + self.process_eye_weights(context, mesh) + progress.step(f"Processed eye weights for {mesh.name}") + + # Step 2: Process twist bones + self.process_twist_bones(context, mesh) + progress.step(f"Processed twist bones for {mesh.name}") + + # Step 3: Merge remaining weights + self.merge_remaining_weights(context, mesh) + progress.step(f"Merged weights for {mesh.name}") + + # Step 4: Cleanup + self.cleanup_vertex_groups(context, mesh) + progress.step(f"Cleaned up weights for {mesh.name}") + + self.report({'INFO'}, t("MMD.weights_processed")) + return {'FINISHED'} - # Fix bone orientations - self.fix_bone_orientations(armature) - progress.step("Fixed bone orientations") - - return {'FINISHED'} + except Exception as e: + logger.error(f"Weight processing failed: {str(e)}") + if hasattr(self, 'initial_states'): + for mesh, state in self.initial_states.items(): + restore_mesh_weights_state(mesh, state) + self.report({'ERROR'}, str(e)) + return {'CANCELLED'} - def fix_zero_length_bones(self, armature: Object) -> None: - """Fix bones with zero length by extending them""" - min_length = 0.001 - for bone in armature.data.edit_bones: - length = (bone.tail - bone.head).length - if length < min_length: - if bone.parent: - bone.tail = bone.head + bone.parent.vector * 0.1 - else: - bone.tail.z = bone.head.z + 0.1 - - def connect_bone_chains(self, armature: Object) -> None: - """Connect bones that should form chains""" - min_distance = bpy.context.scene.avatar_toolkit.connect_bones_min_distance - - for bone in armature.data.edit_bones: - if len(bone.children) == 1: - child = bone.children[0] - distance = (bone.tail - child.head).length - if distance < min_distance: - child.use_connect = True - child.head = bone.tail - - def fix_bone_rolls(self, armature: Object) -> None: - """Fix bone roll values for proper orientation""" - for bone in armature.data.edit_bones: - if 'spine' in bone.name.lower() or 'chest' in bone.name.lower(): - bone.roll = 0 - elif 'shoulder' in bone.name.lower(): - bone.roll = 0 if 'left' in bone.name.lower() else np.pi - -class AvatarToolkit_OT_CleanupOperations(Operator): - """Cleanup unused data and objects""" - bl_idname = "avatar_toolkit.cleanup_operations" - bl_label = t("MMDTools.cleanup_operations") - bl_description = t("MMDTools.cleanup_operations_desc") +class AvatarToolkit_OT_FixMMDHierarchy(Operator): + bl_idname = "avatar_toolkit.mmd_fix_hierarchy" + bl_label = t("MMD.fix_hierarchy") bl_options = {'REGISTER', 'UNDO'} - def execute(self, context: Context) -> Set[str]: - armature = get_active_armature(context) + def fix_bone_parenting(self, armature: Object) -> None: + """Fix bone parenting to match standard hierarchy""" + bpy.ops.object.mode_set(mode='EDIT') + edit_bones = armature.data.edit_bones - with ProgressTracker(context, 4, "Cleanup Operations") as progress: - # Remove rigidbodies and joints - self.remove_physics_objects(armature) - progress.step("Removed physics objects") - - # Clear unused animation data - self.clear_unused_animations(armature) - progress.step("Cleared unused animations") - - # Remove empty objects - self.remove_empty_objects() - progress.step("Removed empty objects") - - # Clean up collections - self.cleanup_collections(armature) - progress.step("Cleaned up collections") - - return {'FINISHED'} - - def remove_physics_objects(self, armature: Object) -> None: - """Remove all physics objects and constraints""" - physics_objects = [obj for obj in bpy.data.objects - if obj.parent == armature and - (obj.rigid_body or obj.rigid_body_constraint)] + # Define parent-child relationships + hierarchy_map = { + 'hips': ['spine', 'left_leg', 'right_leg'], + 'spine': ['chest'], + 'chest': ['upper_chest', 'left_shoulder', 'right_shoulder'], + 'upper_chest': ['neck'], + 'neck': ['head'], + 'head': ['left_eye', 'right_eye'], + 'left_shoulder': ['left_arm'], + 'right_shoulder': ['right_arm'], + 'left_arm': ['left_elbow'], + 'right_arm': ['right_elbow'], + 'left_elbow': ['left_wrist'], + 'right_elbow': ['right_wrist'], + 'left_leg': ['left_knee'], + 'right_leg': ['right_knee'], + 'left_knee': ['left_ankle'], + 'right_knee': ['right_ankle'], + 'left_ankle': ['left_toe'], + 'right_ankle': ['right_toe'] + } - for obj in physics_objects: - bpy.data.objects.remove(obj, do_unlink=True) + # Apply parenting + for parent_name, children in hierarchy_map.items(): + parent_bone = None + for bone in edit_bones: + if simplify_bonename(bone.name) in bone_names[parent_name]: + parent_bone = bone + break + + if parent_bone: + for child_name in children: + for bone in edit_bones: + if simplify_bonename(bone.name) in bone_names[child_name]: + bone.parent = parent_bone - def clear_unused_animations(self, armature: Object) -> None: - """Remove unused animation data""" - if armature.animation_data: - if armature.animation_data.action and armature.animation_data.action.users == 0: - bpy.data.actions.remove(armature.animation_data.action) + def connect_bones(self, context: Context, armature: Object) -> None: + """Connect bones to their children where appropriate""" + if not context.scene.avatar_toolkit.mmd_connect_bones: + return - # Clear unused NLA tracks - if armature.animation_data.nla_tracks: - for track in armature.animation_data.nla_tracks: - if not track.strips: - armature.animation_data.nla_tracks.remove(track) - - def remove_empty_objects(self) -> None: - """Remove empty objects from the scene""" - empty_objects = [obj for obj in bpy.data.objects - if obj.type == 'EMPTY' and not obj.children] + bpy.ops.object.mode_set(mode='EDIT') + edit_bones = armature.data.edit_bones + min_distance = context.scene.avatar_toolkit.connect_bones_min_distance - for obj in empty_objects: - bpy.data.objects.remove(obj, do_unlink=True) + for bone in edit_bones: + if bone.children: + for child in bone.children: + # Check if bones are close enough to connect + distance = (bone.tail - child.head).length + if distance < min_distance: + bone.tail = child.head + child.use_connect = True - def cleanup_collections(self, armature: Object) -> None: - """Clean up and organize collections""" - # Remove empty collections - for collection in bpy.data.collections: - if not collection.objects and not collection.children: - bpy.data.collections.remove(collection) + def validate_hierarchy(self, armature: Object) -> bool: + """Validate final bone hierarchy""" + # Check essential parent-child relationships + essential_pairs = [ + ('spine', 'hips'), + ('chest', 'spine'), + ('neck', 'chest'), + ('head', 'neck') + ] + + for child, parent in essential_pairs: + if not validate_bone_hierarchy(armature.data.bones, parent, child): + return False - # Ensure armature is in main collection - if armature.users_collection[0] != bpy.context.scene.collection: - bpy.context.scene.collection.objects.link(armature) \ No newline at end of file + return True + + def execute(self, context: Context) -> Set[str]: + try: + armature = get_active_armature(context) + + # Save initial state + if context.scene.avatar_toolkit.save_backup_state: + self.initial_state = save_armature_state(armature) + + with ProgressTracker(context, 3, "Fixing Bone Hierarchy") as progress: + # Step 1: Fix bone parenting + self.fix_bone_parenting(armature) + progress.step("Fixed bone parenting") + + # Step 2: Connect bones + self.connect_bones(context, armature) + progress.step("Connected bones") + + # Step 3: Validate hierarchy + if not self.validate_hierarchy(armature): + self.report({'WARNING'}, t("MMD.hierarchy_validation_warning")) + progress.step("Validated hierarchy") + + self.report({'INFO'}, t("MMD.hierarchy_fixed")) + return {'FINISHED'} + + except Exception as e: + logger.error(f"Hierarchy fix failed: {str(e)}") + if hasattr(self, 'initial_state'): + restore_armature_state(armature, self.initial_state) + self.report({'ERROR'}, str(e)) + return {'CANCELLED'} + +class AvatarToolkit_OT_CleanupMMDArmature(Operator): + bl_idname = "avatar_toolkit.mmd_cleanup_armature" + bl_label = t("MMD.cleanup_armature") + bl_options = {'REGISTER', 'UNDO'} + + def remove_unused_bones(self, context: Context, armature: Object) -> None: + """Remove bones that aren't in the standard hierarchy or affecting weights""" + bpy.ops.object.mode_set(mode='EDIT') + edit_bones = armature.data.edit_bones + + # Get all bones affecting vertex groups + used_bones = set() + for mesh in get_all_meshes(context): + used_bones.update(group.name for group in mesh.vertex_groups) + + # Add essential bones from dictionary + essential_bones = set(bone_names.keys()) + + # Remove non-essential, unused bones + for bone in edit_bones[:]: # Slice to avoid modification during iteration + simplified_name = simplify_bonename(bone.name) + if (not any(simplified_name in variations for variations in bone_names.values()) and + bone.name not in used_bones): + edit_bones.remove(bone) + + def fix_bone_orientations(self, armature: Object) -> None: + """Fix bone orientations for Unity/VRChat compatibility""" + bpy.ops.object.mode_set(mode='EDIT') + edit_bones = armature.data.edit_bones + + # Standard bone alignments + alignments = { + 'spine': (0, 0, 1), # Points up + 'chest': (0, 0, 1), + 'neck': (0, 0, 1), + 'head': (0, 0, 1), + 'shoulder': (1, 0, 0), # Points outward + 'arm': (0, -1, 0), # Points down + 'elbow': (0, -1, 0), + 'leg': (0, -1, 0), + 'knee': (0, -1, 0), + 'foot': (1, 0, 0), # Points forward + } + + for bone in edit_bones: + simplified_name = simplify_bonename(bone.name) + for bone_type, direction in alignments.items(): + if bone_type in simplified_name: + # Calculate new tail position while maintaining length + length = (bone.tail - bone.head).length + bone.tail = bone.head + Vector(direction) * length + break + + def execute(self, context: Context) -> Set[str]: + try: + armature = get_active_armature(context) + + # Save initial state + if context.scene.avatar_toolkit.save_backup_state: + self.initial_state = save_armature_state(armature) + + with ProgressTracker(context, 2, "Cleaning Up Armature") as progress: + # Step 1: Remove unused bones + self.remove_unused_bones(context, armature) + progress.step("Removed unused bones") + + # Step 2: Fix bone orientations + self.fix_bone_orientations(armature) + progress.step("Fixed bone orientations") + + self.report({'INFO'}, t("MMD.cleanup_completed")) + return {'FINISHED'} + + except Exception as e: + logger.error(f"Armature cleanup failed: {str(e)}") + if hasattr(self, 'initial_state'): + restore_armature_state(armature, self.initial_state) + self.report({'ERROR'}, str(e)) + return {'CANCELLED'} diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index d42172e..7067f0b 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -188,34 +188,24 @@ "Tools.shapekey_tolerance_desc": "Minimum difference to consider a shape key as used", "Tools.shapekeys_removed": "Removed {count} unused shape keys", - "MMDTools.label": "MMD Tools", - "MMDTools.basic_tools": "Basic MMD Tools", - "MMDTools.advanced_tools": "Advanced Tools", - "MMDTools.settings": "MMD Settings", - "MMDTools.cleanup": "Cleanup Tools", - "MMDTools.fix_bone_names": "Fix Bone Names", - "MMDTools.fix_bone_names_desc": "Standardize and fix bone names", - "MMDTools.fix_hierarchy": "Fix Bone Hierarchy", - "MMDTools.fix_hierarchy_desc": "Fix bone parenting and hierarchy", - "MMDTools.fix_weights": "Fix Bone Weights", - "MMDTools.fix_weights_desc": "Clean up and normalize bone weights", - "MMDTools.fix_mmd_features": "Fix MMD Features", - "MMDTools.fix_mmd_features_desc": "Fix MMD-specific features and settings", - "MMDTools.advanced_bone_ops": "Advanced Bone Operations", - "MMDTools.advanced_bone_ops_desc": "Perform advanced bone fixes and cleanup", - "MMDTools.keep_upper_chest": "Keep Upper Chest", - "MMDTools.keep_upper_chest_desc": "Keep the upper chest bone during cleanup", - "MMDTools.remove_unused": "Remove Unused Bones", - "MMDTools.remove_unused_desc": "Remove bones with no weights or influence", - "MMDTools.merge_distance": "Merge Distance", - "MMDTools.merge_distance_desc": "Distance threshold for merging vertices", - "MMDTools.cleanup_shapekeys": "Clean Shape Keys", - "MMDTools.cleanup_shapekeys_desc": "Remove unused and duplicate shape keys", - "MMDTools.bones_renamed": "Bone names standardized successfully", - "MMDTools.hierarchy_fixed": "Bone hierarchy fixed successfully", - "MMDTools.weights_fixed": "Bone weights cleaned and normalized", - "MMDTools.no_meshes": "No meshes found to process", - "MMDTools.not_mmd_model": "Selected armature is not an MMD model", + "MMD.label": "MMD Tools", + "MMD.bone_standardization": "Bone Standardization", + "MMD.weight_processing": "Weight Processing", + "MMD.hierarchy": "Bone Hierarchy", + "MMD.cleanup": "Cleanup", + "MMD.no_armature": "No armature selected", + "MMD.no_meshes": "No meshes found", + "MMD.validation.rigify_unsupported": "Rigify armatures are not supported", + "MMD.validation.multi_user_mesh": "Multi-user mesh detected: {mesh}", + "MMD.bones_standardized": "Bones standardized successfully", + "MMD.weights_processed": "Weights processed successfully", + "MMD.hierarchy_fixed": "Bone hierarchy fixed successfully", + "MMD.hierarchy_validation_warning": "Some hierarchy relationships could not be validated", + "MMD.cleanup_completed": "Armature cleanup completed", + "MMD.process_twist_bones": "Process Twist Bones", + "MMD.process_twist_bones_desc": "Transfer weights from twist bones to their parent bones", + "MMD.connect_bones": "Connect Bones", + "MMD.connect_bones_desc": "Connect bones in chain where appropriate", "Settings.label": "Settings", "Settings.language": "Language", diff --git a/ui/mmd_panel.py b/ui/mmd_panel.py index 39f5821..a4b384b 100644 --- a/ui/mmd_panel.py +++ b/ui/mmd_panel.py @@ -5,48 +5,42 @@ from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME from ..core.translations import t class AvatarToolKit_PT_MMDPanel(Panel): - """Panel containing MMD-specific tools and operations""" - bl_label = t("MMDTools.label") + """Panel containing MMD conversion and optimization tools""" + bl_label = t("MMD.label") bl_idname = "OBJECT_PT_avatar_toolkit_mmd" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = CATEGORY_NAME bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname - bl_order = 3 + bl_order = 2 def draw(self, context: Context) -> None: - """Draw the MMD tools panel interface""" - layout = self.layout + layout: UILayout = self.layout - # Basic MMD Tools Box - basic_box = layout.box() - col = basic_box.column(align=True) - col.label(text=t("MMDTools.basic_tools"), icon='ARMATURE_DATA') + # Bone Standardization Box + bone_box: UILayout = layout.box() + col: UILayout = bone_box.column(align=True) + col.label(text=t("MMD.bone_standardization"), icon='ARMATURE_DATA') col.separator(factor=0.5) - col.operator("avatar_toolkit.fix_bone_names", icon='SORTALPHA') - col.operator("avatar_toolkit.fix_bone_hierarchy", icon='BONE_DATA') - col.operator("avatar_toolkit.fix_bone_weights", icon='GROUP_BONE') + col.operator("avatar_toolkit.mmd_standardize_bones", icon='BONE_DATA') - # Advanced MMD Tools Box - advanced_box = layout.box() - col = advanced_box.column(align=True) - col.label(text=t("MMDTools.advanced_tools"), icon='MODIFIER') + # Weight Processing Box + weight_box: UILayout = layout.box() + col = weight_box.column(align=True) + col.label(text=t("MMD.weight_processing"), icon='GROUP_VERTEX') col.separator(factor=0.5) - col.operator("avatar_toolkit.fix_mmd_features", icon='SHAPEKEY_DATA') - col.operator("avatar_toolkit.advanced_bone_ops", icon='CONSTRAINT_BONE') + col.operator("avatar_toolkit.mmd_process_weights", icon='WPAINT_HLT') - # Settings Box - settings_box = layout.box() - col = settings_box.column(align=True) - col.label(text=t("MMDTools.settings"), icon='PREFERENCES') + # Hierarchy Box + hierarchy_box: UILayout = layout.box() + col = hierarchy_box.column(align=True) + col.label(text=t("MMD.hierarchy"), icon='OUTLINER') col.separator(factor=0.5) - col.prop(context.scene.avatar_toolkit, "mmd_keep_upper_chest") - col.prop(context.scene.avatar_toolkit, "mmd_remove_unused_bones") - col.prop(context.scene.avatar_toolkit, "mmd_cleanup_shapekeys") + col.operator("avatar_toolkit.mmd_fix_hierarchy", icon='CONSTRAINT_BONE') # Cleanup Box - cleanup_box = layout.box() + cleanup_box: UILayout = layout.box() col = cleanup_box.column(align=True) - col.label(text=t("MMDTools.cleanup"), icon='TRASH') + col.label(text=t("MMD.cleanup"), icon='BRUSH_DATA') col.separator(factor=0.5) - col.operator("avatar_toolkit.cleanup_operations", icon='BRUSH_DATA') + col.operator("avatar_toolkit.mmd_cleanup_armature", icon='MODIFIER')