From 1e0fe403aa32e50688007043e6bfe77a2e4ec36f Mon Sep 17 00:00:00 2001 From: Yusarina Date: Fri, 13 Dec 2024 01:59:28 +0000 Subject: [PATCH] Re-do 3rd attempt I hate MMD stuff --- core/common.py | 58 --- core/properties.py | 48 +-- functions/mmd_tools.py | 907 ++++++++++++++++++++--------------------- ui/mmd_panel.py | 37 +- 4 files changed, 473 insertions(+), 577 deletions(-) diff --git a/core/common.py b/core/common.py index e5e817c..916e6ef 100644 --- a/core/common.py +++ b/core/common.py @@ -485,61 +485,3 @@ def remove_unused_shapekeys(mesh_obj: Object, tolerance: float = 0.001) -> int: removed_count += 1 return removed_count - -def save_armature_state(armature: Object) -> Dict[str, Any]: - """Save current armature state for recovery""" - state = { - 'bones': {}, - 'pose': {}, - 'settings': {} - } - - # Save bone data - for bone in armature.data.bones: - state['bones'][bone.name] = { - 'head': bone.head_local.copy(), - 'tail': bone.tail_local.copy(), - 'roll': bone.roll, - 'parent': bone.parent.name if bone.parent else None - } - - # Save pose data if exists - if armature.pose: - for bone in armature.pose.bones: - state['pose'][bone.name] = { - 'location': bone.location.copy(), - 'rotation': bone.rotation_quaternion.copy(), - 'scale': bone.scale.copy() - } - - return state - -def restore_armature_state(armature: Object, state: Dict[str, Any]) -> None: - """Restore armature from saved state""" - bpy.ops.object.mode_set(mode='EDIT') - - # Restore bones - for name, data in state['bones'].items(): - if name in armature.data.edit_bones: - bone = armature.data.edit_bones[name] - bone.head = data['head'] - bone.tail = data['tail'] - bone.roll = data['roll'] - - # Restore parenting - for name, data in state['bones'].items(): - if data['parent'] and name in armature.data.edit_bones: - bone = armature.data.edit_bones[name] - if data['parent'] in armature.data.edit_bones: - bone.parent = armature.data.edit_bones[data['parent']] - - bpy.ops.object.mode_set(mode='POSE') - - # Restore pose if exists - if 'pose' in state: - for name, data in state['pose'].items(): - if name in armature.pose.bones: - bone = armature.pose.bones[name] - bone.location = data['location'] - bone.rotation_quaternion = data['rotation'] - bone.scale = data['scale'] diff --git a/core/properties.py b/core/properties.py index 83be578..6725b26 100644 --- a/core/properties.py +++ b/core/properties.py @@ -87,53 +87,31 @@ class AvatarToolkitSceneProperties(PropertyGroup): ) merge_twist_bones: BoolProperty( - name=t("Tools.merge_twist_bones"), - description=t("Tools.merge_twist_bones_desc"), + name=t("MMD.merge_twist_bones"), + description=t("MMD.merge_twist_bones_desc"), default=True ) - clean_weights_threshold: FloatProperty( - name=t("Tools.clean_weights_threshold"), - description=t("Tools.clean_weights_threshold_desc"), - default=0.01, - min=0.0000001, - max=0.9999999 + keep_twist_bones: BoolProperty( + name=t("MMD.keep_twist_bones"), + description=t("MMD.keep_twist_bones_desc"), + default=False ) - connect_bones_min_distance: FloatProperty( - name=t("Tools.connect_bones_min_distance"), - description=t("Tools.connect_bones_min_distance_desc"), - default=0.005, - min=0.001, - max=0.1 + keep_upper_chest: BoolProperty( + name=t("MMD.keep_upper_chest"), + description=t("MMD.keep_upper_chest_desc"), + default=True ) merge_weights_threshold: FloatProperty( - name=t("Tools.merge_weights_threshold"), - description=t("Tools.merge_weights_threshold_desc"), + name=t("MMD.merge_weights_threshold"), + description=t("MMD.merge_weights_threshold_desc"), default=0.01, - min=0.0001, + min=0.0, max=1.0 ) - mmd_process_twist_bones: BoolProperty( - name=t("MMD.process_twist_bones"), - description=t("MMD.process_twist_bones_desc"), - default=True - ) - - mmd_connect_bones: BoolProperty( - name=t("MMD.connect_bones"), - description=t("MMD.connect_bones_desc"), - default=True - ) - - save_backup_state: BoolProperty( - name="Save Backup State", - description="Save the initial state of the armature before standardizing bones", - default=False - ) - def register() -> None: """Register the Avatar Toolkit property group""" logger.info("Registering Avatar Toolkit properties") diff --git a/functions/mmd_tools.py b/functions/mmd_tools.py index 8a85799..0ce96f3 100644 --- a/functions/mmd_tools.py +++ b/functions/mmd_tools.py @@ -1,62 +1,190 @@ import bpy -from typing import Tuple, Set, Dict -from bpy.types import Operator, Context, Object from mathutils import Vector +from typing import Dict, List, Tuple, Set, Optional +from bpy.types import Object, Armature, EditBone, Bone, Operator, Context +from ..core.logging_setup import logger from ..core.common import ( ProgressTracker, get_active_armature, - validate_meshes, - simplify_bonename, - duplicate_bone_chain, - save_armature_state, - restore_armature_state, - get_all_meshes, - validate_bone_hierarchy, - transfer_vertex_weights, - get_vertex_weights + validate_armature, + get_vertex_weights, + transfer_vertex_weights ) -from ..core.logging_setup import logger from ..core.translations import t from ..core.dictionaries import bone_names -class AvatarToolkit_OT_StandardizeMMDBones(Operator): - bl_idname = "avatar_toolkit.mmd_standardize_bones" - bl_label = t("MMD.standardize_bones") +class AVATAR_TOOLKIT_OT_StandardizeMmd(Operator): + """MMD Bone standardization system""" + bl_idname = "avatar_toolkit.standardize_mmd" + bl_label = t("MMD.standardize") bl_options = {'REGISTER', 'UNDO'} - - def standardize_bone_names(self, armature: Object) -> None: - """Standardize bone names using MMD to Unity/VRChat conventions""" - for bone in armature.data.bones: - simplified_name = simplify_bonename(bone.name) - for standard_name, variations in bone_names.items(): - if simplified_name in variations: - bone.name = standard_name - break - - def process_lr_bones(self, armature: Object) -> None: - """Process left/right bone pairs for consistency""" - for bone in armature.data.bones: - if bone.name.endswith(('_l', '_r', '.l', '.r', 'Left', 'Right')): - base_name = bone.name.rsplit('_', 1)[0] - side = '_l' if any(s in bone.name.lower() for s in ('left', '_l', '.l')) else '_r' - bone.name = f"{base_name}{side}" - - def resolve_name_conflicts(self, armature: Object) -> None: - """Handle duplicate bone names""" - used_names = set() - for bone in armature.data.bones: - base_name = bone.name - counter = 1 - while bone.name in used_names: - bone.name = f"{base_name}_{counter}" - counter += 1 - used_names.add(bone.name) - - def process_spine_chain(self, armature: Object) -> None: - """Process spine bones for VRChat compatibility""" - bpy.ops.object.mode_set(mode='EDIT') - edit_bones = armature.data.edit_bones + + def __init__(self): + self.bone_mapping: Dict[str, str] = {} + self.processed_bones: Set[str] = set() + def execute(self, context: Context) -> Set[str]: + self.armature = get_active_armature(context) + + if not self.armature: + self.report({'ERROR'}, t("MMD.no_armature")) + return {'CANCELLED'} + + try: + with ProgressTracker(context, 5, "MMD Standardization") as progress: + # Step 1: Process bone names + self.process_bone_names(context) + progress.step("Processed bone names") + + # Step 2: Fix bone structure + self.fix_bone_structure(context) + progress.step("Fixed bone structure") + + # Step 3: Process weights + self.process_weights(context) + progress.step("Processed weights") + + # Step 4: Clean up + self.cleanup_armature(context) + progress.step("Cleaned up armature") + + # Step 5: Final validation + self.validate_results(context) + progress.step("Validated results") + + self.report({'INFO'}, t("MMD.standardization_complete")) + return {'FINISHED'} + + except Exception as e: + logger.error(f"MMD Standardization failed: {str(e)}") + self.report({'ERROR'}, str(e)) + return {'CANCELLED'} + + def standardize_armature(self) -> Tuple[bool, str]: + """Main standardization process""" + if not self.armature: + return False, t("MMD.no_armature") + + try: + with ProgressTracker(self.context, 5, "MMD Standardization") as progress: + # Step 1: Process bone names + self.process_bone_names() + progress.step("Processed bone names") + + # Step 2: Fix bone structure + self.fix_bone_structure() + progress.step("Fixed bone structure") + + # Step 3: Process weights + self.process_weights() + progress.step("Processed weights") + + # Step 4: Clean up + self.cleanup_armature() + progress.step("Cleaned up armature") + + # Step 5: Final validation + self.validate_results() + progress.step("Validated results") + + return True, t("MMD.standardization_complete") + + except Exception as e: + logger.error(f"MMD Standardization failed: {str(e)}") + return False, str(e) + + def process_bone_names(self, context: Context) -> None: + """Process and standardize bone names""" + bpy.ops.object.mode_set(mode='EDIT') + edit_bones = self.armature.data.edit_bones + + for bone in edit_bones: + new_name = self.standardize_bone_name(bone.name) + if new_name != bone.name: + self.bone_mapping[bone.name] = new_name + bone.name = new_name + + def translate_japanese_bone_name(self, name: str) -> str: + """Translate Japanese bone names to English standardized names""" + from ..core.dictionaries import bone_names + + # Convert to lowercase for matching + name_lower = name.lower() + + # Check each bone category for Japanese character matches + for bone_category, variations in bone_names.items(): + for variation in variations: + if variation in name_lower: + # If Japanese characters are found, return the standardized name + return bone_category + + # If no match found, return original name + return name + + def standardize_bone_name(self, name: str) -> str: + """Standardize individual bone names""" + # First translate Japanese names + result = self.translate_japanese_bone_name(name) + + # Remove common prefixes + prefixes = ['ValveBiped_', 'Bip01_', 'MMD_', 'Armature|'] + for prefix in prefixes: + if result.lower().startswith(prefix.lower()): + result = result[len(prefix):] + + # Handle left/right conventions + if result.endswith('_L') or result.endswith('.L'): + result = f"{result[:-2]}.L" + elif result.endswith('_R') or result.endswith('.R'): + result = f"{result[:-2]}.R" + + return result + + def fix_bone_structure(self, context: Context) -> None: + """Fix bone hierarchy and orientations""" + bpy.ops.object.mode_set(mode='EDIT') + edit_bones = self.armature.data.edit_bones + + # Process spine hierarchy + self.process_spine_chain(context) + + # Fix bone orientations + self.fix_bone_orientations(context) + + # Connect appropriate bones + self.connect_bones(context) + + def process_weights(self, context: Context) -> None: + """Process and clean up vertex weights""" + for mesh in self.get_associated_meshes(context): + # Transfer weights based on bone mapping + for old_name, new_name in self.bone_mapping.items(): + if old_name != new_name: + transfer_vertex_weights(mesh, old_name, new_name) + + # Clean up zero weights + self.cleanup_vertex_groups(mesh, context) + + def cleanup_armature(self, context: Context) -> None: + """Perform final cleanup operations""" + # Remove unused bones + self.remove_unused_bones(context) + + # Clean up constraints + self.cleanup_constraints(context) + + # Fix zero-length bones + self.fix_zero_length_bones(context) + + def get_associated_meshes(self, context: Context) -> List[Object]: + """Get all mesh objects associated with the armature""" + return [obj for obj in bpy.data.objects + if obj.type == 'MESH' + and obj.parent == self.armature] + + def process_spine_chain(self, context: Context) -> None: + """Process and fix spine bone chain hierarchy""" + edit_bones = self.armature.data.edit_bones spine_bones = { 'hips': None, 'spine': None, @@ -66,437 +194,308 @@ class AvatarToolkit_OT_StandardizeMMDBones(Operator): 'head': None } - # Map existing spine bones + # Find spine bones using bone_names dictionary for bone in edit_bones: - simplified = simplify_bonename(bone.name) - for spine_name in spine_bones.keys(): - if simplified in bone_names[spine_name]: - spine_bones[spine_name] = bone + for spine_part, _ in spine_bones.items(): + if any(alt_name in bone.name.lower() for alt_name in bone_names[spine_part]): + spine_bones[spine_part] = bone break - # Create missing spine bones - if spine_bones['spine'] and not spine_bones['chest']: - chest = edit_bones.new('chest') - chest.head = spine_bones['spine'].tail - chest.tail = spine_bones['neck'].head if spine_bones['neck'] else spine_bones['head'].head - spine_bones['chest'] = chest - # Set up spine hierarchy - if spine_bones['hips']: - for i, key in enumerate(['spine', 'chest', 'upper_chest', 'neck', 'head']): - if spine_bones[key]: - prev_key = list(spine_bones.keys())[i] - if spine_bones[prev_key]: - spine_bones[key].parent = spine_bones[prev_key] - - def correct_bone_orientations(self, armature: Object) -> None: - """Automatically correct bone orientations to align with Unity's axes""" - bpy.ops.object.mode_set(mode='EDIT') - edit_bones = armature.data.edit_bones - - # Define standard orientations - orientations = { - 'spine': Vector((0, 0, 1)), # Points up - 'chest': Vector((0, 0, 1)), - 'neck': Vector((0, 0, 1)), - 'head': Vector((0, 0, 1)), - 'shoulder': Vector((1, 0, 0)), # Points outward - 'arm': Vector((0, -1, 0)), # Points down - 'elbow': Vector((0, -1, 0)), - 'leg': Vector((0, -1, 0)), - 'knee': Vector((0, -1, 0)), - 'foot': Vector((1, 0, 0)), # Points forward - } - - for bone in edit_bones: - simplified_name = simplify_bonename(bone.name) - for bone_type, direction in orientations.items(): - if bone_type in simplified_name: - # Calculate new tail position while maintaining length - length = (bone.tail - bone.head).length - bone.tail = bone.head + direction * length - break - - @classmethod - def poll(cls, context: Context) -> bool: - """Check if there is an active armature in the scene""" - return get_active_armature(context) is not None - - def execute(self, context: Context) -> Set[str]: - try: - armature = get_active_armature(context) - - # Save initial state if enabled - if context.scene.avatar_toolkit.save_backup_state: - self.initial_state = save_armature_state(armature) - - with ProgressTracker(context, 6, "Standardizing Bones") as progress: - # Step 1: Standardize bone names - self.standardize_bone_names(armature) - progress.step("Standardized bone names") - - # Step 3: Process left/right bones - self.process_lr_bones(armature) - progress.step("Processed left/right bones") - - # Step 4: Handle name conflicts - self.resolve_name_conflicts(armature) - progress.step("Resolved naming conflicts") - - # Step 5: Process spine chain - self.process_spine_chain(armature) - progress.step("Processed spine chain") - - # Step 6: Correct bone orientations - self.correct_bone_orientations(armature) - progress.step("Corrected bone orientations") - - self.report({'INFO'}, t("MMD.bones_standardized")) - return {'FINISHED'} - - except Exception as e: - logger.error(f"Bone standardization failed: {str(e)}") - if hasattr(self, 'initial_state'): - restore_armature_state(armature, self.initial_state) - self.report({'ERROR'}, str(e)) - return {'CANCELLED'} - -class AvatarToolkit_OT_ProcessMMDWeights(Operator): - bl_idname = "avatar_toolkit.mmd_process_weights" - bl_label = t("MMD.process_weights") - bl_options = {'REGISTER', 'UNDO'} - - def merge_bone_weights(self, context: Context, mesh: Object, source: str, target: str) -> None: - """Transfer weights from source bone to target bone""" - transfer_vertex_weights( - mesh, - source, - target, - context.scene.avatar_toolkit.merge_weights_threshold - ) - - def process_eye_weights(self, context: Context, mesh: Object) -> None: - """Handle special cases for eye bone weights""" - eye_bones = { - 'eye_l': ['eyel', 'lefteye', 'eye.l'], - 'eye_r': ['eyer', 'righteye', 'eye.r'] - } - - for target, sources in eye_bones.items(): - for source in sources: - if source in mesh.vertex_groups: - self.merge_bone_weights(context, mesh, source, target) - - def process_twist_bones(self, context: Context, mesh: Object) -> None: - """Process and merge twist bone weights""" - if not context.scene.avatar_toolkit.mmd_process_twist_bones: - return - - twist_pairs = [ - ('arm_twist_l', 'left_arm'), - ('arm_twist_r', 'right_arm'), - ('forearm_twist_l', 'left_elbow'), - ('forearm_twist_r', 'right_elbow') + hierarchy = [ + ('hips', 'spine'), + ('spine', 'chest'), + ('chest', 'neck'), + ('neck', 'head') ] - for twist, target in twist_pairs: - if twist in mesh.vertex_groups: - self.merge_bone_weights(context, mesh, twist, target) + for parent_name, child_name in hierarchy: + parent = spine_bones.get(parent_name) + child = spine_bones.get(child_name) + if parent and child: + child.parent = parent + child.use_connect = True - def cleanup_vertex_groups(self, context: Context, mesh: Object) -> None: - """Remove empty and unused vertex groups""" - threshold = context.scene.avatar_toolkit.clean_weights_threshold + def fix_bone_orientations(self, context: Context) -> None: + """Fix bone orientations for standard pose compatibility""" + edit_bones = self.armature.data.edit_bones - # Get list of used bones from armature - armature = mesh.find_armature() - if not armature: - return - - valid_bones = set(bone.name for bone in armature.data.bones) + # Process arm bones + arm_pairs = [ + ('upper_arm', 'forearm'), + ('forearm', 'hand') + ] - # Remove unused groups - for group in mesh.vertex_groups[:]: - if group.name not in valid_bones: - mesh.vertex_groups.remove(group) + for side in ['.L', '.R']: + for parent, child in arm_pairs: + parent_bone = next((b for b in edit_bones if b.name.lower().startswith(parent) and b.name.endswith(side)), None) + child_bone = next((b for b in edit_bones if b.name.lower().startswith(child) and b.name.endswith(side)), None) + + if parent_bone and child_bone: + child_bone.use_connect = True + child_bone.use_inherit_rotation = True + + # Process leg bones + leg_pairs = [ + ('thigh', 'shin'), + ('shin', 'foot') + ] + + for side in ['.L', '.R']: + for parent, child in leg_pairs: + parent_bone = next((b for b in edit_bones if b.name.lower().startswith(parent) and b.name.endswith(side)), None) + child_bone = next((b for b in edit_bones if b.name.lower().startswith(child) and b.name.endswith(side)), None) + + if parent_bone and child_bone: + child_bone.use_connect = True + child_bone.use_inherit_rotation = True + + def remove_unused_bones(self, context: Context) -> None: + """Remove unused and unnecessary bones from the armature""" + bpy.ops.object.mode_set(mode='EDIT') + edit_bones = self.armature.data.edit_bones + + # Get list of bones that have vertex weights + used_bones = set() + for mesh in self.get_associated_meshes(context): + for group in mesh.vertex_groups: + used_bones.add(group.name) + + # Get list of bones to keep based on settings + toolkit = context.scene.avatar_toolkit + keep_upper_chest = toolkit.keep_upper_chest + keep_twist = toolkit.keep_twist_bones + + # Remove unused bones + for bone in edit_bones: + # Skip if bone has weights + if bone.name in used_bones: continue - # Check if group has any weights above threshold - has_weights = False - for vert in mesh.data.vertices: - for group_element in vert.groups: - if group_element.group == group.index: - if group_element.weight > threshold: - has_weights = True - break - if has_weights: - break - - if not has_weights: - mesh.vertex_groups.remove(group) + # Skip if bone is upper chest and we want to keep it + if 'upper_chest' in bone.name.lower() and keep_upper_chest: + continue + + # Skip if bone is twist bone and we want to keep them + if 'twist' in bone.name.lower() and keep_twist: + continue + + # Remove the bone + edit_bones.remove(bone) - def merge_remaining_weights(self, context: Context, mesh: Object) -> None: - """Process remaining weight merging cases""" - # Common MMD weight merge pairs - merge_pairs = [ - # Finger weights - ('pinky', 'pinkie'), - ('thumb0', 'thumb_0'), - ('index0', 'index_0'), - ('middle0', 'middle_0'), - ('ring0', 'ring_0'), - - # Additional arm weights - ('upperarm', 'arm'), - ('lowerarm', 'elbow'), - ('wrist', 'hand'), - - # Leg weights - ('upperleg', 'leg'), - ('lowerleg', 'knee'), - ('ankle', 'foot'), - - # Spine weights - ('spine1', 'chest'), - ('spine2', 'upper_chest'), + def connect_bones(self, context: Context) -> None: + """Connect bones that should be connected in the hierarchy""" + edit_bones = self.armature.data.edit_bones + + connect_chains = [ + ['hips', 'spine', 'chest', 'neck', 'head'], + ['shoulder.L', 'upper_arm.L', 'forearm.L', 'hand.L'], + ['shoulder.R', 'upper_arm.R', 'forearm.R', 'hand.R'], + ['thigh.L', 'shin.L', 'foot.L', 'toe.L'], + ['thigh.R', 'shin.R', 'foot.R', 'toe.R'] ] - for source, target in merge_pairs: - for suffix in ['_l', '_r', '.l', '.r']: - source_name = f"{source}{suffix}" - target_name = f"{target}{suffix}" - if source_name in mesh.vertex_groups: - self.merge_bone_weights(context, mesh, source_name, target_name) + for chain in connect_chains: + prev_bone = None + for bone_name in chain: + bone = next((b for b in edit_bones if b.name.lower().endswith(bone_name.lower())), None) + if bone and prev_bone: + bone.parent = prev_bone + bone.use_connect = True + prev_bone = bone + + def cleanup_vertex_groups(self, mesh_obj: Object, context: Context) -> None: + """Clean up vertex groups by removing zero weights and merging similar groups""" + threshold = context.scene.avatar_toolkit.merge_weights_threshold + + # Get list of vertex groups + vertex_groups = mesh_obj.vertex_groups + + # Track groups to remove + groups_to_remove = set() + + # Check each vertex group + for group in vertex_groups: + weights = get_vertex_weights(mesh_obj, group.name) + + # If no weights above threshold, mark for removal + if not any(weight > threshold for weight in weights.values()): + groups_to_remove.add(group.name) + + # Remove empty groups + for group_name in groups_to_remove: + group = vertex_groups.get(group_name) + if group: + vertex_groups.remove(group) + + def validate_results(self, context: Context) -> None: + """Validate the results of standardization""" + valid, messages = validate_armature(self.armature) + if not valid: + raise ValueError("\n".join(messages)) + + def cleanup_constraints(self, context: Context) -> None: + """Clean up and fix bone constraints""" + bpy.ops.object.mode_set(mode='POSE') + + # Process each pose bone + for pose_bone in self.armature.pose.bones: + constraints_to_remove = [] + + for constraint in pose_bone.constraints: + should_remove = False + + # Handle IK constraints + if constraint.type == 'IK': + if not constraint.target or constraint.target != self.armature: + should_remove = True + elif not constraint.subtarget or constraint.subtarget not in self.armature.data.bones: + should_remove = True + + # Handle MMD additional rotation constraints + elif constraint.name == 'mmd_additional_rotation': + if not constraint.target or constraint.target != self.armature: + should_remove = True + elif not constraint.subtarget or constraint.subtarget not in self.armature.data.bones: + should_remove = True + + # Handle transformation constraints + elif constraint.type in {'COPY_ROTATION', 'COPY_LOCATION', 'COPY_TRANSFORMS'}: + if not constraint.target or constraint.target != self.armature: + should_remove = True + elif not constraint.subtarget or constraint.subtarget not in self.armature.data.bones: + should_remove = True + + if should_remove: + constraints_to_remove.append(constraint) + + # Remove invalid constraints + for constraint in constraints_to_remove: + pose_bone.constraints.remove(constraint) + + def fix_zero_length_bones(self, context: Context) -> None: + """Fix zero-length bones by setting minimal length""" + bpy.ops.object.mode_set(mode='EDIT') + edit_bones = self.armature.data.edit_bones + + min_length = 0.01 # Minimum bone length in Blender units + + for bone in edit_bones: + # Calculate bone length + bone_length = (bone.tail - bone.head).length + + if bone_length < min_length: + # Set minimal length while preserving direction + if bone.parent: + # Use parent's orientation as reference + direction = bone.parent.tail - bone.parent.head + direction.normalize() + else: + # Default to Z-axis if no parent + direction = mathutils.Vector((0, 0, 1)) + + bone.tail = bone.head + (direction * min_length) + +class FixUnmovableBonesOperator(bpy.types.Operator): + bl_idname = "avatar_toolkit.fix_unmovable_bones" + bl_label = t("MMD.fix_unmovable_bones") + bl_description = t("MMD.fix_unmovable_bones_desc") + bl_options = {'REGISTER', 'UNDO'} @classmethod - def poll(cls, context: Context) -> bool: - """Check if there is an active armature in the scene""" - return get_active_armature(context) is not None + def poll(cls, context): + armature = get_active_armature(context) + return armature is not None and armature.type == 'ARMATURE' + + def execute(self, context): + armature = get_active_armature(context) + if not armature: + self.report({'ERROR'}, t("MMD.no_armature")) + return {'CANCELLED'} - def execute(self, context: Context) -> Set[str]: try: - meshes = get_all_meshes(context) - - # Save initial state - if context.scene.avatar_toolkit.save_backup_state: - self.initial_states = {mesh: get_vertex_weights(mesh) for mesh in meshes} - - with ProgressTracker(context, len(meshes) * 4, "Processing Weights") as progress: + with ProgressTracker(context, 2, "Unlocking Transforms") as progress: + # Unlock armature transforms + progress.step("Unlocking armature transforms") + for attr in ('location', 'rotation', 'scale'): + for i in range(3): + setattr(armature, f"lock_{attr}", [False] * 3) + + # Unlock bone transforms + progress.step("Unlocking bone transforms") + for bone in armature.pose.bones: + for attr in ('location', 'rotation', 'scale'): + setattr(bone, f"lock_{attr}", [False] * 3) + + self.report({'INFO'}, t("MMD.transforms_unlocked")) + return {'FINISHED'} + + except Exception as e: + logger.error(f"Error unlocking transforms: {str(e)}") + self.report({'ERROR'}, str(e)) + return {'CANCELLED'} + +class ReparentMeshesOperator(bpy.types.Operator): + bl_idname = "avatar_toolkit.reparent_meshes" + bl_label = t("MMD.reparent_meshes") + bl_description = t("MMD.reparent_meshes_desc") + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + armature = get_active_armature(context) + return armature is not None and get_all_meshes(context) + + def execute(self, context): + armature = get_active_armature(context) + if not armature: + self.report({'ERROR'}, t("MMD.no_armature")) + return {'CANCELLED'} + + meshes = get_all_meshes(context) + if not meshes: + self.report({'ERROR'}, t("MMD.no_meshes")) + return {'CANCELLED'} + + try: + with ProgressTracker(context, len(meshes) + 1, "Reparenting Meshes") as progress: + # Get or create main collection + main_collection = self._get_main_collection(context) + progress.step("Setting up collections") + + # Process each mesh for mesh in meshes: - # Step 1: Process eye weights - self.process_eye_weights(context, mesh) - progress.step(f"Processed eye weights for {mesh.name}") - - # Step 2: Process twist bones - self.process_twist_bones(context, mesh) - progress.step(f"Processed twist bones for {mesh.name}") - - # Step 3: Merge remaining weights - self.merge_remaining_weights(context, mesh) - progress.step(f"Merged weights for {mesh.name}") - - # Step 4: Cleanup - self.cleanup_vertex_groups(context, mesh) - progress.step(f"Cleaned up weights for {mesh.name}") + progress.step(f"Processing {mesh.name}") + self._process_mesh(mesh, armature, main_collection) - self.report({'INFO'}, t("MMD.weights_processed")) + self.report({'INFO'}, t("MMD.reparenting_complete")) return {'FINISHED'} - + except Exception as e: - logger.error(f"Weight processing failed: {str(e)}") - if hasattr(self, 'initial_states'): - for mesh, state in self.initial_states.items(): - restore_mesh_weights_state(mesh, state) + logger.error(f"Error reparenting meshes: {str(e)}") self.report({'ERROR'}, str(e)) return {'CANCELLED'} -class AvatarToolkit_OT_FixMMDHierarchy(Operator): - bl_idname = "avatar_toolkit.mmd_fix_hierarchy" - bl_label = t("MMD.fix_hierarchy") - bl_options = {'REGISTER', 'UNDO'} + def _get_main_collection(self, context) -> bpy.types.Collection: + """Get or create the main collection for the armature""" + if hasattr(context.scene, 'collection'): + return context.scene.collection + return context.scene.collection - def fix_bone_parenting(self, armature: Object) -> None: - """Fix bone parenting to match standard hierarchy""" - bpy.ops.object.mode_set(mode='EDIT') - edit_bones = armature.data.edit_bones - - # Define parent-child relationships - hierarchy_map = { - 'hips': ['spine', 'left_leg', 'right_leg'], - 'spine': ['chest'], - 'chest': ['upper_chest', 'left_shoulder', 'right_shoulder'], - 'upper_chest': ['neck'], - 'neck': ['head'], - 'head': ['left_eye', 'right_eye'], - 'left_shoulder': ['left_arm'], - 'right_shoulder': ['right_arm'], - 'left_arm': ['left_elbow'], - 'right_arm': ['right_elbow'], - 'left_elbow': ['left_wrist'], - 'right_elbow': ['right_wrist'], - 'left_leg': ['left_knee'], - 'right_leg': ['right_knee'], - 'left_knee': ['left_ankle'], - 'right_knee': ['right_ankle'], - 'left_ankle': ['left_toe'], - 'right_ankle': ['right_toe'] - } - - # Apply parenting - for parent_name, children in hierarchy_map.items(): - parent_bone = None - for bone in edit_bones: - if simplify_bonename(bone.name) in bone_names[parent_name]: - parent_bone = bone - break - - if parent_bone: - for child_name in children: - for bone in edit_bones: - if simplify_bonename(bone.name) in bone_names[child_name]: - bone.parent = parent_bone + def _process_mesh(self, mesh: bpy.types.Object, + armature: bpy.types.Object, + main_collection: bpy.types.Collection) -> None: + """Process individual mesh parenting and collection management""" + # Unlink from other collections + for col in mesh.users_collection: + if col != main_collection: + col.objects.unlink(mesh) - def connect_bones(self, context: Context, armature: Object) -> None: - """Connect bones to their children where appropriate""" - if not context.scene.avatar_toolkit.mmd_connect_bones: - return - - bpy.ops.object.mode_set(mode='EDIT') - edit_bones = armature.data.edit_bones - min_distance = context.scene.avatar_toolkit.connect_bones_min_distance - - for bone in edit_bones: - if bone.children: - for child in bone.children: - # Check if bones are close enough to connect - distance = (bone.tail - child.head).length - if distance < min_distance: - bone.tail = child.head - child.use_connect = True + # Ensure mesh is in main collection + if mesh.name not in main_collection.objects: + main_collection.objects.link(mesh) - def validate_hierarchy(self, armature: Object) -> bool: - """Validate final bone hierarchy""" - # Check essential parent-child relationships - essential_pairs = [ - ('spine', 'hips'), - ('chest', 'spine'), - ('neck', 'chest'), - ('head', 'neck') - ] - - for child, parent in essential_pairs: - if not validate_bone_hierarchy(armature.data.bones, parent, child): - return False - - return True - - def execute(self, context: Context) -> Set[str]: - try: - armature = get_active_armature(context) - - # Save initial state - if context.scene.avatar_toolkit.save_backup_state: - self.initial_state = save_armature_state(armature) - - with ProgressTracker(context, 3, "Fixing Bone Hierarchy") as progress: - # Step 1: Fix bone parenting - self.fix_bone_parenting(armature) - progress.step("Fixed bone parenting") - - # Step 2: Connect bones - self.connect_bones(context, armature) - progress.step("Connected bones") - - # Step 3: Validate hierarchy - if not self.validate_hierarchy(armature): - self.report({'WARNING'}, t("MMD.hierarchy_validation_warning")) - progress.step("Validated hierarchy") - - self.report({'INFO'}, t("MMD.hierarchy_fixed")) - return {'FINISHED'} - - except Exception as e: - logger.error(f"Hierarchy fix failed: {str(e)}") - if hasattr(self, 'initial_state'): - restore_armature_state(armature, self.initial_state) - self.report({'ERROR'}, str(e)) - return {'CANCELLED'} - -class AvatarToolkit_OT_CleanupMMDArmature(Operator): - bl_idname = "avatar_toolkit.mmd_cleanup_armature" - bl_label = t("MMD.cleanup_armature") - bl_options = {'REGISTER', 'UNDO'} - - def remove_unused_bones(self, context: Context, armature: Object) -> None: - """Remove bones that aren't in the standard hierarchy or affecting weights""" - bpy.ops.object.mode_set(mode='EDIT') - edit_bones = armature.data.edit_bones - - # Get all bones affecting vertex groups - used_bones = set() - for mesh in get_all_meshes(context): - used_bones.update(group.name for group in mesh.vertex_groups) - - # Add essential bones from dictionary - essential_bones = set(bone_names.keys()) - - # Remove non-essential, unused bones - for bone in edit_bones[:]: # Slice to avoid modification during iteration - simplified_name = simplify_bonename(bone.name) - if (not any(simplified_name in variations for variations in bone_names.values()) and - bone.name not in used_bones): - edit_bones.remove(bone) - - def fix_bone_orientations(self, armature: Object) -> None: - """Fix bone orientations for Unity/VRChat compatibility""" - bpy.ops.object.mode_set(mode='EDIT') - edit_bones = armature.data.edit_bones - - # Standard bone alignments - alignments = { - 'spine': (0, 0, 1), # Points up - 'chest': (0, 0, 1), - 'neck': (0, 0, 1), - 'head': (0, 0, 1), - 'shoulder': (1, 0, 0), # Points outward - 'arm': (0, -1, 0), # Points down - 'elbow': (0, -1, 0), - 'leg': (0, -1, 0), - 'knee': (0, -1, 0), - 'foot': (1, 0, 0), # Points forward - } - - for bone in edit_bones: - simplified_name = simplify_bonename(bone.name) - for bone_type, direction in alignments.items(): - if bone_type in simplified_name: - # Calculate new tail position while maintaining length - length = (bone.tail - bone.head).length - bone.tail = bone.head + Vector(direction) * length - break - - def execute(self, context: Context) -> Set[str]: - try: - armature = get_active_armature(context) - - # Save initial state - if context.scene.avatar_toolkit.save_backup_state: - self.initial_state = save_armature_state(armature) - - with ProgressTracker(context, 2, "Cleaning Up Armature") as progress: - # Step 1: Remove unused bones - self.remove_unused_bones(context, armature) - progress.step("Removed unused bones") - - # Step 2: Fix bone orientations - self.fix_bone_orientations(armature) - progress.step("Fixed bone orientations") - - self.report({'INFO'}, t("MMD.cleanup_completed")) - return {'FINISHED'} - - except Exception as e: - logger.error(f"Armature cleanup failed: {str(e)}") - if hasattr(self, 'initial_state'): - restore_armature_state(armature, self.initial_state) - self.report({'ERROR'}, str(e)) - return {'CANCELLED'} + # Set parent to armature + mesh.parent = armature + if not mesh.parent_type == 'ARMATURE': + mesh.parent_type = 'ARMATURE' \ No newline at end of file diff --git a/ui/mmd_panel.py b/ui/mmd_panel.py index a4b384b..4232bfb 100644 --- a/ui/mmd_panel.py +++ b/ui/mmd_panel.py @@ -1,46 +1,23 @@ import bpy from typing import Set -from bpy.types import Panel, Context, UILayout, Operator +from bpy.types import Panel, Context, UILayout from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME from ..core.translations import t class AvatarToolKit_PT_MMDPanel(Panel): - """Panel containing MMD conversion and optimization tools""" + """Panel containing MMD bone standardization tools""" bl_label = t("MMD.label") bl_idname = "OBJECT_PT_avatar_toolkit_mmd" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = CATEGORY_NAME bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname - bl_order = 2 + bl_order = 3 def draw(self, context: Context) -> None: layout: UILayout = self.layout + toolkit = context.scene.avatar_toolkit - # Bone Standardization Box - bone_box: UILayout = layout.box() - col: UILayout = bone_box.column(align=True) - col.label(text=t("MMD.bone_standardization"), icon='ARMATURE_DATA') - col.separator(factor=0.5) - col.operator("avatar_toolkit.mmd_standardize_bones", icon='BONE_DATA') - - # Weight Processing Box - weight_box: UILayout = layout.box() - col = weight_box.column(align=True) - col.label(text=t("MMD.weight_processing"), icon='GROUP_VERTEX') - col.separator(factor=0.5) - col.operator("avatar_toolkit.mmd_process_weights", icon='WPAINT_HLT') - - # Hierarchy Box - hierarchy_box: UILayout = layout.box() - col = hierarchy_box.column(align=True) - col.label(text=t("MMD.hierarchy"), icon='OUTLINER') - col.separator(factor=0.5) - col.operator("avatar_toolkit.mmd_fix_hierarchy", icon='CONSTRAINT_BONE') - - # Cleanup Box - cleanup_box: UILayout = layout.box() - col = cleanup_box.column(align=True) - col.label(text=t("MMD.cleanup"), icon='BRUSH_DATA') - col.separator(factor=0.5) - col.operator("avatar_toolkit.mmd_cleanup_armature", icon='MODIFIER') + # Add merge twist bones option + layout.prop(toolkit, "keep_twist_bones") + layout.operator("avatar_toolkit.standardize_mmd", icon='BONE_DATA')