Merge branch 'Alpha-4' into Alpha-3
This commit is contained in:
@@ -8,6 +8,7 @@ from ...core.translations import t
|
||||
import traceback
|
||||
from ...core.common import (
|
||||
get_all_meshes,
|
||||
get_meshes_for_armature,
|
||||
fix_zero_length_bones,
|
||||
remove_unused_vertex_groups,
|
||||
clear_unused_data_blocks,
|
||||
@@ -28,10 +29,32 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return len(get_all_meshes(context)) > 1
|
||||
# Check if we have valid armature selections for merging
|
||||
base_armature_name: str = context.scene.avatar_toolkit.merge_armature_into
|
||||
merge_armature_name: str = context.scene.avatar_toolkit.merge_armature
|
||||
|
||||
if not base_armature_name or not merge_armature_name:
|
||||
return False
|
||||
|
||||
base_armature: Optional[Object] = bpy.data.objects.get(base_armature_name)
|
||||
merge_armature: Optional[Object] = bpy.data.objects.get(merge_armature_name)
|
||||
|
||||
return (base_armature is not None and
|
||||
merge_armature is not None and
|
||||
base_armature.type == 'ARMATURE' and
|
||||
merge_armature.type == 'ARMATURE' and
|
||||
base_armature != merge_armature)
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
try:
|
||||
# Store original mode to restore later
|
||||
original_mode: str = context.mode
|
||||
logger.debug(f"Original mode: {original_mode}")
|
||||
|
||||
# Switch to object mode if not already
|
||||
if context.mode != 'OBJECT':
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
wm = context.window_manager
|
||||
wm.progress_begin(0, 100)
|
||||
|
||||
@@ -48,6 +71,9 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
|
||||
#Store current armature settings that can mess us up.
|
||||
data_breaking_base = store_breaking_settings_armature(base_armature)
|
||||
data_breaking_merge = store_breaking_settings_armature(merge_armature)
|
||||
|
||||
# Store the merge armature name before it gets removed during join
|
||||
merge_armature_name_stored = merge_armature.name
|
||||
|
||||
# Remove Rigid Bodies and Joints
|
||||
delete_rigidbodies_and_joints(base_armature)
|
||||
@@ -77,14 +103,40 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
|
||||
wm.progress_end()
|
||||
|
||||
restore_breaking_settings_armature(base_armature, data_breaking_base)
|
||||
if merge_armature_name_stored in bpy.data.objects:
|
||||
merge_armature_obj = bpy.data.objects[merge_armature_name_stored]
|
||||
restore_breaking_settings_armature(merge_armature_obj, data_breaking_merge)
|
||||
|
||||
# Restore original mode if it wasn't OBJECT
|
||||
try:
|
||||
if original_mode == 'EDIT_ARMATURE':
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
elif original_mode == 'POSE':
|
||||
bpy.ops.object.mode_set(mode='POSE')
|
||||
elif original_mode != 'OBJECT':
|
||||
logger.debug(f"Restoring to original mode: {original_mode}")
|
||||
# For other modes, stay in object mode as it's safest
|
||||
except Exception:
|
||||
logger.warning(f"Could not restore original mode: {original_mode}")
|
||||
|
||||
self.report({'INFO'}, t('MergeArmature.success'))
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error merging armatures:", exception=e)
|
||||
logger.error(f"Error merging armatures: {str(e)}\n{traceback.format_exc()}")
|
||||
self.report({'ERROR'}, traceback.format_exc())
|
||||
|
||||
# Try to restore original mode even on error
|
||||
try:
|
||||
if 'original_mode' in locals() and original_mode != 'OBJECT':
|
||||
if original_mode == 'EDIT_ARMATURE':
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
elif original_mode == 'POSE':
|
||||
bpy.ops.object.mode_set(mode='POSE')
|
||||
except Exception:
|
||||
logger.warning("Could not restore mode after error")
|
||||
|
||||
return {'CANCELLED'}
|
||||
|
||||
def delete_rigidbodies_and_joints(armature: Object) -> None:
|
||||
|
||||
@@ -186,7 +186,6 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
|
||||
if not armature:
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Store initial transforms
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
initial_transforms: Dict[str, Dict[str, Any]] = {}
|
||||
data_breaking = store_breaking_settings_armature(armature)
|
||||
@@ -200,56 +199,61 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
|
||||
'parent': bone.parent.name if bone.parent else None
|
||||
}
|
||||
|
||||
# Get weighted bones
|
||||
# Get bones with any weight
|
||||
weighted_bones: List[str] = []
|
||||
meshes = get_all_meshes(context)
|
||||
zero_weight_bones: List[str] = []
|
||||
|
||||
for mesh in meshes:
|
||||
mesh_data: Mesh = mesh.data
|
||||
for vertex in mesh_data.vertices:
|
||||
for vertex in mesh.data.vertices:
|
||||
for group in vertex.groups:
|
||||
if group.weight > context.scene.avatar_toolkit.merge_weights_threshold:
|
||||
weighted_bones.append(mesh.vertex_groups[group.group].name)
|
||||
vg = mesh.vertex_groups[group.group]
|
||||
if vg.name not in weighted_bones:
|
||||
weighted_bones.append(vg.name)
|
||||
|
||||
# Process bone removal
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
armature_data: Armature = armature.data
|
||||
armature_data = armature.data
|
||||
removed_count = 0
|
||||
zero_weight_bones: List[str] = []
|
||||
|
||||
for bone in armature_data.edit_bones[:]: # Create a copy of the list
|
||||
if (bone.name not in weighted_bones and
|
||||
not self.should_preserve_bone(bone.name, context)):
|
||||
|
||||
if context.scene.avatar_toolkit.list_only_mode:
|
||||
zero_weight_bones.append(bone.name)
|
||||
continue
|
||||
def is_zero_weight_chain(bone, weighted_bones, preserve_check_fn):
|
||||
if bone.name in weighted_bones or preserve_check_fn(bone.name, context):
|
||||
return False
|
||||
return all(is_zero_weight_chain(child, weighted_bones, preserve_check_fn) for child in bone.children)
|
||||
|
||||
# Store children data
|
||||
children = bone.children
|
||||
children_data = {child.name: initial_transforms[child.name] for child in children}
|
||||
for bone in armature_data.edit_bones[:]:
|
||||
if bone.name in weighted_bones or self.should_preserve_bone(bone.name, context):
|
||||
continue
|
||||
|
||||
# Reparent children
|
||||
for child in children:
|
||||
if not is_zero_weight_chain(bone, weighted_bones, self.should_preserve_bone):
|
||||
continue
|
||||
|
||||
if context.scene.avatar_toolkit.list_only_mode:
|
||||
zero_weight_bones.append(bone.name)
|
||||
continue
|
||||
|
||||
# Traverse and collect the full empty chain
|
||||
stack = [bone]
|
||||
chain = []
|
||||
|
||||
while stack:
|
||||
b = stack.pop()
|
||||
chain.append(b)
|
||||
stack.extend(b.children)
|
||||
|
||||
for b in reversed(chain): # Remove children before parents
|
||||
for child in b.children:
|
||||
child.use_connect = False
|
||||
if bone.parent:
|
||||
child.parent = bone.parent
|
||||
|
||||
# Remove bone
|
||||
armature_data.edit_bones.remove(bone)
|
||||
removed_count += 1
|
||||
|
||||
# Restore children positions
|
||||
for child_name, data in children_data.items():
|
||||
if child_name in armature_data.edit_bones:
|
||||
child = armature_data.edit_bones[child_name]
|
||||
restore_bone_transforms(child, data)
|
||||
if b.parent:
|
||||
child.parent = b.parent
|
||||
if b.name in armature_data.edit_bones:
|
||||
armature_data.edit_bones.remove(b)
|
||||
removed_count += 1
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
|
||||
if context.scene.avatar_toolkit.list_only_mode:
|
||||
self.populate_bone_list(context, zero_weight_bones)
|
||||
return {'FINISHED'}
|
||||
|
||||
restore_breaking_settings_armature(armature, data_breaking)
|
||||
self.report({'INFO'}, t("Tools.clean_weights_success", count=removed_count))
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -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'}
|
||||
@@ -119,8 +119,10 @@ class AvatarToolkit_OT_ExplodeMesh(Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
|
||||
return context.view_layer.objects.active.type == "MESH" and len(context.view_layer.objects.selected) == 1
|
||||
active_obj = context.view_layer.objects.active
|
||||
return (active_obj is not None and
|
||||
active_obj.type == "MESH" and
|
||||
len(context.view_layer.objects.selected) == 1)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import traceback
|
||||
import bpy
|
||||
from typing import Dict, List, Set, Optional, Tuple, Any
|
||||
from bpy.types import Operator, Context, Object, PoseBone, EditBone, Bone, Constraint
|
||||
from ...core.common import get_active_armature
|
||||
from ...core.common import get_active_armature, transfer_vertex_weights, get_all_meshes
|
||||
from ...core.logging_setup import logger
|
||||
from ...core.translations import t
|
||||
from ...core.dictionaries import rigify_unity_names, rigify_basic_unity_names, rigify_unnecessary_bones
|
||||
@@ -69,19 +69,50 @@ class AvatarToolkit_OT_ConvertRigifyToUnity(Operator):
|
||||
|
||||
# Set armature as active object before mode switch
|
||||
bpy.context.view_layer.objects.active = armature
|
||||
|
||||
# Get all meshes for weight transfer
|
||||
meshes = get_all_meshes(bpy.context)
|
||||
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
bones_to_remove: List[str] = []
|
||||
for bone in armature.data.edit_bones:
|
||||
if any(pattern in bone.name.lower() for pattern in rigify_unnecessary_bones):
|
||||
bone_name_lower = bone.name.lower()
|
||||
if any(bone_name_lower.startswith(pattern) or bone_name_lower == pattern
|
||||
for pattern in rigify_unnecessary_bones):
|
||||
bones_to_remove.append(bone.name)
|
||||
|
||||
|
||||
# Check for neck bones that need merging
|
||||
merge_neck_bones = 'spine.004' in armature.data.edit_bones and 'spine.005' in armature.data.edit_bones
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
# Transfer weights from bones being removed
|
||||
for bone_name in bones_to_remove:
|
||||
if bone_name in armature.data.bones:
|
||||
logger.debug(f"Transferring weights from bone: {bone_name}")
|
||||
for mesh in meshes:
|
||||
if bone_name in mesh.vertex_groups:
|
||||
# Remove the vertex group since we don't need the weights
|
||||
mesh.vertex_groups.remove(mesh.vertex_groups[bone_name])
|
||||
|
||||
# Transfer weights for neck bone merging
|
||||
if merge_neck_bones:
|
||||
logger.debug("Transferring weights from spine.005 to spine.004")
|
||||
for mesh in meshes:
|
||||
if 'spine.005' in mesh.vertex_groups:
|
||||
transfer_vertex_weights(mesh, 'spine.005', 'spine.004')
|
||||
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
# Remove unnecessary bones
|
||||
for bone_name in bones_to_remove:
|
||||
if bone_name in armature.data.edit_bones:
|
||||
logger.debug(f"Removing bone: {bone_name}")
|
||||
armature.data.edit_bones.remove(armature.data.edit_bones[bone_name])
|
||||
|
||||
if 'spine.004' in armature.data.edit_bones and 'spine.005' in armature.data.edit_bones:
|
||||
# Merge neck bones
|
||||
if merge_neck_bones:
|
||||
logger.debug("Merging neck bones")
|
||||
neck_start = armature.data.edit_bones['spine.004']
|
||||
neck_end = armature.data.edit_bones['spine.005']
|
||||
@@ -89,6 +120,7 @@ class AvatarToolkit_OT_ConvertRigifyToUnity(Operator):
|
||||
armature.data.edit_bones.remove(neck_end)
|
||||
neck_start.name = "Neck"
|
||||
|
||||
# Rename head bone
|
||||
if 'spine.006' in armature.data.edit_bones:
|
||||
logger.debug("Renaming head bone")
|
||||
head_bone = armature.data.edit_bones['spine.006']
|
||||
@@ -137,6 +169,22 @@ class AvatarToolkit_OT_ConvertRigifyToUnity(Operator):
|
||||
if bone_name in armature.data.bones:
|
||||
armature.data.bones[bone_name].use_deform = False
|
||||
|
||||
# Get all meshes for weight transfer
|
||||
meshes = get_all_meshes(bpy.context)
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
for bone_name in remove_bones_in_chain:
|
||||
if bone_name in armature.data.bones:
|
||||
parent_name = armature.data.bones[bone_name].parent.name if armature.data.bones[bone_name].parent else None
|
||||
if parent_name:
|
||||
logger.debug(f"Transferring weights from {bone_name} to {parent_name}")
|
||||
for mesh in meshes:
|
||||
if bone_name in mesh.vertex_groups and parent_name in mesh.vertex_groups:
|
||||
transfer_vertex_weights(mesh, bone_name, parent_name)
|
||||
elif bone_name in mesh.vertex_groups:
|
||||
# Remove weights if no parent to merge to
|
||||
mesh.vertex_groups.remove(mesh.vertex_groups[bone_name])
|
||||
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
for bone_name in remove_bones_in_chain:
|
||||
if bone_name in armature.data.bones:
|
||||
@@ -190,6 +238,17 @@ class AvatarToolkit_OT_ConvertRigifyToUnity(Operator):
|
||||
("DEF-thigh_twist.R", "DEF-thigh.R")
|
||||
]
|
||||
|
||||
# Get all meshes for weight transfer
|
||||
meshes = get_all_meshes(bpy.context)
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
for twist_bone, parent_bone in twist_bones:
|
||||
if twist_bone in armature.data.bones and parent_bone in armature.data.bones:
|
||||
logger.debug(f"Transferring weights from {twist_bone} to {parent_bone}")
|
||||
for mesh in meshes:
|
||||
if twist_bone in mesh.vertex_groups:
|
||||
transfer_vertex_weights(mesh, twist_bone, parent_bone)
|
||||
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
for twist_bone, parent_bone in twist_bones:
|
||||
if twist_bone in armature.data.edit_bones and parent_bone in armature.data.edit_bones:
|
||||
|
||||
@@ -13,7 +13,9 @@ from ...core.dictionaries import (
|
||||
bone_hierarchy,
|
||||
acceptable_bone_names,
|
||||
acceptable_bone_hierarchy,
|
||||
non_standard_mappings
|
||||
non_standard_mappings,
|
||||
reverse_bone_lookup,
|
||||
simplify_bonename
|
||||
)
|
||||
|
||||
class AvatarToolkit_OT_StandardizeArmature(Operator):
|
||||
@@ -53,12 +55,6 @@ class AvatarToolkit_OT_StandardizeArmature(Operator):
|
||||
|
||||
logger.info(f"Starting armature standardization for {armature.name}")
|
||||
|
||||
is_valid, _, _ = validate_armature(armature)
|
||||
if is_valid:
|
||||
logger.info("Armature already meets standards, no changes needed")
|
||||
self.report({'INFO'}, t("Tools.standardize_already_valid"))
|
||||
return {'FINISHED'}
|
||||
|
||||
original_mode: str = context.mode
|
||||
logger.debug(f"Original mode: {original_mode}")
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
@@ -88,7 +84,7 @@ class AvatarToolkit_OT_StandardizeArmature(Operator):
|
||||
logger.info(f"Fixed {fixed_scale} scale issues")
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
is_valid, messages, _ = validate_armature(armature)
|
||||
is_valid, messages, _ = validate_armature(armature, override_mode='STRICT')
|
||||
|
||||
if is_valid:
|
||||
logger.info("Armature successfully standardized")
|
||||
@@ -134,17 +130,14 @@ class AvatarToolkit_OT_StandardizeArmature(Operator):
|
||||
existing_standard_bones.add(bone.name)
|
||||
logger.debug(f"Found existing standard bone: {bone.name}")
|
||||
|
||||
# Build a mapping of non-standard bone names to standard names
|
||||
# Use the reverse bone lookup that's already built and simplified
|
||||
name_mapping: Dict[str, str] = {}
|
||||
for category, standard_name in standard_bones.items():
|
||||
# Skip if this standard bone already exists
|
||||
if standard_name in existing_standard_bones:
|
||||
continue
|
||||
|
||||
# Get all variants for this category
|
||||
if category in non_standard_mappings:
|
||||
for variant in non_standard_mappings[category]:
|
||||
name_mapping[variant.lower()] = standard_name
|
||||
for simplified_name, category in reverse_bone_lookup.items():
|
||||
if category in standard_bones:
|
||||
standard_name = standard_bones[category]
|
||||
# Skip if this standard bone already exists
|
||||
if standard_name not in existing_standard_bones:
|
||||
name_mapping[simplified_name] = standard_name
|
||||
|
||||
# First pass: identify bones to rename
|
||||
bones_to_rename: Dict[str, str] = {}
|
||||
@@ -155,20 +148,14 @@ class AvatarToolkit_OT_StandardizeArmature(Operator):
|
||||
if original_name in standard_bones.values():
|
||||
continue
|
||||
|
||||
simplified_name: str = original_name.lower().replace(' ', '').replace('_', '').replace('.', '')
|
||||
simplified_name: str = simplify_bonename(original_name)
|
||||
|
||||
# Check if this bone matches any known pattern
|
||||
for variant, standard_name in name_mapping.items():
|
||||
# More precise matching - exact match or with common separators
|
||||
if (variant == simplified_name or
|
||||
variant == original_name.lower() or
|
||||
f"{variant}_" in simplified_name or
|
||||
f"{variant}." in simplified_name):
|
||||
|
||||
if original_name != standard_name:
|
||||
bones_to_rename[original_name] = standard_name
|
||||
logger.debug(f"Identified bone to rename: {original_name} -> {standard_name}")
|
||||
break
|
||||
# Check if this simplified bone name has a standard mapping
|
||||
if simplified_name in name_mapping:
|
||||
standard_name = name_mapping[simplified_name]
|
||||
if original_name != standard_name:
|
||||
bones_to_rename[original_name] = standard_name
|
||||
logger.debug(f"Identified bone to rename: {original_name} -> {standard_name}")
|
||||
|
||||
# Special case for spine/chest hierarchy
|
||||
# If we don't have an upper chest, don't rename chest to upper chest because it will break hierarchy
|
||||
|
||||
@@ -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 = t("VRM.convert_to_unity.label")
|
||||
bl_description = t("VRM.convert_to_unity.desc")
|
||||
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'}, t("VRM.no_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'}, t("VRM.validation.hierarchy_passed"))
|
||||
else:
|
||||
logger.warning("Unity hierarchy validation found issues")
|
||||
self.report({'WARNING'}, t("VRM.validation.hierarchy_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'}, t("VRM.validation.armature_passed"))
|
||||
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'}, t("VRM.validation.failed", error=str(e)))
|
||||
|
||||
return {'FINISHED'}
|
||||
Reference in New Issue
Block a user