Merge branch 'Alpha-4' into Alpha-3

This commit is contained in:
Onan Chew
2025-10-06 19:28:01 -04:00
committed by GitHub
24 changed files with 4676 additions and 269 deletions
+54 -2
View File
@@ -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:
+39 -35
View File
@@ -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'}
+102
View File
@@ -0,0 +1,102 @@
import bpy
from bpy.types import Operator
from ...core.logging_setup import logger
class AvatarToolkit_OT_RemoveAllColliders(Operator):
"""Remove all objects with 'collider' in their name"""
bl_idname = "avatar_toolkit.remove_all_colliders"
bl_label = "Remove All Colliders"
bl_description = "Remove all objects that have 'collider' in their name"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
logger.info("Starting standalone collider removal")
# Store current mode and active object
current_mode = bpy.context.mode
original_active = bpy.context.view_layer.objects.active
# Switch to object mode
if current_mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
try:
# Find all collider objects
collider_names = []
all_objects = list(bpy.data.objects)
logger.info(f"Scanning {len(all_objects)} objects for colliders")
for obj in all_objects:
if 'collider' in obj.name.lower():
collider_names.append(obj.name)
logger.info(f"Found collider: {obj.name}")
if not collider_names:
self.report({'INFO'}, "No collider objects found")
logger.info("No collider objects found")
return {'FINISHED'}
logger.info(f"Found {len(collider_names)} collider objects to remove")
self.report({'INFO'}, f"Found {len(collider_names)} collider objects")
# Remove each collider
removed_count = 0
failed_count = 0
for obj_name in collider_names:
try:
if obj_name in bpy.data.objects:
obj = bpy.data.objects[obj_name]
# Deselect all objects first
bpy.ops.object.select_all(action='DESELECT')
# Select and make active
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
# Delete the object
bpy.ops.object.delete(use_global=False)
removed_count += 1
logger.info(f"Removed collider: {obj_name}")
else:
logger.debug(f"Object {obj_name} no longer exists")
except Exception as e:
failed_count += 1
logger.error(f"Failed to remove {obj_name}: {str(e)}")
self.report({'WARNING'}, f"Failed to remove {obj_name}: {str(e)}")
# Report results
if removed_count > 0:
success_msg = f"Successfully removed {removed_count} collider objects"
logger.info(success_msg)
self.report({'INFO'}, success_msg)
if failed_count > 0:
failure_msg = f"Failed to remove {failed_count} collider objects"
logger.warning(failure_msg)
self.report({'WARNING'}, failure_msg)
except Exception as e:
error_msg = f"Error during collider removal: {str(e)}"
logger.error(error_msg)
self.report({'ERROR'}, error_msg)
return {'CANCELLED'}
finally:
# Restore original state
try:
if original_active and original_active.name in bpy.data.objects:
bpy.context.view_layer.objects.active = original_active
if current_mode != 'OBJECT':
bpy.ops.object.mode_set(mode=current_mode)
except:
pass
return {'FINISHED'}
+4 -2
View File
@@ -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)
+63 -4
View File
@@ -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:
+18 -31
View File
@@ -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
+88
View File
@@ -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'}