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
+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