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:
+66
-12
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
@@ -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'}
|
||||
@@ -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'}
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user