diff --git a/README.md b/README.md index b961eb6..74c8a9e 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,8 @@ See everything Avatar Toolkit has ot offer [here](https://avatartoolkit.xyz/lega ## Requirements 1) Blender Version -- Blender 4.4 or newer is required -- Blender 4.4 is the current recommended version +- Blender 4.5 or newer is required +- Blender 4.5 is the current recommended version 2) Python Requirements @@ -42,7 +42,7 @@ See everything Avatar Toolkit has ot offer [here](https://avatartoolkit.xyz/lega 3) Recommended Setup - Download Blender directly from https://blender.org -- Use Blender 4.4 for the best experience +- Use Blender 4.5 for the best experience #### Additional Plugins Requirements. Currently None. diff --git a/blender_manifest.toml b/blender_manifest.toml index 77dd551..c61abd6 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -3,13 +3,13 @@ schema_version = "1.0.0" id = "avatar_toolkit" -version = "0.3.0" +version = "0.4.0" name = "Avatar Toolkit" tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games." maintainer = "Team NekoNeo" type = "add-on" -blender_version_min = "4.4.0" +blender_version_min = "4.5.0" license = [ "SPDX:GPL-3.0-or-later", diff --git a/core/armature_validation.py b/core/armature_validation.py index 7a72d7d..3b5330b 100644 --- a/core/armature_validation.py +++ b/core/armature_validation.py @@ -211,9 +211,163 @@ def validate_bone_hierarchy(bones: Dict[str, Bone], parent_name: str, child_name return False return bones[child_name].parent == bones[parent_name] +def extract_bone_side_info(bone_name: str) -> Tuple[str, str]: + """ + Extract base bone name and side indicator from a bone name. + Returns (base_name, side) where side is 'L', 'R', or '' + """ + normalized = simplify_bonename(bone_name) + original = bone_name + + # Common left/right patterns to check + left_patterns = [ + 'left', 'l', 'lft', 'lt', + '.l', '_l', '-l', ' l', + '左', 'ひだり' + ] + + right_patterns = [ + 'right', 'r', 'rgt', 'rt', + '.r', '_r', '-r', ' r', + '右', 'みぎ' + ] + + # Check for left patterns + for pattern in left_patterns: + pattern_norm = simplify_bonename(pattern) + if normalized.startswith(pattern_norm): + base = normalized[len(pattern_norm):] + if base: # Make sure there's something left + return base, 'L' + elif normalized.endswith(pattern_norm): + base = normalized[:-len(pattern_norm)] + if base: + return base, 'L' + elif pattern_norm in normalized: + # Handle cases like ArmLeft + parts = normalized.split(pattern_norm) + if len(parts) == 2: + base = parts[0] + parts[1] + if base: + return base, 'L' + + # Check for right patterns + for pattern in right_patterns: + pattern_norm = simplify_bonename(pattern) + if normalized.startswith(pattern_norm): + base = normalized[len(pattern_norm):] + if base: + return base, 'R' + elif normalized.endswith(pattern_norm): + base = normalized[:-len(pattern_norm)] + if base: + return base, 'R' + elif pattern_norm in normalized: + parts = normalized.split(pattern_norm) + if len(parts) == 2: + base = parts[0] + parts[1] + if base: + return base, 'R' + + return normalized, '' + +def find_symmetric_bone_pairs(bones: Dict[str, Bone]) -> Dict[str, Tuple[List[str], List[str]]]: + """ + Automatically find symmetric bone pairs in the armature. + Returns dict mapping base_name to (left_bones, right_bones) + """ + bone_groups = {} + + for bone_name in bones.keys(): + base, side = extract_bone_side_info(bone_name) + + if side: + if base not in bone_groups: + bone_groups[base] = {'L': [], 'R': []} + bone_groups[base][side].append(bone_name) + + symmetric_pairs = {} + for base, sides in bone_groups.items(): + if sides['L'] and sides['R']: + symmetric_pairs[base] = (sides['L'], sides['R']) + + return symmetric_pairs + +def validate_armature_symmetry(armature: Object) -> Tuple[bool, List[str]]: + """ + Comprehensive symmetry validation that provides detailed feedback + """ + if not armature or armature.type != 'ARMATURE': + return False, ["Invalid armature"] + + bones = {bone.name: bone for bone in armature.data.bones} + symmetric_pairs = find_symmetric_bone_pairs(bones) + + messages = [] + is_symmetric = True + + if symmetric_pairs: + messages.append("Found symmetric bone pairs:") + for base, (left_bones, right_bones) in symmetric_pairs.items(): + left_count = len(left_bones) + right_count = len(right_bones) + + if left_count == right_count: + messages.append(f" ✓ {base}: {left_count} bones on each side") + for l_bone, r_bone in zip(sorted(left_bones), sorted(right_bones)): + messages.append(f" {l_bone} ↔ {r_bone}") + else: + is_symmetric = False + messages.append(f" ✗ {base}: {left_count} left, {right_count} right bones") + messages.append(f" Left: {', '.join(sorted(left_bones))}") + messages.append(f" Right: {', '.join(sorted(right_bones))}") + else: + messages.append("No symmetric bone pairs detected") + is_symmetric = False + + return is_symmetric, messages + def validate_symmetry(bones: Dict[str, Bone], base: str, left: str, right: str) -> bool: """Validate if matching left and right bones exist for a given base bone name""" - # Extract left and right bone names from both hierarchies + # First try the new intelligent detection + symmetric_pairs = find_symmetric_bone_pairs(bones) + + # Look for bones that match the requested base type + matching_left_bones = [] + matching_right_bones = [] + + # Check each detected symmetric pair + for pair_base, (left_bones, right_bones) in symmetric_pairs.items(): + if base.lower() in pair_base.lower() or pair_base.lower() in base.lower(): + matching_left_bones.extend(left_bones) + matching_right_bones.extend(right_bones) + + if matching_left_bones or matching_right_bones: + left_bases = {} + right_bases = {} + + for bone_name in matching_left_bones: + bone_base, side = extract_bone_side_info(bone_name) + if bone_base not in left_bases: + left_bases[bone_base] = [] + left_bases[bone_base].append(bone_name) + + for bone_name in matching_right_bones: + bone_base, side = extract_bone_side_info(bone_name) + if bone_base not in right_bases: + right_bases[bone_base] = [] + right_bases[bone_base].append(bone_name) + + all_bases = set(left_bases.keys()) | set(right_bases.keys()) + for bone_base in all_bases: + left_count = len(left_bases.get(bone_base, [])) + right_count = len(right_bases.get(bone_base, [])) + if left_count != right_count: + return False + + return len(all_bases) > 0 + + # Fallback to original dictionary-based method left_bone_names = set() right_bone_names = set() diff --git a/core/common.py b/core/common.py index 01673b6..e3a9067 100644 --- a/core/common.py +++ b/core/common.py @@ -653,6 +653,9 @@ def store_breaking_settings_armature(armature: bpy.types.Object) -> ArmatureData return (armature_data.use_mirror_x, armature.pose.use_mirror_x) def restore_breaking_settings_armature(armature: bpy.types.Object, data: ArmatureData) -> None: + # Check if armature object is still valid (not removed) + if not armature or armature.name not in bpy.data.objects: + return armature_data: bpy.types.Armature = armature.data armature_data.use_mirror_x, armature.pose.use_mirror_x = data diff --git a/core/dictionaries.py b/core/dictionaries.py index 58aeabd..3e4be29 100644 --- a/core/dictionaries.py +++ b/core/dictionaries.py @@ -456,63 +456,63 @@ standard_bones = { 'neck': 'Neck', 'head': 'Head', - # Arms - 'left_shoulder': 'Shoulder.L', - 'left_arm': 'UpperArm.L', - 'left_elbow': 'LowerArm.L', - 'left_wrist': 'Hand.L', - 'right_shoulder': 'Shoulder.R', - 'right_arm': 'UpperArm.R', - 'right_elbow': 'LowerArm.R', - 'right_wrist': 'Hand.R', + # Arms + 'left_shoulder': 'Shoulder_L', + 'left_arm': 'UpperArm_L', + 'left_elbow': 'LowerArm_L', + 'left_wrist': 'Hand_L', + 'right_shoulder': 'Shoulder_R', + 'right_arm': 'UpperArm_R', + 'right_elbow': 'LowerArm_R', + 'right_wrist': 'Hand_R', # Legs - 'left_leg': 'UpperLeg.L', - 'left_knee': 'LowerLeg.L', - 'left_ankle': 'Foot.L', - 'left_toe': 'Toes.L', - 'right_leg': 'UpperLeg.R', - 'right_knee': 'LowerLeg.R', - 'right_ankle': 'Foot.R', - 'right_toe': 'Toes.R', + 'left_leg': 'UpperLeg_L', + 'left_knee': 'LowerLeg_L', + 'left_ankle': 'Foot_L', + 'left_toe': 'Toe_L', + 'right_leg': 'UpperLeg_R', + 'right_knee': 'LowerLeg_R', + 'right_ankle': 'Foot_R', + 'right_toe': 'Toe_R', # Fingers Left - 'thumb_1_l': 'Thumb1.L', - 'thumb_2_l': 'Thumb2.L', - 'thumb_3_l': 'Thumb3.L', - 'index_1_l': 'Index1.L', - 'index_2_l': 'Index2.L', - 'index_3_l': 'Index3.L', - 'middle_1_l': 'Middle1.L', - 'middle_2_l': 'Middle2.L', - 'middle_3_l': 'Middle3.L', - 'ring_1_l': 'Ring1.L', - 'ring_2_l': 'Ring2.L', - 'ring_3_l': 'Ring3.L', - 'pinkie_1_l': 'Pinky1.L', - 'pinkie_2_l': 'Pinky2.L', - 'pinkie_3_l': 'Pinky3.L', + 'thumb_1_l': 'Thumb_L', + 'thumb_2_l': 'Thumb_L.001', + 'thumb_3_l': 'Thumb_L.002', + 'index_1_l': 'Index_L', + 'index_2_l': 'Index_L.001', + 'index_3_l': 'Index_L.002', + 'middle_1_l': 'Middle_L', + 'middle_2_l': 'Middle_L.001', + 'middle_3_l': 'Middle_L.002', + 'ring_1_l': 'Ring_L', + 'ring_2_l': 'Ring_L.001', + 'ring_3_l': 'Ring_L.002', + 'pinkie_1_l': 'Pinky_L', + 'pinkie_2_l': 'Pinky_L.001', + 'pinkie_3_l': 'Pinky_L.002', # Fingers Right - 'thumb_1_r': 'Thumb1.R', - 'thumb_2_r': 'Thumb2.R', - 'thumb_3_r': 'Thumb3.R', - 'index_1_r': 'Index1.R', - 'index_2_r': 'Index2.R', - 'index_3_r': 'Index3.R', - 'middle_1_r': 'Middle1.R', - 'middle_2_r': 'Middle2.R', - 'middle_3_r': 'Middle3.R', - 'ring_1_r': 'Ring1.R', - 'ring_2_r': 'Ring2.R', - 'ring_3_r': 'Ring3.R', - 'pinkie_1_r': 'Pinky1.R', - 'pinkie_2_r': 'Pinky2.R', - 'pinkie_3_r': 'Pinky3.R', + 'thumb_1_r': 'Thumb_R', + 'thumb_2_r': 'Thumb_R.001', + 'thumb_3_r': 'Thumb_R.002', + 'index_1_r': 'Index_R', + 'index_2_r': 'Index_R.001', + 'index_3_r': 'Index_R.002', + 'middle_1_r': 'Middle_R', + 'middle_2_r': 'Middle_R.001', + 'middle_3_r': 'Middle_R.002', + 'ring_1_r': 'Ring_R', + 'ring_2_r': 'Ring_R.001', + 'ring_3_r': 'Ring_R.002', + 'pinkie_1_r': 'Pinky_R', + 'pinkie_2_r': 'Pinky_R.001', + 'pinkie_3_r': 'Pinky_R.002', # Eyes - 'left_eye': 'Eye.L', - 'right_eye': 'Eye.R', + 'left_eye': 'Eye_L', + 'right_eye': 'Eye_R' # Breast bones 'breast_1_l': 'Breast1_L', @@ -529,46 +529,48 @@ bone_hierarchy = [ ('Chest', 'Chest.Up'), ('Chest.Up', 'Neck'), ('Neck', 'Head'), - ('Head', 'Eye.L'), - ('Head', 'Eye.R'), + ('Head', 'Eye_L'), + ('Head', 'Eye_R'), # Left Arm Chain - ('Chest.Up', 'UpperArm.L'), - ('UpperArm.L', 'LowerArm.L'), - ('LowerArm.L', 'Hand.L'), + ('Chest.Up', 'Shoulder_L'), + ('Shoulder_L', 'UpperArm_L'), + ('UpperArm_L', 'LowerArm_L'), + ('LowerArm_L', 'Hand_L'), # Right Arm Chain - ('Chest.Up', 'UpperArm.R'), - ('UpperArm.R', 'LowerArm.R'), - ('LowerArm.R', 'Hand.R'), + ('Chest.Up', 'Shoulder_R'), + ('Shoulder_R', 'UpperArm_R'), + ('UpperArm_R', 'LowerArm_R'), + ('LowerArm_R', 'Hand_R'), # Left Leg Chain - ('Hips', 'UpperLeg.L'), - ('UpperLeg.L', 'LowerLeg.L'), - ('LowerLeg.L', 'Foot.L'), - ('Foot.L', 'Toes.L'), + ('Hips', 'UpperLeg_L'), + ('UpperLeg_L', 'LowerLeg_L'), + ('LowerLeg_L', 'Foot_L'), + ('Foot_L', 'Toe_L'), # Right Leg Chain - ('Hips', 'UpperLeg.R'), - ('UpperLeg.R', 'LowerLeg.R'), - ('LowerLeg.R', 'Foot.R'), - ('Foot.R', 'Toes.R') + ('Hips', 'UpperLeg_R'), + ('UpperLeg_R', 'LowerLeg_R'), + ('LowerLeg_R', 'Foot_R'), + ('Foot_R', 'Toe_R') ] finger_hierarchy = { 'left': [ - ('Hand.L', 'Thumb1.L', 'Thumb2.L', 'Thumb3.L'), - ('Hand.L', 'Index1.L', 'Index2.L', 'Index3.L'), - ('Hand.L', 'Middle1.L', 'Middle2.L', 'Middle3.L'), - ('Hand.L', 'Ring1.L', 'Ring2.L', 'Ring3.L'), - ('Hand.L', 'Pinky1.L', 'Pinky2.L', 'Pinky3.L') + ('Hand_L', 'Thumb_L', 'Thumb_L.001', 'Thumb_L.002'), + ('Hand_L', 'Index_L', 'Index_L.001', 'Index_L.002'), + ('Hand_L', 'Middle_L', 'Middle_L.001', 'Middle_L.002'), + ('Hand_L', 'Ring_L', 'Ring_L.001', 'Ring_L.002'), + ('Hand_L', 'Pinky_L', 'Pinky_L.001', 'Pinky_L.002') ], 'right': [ - ('Hand.R', 'Thumb1.R', 'Thumb2.R', 'Thumb3.R'), - ('Hand.R', 'Index1.R', 'Index2.R', 'Index3.R'), - ('Hand.R', 'Middle1.R', 'Middle2.R', 'Middle3.R'), - ('Hand.R', 'Ring1.R', 'Ring2.R', 'Ring3.R'), - ('Hand.R', 'Pinky1.R', 'Pinky2.R', 'Pinky3.R') + ('Hand_R', 'Thumb_R', 'Thumb_R.001', 'Thumb_R.002'), + ('Hand_R', 'Index_R', 'Index_R.001', 'Index_R.002'), + ('Hand_R', 'Middle_R', 'Middle_R.001', 'Middle_R.002'), + ('Hand_R', 'Ring_R', 'Ring_R.001', 'Ring_R.002'), + ('Hand_R', 'Pinky_R', 'Pinky_R.001', 'Pinky_R.002') ] } @@ -601,6 +603,8 @@ acceptable_bone_hierarchy = [ ('Head', 'Eye_R'), ('Head', 'LeftEye'), ('Head', 'RightEye'), + ('Head', 'Eye.L'), + ('Head', 'Eye.R'), # Unity humanoid naming ('Hips', 'Spine'), @@ -611,6 +615,40 @@ acceptable_bone_hierarchy = [ ('Head', 'LeftEye'), ('Head', 'RightEye'), + # Old standard bone hierarchy patterns + ('Chest.Up', 'UpperArm.L'), + ('UpperArm.L', 'LowerArm.L'), + ('LowerArm.L', 'Hand.L'), + ('Chest.Up', 'UpperArm.R'), + ('UpperArm.R', 'LowerArm.R'), + ('LowerArm.R', 'Hand.R'), + ('Hips', 'UpperLeg.L'), + ('UpperLeg.L', 'LowerLeg.L'), + ('LowerLeg.L', 'Foot.L'), + ('Foot.L', 'Toes.L'), + ('Hips', 'UpperLeg.R'), + ('UpperLeg.R', 'LowerLeg.R'), + ('LowerLeg.R', 'Foot.R'), + ('Foot.R', 'Toes.R'), + + # New standard bone hierarchy patterns (with shoulders) + ('Chest.Up', 'Shoulder_L'), + ('Shoulder_L', 'UpperArm_L'), + ('UpperArm_L', 'LowerArm_L'), + ('LowerArm_L', 'Hand_L'), + ('Chest.Up', 'Shoulder_R'), + ('Shoulder_R', 'UpperArm_R'), + ('UpperArm_R', 'LowerArm_R'), + ('LowerArm_R', 'Hand_R'), + ('Hips', 'UpperLeg_L'), + ('UpperLeg_L', 'LowerLeg_L'), + ('LowerLeg_L', 'Foot_L'), + ('Foot_L', 'Toe_L'), + ('Hips', 'UpperLeg_R'), + ('UpperLeg_R', 'LowerLeg_R'), + ('LowerLeg_R', 'Foot_R'), + ('Foot_R', 'Toe_R'), + ] acceptable_bone_names = { @@ -618,59 +656,75 @@ acceptable_bone_names = { 'chest': ['Chest', 'spine1', 'Spine1', 'spine_01', 'SPINE1', 'Spine01'], 'neck': ['Neck', 'neck_01', 'Neck01'], 'head': ['Head', 'head_01', 'Head01'], - 'eye_l': ['Eye_L', 'LeftEye', 'lefteye', 'eye_left', 'EyeLeft'], - 'eye_r': ['Eye_R', 'RightEye', 'righteye', 'eye_right', 'EyeRight'], + 'eye_l': ['Eye_L', 'LeftEye', 'lefteye', 'eye_left', 'EyeLeft', 'Eye.L'], + 'eye_r': ['Eye_R', 'RightEye', 'righteye', 'eye_right', 'EyeRight', 'Eye.R'], - 'shoulder_r': ['Shoulder.R', 'clavicle_r', 'ClavicleRight', 'RightShoulder'], - 'arm_r': ['Arm.R', 'upperarm_r', 'UpperArmRight', 'RightArm'], - 'elbow_r': ['Elbow.R', 'lowerarm_r', 'ForearmRight', 'RightForeArm'], - 'wrist_r': ['Wrist.R', 'hand_r', 'HandRight', 'RightHand'], - 'leg_r': ['Leg.R', 'thigh_r', 'ThighRight', 'RightLeg', 'RightUpLeg'], - 'knee_r': ['Knee.R', 'calf_r', 'CalfRight', 'RightShin', 'RightLowerLeg'], - 'foot_r': ['Foot.R', 'foot_r', 'FootRight', 'RightFoot'], - 'toes_r': ['Toes.R', 'ball_r', 'ToeRight', 'RightToeBase'], + 'shoulder_r': ['Shoulder.R', 'clavicle_r', 'ClavicleRight', 'RightShoulder', 'Shoulder_R'], + 'arm_r': ['Arm.R', 'upperarm_r', 'UpperArmRight', 'RightArm', 'UpperArm.R', 'UpperArm_R'], + 'elbow_r': ['Elbow.R', 'lowerarm_r', 'ForearmRight', 'RightForeArm', 'LowerArm.R', 'LowerArm_R'], + 'wrist_r': ['Wrist.R', 'hand_r', 'HandRight', 'RightHand', 'Hand.R', 'Hand_R'], + 'leg_r': ['Leg.R', 'thigh_r', 'ThighRight', 'RightLeg', 'RightUpLeg', 'UpperLeg.R', 'UpperLeg_R'], + 'knee_r': ['Knee.R', 'calf_r', 'CalfRight', 'RightShin', 'RightLowerLeg', 'LowerLeg.R', 'LowerLeg_R'], + 'foot_r': ['Foot.R', 'foot_r', 'FootRight', 'RightFoot', 'Foot_R'], + 'toes_r': ['Toes.R', 'ball_r', 'ToeRight', 'RightToeBase', 'Toe_R'], - 'shoulder_l': ['Shoulder.L', 'clavicle_l', 'ClavicleLeft', 'LeftShoulder'], - 'arm_l': ['Arm.L', 'upperarm_l', 'UpperArmLeft', 'LeftArm'], - 'elbow_l': ['Elbow.L', 'lowerarm_l', 'ForearmLeft', 'LeftForeArm'], - 'wrist_l': ['Wrist.L', 'hand_l', 'HandLeft', 'LeftHand'], - 'leg_l': ['Leg.L', 'thigh_l', 'ThighLeft', 'LeftLeg', 'LeftUpLeg'], - 'knee_l': ['Knee.L', 'calf_l', 'CalfLeft', 'LeftShin', 'LeftLowerLeg'], - 'foot_l': ['Foot.L', 'foot_l', 'FootLeft', 'LeftFoot'], - 'toes_l': ['Toes.L', 'ball_l', 'ToeLeft', 'LeftToeBase'], + 'shoulder_l': ['Shoulder.L', 'clavicle_l', 'ClavicleLeft', 'LeftShoulder', 'Shoulder_L'], + 'arm_l': ['Arm.L', 'upperarm_l', 'UpperArmLeft', 'LeftArm', 'UpperArm.L', 'UpperArm_L'], + 'elbow_l': ['Elbow.L', 'lowerarm_l', 'ForearmLeft', 'LeftForeArm', 'LowerArm.L', 'LowerArm_L'], + 'wrist_l': ['Wrist.L', 'hand_l', 'HandLeft', 'LeftHand', 'Hand.L', 'Hand_L'], + 'leg_l': ['Leg.L', 'thigh_l', 'ThighLeft', 'LeftLeg', 'LeftUpLeg', 'UpperLeg.L', 'UpperLeg_L'], + 'knee_l': ['Knee.L', 'calf_l', 'CalfLeft', 'LeftShin', 'LeftLowerLeg', 'LowerLeg.L', 'LowerLeg_L'], + 'foot_l': ['Foot.L', 'foot_l', 'FootLeft', 'LeftFoot', 'Foot_L'], + 'toes_l': ['Toes.L', 'ball_l', 'ToeLeft', 'LeftToeBase', 'Toe_L'], # Add finger bones for left hand - 'thumb_0_l': ['Thumb0_L'], - 'thumb_1_l': ['Thumb1_L'], - 'thumb_2_l': ['Thumb2_L'], - 'index_1_l': ['IndexFinger1_L'], - 'index_2_l': ['IndexFinger2_L'], - 'index_3_l': ['IndexFinger3_L'], - 'middle_1_l': ['MiddleFinger1_L'], - 'middle_2_l': ['MiddleFinger2_L'], - 'middle_3_l': ['MiddleFinger3_L'], - 'ring_1_l': ['RingFinger1_L'], - 'ring_2_l': ['RingFinger2_L'], - 'ring_3_l': ['RingFinger3_L'], + 'thumb_0_l': ['Thumb0_L', 'Thumb0.L'], + 'thumb_1_l': ['Thumb1_L', 'Thumb1.L', 'Thumb_L'], + 'thumb_2_l': ['Thumb2_L', 'Thumb2.L', 'Thumb_L.001'], + 'thumb_3_l': ['Thumb3_L', 'Thumb3.L', 'Thumb_L.002'], + 'index_1_l': ['IndexFinger1_L', 'IndexFinger1.L', 'Index1.L', 'Index_L'], + 'index_2_l': ['IndexFinger2_L', 'IndexFinger2.L', 'Index2.L', 'Index_L.001'], + 'index_3_l': ['IndexFinger3_L', 'IndexFinger3.L', 'Index3.L', 'Index_L.002'], + 'middle_1_l': ['MiddleFinger1_L', 'MiddleFinger1.L', 'Middle1.L', 'Middle_L'], + 'middle_2_l': ['MiddleFinger2_L', 'MiddleFinger2.L', 'Middle2.L', 'Middle_L.001'], + 'middle_3_l': ['MiddleFinger3_L', 'MiddleFinger3.L', 'Middle3.L', 'Middle_L.002'], + 'ring_1_l': ['RingFinger1_L', 'RingFinger1.L', 'Ring1.L', 'Ring_L'], + 'ring_2_l': ['RingFinger2_L', 'RingFinger2.L', 'Ring2.L', 'Ring_L.001'], + 'ring_3_l': ['RingFinger3_L', 'RingFinger3.L', 'Ring3.L', 'Ring_L.002'], + 'pinky_1_l': ['Pinky1_L', 'Pinky1.L', 'Pinky_L'], + 'pinky_2_l': ['Pinky2_L', 'Pinky2.L', 'Pinky_L.001'], + 'pinky_3_l': ['Pinky3_L', 'Pinky3.L', 'Pinky_L.002'], # Add finger bones for right hand - 'thumb_0_r': ['Thumb0_R', 'ThumbO_R'], - 'thumb_1_r': ['Thumb1_R'], - 'thumb_2_r': ['Thumb2_R'], - 'index_1_r': ['IndexFinger1_R'], - 'index_2_r': ['IndexFinger2_R'], - 'index_3_r': ['IndexFinger3_R'], - 'middle_1_r': ['MiddleFinger1_R'], - 'middle_2_r': ['MiddleFinger2_R'], - 'middle_3_r': ['MiddleFinger3_R'], - 'ring_1_r': ['RingFinger1_R'], - 'ring_2_r': ['RingFinger2_R'], - 'ring_3_r': ['RingFinger3_R'], + 'thumb_0_r': ['Thumb0_R', 'Thumb0.R', 'ThumbO_R'], + 'thumb_1_r': ['Thumb1_R', 'Thumb1.R', 'Thumb_R'], + 'thumb_2_r': ['Thumb2_R', 'Thumb2.R', 'Thumb_R.001'], + 'thumb_3_r': ['Thumb3_R', 'Thumb3.R', 'Thumb_R.002'], + 'index_1_r': ['IndexFinger1_R', 'IndexFinger1.R', 'Index1.R', 'Index_R'], + 'index_2_r': ['IndexFinger2_R', 'IndexFinger2.R', 'Index2.R', 'Index_R.001'], + 'index_3_r': ['IndexFinger3_R', 'IndexFinger3.R', 'Index3.R', 'Index_R.002'], + 'middle_1_r': ['MiddleFinger1_R', 'MiddleFinger1.R', 'Middle1.R', 'Middle_R'], + 'middle_2_r': ['MiddleFinger2_R', 'MiddleFinger2.R', 'Middle2.R', 'Middle_R.001'], + 'middle_3_r': ['MiddleFinger3_R', 'MiddleFinger3.R', 'Middle3.R', 'Middle_R.002'], + 'ring_1_r': ['RingFinger1_R', 'RingFinger1.R', 'Ring1.R', 'Ring_R'], + 'ring_2_r': ['RingFinger2_R', 'RingFinger2.R', 'Ring2.R', 'Ring_R.001'], + 'ring_3_r': ['RingFinger3_R', 'RingFinger3.R', 'Ring3.R', 'Ring_R.002'], + 'pinky_1_r': ['Pinky1_R', 'Pinky1.R', 'Pinky_R'], + 'pinky_2_r': ['Pinky2_R', 'Pinky2.R', 'Pinky_R.001'], + 'pinky_3_r': ['Pinky3_R', 'Pinky3.R', 'Pinky_R.002'], - 'breast_upper_1_l': ['BreastUpper1_L'], - 'breast_upper_2_l': ['BreastUpper2_L'], - 'breast_upper_1_r': ['BreastUpper1_R'], - 'breast_upper_2_r': ['BreastUpper2_R'], + 'breast_upper_1_l': ['BreastUpper1_L', 'BreastUpper1.L'], + 'breast_upper_2_l': ['BreastUpper2_L', 'BreastUpper2.L'], + 'breast_upper_1_r': ['BreastUpper1_R', 'BreastUpper1.R'], + 'breast_upper_2_r': ['BreastUpper2_R', 'BreastUpper2.R'], + + # Little finger bones + 'little_finger_1_l': ['LittleFinger1_L', 'LittleFinger1.L'], + 'little_finger_2_l': ['LittleFinger2_L', 'LittleFinger2.L'], + 'little_finger_3_l': ['LittleFinger3_L', 'LittleFinger3.L'], + 'little_finger_1_r': ['LittleFinger1_R', 'LittleFinger1.R'], + 'little_finger_2_r': ['LittleFinger2_R', 'LittleFinger2.R'], + 'little_finger_3_r': ['LittleFinger3_R', 'LittleFinger3.R'], 'ear_upper_l': ['UpperEar.L', 'Upper Ear.L', 'Upper Ear_L'], 'ear_upper_r': ['UpperEar.R', 'Upper Ear.R', 'Upper Ear_R'], @@ -790,17 +844,17 @@ non_standard_mappings = { 'left_arm': [ 'mixamorig:LeftArm', 'mixamorig_LeftArm', 'ORG-upper_arm.L', 'upper_arm.L', - 'lShldrBend', 'lShldrTwist', 'lArm' + 'lShldrBend', 'lShldrTwist', 'lArm', 'UpperArm.L' ], 'left_elbow': [ 'mixamorig:LeftForeArm', 'mixamorig_LeftForeArm', 'ORG-forearm.L', 'forearm.L', - 'lForearmBend', 'lElbow', 'lForeArm' + 'lForearmBend', 'lElbow', 'lForeArm', 'LowerArm.L' ], 'left_wrist': [ 'mixamorig:LeftHand', 'mixamorig_LeftHand', 'ORG-hand.L', 'hand.L', - 'lHand', 'lWrist' + 'lHand', 'lWrist', 'Hand.L' ], 'right_shoulder': [ @@ -811,59 +865,59 @@ non_standard_mappings = { 'right_arm': [ 'mixamorig:RightArm', 'mixamorig_RightArm', 'ORG-upper_arm.R', 'upper_arm.R', - 'rShldrBend', 'rShldrTwist', 'rArm' + 'rShldrBend', 'rShldrTwist', 'rArm', 'UpperArm.R' ], 'right_elbow': [ 'mixamorig:RightForeArm', 'mixamorig_RightForeArm', 'ORG-forearm.R', 'forearm.R', - 'rForearmBend', 'rElbow', 'rForeArm' + 'rForearmBend', 'rElbow', 'rForeArm', 'LowerArm.R' ], 'right_wrist': [ 'mixamorig:RightHand', 'mixamorig_RightHand', 'ORG-hand.R', 'hand.R', - 'rHand', 'rWrist' + 'rHand', 'rWrist', 'Hand.R' ], 'left_leg': [ 'mixamorig:LeftUpLeg', 'mixamorig_LeftUpLeg', 'ORG-thigh.L', 'thigh.L', - 'lThighBend', 'lThigh' + 'lThighBend', 'lThigh', 'UpperLeg.L' ], 'left_knee': [ 'mixamorig:LeftLeg', 'mixamorig_LeftLeg', 'ORG-shin.L', 'shin.L', - 'lShin', 'lKnee', 'lLeg' + 'lShin', 'lKnee', 'lLeg', 'LowerLeg.L' ], 'left_ankle': [ 'mixamorig:LeftFoot', 'mixamorig_LeftFoot', 'ORG-foot.L', 'foot.L', - 'lFoot', 'lAnkle' + 'lFoot', 'lAnkle', 'Foot.L' ], 'left_toe': [ 'mixamorig:LeftToeBase', 'mixamorig_LeftToeBase', 'ORG-toe.L', 'toe.L', - 'lToe' + 'lToe', 'Toes.L' ], 'right_leg': [ 'mixamorig:RightUpLeg', 'mixamorig_RightUpLeg', 'ORG-thigh.R', 'thigh.R', - 'rThighBend', 'rThigh' + 'rThighBend', 'rThigh', 'UpperLeg.R' ], 'right_knee': [ 'mixamorig:RightLeg', 'mixamorig_RightLeg', 'ORG-shin.R', 'shin.R', - 'rShin', 'rKnee', 'rLeg' + 'rShin', 'rKnee', 'rLeg', 'LowerLeg.R' ], 'right_ankle': [ 'mixamorig:RightFoot', 'mixamorig_RightFoot', 'ORG-foot.R', 'foot.R', - 'rFoot', 'rAnkle' + 'rFoot', 'rAnkle', 'Foot.R' ], 'right_toe': [ 'mixamorig:RightToeBase', 'mixamorig_RightToeBase', 'ORG-toe.R', 'toe.R', - 'rToe' + 'rToe', 'Toes.R' ], 'thumb_1_l': [ @@ -1029,12 +1083,12 @@ non_standard_mappings = { 'left_eye': [ 'mixamorig:LeftEye', 'mixamorig_LeftEye', 'ORG-eye.L', 'eye.L', - 'lEye' + 'lEye', 'Eye.L' ], 'right_eye': [ 'mixamorig:RightEye', 'mixamorig_RightEye', 'ORG-eye.R', 'eye.R', - 'rEye' + 'rEye', 'Eye.R' ] } diff --git a/core/updater.py b/core/updater.py index e1c30ec..068baeb 100644 --- a/core/updater.py +++ b/core/updater.py @@ -20,7 +20,7 @@ GITHUB_REPO = "teamneoneko/Avatar-Toolkit" # Define which version series this installation can update to # For example: ["0.1"] means only look for 0.1.x updates # ["0.2", "0.3"] would look for both 0.2.x and 0.3.x updates -ALLOWED_VERSION_SERIES = ["0.3"] +ALLOWED_VERSION_SERIES = ["0.4"] is_checking_for_update: bool = False update_needed: bool = False diff --git a/functions/custom_tools/armature_merging.py b/functions/custom_tools/armature_merging.py index 69fbd73..0a73586 100644 --- a/functions/custom_tools/armature_merging.py +++ b/functions/custom_tools/armature_merging.py @@ -48,6 +48,9 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator): #Store current armature settings that can mess us up. data_breaking_base = store_breaking_settings_armature(base_armature) data_breaking_merge = store_breaking_settings_armature(merge_armature) + + # Store the merge armature name before it gets removed during join + merge_armature_name_stored = merge_armature.name # Remove Rigid Bodies and Joints delete_rigidbodies_and_joints(base_armature) @@ -77,14 +80,17 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator): wm.progress_end() restore_breaking_settings_armature(base_armature, data_breaking_base) - restore_breaking_settings_armature(merge_armature, data_breaking_merge) + + if merge_armature_name_stored in bpy.data.objects: + merge_armature_obj = bpy.data.objects[merge_armature_name_stored] + restore_breaking_settings_armature(merge_armature_obj, data_breaking_merge) self.report({'INFO'}, t('MergeArmature.success')) return {'FINISHED'} except Exception as e: - logger.error(f"Error merging armatures:", exception=e) + logger.error(f"Error merging armatures: {str(e)}\n{traceback.format_exc()}") self.report({'ERROR'}, traceback.format_exc()) return {'CANCELLED'} diff --git a/functions/tools/bone_tools.py b/functions/tools/bone_tools.py index 667286d..b185dc3 100644 --- a/functions/tools/bone_tools.py +++ b/functions/tools/bone_tools.py @@ -186,7 +186,6 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator): if not armature: return {'CANCELLED'} - # Store initial transforms bpy.ops.object.mode_set(mode='EDIT') initial_transforms: Dict[str, Dict[str, Any]] = {} data_breaking = store_breaking_settings_armature(armature) @@ -200,56 +199,61 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator): 'parent': bone.parent.name if bone.parent else None } - # Get weighted bones + # Get bones with any weight weighted_bones: List[str] = [] meshes = get_all_meshes(context) - zero_weight_bones: List[str] = [] - for mesh in meshes: - mesh_data: Mesh = mesh.data - for vertex in mesh_data.vertices: + for vertex in mesh.data.vertices: for group in vertex.groups: if group.weight > context.scene.avatar_toolkit.merge_weights_threshold: - weighted_bones.append(mesh.vertex_groups[group.group].name) + vg = mesh.vertex_groups[group.group] + if vg.name not in weighted_bones: + weighted_bones.append(vg.name) - # Process bone removal - bpy.ops.object.mode_set(mode='EDIT') - armature_data: Armature = armature.data + armature_data = armature.data removed_count = 0 + zero_weight_bones: List[str] = [] - for bone in armature_data.edit_bones[:]: # Create a copy of the list - if (bone.name not in weighted_bones and - not self.should_preserve_bone(bone.name, context)): - - if context.scene.avatar_toolkit.list_only_mode: - zero_weight_bones.append(bone.name) - continue + def is_zero_weight_chain(bone, weighted_bones, preserve_check_fn): + if bone.name in weighted_bones or preserve_check_fn(bone.name, context): + return False + return all(is_zero_weight_chain(child, weighted_bones, preserve_check_fn) for child in bone.children) - # Store children data - children = bone.children - children_data = {child.name: initial_transforms[child.name] for child in children} + for bone in armature_data.edit_bones[:]: + if bone.name in weighted_bones or self.should_preserve_bone(bone.name, context): + continue - # Reparent children - for child in children: + if not is_zero_weight_chain(bone, weighted_bones, self.should_preserve_bone): + continue + + if context.scene.avatar_toolkit.list_only_mode: + zero_weight_bones.append(bone.name) + continue + + # Traverse and collect the full empty chain + stack = [bone] + chain = [] + + while stack: + b = stack.pop() + chain.append(b) + stack.extend(b.children) + + for b in reversed(chain): # Remove children before parents + for child in b.children: child.use_connect = False - if bone.parent: - child.parent = bone.parent - - # Remove bone - armature_data.edit_bones.remove(bone) - removed_count += 1 - - # Restore children positions - for child_name, data in children_data.items(): - if child_name in armature_data.edit_bones: - child = armature_data.edit_bones[child_name] - restore_bone_transforms(child, data) + if b.parent: + child.parent = b.parent + if b.name in armature_data.edit_bones: + armature_data.edit_bones.remove(b) + removed_count += 1 bpy.ops.object.mode_set(mode='OBJECT') - + if context.scene.avatar_toolkit.list_only_mode: self.populate_bone_list(context, zero_weight_bones) return {'FINISHED'} + restore_breaking_settings_armature(armature, data_breaking) self.report({'INFO'}, t("Tools.clean_weights_success", count=removed_count)) return {'FINISHED'} diff --git a/functions/tools/rigify_converter.py b/functions/tools/rigify_converter.py index 5401f18..f15c7e4 100644 --- a/functions/tools/rigify_converter.py +++ b/functions/tools/rigify_converter.py @@ -2,7 +2,7 @@ import traceback import bpy from typing import Dict, List, Set, Optional, Tuple, Any from bpy.types import Operator, Context, Object, PoseBone, EditBone, Bone, Constraint -from ...core.common import get_active_armature +from ...core.common import get_active_armature, transfer_vertex_weights, get_all_meshes from ...core.logging_setup import logger from ...core.translations import t from ...core.dictionaries import rigify_unity_names, rigify_basic_unity_names, rigify_unnecessary_bones @@ -69,19 +69,50 @@ class AvatarToolkit_OT_ConvertRigifyToUnity(Operator): # Set armature as active object before mode switch bpy.context.view_layer.objects.active = armature + + # Get all meshes for weight transfer + meshes = get_all_meshes(bpy.context) + bpy.ops.object.mode_set(mode='EDIT') bones_to_remove: List[str] = [] for bone in armature.data.edit_bones: - if any(pattern in bone.name.lower() for pattern in rigify_unnecessary_bones): + bone_name_lower = bone.name.lower() + if any(bone_name_lower.startswith(pattern) or bone_name_lower == pattern + for pattern in rigify_unnecessary_bones): bones_to_remove.append(bone.name) - + + # Check for neck bones that need merging + merge_neck_bones = 'spine.004' in armature.data.edit_bones and 'spine.005' in armature.data.edit_bones + + bpy.ops.object.mode_set(mode='OBJECT') + + # Transfer weights from bones being removed + for bone_name in bones_to_remove: + if bone_name in armature.data.bones: + logger.debug(f"Transferring weights from bone: {bone_name}") + for mesh in meshes: + if bone_name in mesh.vertex_groups: + # Remove the vertex group since we don't need the weights + mesh.vertex_groups.remove(mesh.vertex_groups[bone_name]) + + # Transfer weights for neck bone merging + if merge_neck_bones: + logger.debug("Transferring weights from spine.005 to spine.004") + for mesh in meshes: + if 'spine.005' in mesh.vertex_groups: + transfer_vertex_weights(mesh, 'spine.005', 'spine.004') + + bpy.ops.object.mode_set(mode='EDIT') + + # Remove unnecessary bones for bone_name in bones_to_remove: if bone_name in armature.data.edit_bones: logger.debug(f"Removing bone: {bone_name}") armature.data.edit_bones.remove(armature.data.edit_bones[bone_name]) - if 'spine.004' in armature.data.edit_bones and 'spine.005' in armature.data.edit_bones: + # Merge neck bones + if merge_neck_bones: logger.debug("Merging neck bones") neck_start = armature.data.edit_bones['spine.004'] neck_end = armature.data.edit_bones['spine.005'] @@ -89,6 +120,7 @@ class AvatarToolkit_OT_ConvertRigifyToUnity(Operator): armature.data.edit_bones.remove(neck_end) neck_start.name = "Neck" + # Rename head bone if 'spine.006' in armature.data.edit_bones: logger.debug("Renaming head bone") head_bone = armature.data.edit_bones['spine.006'] @@ -137,6 +169,22 @@ class AvatarToolkit_OT_ConvertRigifyToUnity(Operator): if bone_name in armature.data.bones: armature.data.bones[bone_name].use_deform = False + # Get all meshes for weight transfer + meshes = get_all_meshes(bpy.context) + + bpy.ops.object.mode_set(mode='OBJECT') + for bone_name in remove_bones_in_chain: + if bone_name in armature.data.bones: + parent_name = armature.data.bones[bone_name].parent.name if armature.data.bones[bone_name].parent else None + if parent_name: + logger.debug(f"Transferring weights from {bone_name} to {parent_name}") + for mesh in meshes: + if bone_name in mesh.vertex_groups and parent_name in mesh.vertex_groups: + transfer_vertex_weights(mesh, bone_name, parent_name) + elif bone_name in mesh.vertex_groups: + # Remove weights if no parent to merge to + mesh.vertex_groups.remove(mesh.vertex_groups[bone_name]) + bpy.ops.object.mode_set(mode='EDIT') for bone_name in remove_bones_in_chain: if bone_name in armature.data.bones: @@ -190,6 +238,17 @@ class AvatarToolkit_OT_ConvertRigifyToUnity(Operator): ("DEF-thigh_twist.R", "DEF-thigh.R") ] + # Get all meshes for weight transfer + meshes = get_all_meshes(bpy.context) + + bpy.ops.object.mode_set(mode='OBJECT') + for twist_bone, parent_bone in twist_bones: + if twist_bone in armature.data.bones and parent_bone in armature.data.bones: + logger.debug(f"Transferring weights from {twist_bone} to {parent_bone}") + for mesh in meshes: + if twist_bone in mesh.vertex_groups: + transfer_vertex_weights(mesh, twist_bone, parent_bone) + bpy.ops.object.mode_set(mode='EDIT') for twist_bone, parent_bone in twist_bones: if twist_bone in armature.data.edit_bones and parent_bone in armature.data.edit_bones: diff --git a/functions/tools/standardize_armature.py b/functions/tools/standardize_armature.py index e88558d..7d5fdcf 100644 --- a/functions/tools/standardize_armature.py +++ b/functions/tools/standardize_armature.py @@ -13,7 +13,9 @@ from ...core.dictionaries import ( bone_hierarchy, acceptable_bone_names, acceptable_bone_hierarchy, - non_standard_mappings + non_standard_mappings, + reverse_bone_lookup, + simplify_bonename ) class AvatarToolkit_OT_StandardizeArmature(Operator): @@ -134,17 +136,14 @@ class AvatarToolkit_OT_StandardizeArmature(Operator): existing_standard_bones.add(bone.name) logger.debug(f"Found existing standard bone: {bone.name}") - # Build a mapping of non-standard bone names to standard names + # Use the reverse bone lookup that's already built and simplified name_mapping: Dict[str, str] = {} - for category, standard_name in standard_bones.items(): - # Skip if this standard bone already exists - if standard_name in existing_standard_bones: - continue - - # Get all variants for this category - if category in non_standard_mappings: - for variant in non_standard_mappings[category]: - name_mapping[variant.lower()] = standard_name + for simplified_name, category in reverse_bone_lookup.items(): + if category in standard_bones: + standard_name = standard_bones[category] + # Skip if this standard bone already exists + if standard_name not in existing_standard_bones: + name_mapping[simplified_name] = standard_name # First pass: identify bones to rename bones_to_rename: Dict[str, str] = {} @@ -155,20 +154,14 @@ class AvatarToolkit_OT_StandardizeArmature(Operator): if original_name in standard_bones.values(): continue - simplified_name: str = original_name.lower().replace(' ', '').replace('_', '').replace('.', '') + simplified_name: str = simplify_bonename(original_name) - # Check if this bone matches any known pattern - for variant, standard_name in name_mapping.items(): - # More precise matching - exact match or with common separators - if (variant == simplified_name or - variant == original_name.lower() or - f"{variant}_" in simplified_name or - f"{variant}." in simplified_name): - - if original_name != standard_name: - bones_to_rename[original_name] = standard_name - logger.debug(f"Identified bone to rename: {original_name} -> {standard_name}") - break + # Check if this simplified bone name has a standard mapping + if simplified_name in name_mapping: + standard_name = name_mapping[simplified_name] + if original_name != standard_name: + bones_to_rename[original_name] = standard_name + logger.debug(f"Identified bone to rename: {original_name} -> {standard_name}") # Special case for spine/chest hierarchy # If we don't have an upper chest, don't rename chest to upper chest because it will break hierarchy diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index e4d2136..667ac37 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -1,7 +1,7 @@ { "authors": ["Avatar Toolkit Team"], "messages": { - "AvatarToolkit.label": "Avatar Toolkit (Alpha 0.3.0)", + "AvatarToolkit.label": "Avatar Toolkit (Alpha 0.4.0)", "AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there", "AvatarToolkit.desc2": "will be issues, if you find any issues,", "AvatarToolkit.desc3": "please report it on our Github.", diff --git a/resources/translations/ja_JP.json b/resources/translations/ja_JP.json index 7429eec..57554c8 100644 --- a/resources/translations/ja_JP.json +++ b/resources/translations/ja_JP.json @@ -1,7 +1,7 @@ { "authors": ["Avatar Toolkit Team"], "messages": { - "AvatarToolkit.label": "アバターツールキット (アルファ 0.3.0)", + "AvatarToolkit.label": "アバターツールキット (アルファ 0.4.0)", "AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、", "AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、", "AvatarToolkit.desc3": "GitHubで報告してください。", diff --git a/resources/translations/ko_KR.json b/resources/translations/ko_KR.json index 38e2938..ca19665 100644 --- a/resources/translations/ko_KR.json +++ b/resources/translations/ko_KR.json @@ -1,7 +1,7 @@ { "authors": ["Avatar Toolkit Team"], "messages": { - "AvatarToolkit.label": "아바타 툴킷 (알파 0.3.0)", + "AvatarToolkit.label": "아바타 툴킷 (알파 0.4.0)", "AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로", "AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면", "AvatarToolkit.desc3": "Github에 보고해 주세요.",