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