From 29f728442ac63a93bd8f83b5e9a27ef71886ee6f Mon Sep 17 00:00:00 2001 From: Yusarina Date: Fri, 1 Aug 2025 14:40:49 +0100 Subject: [PATCH 1/5] Initial VRM Conversion VRM Conversion, converts the vrm armature and removes colliders as there are not used in Unity. There some bugs and i need to optimise it and etc. Also we need to remove root empty bone as it's useless in Unity. Ran out of time to finish it but proof of concept it works lol. However dont want to release it unto Alpha 4 as it need to be tested and i may seperate some things into different buttons but i have not decided. --- core/dictionaries.py | 80 ++++- core/properties.py | 7 + core/vrm_unity_converter.py | 454 ++++++++++++++++++++++++ functions/tools/collider_removal.py | 102 ++++++ functions/tools/vrm_unity_conversion.py | 86 +++++ ui/vrm_unity_panel.py | 85 +++++ 6 files changed, 801 insertions(+), 13 deletions(-) create mode 100644 core/vrm_unity_converter.py create mode 100644 functions/tools/collider_removal.py create mode 100644 functions/tools/vrm_unity_conversion.py create mode 100644 ui/vrm_unity_panel.py diff --git a/core/dictionaries.py b/core/dictionaries.py index 8e11fdf..e0344d3 100644 --- a/core/dictionaries.py +++ b/core/dictionaries.py @@ -257,7 +257,7 @@ bone_names = { ], } -# Add VRM bone name variations +# Add VRM bone name variations bone_names.update({ 'hips': bone_names['hips'] + ['jbipchips', 'jhips', 'vrmhips'], 'spine': bone_names['spine'] + ['jbipcspine', 'jspine', 'vrmspine'], @@ -266,19 +266,73 @@ bone_names.update({ 'neck': bone_names['neck'] + ['jbipcneck', 'jneck', 'vrmneck'], 'head': bone_names['head'] + ['jbipchead', 'jhead', 'vrmhead'], - # VRM specific finger naming - 'thumb_0_l': bone_names['thumb_0_l'] + ['thumbmetacarpall', 'jthumb1l'], - 'index_0_l': bone_names['index_0_l'] + ['indexmetacarpall', 'jindex1l'], - 'middle_0_l': bone_names['middle_0_l'] + ['middlemetacarpall', 'jmiddle1l'], - 'ring_0_l': bone_names['ring_0_l'] + ['ringmetacarpall', 'jring1l'], - 'pinkie_0_l': bone_names['pinkie_0_l'] + ['littlemetacarpall', 'jlittle1l'], + # VRM arms + 'left_shoulder': bone_names['left_shoulder'] + ['jbipllshoulder', 'jlshoulder'], + 'left_arm': bone_names['left_arm'] + ['jbiplupperarm', 'jlupperarm'], + 'left_elbow': bone_names['left_elbow'] + ['jbipllforearm', 'jlforearm'], + 'left_wrist': bone_names['left_wrist'] + ['jbipllhand', 'jlhand'], - # Mirror for right side - 'thumb_0_r': bone_names['thumb_0_r'] + ['thumbmetacarpalr', 'jthumb1r'], - 'index_0_r': bone_names['index_0_r'] + ['indexmetacarpalr', 'jindex1r'], - 'middle_0_r': bone_names['middle_0_r'] + ['middlemetacarpalr', 'jmiddle1r'], - 'ring_0_r': bone_names['ring_0_r'] + ['ringmetacarpalr', 'jring1r'], - 'pinkie_0_r': bone_names['pinkie_0_r'] + ['littlemetacarpalr', 'jlittle1r'] + 'right_shoulder': bone_names['right_shoulder'] + ['jbiprlshoulder', 'jrshoulder'], + 'right_arm': bone_names['right_arm'] + ['jbiprrupperarm', 'jrupperarm'], + 'right_elbow': bone_names['right_elbow'] + ['jbiprrforearm', 'jrforearm'], + 'right_wrist': bone_names['right_wrist'] + ['jbiprrhand', 'jrhand'], + + # VRM legs + 'left_leg': bone_names['left_leg'] + ['jbiplupperleg', 'jlupperleg'], + 'left_knee': bone_names['left_knee'] + ['jbipllowerleg', 'jllowerleg'], + 'left_ankle': bone_names['left_ankle'] + ['jbipllfoot', 'jlfoot'], + 'left_toe': bone_names['left_toe'] + ['jbiplltoe', 'jltoe'], + + 'right_leg': bone_names['right_leg'] + ['jbiprrupperleg', 'jrupperleg'], + 'right_knee': bone_names['right_knee'] + ['jbiprrlowerleg', 'jrlowerleg'], + 'right_ankle': bone_names['right_ankle'] + ['jbiprrfoot', 'jrfoot'], + 'right_toe': bone_names['right_toe'] + ['jbiprrtoe', 'jrtoe'], + + # VRM eyes + 'left_eye': bone_names['left_eye'] + ['jbipcleye', 'jleye'], + 'right_eye': bone_names['right_eye'] + ['jbipcreye', 'jreye'], + + # VRM fingers - Left + 'thumb_1_l': bone_names['thumb_1_l'] + ['jbipllthumb1', 'jlthumb1'], + 'thumb_2_l': bone_names['thumb_2_l'] + ['jbipllthumb2', 'jlthumb2'], + 'thumb_3_l': bone_names['thumb_3_l'] + ['jbipllthumb3', 'jlthumb3'], + + 'index_1_l': bone_names['index_1_l'] + ['jbipllindex1', 'jlindex1'], + 'index_2_l': bone_names['index_2_l'] + ['jbipllindex2', 'jlindex2'], + 'index_3_l': bone_names['index_3_l'] + ['jbipllindex3', 'jlindex3'], + + 'middle_1_l': bone_names['middle_1_l'] + ['jbipllmiddle1', 'jlmiddle1'], + 'middle_2_l': bone_names['middle_2_l'] + ['jbipllmiddle2', 'jlmiddle2'], + 'middle_3_l': bone_names['middle_3_l'] + ['jbipllmiddle3', 'jlmiddle3'], + + 'ring_1_l': bone_names['ring_1_l'] + ['jbipllring1', 'jlring1'], + 'ring_2_l': bone_names['ring_2_l'] + ['jbipllring2', 'jlring2'], + 'ring_3_l': bone_names['ring_3_l'] + ['jbipllring3', 'jlring3'], + + 'pinkie_1_l': bone_names['pinkie_1_l'] + ['jbipllpinky1', 'jlpinky1'], + 'pinkie_2_l': bone_names['pinkie_2_l'] + ['jbipllpinky2', 'jlpinky2'], + 'pinkie_3_l': bone_names['pinkie_3_l'] + ['jbipllpinky3', 'jlpinky3'], + + # VRM fingers - Right + 'thumb_1_r': bone_names['thumb_1_r'] + ['jbiprthumb1', 'jrthumb1'], + 'thumb_2_r': bone_names['thumb_2_r'] + ['jbiprthumb2', 'jrthumb2'], + 'thumb_3_r': bone_names['thumb_3_r'] + ['jbiprthumb3', 'jrthumb3'], + + 'index_1_r': bone_names['index_1_r'] + ['jbiprindex1', 'jrindex1'], + 'index_2_r': bone_names['index_2_r'] + ['jbiprindex2', 'jrindex2'], + 'index_3_r': bone_names['index_3_r'] + ['jbiprindex3', 'jrindex3'], + + 'middle_1_r': bone_names['middle_1_r'] + ['jbiprmiddle1', 'jrmiddle1'], + 'middle_2_r': bone_names['middle_2_r'] + ['jbiprmiddle2', 'jrmiddle2'], + 'middle_3_r': bone_names['middle_3_r'] + ['jbiprmiddle3', 'jrmiddle3'], + + 'ring_1_r': bone_names['ring_1_r'] + ['jbiprring1', 'jrring1'], + 'ring_2_r': bone_names['ring_2_r'] + ['jbiprring2', 'jrring2'], + 'ring_3_r': bone_names['ring_3_r'] + ['jbiprring3', 'jrring3'], + + 'pinkie_1_r': bone_names['pinkie_1_r'] + ['jbiprpinky1', 'jrpinky1'], + 'pinkie_2_r': bone_names['pinkie_2_r'] + ['jbiprpinky2', 'jrpinky2'], + 'pinkie_3_r': bone_names['pinkie_3_r'] + ['jbiprpinky3', 'jrpinky3'] }) # array taken from cats diff --git a/core/properties.py b/core/properties.py index ef9243d..281a3b8 100644 --- a/core/properties.py +++ b/core/properties.py @@ -608,6 +608,13 @@ class AvatarToolkitSceneProperties(PropertyGroup): update=update_log_level ) + # VRM Conversion Properties + vrm_remove_colliders: BoolProperty( + name="Remove Colliders", + description="Remove VRM collider bones during conversion", + default=True + ) + def register() -> None: """Register the Avatar Toolkit property group""" logger.info("Registering Avatar Toolkit properties") diff --git a/core/vrm_unity_converter.py b/core/vrm_unity_converter.py new file mode 100644 index 0000000..b1b0bee --- /dev/null +++ b/core/vrm_unity_converter.py @@ -0,0 +1,454 @@ +import bpy +from typing import Dict, List, Optional, Tuple, Set +from bpy.types import Object, Bone +from .common import get_active_armature +from .dictionaries import simplify_bonename, standard_bones, bone_hierarchy +from .logging_setup import logger + + +def detect_vrm_armature(armature: Object) -> bool: + """ + Detect if armature uses VRM bone naming conventions + """ + if not armature or armature.type != 'ARMATURE': + return False + + vrm_patterns = [ + 'jbipchips', 'jbipcspine', 'jbipcchest', 'jbipcneck', 'jbipchead', + 'jbiprlshoulder', 'jbiprrupperarm', 'jbiprrforearm', 'jbiprrhand', + 'jbipllshoulder', 'jbiplupperarm', 'jbipllforearm', 'jbipllhand', + 'jbiprrupperleg', 'jbiprrlowerleg', 'jbiprrfoot', 'jbiprrtoe', + 'jbiplupperleg', 'jbipllowerleg', 'jbipllfoot', 'jbiplltoe', + 'jbipc', 'jbipr', 'jbipl' + ] + + found_vrm_bones = 0 + for bone_name in armature.data.bones.keys(): + simplified_name = simplify_bonename(bone_name) + if simplified_name.startswith('jbip') or any(pattern in simplified_name for pattern in vrm_patterns): + found_vrm_bones += 1 + + # Consider it VRM if we find at least 5 VRM bones + logger.debug(f"Found {found_vrm_bones} VRM bones in armature {armature.name}") + return found_vrm_bones >= 5 + + +def get_vrm_to_unity_mapping() -> Dict[str, str]: + """ + Get mapping from VRM bone names to Unity humanoid bone names + """ + return { + # Core structure + 'jbipchips': standard_bones['hips'], + 'jbipcspine': standard_bones['spine'], + 'jbipcchest': standard_bones['chest'], + 'jbipcupperchest': standard_bones.get('upper_chest', 'UpperChest'), + 'jbipcneck': standard_bones['neck'], + 'jbipchead': standard_bones['head'], + + # Left arm + 'jbipllshoulder': standard_bones.get('left_shoulder', 'LeftShoulder'), + 'jbiplupperarm': standard_bones['left_arm'], + 'jbipllforearm': standard_bones['left_elbow'], + 'jbipllhand': standard_bones['left_wrist'], + + # Right arm + 'jbiprlshoulder': standard_bones.get('right_shoulder', 'RightShoulder'), + 'jbiprrupperarm': standard_bones['right_arm'], + 'jbiprrforearm': standard_bones['right_elbow'], + 'jbiprrhand': standard_bones['right_wrist'], + + # Left leg + 'jbiplupperleg': standard_bones['left_leg'], + 'jbipllowerleg': standard_bones['left_knee'], + 'jbipllfoot': standard_bones['left_ankle'], + 'jbiplltoe': standard_bones['left_toe'], + + # Right leg + 'jbiprrupperleg': standard_bones['right_leg'], + 'jbiprrlowerleg': standard_bones['right_knee'], + 'jbiprrfoot': standard_bones['right_ankle'], + 'jbiprrtoe': standard_bones['right_toe'], + + # Eyes + 'jbipcleye': standard_bones.get('left_eye', 'Eye.L'), + 'jbipcreye': standard_bones.get('right_eye', 'Eye.R'), + + # Fingers - Left thumb + 'jbipllthumb1': standard_bones.get('thumb_1_l', 'Thumb1.L'), + 'jbipllthumb2': standard_bones.get('thumb_2_l', 'Thumb2.L'), + 'jbipllthumb3': standard_bones.get('thumb_3_l', 'Thumb3.L'), + + # Fingers - Left index + 'jbipllindex1': standard_bones.get('index_1_l', 'Index1.L'), + 'jbipllindex2': standard_bones.get('index_2_l', 'Index2.L'), + 'jbipllindex3': standard_bones.get('index_3_l', 'Index3.L'), + + # Fingers - Left middle + 'jbipllmiddle1': standard_bones.get('middle_1_l', 'Middle1.L'), + 'jbipllmiddle2': standard_bones.get('middle_2_l', 'Middle2.L'), + 'jbipllmiddle3': standard_bones.get('middle_3_l', 'Middle3.L'), + + # Fingers - Left ring + 'jbipllring1': standard_bones.get('ring_1_l', 'Ring1.L'), + 'jbipllring2': standard_bones.get('ring_2_l', 'Ring2.L'), + 'jbipllring3': standard_bones.get('ring_3_l', 'Ring3.L'), + + # Fingers - Left pinky + 'jbipllpinky1': standard_bones.get('pinkie_1_l', 'Pinky1.L'), + 'jbipllpinky2': standard_bones.get('pinkie_2_l', 'Pinky2.L'), + 'jbipllpinky3': standard_bones.get('pinkie_3_l', 'Pinky3.L'), + + # Fingers - Right thumb + 'jbiprthumb1': standard_bones.get('thumb_1_r', 'Thumb1.R'), + 'jbiprthumb2': standard_bones.get('thumb_2_r', 'Thumb2.R'), + 'jbiprthumb3': standard_bones.get('thumb_3_r', 'Thumb3.R'), + + # Fingers - Right index + 'jbiprindex1': standard_bones.get('index_1_r', 'Index1.R'), + 'jbiprindex2': standard_bones.get('index_2_r', 'Index2.R'), + 'jbiprindex3': standard_bones.get('index_3_r', 'Index3.R'), + + # Fingers - Right middle + 'jbiprmiddle1': standard_bones.get('middle_1_r', 'Middle1.R'), + 'jbiprmiddle2': standard_bones.get('middle_2_r', 'Middle2.R'), + 'jbiprmiddle3': standard_bones.get('middle_3_r', 'Middle3.R'), + + # Fingers - Right ring + 'jbiprring1': standard_bones.get('ring_1_r', 'Ring1.R'), + 'jbiprring2': standard_bones.get('ring_2_r', 'Ring2.R'), + 'jbiprring3': standard_bones.get('ring_3_r', 'Ring3.R'), + + # Fingers - Right pinky + 'jbiprpinky1': standard_bones.get('pinkie_1_r', 'Pinky1.R'), + 'jbiprpinky2': standard_bones.get('pinkie_2_r', 'Pinky2.R'), + 'jbiprpinky3': standard_bones.get('pinkie_3_r', 'Pinky3.R'), + } + + +def find_vrm_bones_in_armature(armature: Object) -> Dict[str, str]: + """ + Find VRM bones in armature and return mapping to their actual names + """ + vrm_mapping = get_vrm_to_unity_mapping() + found_bones = {} + + for bone_name in armature.data.bones.keys(): + simplified_name = simplify_bonename(bone_name) + + # Check if this bone matches any VRM pattern + for vrm_pattern, unity_name in vrm_mapping.items(): + if simplified_name == vrm_pattern: + found_bones[bone_name] = unity_name + logger.debug(f"Found VRM bone: {bone_name} -> {unity_name}") + break + + if simplified_name.startswith('jbip') and bone_name not in found_bones: + unity_equivalent = guess_unity_name_from_vrm(simplified_name) + if unity_equivalent: + found_bones[bone_name] = unity_equivalent + logger.debug(f"Guessed VRM bone mapping: {bone_name} -> {unity_equivalent}") + + return found_bones + + +def guess_unity_name_from_vrm(vrm_simplified: str) -> Optional[str]: + """ + Attempt to guess Unity bone name from VRM simplified name + """ + # Map common VRM patterns to Unity equivalents + pattern_mappings = { + 'jbipcupperchest': 'UpperChest', + 'jbipcchest': 'Chest', + 'jbipcspine': 'Spine', + 'jbipchips': 'Hips', + 'jbipcneck': 'Neck', + 'jbipchead': 'Head', + + # Left arm + 'jbipllclavicle': 'LeftShoulder', + 'jbipllshoulder': 'LeftShoulder', + 'jbiplupperarm': 'LeftUpperArm', + 'jbipllforearm': 'LeftLowerArm', + 'jbipllhand': 'LeftHand', + + # Right arm + 'jbiprrclavicle': 'RightShoulder', + 'jbiprlshoulder': 'RightShoulder', + 'jbiprrupperarm': 'RightUpperArm', + 'jbiprrforearm': 'RightLowerArm', + 'jbiprrhand': 'RightHand', + + # Left leg + 'jbiplupperleg': 'LeftUpperLeg', + 'jbipllowerleg': 'LeftLowerLeg', + 'jbipllfoot': 'LeftFoot', + 'jbiplltoe': 'LeftToes', + + # Right leg + 'jbiprrupperleg': 'RightUpperLeg', + 'jbiprrlowerleg': 'RightLowerLeg', + 'jbiprrfoot': 'RightFoot', + 'jbiprrtoe': 'RightToes', + + # Eyes + 'jbipcleye': 'LeftEye', + 'jbipcreye': 'RightEye' + } + + return pattern_mappings.get(vrm_simplified) + + +def is_vrm_collider_object(obj_name: str) -> bool: + """ + Test if an object name represents a VRM collider + """ + obj_name_lower = obj_name.lower() + collider_patterns = ['collider', 'collision', 'dynamic', 'spring', 'physics', 'secondary'] + + # Must contain a collider pattern + contains_collider = any(pattern in obj_name_lower for pattern in collider_patterns) + if not contains_collider: + return False + + # Must be VRM-related (multiple detection methods) + is_vrm = ( + 'j_bip' in obj_name_lower or + 'jbip' in simplify_bonename(obj_name) or + any(vrm_part in obj_name_lower for vrm_part in ['j_bip_c_', 'j_bip_l_', 'j_bip_r_']) + ) + + return is_vrm + + +def remove_vrm_colliders(armature: Object = None) -> Tuple[int, List[str]]: + """ + Simple approach: Remove ALL objects with 'collider' in their name and clean up empty collections + Returns tuple of (removed_count, removed_object_names) + """ + objects_to_remove = [] + removed_names = [] + collections_to_check = set() + + # Store the current mode and active object + current_mode = bpy.context.mode + original_active = bpy.context.view_layer.objects.active + + if current_mode != 'OBJECT': + bpy.ops.object.mode_set(mode='OBJECT') + + try: + logger.info("Starting simple collider removal - removing ALL objects with 'collider' in name") + + collider_object_names = [] + for obj in bpy.data.objects: + if 'collider' in obj.name.lower(): + collider_object_names.append(obj.name) + # Track collections this object is in + for collection in obj.users_collection: + collections_to_check.add(collection) + logger.info(f"Found collider object: {obj.name}") + + logger.info(f"Found {len(collider_object_names)} collider objects to remove") + + # Remove collider objects by name + removed_count = 0 + for obj_name in collider_object_names: + try: + # Check if object still exists + if obj_name in bpy.data.objects: + obj = bpy.data.objects[obj_name] + logger.info(f"Removing collider object: {obj_name}") + + # Remove from all collections first + for collection in list(obj.users_collection): + collection.objects.unlink(obj) + logger.debug(f" Unlinked from collection: {collection.name}") + + bpy.data.objects.remove(obj, do_unlink=True) + removed_count += 1 + removed_names.append(obj_name) + logger.info(f" Successfully removed: {obj_name}") + else: + logger.debug(f"Object {obj_name} already removed") + + except Exception as e: + logger.error(f"Failed to remove collider object {obj_name}: {str(e)}") + import traceback + logger.error(f"Traceback: {traceback.format_exc()}") + + logger.info(f"Successfully removed {removed_count} collider objects") + + # Clean up empty collections + empty_collections_removed = 0 + for collection in list(collections_to_check): + try: + # Check if collection is now empty and not the master collection + if (len(collection.objects) == 0 and + len(collection.children) == 0 and + collection.name != "Collection" and + collection.name != "Master Collection"): + + logger.info(f"Removing empty collection: {collection.name}") + + if collection in bpy.context.scene.collection.children: + bpy.context.scene.collection.children.unlink(collection) + + bpy.data.collections.remove(collection) + empty_collections_removed += 1 + logger.info(f" Successfully removed collection: {collection.name}") + + except Exception as e: + logger.warning(f"Failed to remove empty collection {collection.name}: {str(e)}") + + if empty_collections_removed > 0: + logger.info(f"Cleaned up {empty_collections_removed} empty collections") + + except Exception as e: + logger.error(f"Error during collider removal: {str(e)}") + return 0, [] + + finally: + if original_active and original_active.name in bpy.data.objects: + bpy.context.view_layer.objects.active = original_active + + if current_mode != 'OBJECT': + try: + bpy.ops.object.mode_set(mode=current_mode) + except: + pass + + logger.info(f"Collider removal complete. Removed {len(removed_names)} objects") + return len(removed_names), removed_names + + +def convert_vrm_to_unity(armature: Object, remove_colliders: bool = True) -> Tuple[bool, List[str], int]: + """ + Convert VRM armature bone names to Unity humanoid format + + Returns: + Tuple of (success, messages, converted_count) + """ + if not armature or armature.type != 'ARMATURE': + return False, ["No valid armature selected"], 0 + + logger.info(f"Starting VRM to Unity conversion for armature: {armature.name}") + + # Check if this is a VRM armature + if not detect_vrm_armature(armature): + return False, ["Selected armature does not appear to be a VRM armature"], 0 + + messages = [] + converted_count = 0 + failed_conversions = [] + collider_count = 0 + + current_mode = bpy.context.mode + if current_mode != 'EDIT': + bpy.context.view_layer.objects.active = armature + bpy.ops.object.mode_set(mode='EDIT') + + try: + # First, remove collider objects and bones if requested + if remove_colliders: + collider_count, removed_colliders = remove_vrm_colliders(armature) + if collider_count > 0: + messages.append(f"Removed {collider_count} VRM collider objects/bones") + logger.info(f"Removed {collider_count} VRM colliders: {removed_colliders}") + + vrm_bones = find_vrm_bones_in_armature(armature) + + if not vrm_bones: + if remove_colliders and collider_count > 0: + messages.append("No VRM bones found to convert (colliders were removed)") + return True, messages, 0 + else: + return False, ["No VRM bones found in armature"], 0 + + if bpy.context.mode != 'EDIT': + bpy.ops.object.mode_set(mode='EDIT') + + # Rename bones + for vrm_bone_name, unity_name in vrm_bones.items(): + if vrm_bone_name in armature.data.edit_bones: + bone = armature.data.edit_bones[vrm_bone_name] + + # Check if target name already exists + if unity_name in armature.data.edit_bones and unity_name != vrm_bone_name: + failed_conversions.append(f"{vrm_bone_name} -> {unity_name} (name conflict)") + continue + + # Rename the bone + bone.name = unity_name + converted_count += 1 + logger.debug(f"Renamed bone: {vrm_bone_name} -> {unity_name}") + + messages.append(f"Successfully converted {converted_count} VRM bones to Unity format") + + if failed_conversions: + messages.append("Failed conversions due to name conflicts:") + messages.extend(failed_conversions) + + logger.info(f"VRM to Unity conversion completed. Converted {converted_count} bones") + + except Exception as e: + logger.error(f"Error during VRM conversion: {str(e)}") + messages.append(f"Error during conversion: {str(e)}") + return False, messages, converted_count + + finally: + # Restore original mode + if current_mode != 'EDIT': + bpy.ops.object.mode_set(mode='OBJECT') + + return converted_count > 0 or (remove_colliders and collider_count > 0), messages, converted_count + + +def validate_unity_hierarchy(armature: Object) -> Tuple[bool, List[str]]: + """ + Validate that the converted armature has proper Unity humanoid hierarchy + """ + if not armature or armature.type != 'ARMATURE': + return False, ["No valid armature to validate"] + + messages = [] + is_valid = True + + # Check for essential Unity bones + essential_unity_bones = [ + standard_bones['hips'], + standard_bones['spine'], + standard_bones['chest'], + standard_bones['neck'], + standard_bones['head'] + ] + + missing_bones = [] + for bone_name in essential_unity_bones: + if bone_name not in armature.data.bones: + missing_bones.append(bone_name) + + if missing_bones: + is_valid = False + messages.append("Missing essential Unity bones:") + messages.extend([f"- {bone}" for bone in missing_bones]) + + # Validate basic hierarchy + hierarchy_issues = [] + for parent_name, child_name in bone_hierarchy: + if parent_name in armature.data.bones and child_name in armature.data.bones: + parent_bone = armature.data.bones[parent_name] + child_bone = armature.data.bones[child_name] + + if child_bone.parent != parent_bone: + hierarchy_issues.append(f"{parent_name} -> {child_name}") + + if hierarchy_issues: + is_valid = False + messages.append("Hierarchy issues found:") + messages.extend([f"- {issue}" for issue in hierarchy_issues]) + + if is_valid: + messages.append("Unity hierarchy validation passed") + + return is_valid, messages \ No newline at end of file diff --git a/functions/tools/collider_removal.py b/functions/tools/collider_removal.py new file mode 100644 index 0000000..61b63bd --- /dev/null +++ b/functions/tools/collider_removal.py @@ -0,0 +1,102 @@ +import bpy +from bpy.types import Operator +from ...core.logging_setup import logger + + +class AvatarToolkit_OT_RemoveAllColliders(Operator): + """Remove all objects with 'collider' in their name""" + bl_idname = "avatar_toolkit.remove_all_colliders" + bl_label = "Remove All Colliders" + bl_description = "Remove all objects that have 'collider' in their name" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + logger.info("Starting standalone collider removal") + + # Store current mode and active object + current_mode = bpy.context.mode + original_active = bpy.context.view_layer.objects.active + + # Switch to object mode + if current_mode != 'OBJECT': + bpy.ops.object.mode_set(mode='OBJECT') + + try: + # Find all collider objects + collider_names = [] + all_objects = list(bpy.data.objects) + + logger.info(f"Scanning {len(all_objects)} objects for colliders") + + for obj in all_objects: + if 'collider' in obj.name.lower(): + collider_names.append(obj.name) + logger.info(f"Found collider: {obj.name}") + + if not collider_names: + self.report({'INFO'}, "No collider objects found") + logger.info("No collider objects found") + return {'FINISHED'} + + logger.info(f"Found {len(collider_names)} collider objects to remove") + self.report({'INFO'}, f"Found {len(collider_names)} collider objects") + + # Remove each collider + removed_count = 0 + failed_count = 0 + + for obj_name in collider_names: + try: + if obj_name in bpy.data.objects: + obj = bpy.data.objects[obj_name] + + # Deselect all objects first + bpy.ops.object.select_all(action='DESELECT') + + # Select and make active + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + + # Delete the object + bpy.ops.object.delete(use_global=False) + + removed_count += 1 + logger.info(f"Removed collider: {obj_name}") + + else: + logger.debug(f"Object {obj_name} no longer exists") + + except Exception as e: + failed_count += 1 + logger.error(f"Failed to remove {obj_name}: {str(e)}") + self.report({'WARNING'}, f"Failed to remove {obj_name}: {str(e)}") + + # Report results + if removed_count > 0: + success_msg = f"Successfully removed {removed_count} collider objects" + logger.info(success_msg) + self.report({'INFO'}, success_msg) + + if failed_count > 0: + failure_msg = f"Failed to remove {failed_count} collider objects" + logger.warning(failure_msg) + self.report({'WARNING'}, failure_msg) + + except Exception as e: + error_msg = f"Error during collider removal: {str(e)}" + logger.error(error_msg) + self.report({'ERROR'}, error_msg) + return {'CANCELLED'} + + finally: + # Restore original state + try: + if original_active and original_active.name in bpy.data.objects: + bpy.context.view_layer.objects.active = original_active + + if current_mode != 'OBJECT': + bpy.ops.object.mode_set(mode=current_mode) + except: + pass + + return {'FINISHED'} \ No newline at end of file diff --git a/functions/tools/vrm_unity_conversion.py b/functions/tools/vrm_unity_conversion.py new file mode 100644 index 0000000..8514fa1 --- /dev/null +++ b/functions/tools/vrm_unity_conversion.py @@ -0,0 +1,86 @@ +import bpy +from bpy.types import Operator +from ...core.common import get_active_armature +from ...core.translations import t +from ...core.vrm_unity_converter import convert_vrm_to_unity, validate_unity_hierarchy +from ...core.logging_setup import logger +from ...core.armature_validation import validate_armature + + +class AvatarToolkit_OT_ConvertVRMToUnity(Operator): + """Convert VRM armature bone names to Unity humanoid format""" + bl_idname = "avatar_toolkit.convert_vrm_to_unity" + bl_label = "Convert VRM to Unity" + bl_description = "Convert VRM armature bone names to Unity humanoid naming convention" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + armature = get_active_armature(context) + return armature is not None + + def execute(self, context): + armature = get_active_armature(context) + if not armature: + logger.warning("No active armature found for VRM conversion") + self.report({'ERROR'}, "No active armature selected") + return {'CANCELLED'} + + logger.info(f"Starting VRM to Unity conversion for armature: {armature.name}") + + # Get collider removal setting + remove_colliders = context.scene.avatar_toolkit.vrm_remove_colliders + logger.info(f"Collider removal setting: {remove_colliders}") + + # Log all objects with 'collider' in name for debugging + collider_objects = [obj.name for obj in bpy.data.objects if 'collider' in obj.name.lower()] + if collider_objects: + logger.info(f"Found {len(collider_objects)} objects with 'collider' in name:") + for obj_name in collider_objects: + logger.info(f" - {obj_name}") + + success, messages, converted_count = convert_vrm_to_unity(armature, remove_colliders) + + if not success: + logger.warning(f"VRM conversion failed: {messages}") + for msg in messages: + self.report({'WARNING'}, msg) + return {'CANCELLED'} + + logger.info(f"VRM conversion completed successfully. Converted {converted_count} bones") + for msg in messages: + self.report({'INFO'}, msg) + + # Validate the converted armature + try: + is_valid, validation_messages = validate_unity_hierarchy(armature) + + if is_valid: + logger.info("Unity hierarchy validation passed") + self.report({'INFO'}, "Unity hierarchy validation passed") + else: + logger.warning("Unity hierarchy validation found issues") + self.report({'WARNING'}, "Conversion completed but hierarchy validation found issues:") + for msg in validation_messages: + self.report({'WARNING'}, msg) + + try: + armature_valid, armature_messages, _ = validate_armature(armature) + if armature_valid: + logger.info("Full armature validation passed") + self.report({'INFO'}, "Armature passes standard validation") + else: + logger.info("Full armature validation found minor issues") + # Don't report these as errors since the conversion was successful + # Just log them for debugging + for msg in armature_messages[:3]: + logger.debug(f"Armature validation: {msg}") + except Exception as e: + logger.warning(f"Error during full armature validation: {str(e)}") + # Don't fail the operation for validation errors + + except Exception as e: + logger.error(f"Error during hierarchy validation: {str(e)}") + self.report({'WARNING'}, f"Conversion completed but validation failed: {str(e)}") + + return {'FINISHED'} \ No newline at end of file diff --git a/ui/vrm_unity_panel.py b/ui/vrm_unity_panel.py new file mode 100644 index 0000000..848cec1 --- /dev/null +++ b/ui/vrm_unity_panel.py @@ -0,0 +1,85 @@ +import bpy +from bpy.types import Panel, Context, UILayout +from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME +from ..core.translations import t +from ..core.common import get_active_armature +from ..core.vrm_unity_converter import detect_vrm_armature +from ..functions.tools.vrm_unity_conversion import AvatarToolkit_OT_ConvertVRMToUnity + + +class AvatarToolKit_PT_VRMUnityPanel(Panel): + """Panel for VRM to Unity conversion tools""" + bl_label = "VRM to Unity" + bl_idname = "OBJECT_PT_avatar_toolkit_vrm_unity" + 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_options = {'DEFAULT_CLOSED'} + + def draw(self, context: Context) -> None: + """Draw the VRM to Unity conversion panel interface""" + layout: UILayout = self.layout + + # VRM Conversion Tools + vrm_box: UILayout = layout.box() + col: UILayout = vrm_box.column(align=True) + col.label(text="VRM Converter", icon='ARMATURE_DATA') + col.separator(factor=0.5) + + # Check if we have an active armature + armature = get_active_armature(context) + + if not armature: + col.label(text="No armature selected", icon='ERROR') + col.label(text="Select an armature to convert") + return + + # Check if the armature appears to be VRM + is_vrm = detect_vrm_armature(armature) + + if is_vrm: + col.label(text=f"Armature: {armature.name}", icon='CHECKMARK') + col.label(text="VRM armature detected", icon='INFO') + col.separator(factor=0.3) + + toolkit = context.scene.avatar_toolkit + col.prop(toolkit, 'vrm_remove_colliders', text="Remove Colliders") + col.separator(factor=0.2) + + col.operator( + AvatarToolkit_OT_ConvertVRMToUnity.bl_idname, + text="Convert to Unity Format", + icon='EXPORT' + ) + + info_box = vrm_box.box() + info_col = info_box.column(align=True) + info_col.label(text="Conversion Info:", icon='INFO') + info_col.label(text="• Renames VRM bones to Unity format") + info_col.label(text="• Removes collider bones (optional)") + info_col.label(text="• Maintains bone hierarchy") + info_col.label(text="• Validates conversion results") + info_col.label(text="• Preserves all animations") + + else: + col.label(text=f"Armature: {armature.name}", icon='ERROR') + col.label(text="No VRM bones detected", icon='CANCEL') + col.separator(factor=0.3) + + row = col.row() + row.enabled = False + row.operator( + AvatarToolkit_OT_ConvertVRMToUnity.bl_idname, + text="Convert to Unity Format", + icon='CANCEL' + ) + + help_box = vrm_box.box() + help_col = help_box.column(align=True) + help_col.label(text="VRM Detection Failed:", icon='QUESTION') + help_col.label(text="• Selected armature is not VRM format") + help_col.label(text="• VRM bones start with 'J_Bip_C_'") + help_col.label(text="• Need at least 5 VRM bones detected") + help_col.label(text="• Check armature bone names") \ No newline at end of file From 543869218c38eaabc193a418437c642c13a433ed Mon Sep 17 00:00:00 2001 From: Yusarina Date: Sat, 2 Aug 2025 01:28:28 +0100 Subject: [PATCH 2/5] Fixes - All bones should convert now - Root bone now get's removed. - Fixed Collections not getting removed --- core/dictionaries.py | 98 ++++---- core/properties.py | 6 + core/vrm_unity_converter.py | 308 +++++++++++++++++++++--- functions/tools/vrm_unity_conversion.py | 6 +- ui/vrm_unity_panel.py | 2 + 5 files changed, 334 insertions(+), 86 deletions(-) diff --git a/core/dictionaries.py b/core/dictionaries.py index e0344d3..34251b0 100644 --- a/core/dictionaries.py +++ b/core/dictionaries.py @@ -266,73 +266,73 @@ bone_names.update({ 'neck': bone_names['neck'] + ['jbipcneck', 'jneck', 'vrmneck'], 'head': bone_names['head'] + ['jbipchead', 'jhead', 'vrmhead'], - # VRM arms - 'left_shoulder': bone_names['left_shoulder'] + ['jbipllshoulder', 'jlshoulder'], + # VRM arms - both simplified patterns + 'left_shoulder': bone_names['left_shoulder'] + ['jbipllshoulder', 'jlshoulder', 'jbiplshoulder'], 'left_arm': bone_names['left_arm'] + ['jbiplupperarm', 'jlupperarm'], - 'left_elbow': bone_names['left_elbow'] + ['jbipllforearm', 'jlforearm'], - 'left_wrist': bone_names['left_wrist'] + ['jbipllhand', 'jlhand'], + 'left_elbow': bone_names['left_elbow'] + ['jbipllforearm', 'jlforearm', 'jbipllowerarm'], + 'left_wrist': bone_names['left_wrist'] + ['jbipllhand', 'jlhand', 'jbiplhand'], - 'right_shoulder': bone_names['right_shoulder'] + ['jbiprlshoulder', 'jrshoulder'], - 'right_arm': bone_names['right_arm'] + ['jbiprrupperarm', 'jrupperarm'], - 'right_elbow': bone_names['right_elbow'] + ['jbiprrforearm', 'jrforearm'], - 'right_wrist': bone_names['right_wrist'] + ['jbiprrhand', 'jrhand'], + 'right_shoulder': bone_names['right_shoulder'] + ['jbiprlshoulder', 'jrshoulder', 'jbiprshoulder'], + 'right_arm': bone_names['right_arm'] + ['jbiprrupperarm', 'jrupperarm', 'jbiprupperarm'], + 'right_elbow': bone_names['right_elbow'] + ['jbiprrforearm', 'jrforearm', 'jbiprforearm', 'jbiprlowerarm'], + 'right_wrist': bone_names['right_wrist'] + ['jbiprrhand', 'jrhand', 'jbiprhand'], - # VRM legs + # VRM legs - both simplified patterns 'left_leg': bone_names['left_leg'] + ['jbiplupperleg', 'jlupperleg'], 'left_knee': bone_names['left_knee'] + ['jbipllowerleg', 'jllowerleg'], - 'left_ankle': bone_names['left_ankle'] + ['jbipllfoot', 'jlfoot'], - 'left_toe': bone_names['left_toe'] + ['jbiplltoe', 'jltoe'], + 'left_ankle': bone_names['left_ankle'] + ['jbipllfoot', 'jlfoot', 'jbiplfoot'], + 'left_toe': bone_names['left_toe'] + ['jbiplltoe', 'jltoe', 'jbipltoebase'], - 'right_leg': bone_names['right_leg'] + ['jbiprrupperleg', 'jrupperleg'], - 'right_knee': bone_names['right_knee'] + ['jbiprrlowerleg', 'jrlowerleg'], - 'right_ankle': bone_names['right_ankle'] + ['jbiprrfoot', 'jrfoot'], - 'right_toe': bone_names['right_toe'] + ['jbiprrtoe', 'jrtoe'], + 'right_leg': bone_names['right_leg'] + ['jbiprrupperleg', 'jrupperleg', 'jbiprupperleg'], + 'right_knee': bone_names['right_knee'] + ['jbiprrlowerleg', 'jrlowerleg', 'jbiprlowerleg'], + 'right_ankle': bone_names['right_ankle'] + ['jbiprrfoot', 'jrfoot', 'jbiprfoot'], + 'right_toe': bone_names['right_toe'] + ['jbiprrtoe', 'jrtoe', 'jbiprtoebase'], # VRM eyes 'left_eye': bone_names['left_eye'] + ['jbipcleye', 'jleye'], 'right_eye': bone_names['right_eye'] + ['jbipcreye', 'jreye'], - # VRM fingers - Left - 'thumb_1_l': bone_names['thumb_1_l'] + ['jbipllthumb1', 'jlthumb1'], - 'thumb_2_l': bone_names['thumb_2_l'] + ['jbipllthumb2', 'jlthumb2'], - 'thumb_3_l': bone_names['thumb_3_l'] + ['jbipllthumb3', 'jlthumb3'], + # VRM fingers - Left (including Little finger variations) + 'thumb_1_l': bone_names['thumb_1_l'] + ['jbipllthumb1', 'jlthumb1', 'jbiplthumb1'], + 'thumb_2_l': bone_names['thumb_2_l'] + ['jbipllthumb2', 'jlthumb2', 'jbiplthumb2'], + 'thumb_3_l': bone_names['thumb_3_l'] + ['jbipllthumb3', 'jlthumb3', 'jbiplthumb3'], - 'index_1_l': bone_names['index_1_l'] + ['jbipllindex1', 'jlindex1'], - 'index_2_l': bone_names['index_2_l'] + ['jbipllindex2', 'jlindex2'], - 'index_3_l': bone_names['index_3_l'] + ['jbipllindex3', 'jlindex3'], + 'index_1_l': bone_names['index_1_l'] + ['jbipllindex1', 'jlindex1', 'jbiplindex1'], + 'index_2_l': bone_names['index_2_l'] + ['jbipllindex2', 'jlindex2', 'jbiplindex2'], + 'index_3_l': bone_names['index_3_l'] + ['jbipllindex3', 'jlindex3', 'jbiplindex3'], - 'middle_1_l': bone_names['middle_1_l'] + ['jbipllmiddle1', 'jlmiddle1'], - 'middle_2_l': bone_names['middle_2_l'] + ['jbipllmiddle2', 'jlmiddle2'], - 'middle_3_l': bone_names['middle_3_l'] + ['jbipllmiddle3', 'jlmiddle3'], + 'middle_1_l': bone_names['middle_1_l'] + ['jbipllmiddle1', 'jlmiddle1', 'jbiplmiddle1'], + 'middle_2_l': bone_names['middle_2_l'] + ['jbipllmiddle2', 'jlmiddle2', 'jbiplmiddle2'], + 'middle_3_l': bone_names['middle_3_l'] + ['jbipllmiddle3', 'jlmiddle3', 'jbiplmiddle3'], - 'ring_1_l': bone_names['ring_1_l'] + ['jbipllring1', 'jlring1'], - 'ring_2_l': bone_names['ring_2_l'] + ['jbipllring2', 'jlring2'], - 'ring_3_l': bone_names['ring_3_l'] + ['jbipllring3', 'jlring3'], + 'ring_1_l': bone_names['ring_1_l'] + ['jbipllring1', 'jlring1', 'jbiplring1'], + 'ring_2_l': bone_names['ring_2_l'] + ['jbipllring2', 'jlring2', 'jbiplring2'], + 'ring_3_l': bone_names['ring_3_l'] + ['jbipllring3', 'jlring3', 'jbiplring3'], - 'pinkie_1_l': bone_names['pinkie_1_l'] + ['jbipllpinky1', 'jlpinky1'], - 'pinkie_2_l': bone_names['pinkie_2_l'] + ['jbipllpinky2', 'jlpinky2'], - 'pinkie_3_l': bone_names['pinkie_3_l'] + ['jbipllpinky3', 'jlpinky3'], + 'pinkie_1_l': bone_names['pinkie_1_l'] + ['jbipllpinky1', 'jlpinky1', 'jbipllittle1', 'jbipllpinkie1'], + 'pinkie_2_l': bone_names['pinkie_2_l'] + ['jbipllpinky2', 'jlpinky2', 'jbipllittle2', 'jbipllpinkie2'], + 'pinkie_3_l': bone_names['pinkie_3_l'] + ['jbipllpinky3', 'jlpinky3', 'jbipllittle3', 'jbipllpinkie3'], - # VRM fingers - Right - 'thumb_1_r': bone_names['thumb_1_r'] + ['jbiprthumb1', 'jrthumb1'], - 'thumb_2_r': bone_names['thumb_2_r'] + ['jbiprthumb2', 'jrthumb2'], - 'thumb_3_r': bone_names['thumb_3_r'] + ['jbiprthumb3', 'jrthumb3'], + # VRM fingers - Right (including Little finger variations) + 'thumb_1_r': bone_names['thumb_1_r'] + ['jbiprthumb1', 'jrthumb1', 'jbiprrrthumb1'], + 'thumb_2_r': bone_names['thumb_2_r'] + ['jbiprthumb2', 'jrthumb2', 'jbiprrrthumb2'], + 'thumb_3_r': bone_names['thumb_3_r'] + ['jbiprthumb3', 'jrthumb3', 'jbiprrrthumb3'], - 'index_1_r': bone_names['index_1_r'] + ['jbiprindex1', 'jrindex1'], - 'index_2_r': bone_names['index_2_r'] + ['jbiprindex2', 'jrindex2'], - 'index_3_r': bone_names['index_3_r'] + ['jbiprindex3', 'jrindex3'], + 'index_1_r': bone_names['index_1_r'] + ['jbiprindex1', 'jrindex1', 'jbiprrrindex1'], + 'index_2_r': bone_names['index_2_r'] + ['jbiprindex2', 'jrindex2', 'jbiprrrindex2'], + 'index_3_r': bone_names['index_3_r'] + ['jbiprindex3', 'jrindex3', 'jbiprrrindex3'], - 'middle_1_r': bone_names['middle_1_r'] + ['jbiprmiddle1', 'jrmiddle1'], - 'middle_2_r': bone_names['middle_2_r'] + ['jbiprmiddle2', 'jrmiddle2'], - 'middle_3_r': bone_names['middle_3_r'] + ['jbiprmiddle3', 'jrmiddle3'], + 'middle_1_r': bone_names['middle_1_r'] + ['jbiprmiddle1', 'jrmiddle1', 'jbiprrmiddle1'], + 'middle_2_r': bone_names['middle_2_r'] + ['jbiprmiddle2', 'jrmiddle2', 'jbiprrmiddle2'], + 'middle_3_r': bone_names['middle_3_r'] + ['jbiprmiddle3', 'jrmiddle3', 'jbiprrmiddle3'], - 'ring_1_r': bone_names['ring_1_r'] + ['jbiprring1', 'jrring1'], - 'ring_2_r': bone_names['ring_2_r'] + ['jbiprring2', 'jrring2'], - 'ring_3_r': bone_names['ring_3_r'] + ['jbiprring3', 'jrring3'], + 'ring_1_r': bone_names['ring_1_r'] + ['jbiprring1', 'jrring1', 'jbiprrrring1'], + 'ring_2_r': bone_names['ring_2_r'] + ['jbiprring2', 'jrring2', 'jbiprrrring2'], + 'ring_3_r': bone_names['ring_3_r'] + ['jbiprring3', 'jrring3', 'jbiprrrring3'], - 'pinkie_1_r': bone_names['pinkie_1_r'] + ['jbiprpinky1', 'jrpinky1'], - 'pinkie_2_r': bone_names['pinkie_2_r'] + ['jbiprpinky2', 'jrpinky2'], - 'pinkie_3_r': bone_names['pinkie_3_r'] + ['jbiprpinky3', 'jrpinky3'] + 'pinkie_1_r': bone_names['pinkie_1_r'] + ['jbiprpinky1', 'jrpinky1', 'jbiprlittle1', 'jbiprrrpinky1'], + 'pinkie_2_r': bone_names['pinkie_2_r'] + ['jbiprpinky2', 'jrpinky2', 'jbiprlittle2', 'jbiprrrpinky2'], + 'pinkie_3_r': bone_names['pinkie_3_r'] + ['jbiprpinky3', 'jrpinky3', 'jbiprlittle3', 'jbiprrrpinky3'] }) # array taken from cats @@ -425,10 +425,12 @@ standard_bones = { 'neck': 'Neck', 'head': 'Head', - # Arms + # 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', diff --git a/core/properties.py b/core/properties.py index 281a3b8..79eed6e 100644 --- a/core/properties.py +++ b/core/properties.py @@ -614,6 +614,12 @@ class AvatarToolkitSceneProperties(PropertyGroup): description="Remove VRM collider bones during conversion", default=True ) + + vrm_remove_root: BoolProperty( + name="Remove Root Bone", + description="Remove unnecessary VRM root bone and make Hips the root bone", + default=True + ) def register() -> None: """Register the Avatar Toolkit property group""" diff --git a/core/vrm_unity_converter.py b/core/vrm_unity_converter.py index b1b0bee..0248cfb 100644 --- a/core/vrm_unity_converter.py +++ b/core/vrm_unity_converter.py @@ -15,10 +15,24 @@ def detect_vrm_armature(armature: Object) -> bool: vrm_patterns = [ 'jbipchips', 'jbipcspine', 'jbipcchest', 'jbipcneck', 'jbipchead', - 'jbiprlshoulder', 'jbiprrupperarm', 'jbiprrforearm', 'jbiprrhand', - 'jbipllshoulder', 'jbiplupperarm', 'jbipllforearm', 'jbipllhand', + # Right arm patterns (both single and double R) + 'jbiprlshoulder', 'jbiprshoulder', 'jbiprupperarm', 'jbiprforearm', 'jbiprhand', 'jbiprlowerarm', + 'jbiprrupperarm', 'jbiprrforearm', 'jbiprrhand', + # Left arm patterns + 'jbipllshoulder', 'jbiplshoulder', 'jbiplupperarm', 'jbipllforearm', 'jbipllhand', 'jbipllowerarm', 'jbiplhand', + # Right leg patterns (both single and double R) + 'jbiprupperleg', 'jbiprlowerleg', 'jbiprfoot', 'jbiprtoe', 'jbiprtoebase', 'jbiprrupperleg', 'jbiprrlowerleg', 'jbiprrfoot', 'jbiprrtoe', - 'jbiplupperleg', 'jbipllowerleg', 'jbipllfoot', 'jbiplltoe', + # Left leg patterns + 'jbiplupperleg', 'jbipllowerleg', 'jbipllfoot', 'jbiplfoot', 'jbiplltoe', 'jbipltoebase', + # Finger patterns + 'jbipllittle1', 'jbiprlittle1', + 'jbiplthumb1', 'jbiplthumb2', 'jbiplthumb3', + 'jbiplindex1', 'jbiplindex2', 'jbiplindex3', + 'jbiplmiddle1', 'jbiplmiddle2', 'jbiplmiddle3', + 'jbiplring1', 'jbiplring2', 'jbiplring3', + # Face eye patterns + 'jadjlfaceeye', 'jadjrfaceeye', 'jbipc', 'jbipr', 'jbipl' ] @@ -48,81 +62,132 @@ def get_vrm_to_unity_mapping() -> Dict[str, str]: # Left arm 'jbipllshoulder': standard_bones.get('left_shoulder', 'LeftShoulder'), + 'jbiplshoulder': standard_bones.get('left_shoulder', 'LeftShoulder'), 'jbiplupperarm': standard_bones['left_arm'], 'jbipllforearm': standard_bones['left_elbow'], + 'jbipllowerarm': standard_bones['left_elbow'], 'jbipllhand': standard_bones['left_wrist'], + 'jbiplhand': standard_bones['left_wrist'], - # Right arm + # Right arm (both jbipr and jbiprr patterns) 'jbiprlshoulder': standard_bones.get('right_shoulder', 'RightShoulder'), + 'jbiprshoulder': standard_bones.get('right_shoulder', 'RightShoulder'), + 'jbiprrshoulder': standard_bones.get('right_shoulder', 'RightShoulder'), + 'jbiprupperarm': standard_bones['right_arm'], 'jbiprrupperarm': standard_bones['right_arm'], + 'jbiprforearm': standard_bones['right_elbow'], 'jbiprrforearm': standard_bones['right_elbow'], + 'jbiprlowerarm': standard_bones['right_elbow'], + 'jbiprhand': standard_bones['right_wrist'], 'jbiprrhand': standard_bones['right_wrist'], # Left leg 'jbiplupperleg': standard_bones['left_leg'], 'jbipllowerleg': standard_bones['left_knee'], 'jbipllfoot': standard_bones['left_ankle'], + 'jbiplfoot': standard_bones['left_ankle'], 'jbiplltoe': standard_bones['left_toe'], + 'jbipltoebase': standard_bones['left_toe'], - # Right leg + # Right leg (both jbipr and jbiprr patterns) + 'jbiprupperleg': standard_bones['right_leg'], 'jbiprrupperleg': standard_bones['right_leg'], + 'jbiprlowerleg': standard_bones['right_knee'], 'jbiprrlowerleg': standard_bones['right_knee'], + 'jbiprfoot': standard_bones['right_ankle'], 'jbiprrfoot': standard_bones['right_ankle'], + 'jbiprtoe': standard_bones['right_toe'], 'jbiprrtoe': standard_bones['right_toe'], + 'jbiprtoebase': standard_bones['right_toe'], # Eyes 'jbipcleye': standard_bones.get('left_eye', 'Eye.L'), 'jbipcreye': standard_bones.get('right_eye', 'Eye.R'), + 'jadjlfaceeye': standard_bones.get('left_eye', 'Eye.L'), + 'jadjrfaceeye': standard_bones.get('right_eye', 'Eye.R'), # Fingers - Left thumb 'jbipllthumb1': standard_bones.get('thumb_1_l', 'Thumb1.L'), 'jbipllthumb2': standard_bones.get('thumb_2_l', 'Thumb2.L'), 'jbipllthumb3': standard_bones.get('thumb_3_l', 'Thumb3.L'), + 'jbiplthumb1': standard_bones.get('thumb_1_l', 'Thumb1.L'), + 'jbiplthumb2': standard_bones.get('thumb_2_l', 'Thumb2.L'), + 'jbiplthumb3': standard_bones.get('thumb_3_l', 'Thumb3.L'), # Fingers - Left index 'jbipllindex1': standard_bones.get('index_1_l', 'Index1.L'), 'jbipllindex2': standard_bones.get('index_2_l', 'Index2.L'), 'jbipllindex3': standard_bones.get('index_3_l', 'Index3.L'), + 'jbiplindex1': standard_bones.get('index_1_l', 'Index1.L'), + 'jbiplindex2': standard_bones.get('index_2_l', 'Index2.L'), + 'jbiplindex3': standard_bones.get('index_3_l', 'Index3.L'), # Fingers - Left middle 'jbipllmiddle1': standard_bones.get('middle_1_l', 'Middle1.L'), 'jbipllmiddle2': standard_bones.get('middle_2_l', 'Middle2.L'), 'jbipllmiddle3': standard_bones.get('middle_3_l', 'Middle3.L'), + 'jbiplmiddle1': standard_bones.get('middle_1_l', 'Middle1.L'), + 'jbiplmiddle2': standard_bones.get('middle_2_l', 'Middle2.L'), + 'jbiplmiddle3': standard_bones.get('middle_3_l', 'Middle3.L'), # Fingers - Left ring 'jbipllring1': standard_bones.get('ring_1_l', 'Ring1.L'), 'jbipllring2': standard_bones.get('ring_2_l', 'Ring2.L'), 'jbipllring3': standard_bones.get('ring_3_l', 'Ring3.L'), + 'jbiplring1': standard_bones.get('ring_1_l', 'Ring1.L'), + 'jbiplring2': standard_bones.get('ring_2_l', 'Ring2.L'), + 'jbiplring3': standard_bones.get('ring_3_l', 'Ring3.L'), # Fingers - Left pinky 'jbipllpinky1': standard_bones.get('pinkie_1_l', 'Pinky1.L'), 'jbipllpinky2': standard_bones.get('pinkie_2_l', 'Pinky2.L'), 'jbipllpinky3': standard_bones.get('pinkie_3_l', 'Pinky3.L'), + 'jbipllittle1': standard_bones.get('pinkie_1_l', 'Pinky1.L'), + 'jbipllittle2': standard_bones.get('pinkie_2_l', 'Pinky2.L'), + 'jbipllittle3': standard_bones.get('pinkie_3_l', 'Pinky3.L'), - # Fingers - Right thumb + # Fingers - Right thumb (both jbipr and jbiprr patterns) 'jbiprthumb1': standard_bones.get('thumb_1_r', 'Thumb1.R'), 'jbiprthumb2': standard_bones.get('thumb_2_r', 'Thumb2.R'), 'jbiprthumb3': standard_bones.get('thumb_3_r', 'Thumb3.R'), + 'jbiprrrthumb1': standard_bones.get('thumb_1_r', 'Thumb1.R'), + 'jbiprrrthumb2': standard_bones.get('thumb_2_r', 'Thumb2.R'), + 'jbiprrrthumb3': standard_bones.get('thumb_3_r', 'Thumb3.R'), # Fingers - Right index 'jbiprindex1': standard_bones.get('index_1_r', 'Index1.R'), 'jbiprindex2': standard_bones.get('index_2_r', 'Index2.R'), 'jbiprindex3': standard_bones.get('index_3_r', 'Index3.R'), + 'jbiprrrindex1': standard_bones.get('index_1_r', 'Index1.R'), + 'jbiprrrindex2': standard_bones.get('index_2_r', 'Index2.R'), + 'jbiprrrindex3': standard_bones.get('index_3_r', 'Index3.R'), # Fingers - Right middle 'jbiprmiddle1': standard_bones.get('middle_1_r', 'Middle1.R'), 'jbiprmiddle2': standard_bones.get('middle_2_r', 'Middle2.R'), 'jbiprmiddle3': standard_bones.get('middle_3_r', 'Middle3.R'), + 'jbiprrmiddle1': standard_bones.get('middle_1_r', 'Middle1.R'), + 'jbiprrmiddle2': standard_bones.get('middle_2_r', 'Middle2.R'), + 'jbiprrmiddle3': standard_bones.get('middle_3_r', 'Middle3.R'), # Fingers - Right ring 'jbiprring1': standard_bones.get('ring_1_r', 'Ring1.R'), 'jbiprring2': standard_bones.get('ring_2_r', 'Ring2.R'), 'jbiprring3': standard_bones.get('ring_3_r', 'Ring3.R'), + 'jbiprrrring1': standard_bones.get('ring_1_r', 'Ring1.R'), + 'jbiprrrring2': standard_bones.get('ring_2_r', 'Ring2.R'), + 'jbiprrrring3': standard_bones.get('ring_3_r', 'Ring3.R'), # Fingers - Right pinky 'jbiprpinky1': standard_bones.get('pinkie_1_r', 'Pinky1.R'), 'jbiprpinky2': standard_bones.get('pinkie_2_r', 'Pinky2.R'), 'jbiprpinky3': standard_bones.get('pinkie_3_r', 'Pinky3.R'), + 'jbiprrrpinky1': standard_bones.get('pinkie_1_r', 'Pinky1.R'), + 'jbiprrrpinky2': standard_bones.get('pinkie_2_r', 'Pinky2.R'), + 'jbiprrrpinky3': standard_bones.get('pinkie_3_r', 'Pinky3.R'), + 'jbiprlittle1': standard_bones.get('pinkie_1_r', 'Pinky1.R'), + 'jbiprlittle2': standard_bones.get('pinkie_2_r', 'Pinky2.R'), + 'jbiprlittle3': standard_bones.get('pinkie_3_r', 'Pinky3.R'), } @@ -167,33 +232,70 @@ def guess_unity_name_from_vrm(vrm_simplified: str) -> Optional[str]: # Left arm 'jbipllclavicle': 'LeftShoulder', - 'jbipllshoulder': 'LeftShoulder', + 'jbipllshoulder': 'LeftShoulder', + 'jbiplshoulder': 'LeftShoulder', 'jbiplupperarm': 'LeftUpperArm', 'jbipllforearm': 'LeftLowerArm', + 'jbipllowerarm': 'LeftLowerArm', 'jbipllhand': 'LeftHand', + 'jbiplhand': 'LeftHand', - # Right arm + # Right arm (both single and double R patterns) 'jbiprrclavicle': 'RightShoulder', 'jbiprlshoulder': 'RightShoulder', + 'jbiprshoulder': 'RightShoulder', + 'jbiprupperarm': 'RightUpperArm', 'jbiprrupperarm': 'RightUpperArm', + 'jbiprforearm': 'RightLowerArm', 'jbiprrforearm': 'RightLowerArm', + 'jbiprlowerarm': 'RightLowerArm', + 'jbiprhand': 'RightHand', 'jbiprrhand': 'RightHand', # Left leg 'jbiplupperleg': 'LeftUpperLeg', 'jbipllowerleg': 'LeftLowerLeg', 'jbipllfoot': 'LeftFoot', + 'jbiplfoot': 'LeftFoot', 'jbiplltoe': 'LeftToes', + 'jbipltoebase': 'LeftToes', - # Right leg + # Right leg (both single and double R patterns) + 'jbiprupperleg': 'RightUpperLeg', 'jbiprrupperleg': 'RightUpperLeg', + 'jbiprlowerleg': 'RightLowerLeg', 'jbiprrlowerleg': 'RightLowerLeg', + 'jbiprfoot': 'RightFoot', 'jbiprrfoot': 'RightFoot', + 'jbiprtoe': 'RightToes', 'jbiprrtoe': 'RightToes', + 'jbiprtoebase': 'RightToes', # Eyes 'jbipcleye': 'LeftEye', - 'jbipcreye': 'RightEye' + 'jbipcreye': 'RightEye', + 'jadjlfaceeye': 'LeftEye', + 'jadjrfaceeye': 'RightEye', + + # Fingers - Left + 'jbiplthumb1': 'LeftThumb1', + 'jbiplthumb2': 'LeftThumb2', + 'jbiplthumb3': 'LeftThumb3', + 'jbiplindex1': 'LeftIndex1', + 'jbiplindex2': 'LeftIndex2', + 'jbiplindex3': 'LeftIndex3', + 'jbiplmiddle1': 'LeftMiddle1', + 'jbiplmiddle2': 'LeftMiddle2', + 'jbiplmiddle3': 'LeftMiddle3', + 'jbiplring1': 'LeftRing1', + 'jbiplring2': 'LeftRing2', + 'jbiplring3': 'LeftRing3', + 'jbipllittle1': 'LeftPinky1', + 'jbipllittle2': 'LeftPinky2', + 'jbipllittle3': 'LeftPinky3', + 'jbiprlittle1': 'RightPinky1', + 'jbiprlittle2': 'RightPinky2', + 'jbiprlittle3': 'RightPinky3' } return pattern_mappings.get(vrm_simplified) @@ -221,10 +323,40 @@ def is_vrm_collider_object(obj_name: str) -> bool: return is_vrm -def remove_vrm_colliders(armature: Object = None) -> Tuple[int, List[str]]: +def remove_collection_from_hierarchy(collection_to_remove) -> bool: + """ + Recursively remove a collection from all parent collections in the hierarchy + """ + removed_from_any_parent = False + + try: + # Check scene collection + scene_collection = bpy.context.scene.collection + if collection_to_remove in scene_collection.children: + scene_collection.children.unlink(collection_to_remove) + logger.debug(f" Unlinked '{collection_to_remove.name}' from scene collection") + removed_from_any_parent = True + + # Check all other collections recursively + for parent_collection in list(bpy.data.collections): + if parent_collection != collection_to_remove and collection_to_remove in parent_collection.children: + try: + parent_collection.children.unlink(collection_to_remove) + logger.debug(f" Unlinked '{collection_to_remove.name}' from parent '{parent_collection.name}'") + removed_from_any_parent = True + except Exception as unlink_error: + logger.warning(f" Failed to unlink '{collection_to_remove.name}' from '{parent_collection.name}': {str(unlink_error)}") + + return removed_from_any_parent + + except Exception as e: + logger.error(f"Error removing collection '{collection_to_remove.name}' from hierarchy: {str(e)}") + return False + + +def remove_vrm_colliders(armature: Object = None) -> Tuple[int, List[str], int]: """ Simple approach: Remove ALL objects with 'collider' in their name and clean up empty collections - Returns tuple of (removed_count, removed_object_names) """ objects_to_remove = [] removed_names = [] @@ -279,34 +411,59 @@ def remove_vrm_colliders(armature: Object = None) -> Tuple[int, List[str]]: logger.info(f"Successfully removed {removed_count} collider objects") - # Clean up empty collections + # Clean up empty collections (prioritize collider-related collections) empty_collections_removed = 0 - for collection in list(collections_to_check): + + # Also check all collections in the scene for collider-related names + all_collections_to_check = set(collections_to_check) + for collection in bpy.data.collections: + collection_name_lower = collection.name.lower() + if any(pattern in collection_name_lower for pattern in ['collider', 'collision', 'physics', 'dynamic']): + all_collections_to_check.add(collection) + logger.debug(f"Found collider-related collection to check: {collection.name}") + + for collection in list(all_collections_to_check): try: - # Check if collection is now empty and not the master collection - if (len(collection.objects) == 0 and - len(collection.children) == 0 and - collection.name != "Collection" and - collection.name != "Master Collection"): + # Check if collection exists and is empty + if collection.name not in bpy.data.collections: + logger.debug(f"Collection {collection.name} already removed") + continue + + collection_name_lower = collection.name.lower() + is_collider_collection = any(pattern in collection_name_lower for pattern in ['collider', 'collision', 'physics', 'dynamic']) + is_empty = len(collection.objects) == 0 and len(collection.children) == 0 + is_protected = collection.name in ["Collection", "Master Collection"] + + # Remove if empty and (was used by colliders OR has collider-related name) + if is_empty and not is_protected and (collection in collections_to_check or is_collider_collection): + logger.info(f"Removing empty {'collider-related ' if is_collider_collection else ''}collection: {collection.name}") - logger.info(f"Removing empty collection: {collection.name}") + # Use helper function to remove from all parent collections + removed_from_parents = remove_collection_from_hierarchy(collection) - if collection in bpy.context.scene.collection.children: - bpy.context.scene.collection.children.unlink(collection) + if not removed_from_parents: + logger.debug(f" Collection {collection.name} was not found in any parent collections") - bpy.data.collections.remove(collection) - empty_collections_removed += 1 - logger.info(f" Successfully removed collection: {collection.name}") + # Remove the collection data + try: + bpy.data.collections.remove(collection) + empty_collections_removed += 1 + logger.info(f" Successfully removed collection: {collection.name}") + except Exception as remove_error: + logger.warning(f" Failed to remove collection {collection.name}: {str(remove_error)}") + # Continue with other collections even if this one fails except Exception as e: logger.warning(f"Failed to remove empty collection {collection.name}: {str(e)}") + import traceback + logger.debug(f"Collection removal traceback: {traceback.format_exc()}") if empty_collections_removed > 0: logger.info(f"Cleaned up {empty_collections_removed} empty collections") except Exception as e: logger.error(f"Error during collider removal: {str(e)}") - return 0, [] + return 0, [], 0 finally: if original_active and original_active.name in bpy.data.objects: @@ -318,16 +475,85 @@ def remove_vrm_colliders(armature: Object = None) -> Tuple[int, List[str]]: except: pass - logger.info(f"Collider removal complete. Removed {len(removed_names)} objects") - return len(removed_names), removed_names + logger.info(f"Collider removal complete. Removed {len(removed_names)} objects and {empty_collections_removed} collections") + return len(removed_names), removed_names, empty_collections_removed -def convert_vrm_to_unity(armature: Object, remove_colliders: bool = True) -> Tuple[bool, List[str], int]: +def remove_vrm_root_bone(armature: Object) -> Tuple[bool, str]: + """ + Remove unnecessary VRM root bone and make Hips the root bone + + """ + if not armature or armature.type != 'ARMATURE': + return False, "No valid armature provided" + + # Look for potential root bones and Hips bone + potential_roots = [] + hips_bone = None + + for bone in armature.data.edit_bones: + bone_name_lower = bone.name.lower() + + # Check if this could be Hips (various naming conventions) + if any(hips_name in bone_name_lower for hips_name in ['hips', 'hip', 'pelvis', 'jbipchips']): + hips_bone = bone + logger.debug(f"Found Hips bone: {bone.name}") + + # Check if this could be a root bone + if bone.parent is None and len(bone.children) > 0: + # Common VRM root bone names + if any(root_name in bone_name_lower for root_name in ['root', 'vrm', 'armature', 'rig']): + potential_roots.append(bone) + logger.debug(f"Found potential root bone: {bone.name}") + + if not hips_bone: + return False, "Could not find Hips bone to promote as root" + + if not potential_roots: + logger.info("No unnecessary root bone found - Hips may already be root") + return True, "No root bone removal needed" + + # Find the root bone that is the parent of Hips + root_to_remove = None + for root_bone in potential_roots: + if hips_bone.parent == root_bone: + root_to_remove = root_bone + break + + if not root_to_remove: + # Check if Hips is already parentless (already root) + if hips_bone.parent is None: + logger.info("Hips bone is already the root bone") + return True, "Hips is already root - no changes needed" + else: + logger.warning(f"Hips bone has parent '{hips_bone.parent.name}' but no matching root found") + return False, "Could not identify safe root bone to remove" + + root_name = root_to_remove.name + logger.info(f"Removing root bone '{root_name}' and promoting Hips to root") + + # Reparent all children of the root bone (except Hips) to Hips + children_to_reparent = [] + for child in root_to_remove.children: + if child != hips_bone: + children_to_reparent.append(child) + ) + hips_bone.parent = None + + for child in children_to_reparent: + child.parent = hips_bone + logger.debug(f"Reparented {child.name} from {root_name} to {hips_bone.name}") + + armature.data.edit_bones.remove(root_to_remove) + + message = f"Removed root bone '{root_name}' - Hips is now the root bone" + logger.info(message) + return True, message + + +def convert_vrm_to_unity(armature: Object, remove_colliders: bool = True, remove_root: bool = True) -> Tuple[bool, List[str], int]: """ Convert VRM armature bone names to Unity humanoid format - - Returns: - Tuple of (success, messages, converted_count) """ if not armature or armature.type != 'ARMATURE': return False, ["No valid armature selected"], 0 @@ -351,15 +577,18 @@ def convert_vrm_to_unity(armature: Object, remove_colliders: bool = True) -> Tup try: # First, remove collider objects and bones if requested if remove_colliders: - collider_count, removed_colliders = remove_vrm_colliders(armature) - if collider_count > 0: - messages.append(f"Removed {collider_count} VRM collider objects/bones") + collider_count, removed_colliders, collections_removed = remove_vrm_colliders(armature) + if collider_count > 0 or collections_removed > 0: + if collections_removed > 0: + messages.append(f"Removed {collider_count} VRM collider objects and {collections_removed} empty collections") + else: + messages.append(f"Removed {collider_count} VRM collider objects") logger.info(f"Removed {collider_count} VRM colliders: {removed_colliders}") vrm_bones = find_vrm_bones_in_armature(armature) if not vrm_bones: - if remove_colliders and collider_count > 0: + if remove_colliders and (collider_count > 0 or collections_removed > 0): messages.append("No VRM bones found to convert (colliders were removed)") return True, messages, 0 else: @@ -368,6 +597,13 @@ def convert_vrm_to_unity(armature: Object, remove_colliders: bool = True) -> Tup if bpy.context.mode != 'EDIT': bpy.ops.object.mode_set(mode='EDIT') + # Remove unnecessary root bone if requested + if remove_root: + root_success, root_message = remove_vrm_root_bone(armature) + messages.append(root_message) + if not root_success: + logger.warning(f"Root bone removal failed: {root_message}") + # Rename bones for vrm_bone_name, unity_name in vrm_bones.items(): if vrm_bone_name in armature.data.edit_bones: diff --git a/functions/tools/vrm_unity_conversion.py b/functions/tools/vrm_unity_conversion.py index 8514fa1..48f0738 100644 --- a/functions/tools/vrm_unity_conversion.py +++ b/functions/tools/vrm_unity_conversion.py @@ -28,9 +28,11 @@ class AvatarToolkit_OT_ConvertVRMToUnity(Operator): logger.info(f"Starting VRM to Unity conversion for armature: {armature.name}") - # Get collider removal setting + # Get conversion settings remove_colliders = context.scene.avatar_toolkit.vrm_remove_colliders + remove_root = context.scene.avatar_toolkit.vrm_remove_root logger.info(f"Collider removal setting: {remove_colliders}") + logger.info(f"Root bone removal setting: {remove_root}") # Log all objects with 'collider' in name for debugging collider_objects = [obj.name for obj in bpy.data.objects if 'collider' in obj.name.lower()] @@ -39,7 +41,7 @@ class AvatarToolkit_OT_ConvertVRMToUnity(Operator): for obj_name in collider_objects: logger.info(f" - {obj_name}") - success, messages, converted_count = convert_vrm_to_unity(armature, remove_colliders) + success, messages, converted_count = convert_vrm_to_unity(armature, remove_colliders, remove_root) if not success: logger.warning(f"VRM conversion failed: {messages}") diff --git a/ui/vrm_unity_panel.py b/ui/vrm_unity_panel.py index 848cec1..edaad43 100644 --- a/ui/vrm_unity_panel.py +++ b/ui/vrm_unity_panel.py @@ -46,6 +46,7 @@ class AvatarToolKit_PT_VRMUnityPanel(Panel): toolkit = context.scene.avatar_toolkit col.prop(toolkit, 'vrm_remove_colliders', text="Remove Colliders") + col.prop(toolkit, 'vrm_remove_root', text="Remove Root Bone") col.separator(factor=0.2) col.operator( @@ -59,6 +60,7 @@ class AvatarToolKit_PT_VRMUnityPanel(Panel): info_col.label(text="Conversion Info:", icon='INFO') info_col.label(text="• Renames VRM bones to Unity format") info_col.label(text="• Removes collider bones (optional)") + info_col.label(text="• Removes root bone, makes Hips root (optional)") info_col.label(text="• Maintains bone hierarchy") info_col.label(text="• Validates conversion results") info_col.label(text="• Preserves all animations") From 634563afb3aa7c641af040625fbc6b970cf558c7 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Sat, 2 Aug 2025 01:52:09 +0100 Subject: [PATCH 3/5] Move bones to the dictionary (most was already in there but there were hardcoded for testing) --- core/dictionaries.py | 4 +- core/vrm_unity_converter.py | 167 ++++-------------------------------- 2 files changed, 17 insertions(+), 154 deletions(-) diff --git a/core/dictionaries.py b/core/dictionaries.py index 34251b0..e8227e2 100644 --- a/core/dictionaries.py +++ b/core/dictionaries.py @@ -289,8 +289,8 @@ bone_names.update({ 'right_toe': bone_names['right_toe'] + ['jbiprrtoe', 'jrtoe', 'jbiprtoebase'], # VRM eyes - 'left_eye': bone_names['left_eye'] + ['jbipcleye', 'jleye'], - 'right_eye': bone_names['right_eye'] + ['jbipcreye', 'jreye'], + 'left_eye': bone_names['left_eye'] + ['jbipcleye', 'jleye', 'jadjlfaceeye'], + 'right_eye': bone_names['right_eye'] + ['jbipcreye', 'jreye', 'jadjrfaceeye'], # VRM fingers - Left (including Little finger variations) 'thumb_1_l': bone_names['thumb_1_l'] + ['jbipllthumb1', 'jlthumb1', 'jbiplthumb1'], diff --git a/core/vrm_unity_converter.py b/core/vrm_unity_converter.py index 0248cfb..21225cd 100644 --- a/core/vrm_unity_converter.py +++ b/core/vrm_unity_converter.py @@ -2,7 +2,7 @@ import bpy from typing import Dict, List, Optional, Tuple, Set from bpy.types import Object, Bone from .common import get_active_armature -from .dictionaries import simplify_bonename, standard_bones, bone_hierarchy +from .dictionaries import simplify_bonename, standard_bones, bone_hierarchy, reverse_bone_lookup from .logging_setup import logger @@ -47,168 +47,31 @@ def detect_vrm_armature(armature: Object) -> bool: return found_vrm_bones >= 5 -def get_vrm_to_unity_mapping() -> Dict[str, str]: - """ - Get mapping from VRM bone names to Unity humanoid bone names - """ - return { - # Core structure - 'jbipchips': standard_bones['hips'], - 'jbipcspine': standard_bones['spine'], - 'jbipcchest': standard_bones['chest'], - 'jbipcupperchest': standard_bones.get('upper_chest', 'UpperChest'), - 'jbipcneck': standard_bones['neck'], - 'jbipchead': standard_bones['head'], - - # Left arm - 'jbipllshoulder': standard_bones.get('left_shoulder', 'LeftShoulder'), - 'jbiplshoulder': standard_bones.get('left_shoulder', 'LeftShoulder'), - 'jbiplupperarm': standard_bones['left_arm'], - 'jbipllforearm': standard_bones['left_elbow'], - 'jbipllowerarm': standard_bones['left_elbow'], - 'jbipllhand': standard_bones['left_wrist'], - 'jbiplhand': standard_bones['left_wrist'], - - # Right arm (both jbipr and jbiprr patterns) - 'jbiprlshoulder': standard_bones.get('right_shoulder', 'RightShoulder'), - 'jbiprshoulder': standard_bones.get('right_shoulder', 'RightShoulder'), - 'jbiprrshoulder': standard_bones.get('right_shoulder', 'RightShoulder'), - 'jbiprupperarm': standard_bones['right_arm'], - 'jbiprrupperarm': standard_bones['right_arm'], - 'jbiprforearm': standard_bones['right_elbow'], - 'jbiprrforearm': standard_bones['right_elbow'], - 'jbiprlowerarm': standard_bones['right_elbow'], - 'jbiprhand': standard_bones['right_wrist'], - 'jbiprrhand': standard_bones['right_wrist'], - - # Left leg - 'jbiplupperleg': standard_bones['left_leg'], - 'jbipllowerleg': standard_bones['left_knee'], - 'jbipllfoot': standard_bones['left_ankle'], - 'jbiplfoot': standard_bones['left_ankle'], - 'jbiplltoe': standard_bones['left_toe'], - 'jbipltoebase': standard_bones['left_toe'], - - # Right leg (both jbipr and jbiprr patterns) - 'jbiprupperleg': standard_bones['right_leg'], - 'jbiprrupperleg': standard_bones['right_leg'], - 'jbiprlowerleg': standard_bones['right_knee'], - 'jbiprrlowerleg': standard_bones['right_knee'], - 'jbiprfoot': standard_bones['right_ankle'], - 'jbiprrfoot': standard_bones['right_ankle'], - 'jbiprtoe': standard_bones['right_toe'], - 'jbiprrtoe': standard_bones['right_toe'], - 'jbiprtoebase': standard_bones['right_toe'], - - # Eyes - 'jbipcleye': standard_bones.get('left_eye', 'Eye.L'), - 'jbipcreye': standard_bones.get('right_eye', 'Eye.R'), - 'jadjlfaceeye': standard_bones.get('left_eye', 'Eye.L'), - 'jadjrfaceeye': standard_bones.get('right_eye', 'Eye.R'), - - # Fingers - Left thumb - 'jbipllthumb1': standard_bones.get('thumb_1_l', 'Thumb1.L'), - 'jbipllthumb2': standard_bones.get('thumb_2_l', 'Thumb2.L'), - 'jbipllthumb3': standard_bones.get('thumb_3_l', 'Thumb3.L'), - 'jbiplthumb1': standard_bones.get('thumb_1_l', 'Thumb1.L'), - 'jbiplthumb2': standard_bones.get('thumb_2_l', 'Thumb2.L'), - 'jbiplthumb3': standard_bones.get('thumb_3_l', 'Thumb3.L'), - - # Fingers - Left index - 'jbipllindex1': standard_bones.get('index_1_l', 'Index1.L'), - 'jbipllindex2': standard_bones.get('index_2_l', 'Index2.L'), - 'jbipllindex3': standard_bones.get('index_3_l', 'Index3.L'), - 'jbiplindex1': standard_bones.get('index_1_l', 'Index1.L'), - 'jbiplindex2': standard_bones.get('index_2_l', 'Index2.L'), - 'jbiplindex3': standard_bones.get('index_3_l', 'Index3.L'), - - # Fingers - Left middle - 'jbipllmiddle1': standard_bones.get('middle_1_l', 'Middle1.L'), - 'jbipllmiddle2': standard_bones.get('middle_2_l', 'Middle2.L'), - 'jbipllmiddle3': standard_bones.get('middle_3_l', 'Middle3.L'), - 'jbiplmiddle1': standard_bones.get('middle_1_l', 'Middle1.L'), - 'jbiplmiddle2': standard_bones.get('middle_2_l', 'Middle2.L'), - 'jbiplmiddle3': standard_bones.get('middle_3_l', 'Middle3.L'), - - # Fingers - Left ring - 'jbipllring1': standard_bones.get('ring_1_l', 'Ring1.L'), - 'jbipllring2': standard_bones.get('ring_2_l', 'Ring2.L'), - 'jbipllring3': standard_bones.get('ring_3_l', 'Ring3.L'), - 'jbiplring1': standard_bones.get('ring_1_l', 'Ring1.L'), - 'jbiplring2': standard_bones.get('ring_2_l', 'Ring2.L'), - 'jbiplring3': standard_bones.get('ring_3_l', 'Ring3.L'), - - # Fingers - Left pinky - 'jbipllpinky1': standard_bones.get('pinkie_1_l', 'Pinky1.L'), - 'jbipllpinky2': standard_bones.get('pinkie_2_l', 'Pinky2.L'), - 'jbipllpinky3': standard_bones.get('pinkie_3_l', 'Pinky3.L'), - 'jbipllittle1': standard_bones.get('pinkie_1_l', 'Pinky1.L'), - 'jbipllittle2': standard_bones.get('pinkie_2_l', 'Pinky2.L'), - 'jbipllittle3': standard_bones.get('pinkie_3_l', 'Pinky3.L'), - - # Fingers - Right thumb (both jbipr and jbiprr patterns) - 'jbiprthumb1': standard_bones.get('thumb_1_r', 'Thumb1.R'), - 'jbiprthumb2': standard_bones.get('thumb_2_r', 'Thumb2.R'), - 'jbiprthumb3': standard_bones.get('thumb_3_r', 'Thumb3.R'), - 'jbiprrrthumb1': standard_bones.get('thumb_1_r', 'Thumb1.R'), - 'jbiprrrthumb2': standard_bones.get('thumb_2_r', 'Thumb2.R'), - 'jbiprrrthumb3': standard_bones.get('thumb_3_r', 'Thumb3.R'), - - # Fingers - Right index - 'jbiprindex1': standard_bones.get('index_1_r', 'Index1.R'), - 'jbiprindex2': standard_bones.get('index_2_r', 'Index2.R'), - 'jbiprindex3': standard_bones.get('index_3_r', 'Index3.R'), - 'jbiprrrindex1': standard_bones.get('index_1_r', 'Index1.R'), - 'jbiprrrindex2': standard_bones.get('index_2_r', 'Index2.R'), - 'jbiprrrindex3': standard_bones.get('index_3_r', 'Index3.R'), - - # Fingers - Right middle - 'jbiprmiddle1': standard_bones.get('middle_1_r', 'Middle1.R'), - 'jbiprmiddle2': standard_bones.get('middle_2_r', 'Middle2.R'), - 'jbiprmiddle3': standard_bones.get('middle_3_r', 'Middle3.R'), - 'jbiprrmiddle1': standard_bones.get('middle_1_r', 'Middle1.R'), - 'jbiprrmiddle2': standard_bones.get('middle_2_r', 'Middle2.R'), - 'jbiprrmiddle3': standard_bones.get('middle_3_r', 'Middle3.R'), - - # Fingers - Right ring - 'jbiprring1': standard_bones.get('ring_1_r', 'Ring1.R'), - 'jbiprring2': standard_bones.get('ring_2_r', 'Ring2.R'), - 'jbiprring3': standard_bones.get('ring_3_r', 'Ring3.R'), - 'jbiprrrring1': standard_bones.get('ring_1_r', 'Ring1.R'), - 'jbiprrrring2': standard_bones.get('ring_2_r', 'Ring2.R'), - 'jbiprrrring3': standard_bones.get('ring_3_r', 'Ring3.R'), - - # Fingers - Right pinky - 'jbiprpinky1': standard_bones.get('pinkie_1_r', 'Pinky1.R'), - 'jbiprpinky2': standard_bones.get('pinkie_2_r', 'Pinky2.R'), - 'jbiprpinky3': standard_bones.get('pinkie_3_r', 'Pinky3.R'), - 'jbiprrrpinky1': standard_bones.get('pinkie_1_r', 'Pinky1.R'), - 'jbiprrrpinky2': standard_bones.get('pinkie_2_r', 'Pinky2.R'), - 'jbiprrrpinky3': standard_bones.get('pinkie_3_r', 'Pinky3.R'), - 'jbiprlittle1': standard_bones.get('pinkie_1_r', 'Pinky1.R'), - 'jbiprlittle2': standard_bones.get('pinkie_2_r', 'Pinky2.R'), - 'jbiprlittle3': standard_bones.get('pinkie_3_r', 'Pinky3.R'), - } def find_vrm_bones_in_armature(armature: Object) -> Dict[str, str]: """ - Find VRM bones in armature and return mapping to their actual names + Find VRM bones in armature and return mapping to their actual names using dictionary lookup """ - vrm_mapping = get_vrm_to_unity_mapping() found_bones = {} for bone_name in armature.data.bones.keys(): simplified_name = simplify_bonename(bone_name) - # Check if this bone matches any VRM pattern - for vrm_pattern, unity_name in vrm_mapping.items(): - if simplified_name == vrm_pattern: + # Check if this bone exists in our reverse lookup dictionary + if simplified_name in reverse_bone_lookup: + standard_bone_key = reverse_bone_lookup[simplified_name] + + # Get the Unity name from standard_bones + if standard_bone_key in standard_bones: + unity_name = standard_bones[standard_bone_key] found_bones[bone_name] = unity_name - logger.debug(f"Found VRM bone: {bone_name} -> {unity_name}") - break + logger.debug(f"Found VRM bone via dictionary: {bone_name} -> {unity_name}") + else: + logger.debug(f"Standard bone key '{standard_bone_key}' not found in standard_bones for bone '{bone_name}'") - if simplified_name.startswith('jbip') and bone_name not in found_bones: + # Fallback for unrecognized VRM bones that start with 'jbip' + elif simplified_name.startswith('jbip') and bone_name not in found_bones: unity_equivalent = guess_unity_name_from_vrm(simplified_name) if unity_equivalent: found_bones[bone_name] = unity_equivalent @@ -537,7 +400,7 @@ def remove_vrm_root_bone(armature: Object) -> Tuple[bool, str]: for child in root_to_remove.children: if child != hips_bone: children_to_reparent.append(child) - ) + hips_bone.parent = None for child in children_to_reparent: From 1ddda1336a3870f631fe653dd0adb10758b85413 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Sat, 2 Aug 2025 14:57:43 +0100 Subject: [PATCH 4/5] Added more bone names --- core/dictionaries.py | 105 ++++++++++++++++++++++--------------------- 1 file changed, 55 insertions(+), 50 deletions(-) diff --git a/core/dictionaries.py b/core/dictionaries.py index e8227e2..f934fc9 100644 --- a/core/dictionaries.py +++ b/core/dictionaries.py @@ -259,80 +259,85 @@ bone_names = { # Add VRM bone name variations bone_names.update({ - 'hips': bone_names['hips'] + ['jbipchips', 'jhips', 'vrmhips'], + 'hips': bone_names['hips'] + ['jbipchips', 'jhips', 'vrmhips', 'leftupperleg', 'rightupperleg'], 'spine': bone_names['spine'] + ['jbipcspine', 'jspine', 'vrmspine'], - 'chest': bone_names['chest'] + ['jbipcchest', 'jchest', 'vrmchest'], - 'upper_chest': bone_names['upper_chest'] + ['jbipcupperchest', 'jupperchest', 'vrmupperchest'], + 'chest': bone_names['chest'] + ['jbipcchest', 'jchest', 'vrmchest', 'upperchest'], + 'upper_chest': bone_names['upper_chest'] + ['jbipcupperchest', 'jupperchest', 'vrmupperchest', 'upperchest'], 'neck': bone_names['neck'] + ['jbipcneck', 'jneck', 'vrmneck'], - 'head': bone_names['head'] + ['jbipchead', 'jhead', 'vrmhead'], + 'head': bone_names['head'] + ['jbipchead', 'jhead', 'vrmhead', 'lefteye', 'righteye'], # VRM arms - both simplified patterns - 'left_shoulder': bone_names['left_shoulder'] + ['jbipllshoulder', 'jlshoulder', 'jbiplshoulder'], - 'left_arm': bone_names['left_arm'] + ['jbiplupperarm', 'jlupperarm'], - 'left_elbow': bone_names['left_elbow'] + ['jbipllforearm', 'jlforearm', 'jbipllowerarm'], - 'left_wrist': bone_names['left_wrist'] + ['jbipllhand', 'jlhand', 'jbiplhand'], + 'left_shoulder': bone_names['left_shoulder'] + ['jbipllshoulder', 'jlshoulder', 'jbiplshoulder', 'leftshoulder'], + 'left_arm': bone_names['left_arm'] + ['jbiplupperarm', 'jlupperarm', 'leftupperarm'], + 'left_elbow': bone_names['left_elbow'] + ['jbipllforearm', 'jlforearm', 'jbipllowerarm', 'leftlowerarm'], + 'left_wrist': bone_names['left_wrist'] + ['jbipllhand', 'jlhand', 'jbiplhand', 'lefthand'], - 'right_shoulder': bone_names['right_shoulder'] + ['jbiprlshoulder', 'jrshoulder', 'jbiprshoulder'], - 'right_arm': bone_names['right_arm'] + ['jbiprrupperarm', 'jrupperarm', 'jbiprupperarm'], - 'right_elbow': bone_names['right_elbow'] + ['jbiprrforearm', 'jrforearm', 'jbiprforearm', 'jbiprlowerarm'], - 'right_wrist': bone_names['right_wrist'] + ['jbiprrhand', 'jrhand', 'jbiprhand'], + 'right_shoulder': bone_names['right_shoulder'] + ['jbiprlshoulder', 'jrshoulder', 'jbiprshoulder', 'rightshoulder'], + 'right_arm': bone_names['right_arm'] + ['jbiprrupperarm', 'jrupperarm', 'jbiprupperarm', 'rightupperarm'], + 'right_elbow': bone_names['right_elbow'] + ['jbiprrforearm', 'jrforearm', 'jbiprforearm', 'jbiprlowerarm', 'rightlowerarm'], + 'right_wrist': bone_names['right_wrist'] + ['jbiprrhand', 'jrhand', 'jbiprhand', 'righthand'], # VRM legs - both simplified patterns - 'left_leg': bone_names['left_leg'] + ['jbiplupperleg', 'jlupperleg'], - 'left_knee': bone_names['left_knee'] + ['jbipllowerleg', 'jllowerleg'], - 'left_ankle': bone_names['left_ankle'] + ['jbipllfoot', 'jlfoot', 'jbiplfoot'], - 'left_toe': bone_names['left_toe'] + ['jbiplltoe', 'jltoe', 'jbipltoebase'], + 'left_leg': bone_names['left_leg'] + ['jbiplupperleg', 'jlupperleg', 'leftupperleg'], + 'left_knee': bone_names['left_knee'] + ['jbipllowerleg', 'jllowerleg', 'leftlowerleg'], + 'left_ankle': bone_names['left_ankle'] + ['jbipllfoot', 'jlfoot', 'jbiplfoot', 'leftfoot'], + 'left_toe': bone_names['left_toe'] + ['jbiplltoe', 'jltoe', 'jbipltoebase', 'lefttoes'], - 'right_leg': bone_names['right_leg'] + ['jbiprrupperleg', 'jrupperleg', 'jbiprupperleg'], - 'right_knee': bone_names['right_knee'] + ['jbiprrlowerleg', 'jrlowerleg', 'jbiprlowerleg'], - 'right_ankle': bone_names['right_ankle'] + ['jbiprrfoot', 'jrfoot', 'jbiprfoot'], - 'right_toe': bone_names['right_toe'] + ['jbiprrtoe', 'jrtoe', 'jbiprtoebase'], + 'right_leg': bone_names['right_leg'] + ['jbiprrupperleg', 'jrupperleg', 'jbiprupperleg', 'rightupperleg'], + 'right_knee': bone_names['right_knee'] + ['jbiprrlowerleg', 'jrlowerleg', 'jbiprlowerleg', 'rightlowerleg'], + 'right_ankle': bone_names['right_ankle'] + ['jbiprrfoot', 'jrfoot', 'jbiprfoot', 'rightfoot'], + 'right_toe': bone_names['right_toe'] + ['jbiprrtoe', 'jrtoe', 'jbiprtoebase', 'righttoes'], # VRM eyes 'left_eye': bone_names['left_eye'] + ['jbipcleye', 'jleye', 'jadjlfaceeye'], 'right_eye': bone_names['right_eye'] + ['jbipcreye', 'jreye', 'jadjrfaceeye'], + # VRM jaw + 'jaw': ['jaw', 'mandible', 'lowerjaw', 'chin', 'あご', 'ik_あご'], + # VRM fingers - Left (including Little finger variations) - 'thumb_1_l': bone_names['thumb_1_l'] + ['jbipllthumb1', 'jlthumb1', 'jbiplthumb1'], - 'thumb_2_l': bone_names['thumb_2_l'] + ['jbipllthumb2', 'jlthumb2', 'jbiplthumb2'], - 'thumb_3_l': bone_names['thumb_3_l'] + ['jbipllthumb3', 'jlthumb3', 'jbiplthumb3'], + 'thumb_0_l': bone_names['thumb_0_l'] + ['jbipllthumb0', 'jlthumb0', 'jbipllthumbmetacarpal', 'jlthumbmetacarpal', 'leftthumbmetacarpal'], + 'thumb_1_l': bone_names['thumb_1_l'] + ['jbipllthumb1', 'jlthumb1', 'jbiplthumb1', 'leftthumbproximal'], + 'thumb_2_l': bone_names['thumb_2_l'] + ['jbipllthumb2', 'jlthumb2', 'jbiplthumb2', 'leftthumbintermediate'], + 'thumb_3_l': bone_names['thumb_3_l'] + ['jbipllthumb3', 'jlthumb3', 'jbiplthumb3', 'leftthumbdistal'], - 'index_1_l': bone_names['index_1_l'] + ['jbipllindex1', 'jlindex1', 'jbiplindex1'], - 'index_2_l': bone_names['index_2_l'] + ['jbipllindex2', 'jlindex2', 'jbiplindex2'], - 'index_3_l': bone_names['index_3_l'] + ['jbipllindex3', 'jlindex3', 'jbiplindex3'], + 'index_1_l': bone_names['index_1_l'] + ['jbipllindex1', 'jlindex1', 'jbiplindex1', 'leftindexproximal'], + 'index_2_l': bone_names['index_2_l'] + ['jbipllindex2', 'jlindex2', 'jbiplindex2', 'leftindexintermediate'], + 'index_3_l': bone_names['index_3_l'] + ['jbipllindex3', 'jlindex3', 'jbiplindex3', 'leftindexdistal'], - 'middle_1_l': bone_names['middle_1_l'] + ['jbipllmiddle1', 'jlmiddle1', 'jbiplmiddle1'], - 'middle_2_l': bone_names['middle_2_l'] + ['jbipllmiddle2', 'jlmiddle2', 'jbiplmiddle2'], - 'middle_3_l': bone_names['middle_3_l'] + ['jbipllmiddle3', 'jlmiddle3', 'jbiplmiddle3'], + 'middle_1_l': bone_names['middle_1_l'] + ['jbipllmiddle1', 'jlmiddle1', 'jbiplmiddle1', 'leftmiddleproximal'], + 'middle_2_l': bone_names['middle_2_l'] + ['jbipllmiddle2', 'jlmiddle2', 'jbiplmiddle2', 'leftmiddleintermediate'], + 'middle_3_l': bone_names['middle_3_l'] + ['jbipllmiddle3', 'jlmiddle3', 'jbiplmiddle3', 'leftmiddledistal'], - 'ring_1_l': bone_names['ring_1_l'] + ['jbipllring1', 'jlring1', 'jbiplring1'], - 'ring_2_l': bone_names['ring_2_l'] + ['jbipllring2', 'jlring2', 'jbiplring2'], - 'ring_3_l': bone_names['ring_3_l'] + ['jbipllring3', 'jlring3', 'jbiplring3'], + 'ring_1_l': bone_names['ring_1_l'] + ['jbipllring1', 'jlring1', 'jbiplring1', 'leftringproximal'], + 'ring_2_l': bone_names['ring_2_l'] + ['jbipllring2', 'jlring2', 'jbiplring2', 'leftringintermediate'], + 'ring_3_l': bone_names['ring_3_l'] + ['jbipllring3', 'jlring3', 'jbiplring3', 'leftringdistal'], - 'pinkie_1_l': bone_names['pinkie_1_l'] + ['jbipllpinky1', 'jlpinky1', 'jbipllittle1', 'jbipllpinkie1'], - 'pinkie_2_l': bone_names['pinkie_2_l'] + ['jbipllpinky2', 'jlpinky2', 'jbipllittle2', 'jbipllpinkie2'], - 'pinkie_3_l': bone_names['pinkie_3_l'] + ['jbipllpinky3', 'jlpinky3', 'jbipllittle3', 'jbipllpinkie3'], + 'pinkie_1_l': bone_names['pinkie_1_l'] + ['jbipllpinky1', 'jlpinky1', 'jbipllittle1', 'jbipllpinkie1', 'leftlittleproximal'], + 'pinkie_2_l': bone_names['pinkie_2_l'] + ['jbipllpinky2', 'jlpinky2', 'jbipllittle2', 'jbipllpinkie2', 'leftlittleintermediate'], + 'pinkie_3_l': bone_names['pinkie_3_l'] + ['jbipllpinky3', 'jlpinky3', 'jbipllittle3', 'jbipllpinkie3', 'leftlittledistal'], # VRM fingers - Right (including Little finger variations) - 'thumb_1_r': bone_names['thumb_1_r'] + ['jbiprthumb1', 'jrthumb1', 'jbiprrrthumb1'], - 'thumb_2_r': bone_names['thumb_2_r'] + ['jbiprthumb2', 'jrthumb2', 'jbiprrrthumb2'], - 'thumb_3_r': bone_names['thumb_3_r'] + ['jbiprthumb3', 'jrthumb3', 'jbiprrrthumb3'], + 'thumb_0_r': bone_names['thumb_0_r'] + ['jbiprthumb0', 'jrthumb0', 'jbiprthumbmetacarpal', 'jrthumbmetacarpal', 'rightthumbmetacarpal'], + 'thumb_1_r': bone_names['thumb_1_r'] + ['jbiprthumb1', 'jrthumb1', 'jbiprrrthumb1', 'rightthumbproximal'], + 'thumb_2_r': bone_names['thumb_2_r'] + ['jbiprthumb2', 'jrthumb2', 'jbiprrrthumb2', 'rightthumbintermediate'], + 'thumb_3_r': bone_names['thumb_3_r'] + ['jbiprthumb3', 'jrthumb3', 'jbiprrrthumb3', 'rightthumbdistal'], - 'index_1_r': bone_names['index_1_r'] + ['jbiprindex1', 'jrindex1', 'jbiprrrindex1'], - 'index_2_r': bone_names['index_2_r'] + ['jbiprindex2', 'jrindex2', 'jbiprrrindex2'], - 'index_3_r': bone_names['index_3_r'] + ['jbiprindex3', 'jrindex3', 'jbiprrrindex3'], + 'index_1_r': bone_names['index_1_r'] + ['jbiprindex1', 'jrindex1', 'jbiprrrindex1', 'rightindexproximal'], + 'index_2_r': bone_names['index_2_r'] + ['jbiprindex2', 'jrindex2', 'jbiprrrindex2', 'rightindexintermediate'], + 'index_3_r': bone_names['index_3_r'] + ['jbiprindex3', 'jrindex3', 'jbiprrrindex3', 'rightindexdistal'], - 'middle_1_r': bone_names['middle_1_r'] + ['jbiprmiddle1', 'jrmiddle1', 'jbiprrmiddle1'], - 'middle_2_r': bone_names['middle_2_r'] + ['jbiprmiddle2', 'jrmiddle2', 'jbiprrmiddle2'], - 'middle_3_r': bone_names['middle_3_r'] + ['jbiprmiddle3', 'jrmiddle3', 'jbiprrmiddle3'], + 'middle_1_r': bone_names['middle_1_r'] + ['jbiprmiddle1', 'jrmiddle1', 'jbiprrmiddle1', 'rightmiddleproximal'], + 'middle_2_r': bone_names['middle_2_r'] + ['jbiprmiddle2', 'jrmiddle2', 'jbiprrmiddle2', 'rightmiddleintermediate'], + 'middle_3_r': bone_names['middle_3_r'] + ['jbiprmiddle3', 'jrmiddle3', 'jbiprrmiddle3', 'rightmiddledistal'], - 'ring_1_r': bone_names['ring_1_r'] + ['jbiprring1', 'jrring1', 'jbiprrrring1'], - 'ring_2_r': bone_names['ring_2_r'] + ['jbiprring2', 'jrring2', 'jbiprrrring2'], - 'ring_3_r': bone_names['ring_3_r'] + ['jbiprring3', 'jrring3', 'jbiprrrring3'], + 'ring_1_r': bone_names['ring_1_r'] + ['jbiprring1', 'jrring1', 'jbiprrrring1', 'rightringproximal'], + 'ring_2_r': bone_names['ring_2_r'] + ['jbiprring2', 'jrring2', 'jbiprrrring2', 'rightringintermediate'], + 'ring_3_r': bone_names['ring_3_r'] + ['jbiprring3', 'jrring3', 'jbiprrrring3', 'rightringdistal'], - 'pinkie_1_r': bone_names['pinkie_1_r'] + ['jbiprpinky1', 'jrpinky1', 'jbiprlittle1', 'jbiprrrpinky1'], - 'pinkie_2_r': bone_names['pinkie_2_r'] + ['jbiprpinky2', 'jrpinky2', 'jbiprlittle2', 'jbiprrrpinky2'], - 'pinkie_3_r': bone_names['pinkie_3_r'] + ['jbiprpinky3', 'jrpinky3', 'jbiprlittle3', 'jbiprrrpinky3'] + 'pinkie_1_r': bone_names['pinkie_1_r'] + ['jbiprpinky1', 'jrpinky1', 'jbiprlittle1', 'jbiprrrpinky1', 'rightlittleproximal'], + 'pinkie_2_r': bone_names['pinkie_2_r'] + ['jbiprpinky2', 'jrpinky2', 'jbiprlittle2', 'jbiprrrpinky2', 'rightlittleintermediate'], + 'pinkie_3_r': bone_names['pinkie_3_r'] + ['jbiprpinky3', 'jrpinky3', 'jbiprlittle3', 'jbiprrrpinky3', 'rightlittledistal'] }) # array taken from cats From c50f275b1b63d7d82cb7fd4776f58ee7f65a03f6 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Sat, 2 Aug 2025 15:11:34 +0100 Subject: [PATCH 5/5] VRM Convert Breast bones --- core/dictionaries.py | 36 +++++++++++++++++++++++++++++++++++- core/vrm_unity_converter.py | 13 ++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/core/dictionaries.py b/core/dictionaries.py index f934fc9..58aeabd 100644 --- a/core/dictionaries.py +++ b/core/dictionaries.py @@ -255,6 +255,24 @@ bone_names = { "right_eye": [ "eyeright", "righteye", "eyer", "reye", "右目", "ik_右目" ], + "breast_1_l": [ + "j_sec_l_bust1", "breast1_l", "leftbreast1", "lbreast1", "bust1_l" + ], + "breast_2_l": [ + "j_sec_l_bust2", "breast2_l", "leftbreast2", "lbreast2", "bust2_l" + ], + "breast_3_l": [ + "j_sec_l_bust3", "breast3_l", "leftbreast3", "lbreast3", "bust3_l" + ], + "breast_1_r": [ + "j_sec_r_bust1", "breast1_r", "rightbreast1", "rbreast1", "bust1_r" + ], + "breast_2_r": [ + "j_sec_r_bust2", "breast2_r", "rightbreast2", "rbreast2", "bust2_r" + ], + "breast_3_r": [ + "j_sec_r_bust3", "breast3_r", "rightbreast3", "rbreast3", "bust3_r" + ] } # Add VRM bone name variations @@ -295,6 +313,14 @@ bone_names.update({ # VRM jaw 'jaw': ['jaw', 'mandible', 'lowerjaw', 'chin', 'あご', 'ik_あご'], + # Breast bones + 'breast_1_l': bone_names['breast_1_l'] + ['jbipcbreast1l', 'jlbreast1'], + 'breast_2_l': bone_names['breast_2_l'] + ['jbipcbreast2l', 'jlbreast2'], + 'breast_3_l': bone_names['breast_3_l'] + ['jbipcbreast3l', 'jlbreast3'], + 'breast_1_r': bone_names['breast_1_r'] + ['jbipcbreast1r', 'jrbreast1'], + 'breast_2_r': bone_names['breast_2_r'] + ['jbipcbreast2r', 'jrbreast2'], + 'breast_3_r': bone_names['breast_3_r'] + ['jbipcbreast3r', 'jrbreast3'], + # VRM fingers - Left (including Little finger variations) 'thumb_0_l': bone_names['thumb_0_l'] + ['jbipllthumb0', 'jlthumb0', 'jbipllthumbmetacarpal', 'jlthumbmetacarpal', 'leftthumbmetacarpal'], 'thumb_1_l': bone_names['thumb_1_l'] + ['jbipllthumb1', 'jlthumb1', 'jbiplthumb1', 'leftthumbproximal'], @@ -486,7 +512,15 @@ standard_bones = { # Eyes 'left_eye': 'Eye.L', - 'right_eye': 'Eye.R' + 'right_eye': 'Eye.R', + + # Breast bones + 'breast_1_l': 'Breast1_L', + 'breast_2_l': 'Breast2_L', + 'breast_3_l': 'Breast3_L', + 'breast_1_r': 'Breast1_R', + 'breast_2_r': 'Breast2_R', + 'breast_3_r': 'Breast3_R' } bone_hierarchy = [ diff --git a/core/vrm_unity_converter.py b/core/vrm_unity_converter.py index 21225cd..73e6458 100644 --- a/core/vrm_unity_converter.py +++ b/core/vrm_unity_converter.py @@ -33,6 +33,9 @@ def detect_vrm_armature(armature: Object) -> bool: 'jbiplring1', 'jbiplring2', 'jbiplring3', # Face eye patterns 'jadjlfaceeye', 'jadjrfaceeye', + # Breast patterns + 'jseclbust1', 'jseclbust2', 'jseclbust3', + 'jsecrbust1', 'jsecrbust2', 'jsecrbust3', 'jbipc', 'jbipr', 'jbipl' ] @@ -158,7 +161,15 @@ def guess_unity_name_from_vrm(vrm_simplified: str) -> Optional[str]: 'jbipllittle3': 'LeftPinky3', 'jbiprlittle1': 'RightPinky1', 'jbiprlittle2': 'RightPinky2', - 'jbiprlittle3': 'RightPinky3' + 'jbiprlittle3': 'RightPinky3', + + # Breast bones + 'jseclbust1': 'Breast1_L', + 'jseclbust2': 'Breast2_L', + 'jseclbust3': 'Breast3_L', + 'jsecrbust1': 'Breast1_R', + 'jsecrbust2': 'Breast2_R', + 'jsecrbust3': 'Breast3_R' } return pattern_mappings.get(vrm_simplified)