Merge branch 'alpha4-vrmconverter' into Alpha-4
This commit is contained in:
+109
-16
@@ -255,30 +255,115 @@ bone_names = {
|
|||||||
"right_eye": [
|
"right_eye": [
|
||||||
"eyeright", "righteye", "eyer", "reye", "右目", "ik_右目"
|
"eyeright", "righteye", "eyer", "reye", "右目", "ik_右目"
|
||||||
],
|
],
|
||||||
|
"breast_1_l": [
|
||||||
|
"j_sec_l_bust1", "breast1_l", "leftbreast1", "lbreast1", "bust1_l"
|
||||||
|
],
|
||||||
|
"breast_2_l": [
|
||||||
|
"j_sec_l_bust2", "breast2_l", "leftbreast2", "lbreast2", "bust2_l"
|
||||||
|
],
|
||||||
|
"breast_3_l": [
|
||||||
|
"j_sec_l_bust3", "breast3_l", "leftbreast3", "lbreast3", "bust3_l"
|
||||||
|
],
|
||||||
|
"breast_1_r": [
|
||||||
|
"j_sec_r_bust1", "breast1_r", "rightbreast1", "rbreast1", "bust1_r"
|
||||||
|
],
|
||||||
|
"breast_2_r": [
|
||||||
|
"j_sec_r_bust2", "breast2_r", "rightbreast2", "rbreast2", "bust2_r"
|
||||||
|
],
|
||||||
|
"breast_3_r": [
|
||||||
|
"j_sec_r_bust3", "breast3_r", "rightbreast3", "rbreast3", "bust3_r"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add VRM bone name variations
|
# Add VRM bone name variations
|
||||||
bone_names.update({
|
bone_names.update({
|
||||||
'hips': bone_names['hips'] + ['jbipchips', 'jhips', 'vrmhips'],
|
'hips': bone_names['hips'] + ['jbipchips', 'jhips', 'vrmhips', 'leftupperleg', 'rightupperleg'],
|
||||||
'spine': bone_names['spine'] + ['jbipcspine', 'jspine', 'vrmspine'],
|
'spine': bone_names['spine'] + ['jbipcspine', 'jspine', 'vrmspine'],
|
||||||
'chest': bone_names['chest'] + ['jbipcchest', 'jchest', 'vrmchest'],
|
'chest': bone_names['chest'] + ['jbipcchest', 'jchest', 'vrmchest', 'upperchest'],
|
||||||
'upper_chest': bone_names['upper_chest'] + ['jbipcupperchest', 'jupperchest', 'vrmupperchest'],
|
'upper_chest': bone_names['upper_chest'] + ['jbipcupperchest', 'jupperchest', 'vrmupperchest', 'upperchest'],
|
||||||
'neck': bone_names['neck'] + ['jbipcneck', 'jneck', 'vrmneck'],
|
'neck': bone_names['neck'] + ['jbipcneck', 'jneck', 'vrmneck'],
|
||||||
'head': bone_names['head'] + ['jbipchead', 'jhead', 'vrmhead'],
|
'head': bone_names['head'] + ['jbipchead', 'jhead', 'vrmhead', 'lefteye', 'righteye'],
|
||||||
|
|
||||||
# VRM specific finger naming
|
# VRM arms - both simplified patterns
|
||||||
'thumb_0_l': bone_names['thumb_0_l'] + ['thumbmetacarpall', 'jthumb1l'],
|
'left_shoulder': bone_names['left_shoulder'] + ['jbipllshoulder', 'jlshoulder', 'jbiplshoulder', 'leftshoulder'],
|
||||||
'index_0_l': bone_names['index_0_l'] + ['indexmetacarpall', 'jindex1l'],
|
'left_arm': bone_names['left_arm'] + ['jbiplupperarm', 'jlupperarm', 'leftupperarm'],
|
||||||
'middle_0_l': bone_names['middle_0_l'] + ['middlemetacarpall', 'jmiddle1l'],
|
'left_elbow': bone_names['left_elbow'] + ['jbipllforearm', 'jlforearm', 'jbipllowerarm', 'leftlowerarm'],
|
||||||
'ring_0_l': bone_names['ring_0_l'] + ['ringmetacarpall', 'jring1l'],
|
'left_wrist': bone_names['left_wrist'] + ['jbipllhand', 'jlhand', 'jbiplhand', 'lefthand'],
|
||||||
'pinkie_0_l': bone_names['pinkie_0_l'] + ['littlemetacarpall', 'jlittle1l'],
|
|
||||||
|
|
||||||
# Mirror for right side
|
'right_shoulder': bone_names['right_shoulder'] + ['jbiprlshoulder', 'jrshoulder', 'jbiprshoulder', 'rightshoulder'],
|
||||||
'thumb_0_r': bone_names['thumb_0_r'] + ['thumbmetacarpalr', 'jthumb1r'],
|
'right_arm': bone_names['right_arm'] + ['jbiprrupperarm', 'jrupperarm', 'jbiprupperarm', 'rightupperarm'],
|
||||||
'index_0_r': bone_names['index_0_r'] + ['indexmetacarpalr', 'jindex1r'],
|
'right_elbow': bone_names['right_elbow'] + ['jbiprrforearm', 'jrforearm', 'jbiprforearm', 'jbiprlowerarm', 'rightlowerarm'],
|
||||||
'middle_0_r': bone_names['middle_0_r'] + ['middlemetacarpalr', 'jmiddle1r'],
|
'right_wrist': bone_names['right_wrist'] + ['jbiprrhand', 'jrhand', 'jbiprhand', 'righthand'],
|
||||||
'ring_0_r': bone_names['ring_0_r'] + ['ringmetacarpalr', 'jring1r'],
|
|
||||||
'pinkie_0_r': bone_names['pinkie_0_r'] + ['littlemetacarpalr', 'jlittle1r']
|
# VRM legs - both simplified patterns
|
||||||
|
'left_leg': bone_names['left_leg'] + ['jbiplupperleg', 'jlupperleg', 'leftupperleg'],
|
||||||
|
'left_knee': bone_names['left_knee'] + ['jbipllowerleg', 'jllowerleg', 'leftlowerleg'],
|
||||||
|
'left_ankle': bone_names['left_ankle'] + ['jbipllfoot', 'jlfoot', 'jbiplfoot', 'leftfoot'],
|
||||||
|
'left_toe': bone_names['left_toe'] + ['jbiplltoe', 'jltoe', 'jbipltoebase', 'lefttoes'],
|
||||||
|
|
||||||
|
'right_leg': bone_names['right_leg'] + ['jbiprrupperleg', 'jrupperleg', 'jbiprupperleg', 'rightupperleg'],
|
||||||
|
'right_knee': bone_names['right_knee'] + ['jbiprrlowerleg', 'jrlowerleg', 'jbiprlowerleg', 'rightlowerleg'],
|
||||||
|
'right_ankle': bone_names['right_ankle'] + ['jbiprrfoot', 'jrfoot', 'jbiprfoot', 'rightfoot'],
|
||||||
|
'right_toe': bone_names['right_toe'] + ['jbiprrtoe', 'jrtoe', 'jbiprtoebase', 'righttoes'],
|
||||||
|
|
||||||
|
# VRM eyes
|
||||||
|
'left_eye': bone_names['left_eye'] + ['jbipcleye', 'jleye', 'jadjlfaceeye'],
|
||||||
|
'right_eye': bone_names['right_eye'] + ['jbipcreye', 'jreye', 'jadjrfaceeye'],
|
||||||
|
|
||||||
|
# VRM jaw
|
||||||
|
'jaw': ['jaw', 'mandible', 'lowerjaw', 'chin', 'あご', 'ik_あご'],
|
||||||
|
|
||||||
|
# Breast bones
|
||||||
|
'breast_1_l': bone_names['breast_1_l'] + ['jbipcbreast1l', 'jlbreast1'],
|
||||||
|
'breast_2_l': bone_names['breast_2_l'] + ['jbipcbreast2l', 'jlbreast2'],
|
||||||
|
'breast_3_l': bone_names['breast_3_l'] + ['jbipcbreast3l', 'jlbreast3'],
|
||||||
|
'breast_1_r': bone_names['breast_1_r'] + ['jbipcbreast1r', 'jrbreast1'],
|
||||||
|
'breast_2_r': bone_names['breast_2_r'] + ['jbipcbreast2r', 'jrbreast2'],
|
||||||
|
'breast_3_r': bone_names['breast_3_r'] + ['jbipcbreast3r', 'jrbreast3'],
|
||||||
|
|
||||||
|
# VRM fingers - Left (including Little finger variations)
|
||||||
|
'thumb_0_l': bone_names['thumb_0_l'] + ['jbipllthumb0', 'jlthumb0', 'jbipllthumbmetacarpal', 'jlthumbmetacarpal', 'leftthumbmetacarpal'],
|
||||||
|
'thumb_1_l': bone_names['thumb_1_l'] + ['jbipllthumb1', 'jlthumb1', 'jbiplthumb1', 'leftthumbproximal'],
|
||||||
|
'thumb_2_l': bone_names['thumb_2_l'] + ['jbipllthumb2', 'jlthumb2', 'jbiplthumb2', 'leftthumbintermediate'],
|
||||||
|
'thumb_3_l': bone_names['thumb_3_l'] + ['jbipllthumb3', 'jlthumb3', 'jbiplthumb3', 'leftthumbdistal'],
|
||||||
|
|
||||||
|
'index_1_l': bone_names['index_1_l'] + ['jbipllindex1', 'jlindex1', 'jbiplindex1', 'leftindexproximal'],
|
||||||
|
'index_2_l': bone_names['index_2_l'] + ['jbipllindex2', 'jlindex2', 'jbiplindex2', 'leftindexintermediate'],
|
||||||
|
'index_3_l': bone_names['index_3_l'] + ['jbipllindex3', 'jlindex3', 'jbiplindex3', 'leftindexdistal'],
|
||||||
|
|
||||||
|
'middle_1_l': bone_names['middle_1_l'] + ['jbipllmiddle1', 'jlmiddle1', 'jbiplmiddle1', 'leftmiddleproximal'],
|
||||||
|
'middle_2_l': bone_names['middle_2_l'] + ['jbipllmiddle2', 'jlmiddle2', 'jbiplmiddle2', 'leftmiddleintermediate'],
|
||||||
|
'middle_3_l': bone_names['middle_3_l'] + ['jbipllmiddle3', 'jlmiddle3', 'jbiplmiddle3', 'leftmiddledistal'],
|
||||||
|
|
||||||
|
'ring_1_l': bone_names['ring_1_l'] + ['jbipllring1', 'jlring1', 'jbiplring1', 'leftringproximal'],
|
||||||
|
'ring_2_l': bone_names['ring_2_l'] + ['jbipllring2', 'jlring2', 'jbiplring2', 'leftringintermediate'],
|
||||||
|
'ring_3_l': bone_names['ring_3_l'] + ['jbipllring3', 'jlring3', 'jbiplring3', 'leftringdistal'],
|
||||||
|
|
||||||
|
'pinkie_1_l': bone_names['pinkie_1_l'] + ['jbipllpinky1', 'jlpinky1', 'jbipllittle1', 'jbipllpinkie1', 'leftlittleproximal'],
|
||||||
|
'pinkie_2_l': bone_names['pinkie_2_l'] + ['jbipllpinky2', 'jlpinky2', 'jbipllittle2', 'jbipllpinkie2', 'leftlittleintermediate'],
|
||||||
|
'pinkie_3_l': bone_names['pinkie_3_l'] + ['jbipllpinky3', 'jlpinky3', 'jbipllittle3', 'jbipllpinkie3', 'leftlittledistal'],
|
||||||
|
|
||||||
|
# VRM fingers - Right (including Little finger variations)
|
||||||
|
'thumb_0_r': bone_names['thumb_0_r'] + ['jbiprthumb0', 'jrthumb0', 'jbiprthumbmetacarpal', 'jrthumbmetacarpal', 'rightthumbmetacarpal'],
|
||||||
|
'thumb_1_r': bone_names['thumb_1_r'] + ['jbiprthumb1', 'jrthumb1', 'jbiprrrthumb1', 'rightthumbproximal'],
|
||||||
|
'thumb_2_r': bone_names['thumb_2_r'] + ['jbiprthumb2', 'jrthumb2', 'jbiprrrthumb2', 'rightthumbintermediate'],
|
||||||
|
'thumb_3_r': bone_names['thumb_3_r'] + ['jbiprthumb3', 'jrthumb3', 'jbiprrrthumb3', 'rightthumbdistal'],
|
||||||
|
|
||||||
|
'index_1_r': bone_names['index_1_r'] + ['jbiprindex1', 'jrindex1', 'jbiprrrindex1', 'rightindexproximal'],
|
||||||
|
'index_2_r': bone_names['index_2_r'] + ['jbiprindex2', 'jrindex2', 'jbiprrrindex2', 'rightindexintermediate'],
|
||||||
|
'index_3_r': bone_names['index_3_r'] + ['jbiprindex3', 'jrindex3', 'jbiprrrindex3', 'rightindexdistal'],
|
||||||
|
|
||||||
|
'middle_1_r': bone_names['middle_1_r'] + ['jbiprmiddle1', 'jrmiddle1', 'jbiprrmiddle1', 'rightmiddleproximal'],
|
||||||
|
'middle_2_r': bone_names['middle_2_r'] + ['jbiprmiddle2', 'jrmiddle2', 'jbiprrmiddle2', 'rightmiddleintermediate'],
|
||||||
|
'middle_3_r': bone_names['middle_3_r'] + ['jbiprmiddle3', 'jrmiddle3', 'jbiprrmiddle3', 'rightmiddledistal'],
|
||||||
|
|
||||||
|
'ring_1_r': bone_names['ring_1_r'] + ['jbiprring1', 'jrring1', 'jbiprrrring1', 'rightringproximal'],
|
||||||
|
'ring_2_r': bone_names['ring_2_r'] + ['jbiprring2', 'jrring2', 'jbiprrrring2', 'rightringintermediate'],
|
||||||
|
'ring_3_r': bone_names['ring_3_r'] + ['jbiprring3', 'jrring3', 'jbiprrrring3', 'rightringdistal'],
|
||||||
|
|
||||||
|
'pinkie_1_r': bone_names['pinkie_1_r'] + ['jbiprpinky1', 'jrpinky1', 'jbiprlittle1', 'jbiprrrpinky1', 'rightlittleproximal'],
|
||||||
|
'pinkie_2_r': bone_names['pinkie_2_r'] + ['jbiprpinky2', 'jrpinky2', 'jbiprlittle2', 'jbiprrrpinky2', 'rightlittleintermediate'],
|
||||||
|
'pinkie_3_r': bone_names['pinkie_3_r'] + ['jbiprpinky3', 'jrpinky3', 'jbiprlittle3', 'jbiprrrpinky3', 'rightlittledistal']
|
||||||
})
|
})
|
||||||
|
|
||||||
# array taken from cats
|
# array taken from cats
|
||||||
@@ -428,6 +513,14 @@ standard_bones = {
|
|||||||
# Eyes
|
# Eyes
|
||||||
'left_eye': 'Eye_L',
|
'left_eye': 'Eye_L',
|
||||||
'right_eye': 'Eye_R'
|
'right_eye': 'Eye_R'
|
||||||
|
|
||||||
|
# Breast bones
|
||||||
|
'breast_1_l': 'Breast1_L',
|
||||||
|
'breast_2_l': 'Breast2_L',
|
||||||
|
'breast_3_l': 'Breast3_L',
|
||||||
|
'breast_1_r': 'Breast1_R',
|
||||||
|
'breast_2_r': 'Breast2_R',
|
||||||
|
'breast_3_r': 'Breast3_R'
|
||||||
}
|
}
|
||||||
|
|
||||||
bone_hierarchy = [
|
bone_hierarchy = [
|
||||||
|
|||||||
@@ -608,6 +608,19 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
update=update_log_level
|
update=update_log_level
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# VRM Conversion Properties
|
||||||
|
vrm_remove_colliders: BoolProperty(
|
||||||
|
name="Remove Colliders",
|
||||||
|
description="Remove VRM collider bones during conversion",
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
vrm_remove_root: BoolProperty(
|
||||||
|
name="Remove Root Bone",
|
||||||
|
description="Remove unnecessary VRM root bone and make Hips the root bone",
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
def register() -> None:
|
def register() -> None:
|
||||||
"""Register the Avatar Toolkit property group"""
|
"""Register the Avatar Toolkit property group"""
|
||||||
logger.info("Registering Avatar Toolkit properties")
|
logger.info("Registering Avatar Toolkit properties")
|
||||||
|
|||||||
@@ -0,0 +1,564 @@
|
|||||||
|
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, reverse_bone_lookup
|
||||||
|
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',
|
||||||
|
# Right arm patterns (both single and double R)
|
||||||
|
'jbiprlshoulder', 'jbiprshoulder', 'jbiprupperarm', 'jbiprforearm', 'jbiprhand', 'jbiprlowerarm',
|
||||||
|
'jbiprrupperarm', 'jbiprrforearm', 'jbiprrhand',
|
||||||
|
# Left arm patterns
|
||||||
|
'jbipllshoulder', 'jbiplshoulder', 'jbiplupperarm', 'jbipllforearm', 'jbipllhand', 'jbipllowerarm', 'jbiplhand',
|
||||||
|
# Right leg patterns (both single and double R)
|
||||||
|
'jbiprupperleg', 'jbiprlowerleg', 'jbiprfoot', 'jbiprtoe', 'jbiprtoebase',
|
||||||
|
'jbiprrupperleg', 'jbiprrlowerleg', 'jbiprrfoot', 'jbiprrtoe',
|
||||||
|
# Left leg patterns
|
||||||
|
'jbiplupperleg', 'jbipllowerleg', 'jbipllfoot', 'jbiplfoot', 'jbiplltoe', 'jbipltoebase',
|
||||||
|
# Finger patterns
|
||||||
|
'jbipllittle1', 'jbiprlittle1',
|
||||||
|
'jbiplthumb1', 'jbiplthumb2', 'jbiplthumb3',
|
||||||
|
'jbiplindex1', 'jbiplindex2', 'jbiplindex3',
|
||||||
|
'jbiplmiddle1', 'jbiplmiddle2', 'jbiplmiddle3',
|
||||||
|
'jbiplring1', 'jbiplring2', 'jbiplring3',
|
||||||
|
# Face eye patterns
|
||||||
|
'jadjlfaceeye', 'jadjrfaceeye',
|
||||||
|
# Breast patterns
|
||||||
|
'jseclbust1', 'jseclbust2', 'jseclbust3',
|
||||||
|
'jsecrbust1', 'jsecrbust2', 'jsecrbust3',
|
||||||
|
'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 find_vrm_bones_in_armature(armature: Object) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Find VRM bones in armature and return mapping to their actual names using dictionary lookup
|
||||||
|
"""
|
||||||
|
found_bones = {}
|
||||||
|
|
||||||
|
for bone_name in armature.data.bones.keys():
|
||||||
|
simplified_name = simplify_bonename(bone_name)
|
||||||
|
|
||||||
|
# Check if this bone exists in our reverse lookup dictionary
|
||||||
|
if simplified_name in reverse_bone_lookup:
|
||||||
|
standard_bone_key = reverse_bone_lookup[simplified_name]
|
||||||
|
|
||||||
|
# Get the Unity name from standard_bones
|
||||||
|
if standard_bone_key in standard_bones:
|
||||||
|
unity_name = standard_bones[standard_bone_key]
|
||||||
|
found_bones[bone_name] = unity_name
|
||||||
|
logger.debug(f"Found VRM bone via dictionary: {bone_name} -> {unity_name}")
|
||||||
|
else:
|
||||||
|
logger.debug(f"Standard bone key '{standard_bone_key}' not found in standard_bones for bone '{bone_name}'")
|
||||||
|
|
||||||
|
# Fallback for unrecognized VRM bones that start with 'jbip'
|
||||||
|
elif simplified_name.startswith('jbip') and bone_name not in found_bones:
|
||||||
|
unity_equivalent = guess_unity_name_from_vrm(simplified_name)
|
||||||
|
if unity_equivalent:
|
||||||
|
found_bones[bone_name] = unity_equivalent
|
||||||
|
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',
|
||||||
|
'jbiplshoulder': 'LeftShoulder',
|
||||||
|
'jbiplupperarm': 'LeftUpperArm',
|
||||||
|
'jbipllforearm': 'LeftLowerArm',
|
||||||
|
'jbipllowerarm': 'LeftLowerArm',
|
||||||
|
'jbipllhand': 'LeftHand',
|
||||||
|
'jbiplhand': 'LeftHand',
|
||||||
|
|
||||||
|
# Right arm (both single and double R patterns)
|
||||||
|
'jbiprrclavicle': 'RightShoulder',
|
||||||
|
'jbiprlshoulder': 'RightShoulder',
|
||||||
|
'jbiprshoulder': 'RightShoulder',
|
||||||
|
'jbiprupperarm': 'RightUpperArm',
|
||||||
|
'jbiprrupperarm': 'RightUpperArm',
|
||||||
|
'jbiprforearm': 'RightLowerArm',
|
||||||
|
'jbiprrforearm': 'RightLowerArm',
|
||||||
|
'jbiprlowerarm': 'RightLowerArm',
|
||||||
|
'jbiprhand': 'RightHand',
|
||||||
|
'jbiprrhand': 'RightHand',
|
||||||
|
|
||||||
|
# Left leg
|
||||||
|
'jbiplupperleg': 'LeftUpperLeg',
|
||||||
|
'jbipllowerleg': 'LeftLowerLeg',
|
||||||
|
'jbipllfoot': 'LeftFoot',
|
||||||
|
'jbiplfoot': 'LeftFoot',
|
||||||
|
'jbiplltoe': 'LeftToes',
|
||||||
|
'jbipltoebase': 'LeftToes',
|
||||||
|
|
||||||
|
# Right leg (both single and double R patterns)
|
||||||
|
'jbiprupperleg': 'RightUpperLeg',
|
||||||
|
'jbiprrupperleg': 'RightUpperLeg',
|
||||||
|
'jbiprlowerleg': 'RightLowerLeg',
|
||||||
|
'jbiprrlowerleg': 'RightLowerLeg',
|
||||||
|
'jbiprfoot': 'RightFoot',
|
||||||
|
'jbiprrfoot': 'RightFoot',
|
||||||
|
'jbiprtoe': 'RightToes',
|
||||||
|
'jbiprrtoe': 'RightToes',
|
||||||
|
'jbiprtoebase': 'RightToes',
|
||||||
|
|
||||||
|
# Eyes
|
||||||
|
'jbipcleye': 'LeftEye',
|
||||||
|
'jbipcreye': 'RightEye',
|
||||||
|
'jadjlfaceeye': 'LeftEye',
|
||||||
|
'jadjrfaceeye': 'RightEye',
|
||||||
|
|
||||||
|
# Fingers - Left
|
||||||
|
'jbiplthumb1': 'LeftThumb1',
|
||||||
|
'jbiplthumb2': 'LeftThumb2',
|
||||||
|
'jbiplthumb3': 'LeftThumb3',
|
||||||
|
'jbiplindex1': 'LeftIndex1',
|
||||||
|
'jbiplindex2': 'LeftIndex2',
|
||||||
|
'jbiplindex3': 'LeftIndex3',
|
||||||
|
'jbiplmiddle1': 'LeftMiddle1',
|
||||||
|
'jbiplmiddle2': 'LeftMiddle2',
|
||||||
|
'jbiplmiddle3': 'LeftMiddle3',
|
||||||
|
'jbiplring1': 'LeftRing1',
|
||||||
|
'jbiplring2': 'LeftRing2',
|
||||||
|
'jbiplring3': 'LeftRing3',
|
||||||
|
'jbipllittle1': 'LeftPinky1',
|
||||||
|
'jbipllittle2': 'LeftPinky2',
|
||||||
|
'jbipllittle3': 'LeftPinky3',
|
||||||
|
'jbiprlittle1': 'RightPinky1',
|
||||||
|
'jbiprlittle2': 'RightPinky2',
|
||||||
|
'jbiprlittle3': 'RightPinky3',
|
||||||
|
|
||||||
|
# Breast bones
|
||||||
|
'jseclbust1': 'Breast1_L',
|
||||||
|
'jseclbust2': 'Breast2_L',
|
||||||
|
'jseclbust3': 'Breast3_L',
|
||||||
|
'jsecrbust1': 'Breast1_R',
|
||||||
|
'jsecrbust2': 'Breast2_R',
|
||||||
|
'jsecrbust3': 'Breast3_R'
|
||||||
|
}
|
||||||
|
|
||||||
|
return pattern_mappings.get(vrm_simplified)
|
||||||
|
|
||||||
|
|
||||||
|
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_collection_from_hierarchy(collection_to_remove) -> bool:
|
||||||
|
"""
|
||||||
|
Recursively remove a collection from all parent collections in the hierarchy
|
||||||
|
"""
|
||||||
|
removed_from_any_parent = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check scene collection
|
||||||
|
scene_collection = bpy.context.scene.collection
|
||||||
|
if collection_to_remove in scene_collection.children:
|
||||||
|
scene_collection.children.unlink(collection_to_remove)
|
||||||
|
logger.debug(f" Unlinked '{collection_to_remove.name}' from scene collection")
|
||||||
|
removed_from_any_parent = True
|
||||||
|
|
||||||
|
# Check all other collections recursively
|
||||||
|
for parent_collection in list(bpy.data.collections):
|
||||||
|
if parent_collection != collection_to_remove and collection_to_remove in parent_collection.children:
|
||||||
|
try:
|
||||||
|
parent_collection.children.unlink(collection_to_remove)
|
||||||
|
logger.debug(f" Unlinked '{collection_to_remove.name}' from parent '{parent_collection.name}'")
|
||||||
|
removed_from_any_parent = True
|
||||||
|
except Exception as unlink_error:
|
||||||
|
logger.warning(f" Failed to unlink '{collection_to_remove.name}' from '{parent_collection.name}': {str(unlink_error)}")
|
||||||
|
|
||||||
|
return removed_from_any_parent
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error removing collection '{collection_to_remove.name}' from hierarchy: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def remove_vrm_colliders(armature: Object = None) -> Tuple[int, List[str], int]:
|
||||||
|
"""
|
||||||
|
Simple approach: Remove ALL objects with 'collider' in their name and clean up empty collections
|
||||||
|
"""
|
||||||
|
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 (prioritize collider-related collections)
|
||||||
|
empty_collections_removed = 0
|
||||||
|
|
||||||
|
# Also check all collections in the scene for collider-related names
|
||||||
|
all_collections_to_check = set(collections_to_check)
|
||||||
|
for collection in bpy.data.collections:
|
||||||
|
collection_name_lower = collection.name.lower()
|
||||||
|
if any(pattern in collection_name_lower for pattern in ['collider', 'collision', 'physics', 'dynamic']):
|
||||||
|
all_collections_to_check.add(collection)
|
||||||
|
logger.debug(f"Found collider-related collection to check: {collection.name}")
|
||||||
|
|
||||||
|
for collection in list(all_collections_to_check):
|
||||||
|
try:
|
||||||
|
# Check if collection exists and is empty
|
||||||
|
if collection.name not in bpy.data.collections:
|
||||||
|
logger.debug(f"Collection {collection.name} already removed")
|
||||||
|
continue
|
||||||
|
|
||||||
|
collection_name_lower = collection.name.lower()
|
||||||
|
is_collider_collection = any(pattern in collection_name_lower for pattern in ['collider', 'collision', 'physics', 'dynamic'])
|
||||||
|
is_empty = len(collection.objects) == 0 and len(collection.children) == 0
|
||||||
|
is_protected = collection.name in ["Collection", "Master Collection"]
|
||||||
|
|
||||||
|
# Remove if empty and (was used by colliders OR has collider-related name)
|
||||||
|
if is_empty and not is_protected and (collection in collections_to_check or is_collider_collection):
|
||||||
|
logger.info(f"Removing empty {'collider-related ' if is_collider_collection else ''}collection: {collection.name}")
|
||||||
|
|
||||||
|
# Use helper function to remove from all parent collections
|
||||||
|
removed_from_parents = remove_collection_from_hierarchy(collection)
|
||||||
|
|
||||||
|
if not removed_from_parents:
|
||||||
|
logger.debug(f" Collection {collection.name} was not found in any parent collections")
|
||||||
|
|
||||||
|
# Remove the collection data
|
||||||
|
try:
|
||||||
|
bpy.data.collections.remove(collection)
|
||||||
|
empty_collections_removed += 1
|
||||||
|
logger.info(f" Successfully removed collection: {collection.name}")
|
||||||
|
except Exception as remove_error:
|
||||||
|
logger.warning(f" Failed to remove collection {collection.name}: {str(remove_error)}")
|
||||||
|
# Continue with other collections even if this one fails
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to remove empty collection {collection.name}: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
logger.debug(f"Collection removal traceback: {traceback.format_exc()}")
|
||||||
|
|
||||||
|
if empty_collections_removed > 0:
|
||||||
|
logger.info(f"Cleaned up {empty_collections_removed} empty collections")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error during collider removal: {str(e)}")
|
||||||
|
return 0, [], 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 and {empty_collections_removed} collections")
|
||||||
|
return len(removed_names), removed_names, empty_collections_removed
|
||||||
|
|
||||||
|
|
||||||
|
def remove_vrm_root_bone(armature: Object) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Remove unnecessary VRM root bone and make Hips the root bone
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not armature or armature.type != 'ARMATURE':
|
||||||
|
return False, "No valid armature provided"
|
||||||
|
|
||||||
|
# Look for potential root bones and Hips bone
|
||||||
|
potential_roots = []
|
||||||
|
hips_bone = None
|
||||||
|
|
||||||
|
for bone in armature.data.edit_bones:
|
||||||
|
bone_name_lower = bone.name.lower()
|
||||||
|
|
||||||
|
# Check if this could be Hips (various naming conventions)
|
||||||
|
if any(hips_name in bone_name_lower for hips_name in ['hips', 'hip', 'pelvis', 'jbipchips']):
|
||||||
|
hips_bone = bone
|
||||||
|
logger.debug(f"Found Hips bone: {bone.name}")
|
||||||
|
|
||||||
|
# Check if this could be a root bone
|
||||||
|
if bone.parent is None and len(bone.children) > 0:
|
||||||
|
# Common VRM root bone names
|
||||||
|
if any(root_name in bone_name_lower for root_name in ['root', 'vrm', 'armature', 'rig']):
|
||||||
|
potential_roots.append(bone)
|
||||||
|
logger.debug(f"Found potential root bone: {bone.name}")
|
||||||
|
|
||||||
|
if not hips_bone:
|
||||||
|
return False, "Could not find Hips bone to promote as root"
|
||||||
|
|
||||||
|
if not potential_roots:
|
||||||
|
logger.info("No unnecessary root bone found - Hips may already be root")
|
||||||
|
return True, "No root bone removal needed"
|
||||||
|
|
||||||
|
# Find the root bone that is the parent of Hips
|
||||||
|
root_to_remove = None
|
||||||
|
for root_bone in potential_roots:
|
||||||
|
if hips_bone.parent == root_bone:
|
||||||
|
root_to_remove = root_bone
|
||||||
|
break
|
||||||
|
|
||||||
|
if not root_to_remove:
|
||||||
|
# Check if Hips is already parentless (already root)
|
||||||
|
if hips_bone.parent is None:
|
||||||
|
logger.info("Hips bone is already the root bone")
|
||||||
|
return True, "Hips is already root - no changes needed"
|
||||||
|
else:
|
||||||
|
logger.warning(f"Hips bone has parent '{hips_bone.parent.name}' but no matching root found")
|
||||||
|
return False, "Could not identify safe root bone to remove"
|
||||||
|
|
||||||
|
root_name = root_to_remove.name
|
||||||
|
logger.info(f"Removing root bone '{root_name}' and promoting Hips to root")
|
||||||
|
|
||||||
|
# Reparent all children of the root bone (except Hips) to Hips
|
||||||
|
children_to_reparent = []
|
||||||
|
for child in root_to_remove.children:
|
||||||
|
if child != hips_bone:
|
||||||
|
children_to_reparent.append(child)
|
||||||
|
|
||||||
|
hips_bone.parent = None
|
||||||
|
|
||||||
|
for child in children_to_reparent:
|
||||||
|
child.parent = hips_bone
|
||||||
|
logger.debug(f"Reparented {child.name} from {root_name} to {hips_bone.name}")
|
||||||
|
|
||||||
|
armature.data.edit_bones.remove(root_to_remove)
|
||||||
|
|
||||||
|
message = f"Removed root bone '{root_name}' - Hips is now the root bone"
|
||||||
|
logger.info(message)
|
||||||
|
return True, message
|
||||||
|
|
||||||
|
|
||||||
|
def convert_vrm_to_unity(armature: Object, remove_colliders: bool = True, remove_root: bool = True) -> Tuple[bool, List[str], int]:
|
||||||
|
"""
|
||||||
|
Convert VRM armature bone names to Unity humanoid format
|
||||||
|
"""
|
||||||
|
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, collections_removed = remove_vrm_colliders(armature)
|
||||||
|
if collider_count > 0 or collections_removed > 0:
|
||||||
|
if collections_removed > 0:
|
||||||
|
messages.append(f"Removed {collider_count} VRM collider objects and {collections_removed} empty collections")
|
||||||
|
else:
|
||||||
|
messages.append(f"Removed {collider_count} VRM collider objects")
|
||||||
|
logger.info(f"Removed {collider_count} VRM colliders: {removed_colliders}")
|
||||||
|
|
||||||
|
vrm_bones = find_vrm_bones_in_armature(armature)
|
||||||
|
|
||||||
|
if not vrm_bones:
|
||||||
|
if remove_colliders and (collider_count > 0 or collections_removed > 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')
|
||||||
|
|
||||||
|
# Remove unnecessary root bone if requested
|
||||||
|
if remove_root:
|
||||||
|
root_success, root_message = remove_vrm_root_bone(armature)
|
||||||
|
messages.append(root_message)
|
||||||
|
if not root_success:
|
||||||
|
logger.warning(f"Root bone removal failed: {root_message}")
|
||||||
|
|
||||||
|
# Rename bones
|
||||||
|
for vrm_bone_name, unity_name in vrm_bones.items():
|
||||||
|
if vrm_bone_name in armature.data.edit_bones:
|
||||||
|
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,88 @@
|
|||||||
|
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 conversion settings
|
||||||
|
remove_colliders = context.scene.avatar_toolkit.vrm_remove_colliders
|
||||||
|
remove_root = context.scene.avatar_toolkit.vrm_remove_root
|
||||||
|
logger.info(f"Collider removal setting: {remove_colliders}")
|
||||||
|
logger.info(f"Root bone removal setting: {remove_root}")
|
||||||
|
|
||||||
|
# Log all objects with 'collider' in name for debugging
|
||||||
|
collider_objects = [obj.name for obj in bpy.data.objects if 'collider' in obj.name.lower()]
|
||||||
|
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, remove_root)
|
||||||
|
|
||||||
|
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,87 @@
|
|||||||
|
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.prop(toolkit, 'vrm_remove_root', text="Remove Root Bone")
|
||||||
|
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="• Removes root bone, makes Hips root (optional)")
|
||||||
|
info_col.label(text="• Maintains bone hierarchy")
|
||||||
|
info_col.label(text="• Validates conversion results")
|
||||||
|
info_col.label(text="• Preserves all animations")
|
||||||
|
|
||||||
|
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