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.
This commit is contained in:
Yusarina
2025-08-01 14:40:49 +01:00
parent e3052d867d
commit 29f728442a
6 changed files with 801 additions and 13 deletions
+66 -12
View File
@@ -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
+7
View File
@@ -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")
+454
View File
@@ -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
+102
View File
@@ -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'}
+86
View File
@@ -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'}
+85
View File
@@ -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")