diff --git a/core/mmd_converter.py b/core/mmd_converter.py index dd4c44e..018820b 100644 --- a/core/mmd_converter.py +++ b/core/mmd_converter.py @@ -13,6 +13,112 @@ from .translations import t from .mmd.translations import jp_to_en_tuples, translateFromJp +# MMD to Unity bone mapping +# Maps MMD bone names (after English translation) to Unity humanoid bone names +mmd_to_unity_bone_map = { + # Root and core + "ParentNode": None, # Remove this + "Center": "Hips", + "センター": "Hips", + "Groove": None, # Remove this + "グルーブ": None, + "Waist": None, # Will be merged into Hips + + # Spine chain + "LowerBody": "Hips", + "下半身": "Hips", + "UpperBody": "Spine", + "上半身": "Spine", + "UpperBody2": "Chest", + "上半身2": "Chest", + "Neck": "Neck", + "首": "Neck", + "Head": "Head", + "頭": "Head", + + # Right leg + "RightLeg": "Right leg", + "右足": "Right leg", + "RightLegD": None, # Remove D variant + "RightKnee": "Right knee", + "右ひざ": "Right knee", + "RightAnkle": "Right ankle", + "右足首": "Right ankle", + "RightToe": "Right toe", + "右つま先": "Right toe", + + # Left leg + "LeftLeg": "Left leg", + "左足": "Left leg", + "LeftLegD": None, # Remove D variant + "LeftKnee": "Left knee", + "左ひざ": "Left knee", + "LeftAnkle": "Left ankle", + "左足首": "Left ankle", + "LeftToe": "Left toe", + "左つま先": "Left toe", + + # Right arm + "RightShoulder": "Right shoulder", + "右肩": "Right shoulder", + "RightArm": "Right arm", + "右腕": "Right arm", + "RightElbow": "Right elbow", + "右ひじ": "Right elbow", + "RightWrist": "Right wrist", + "右手首": "Right wrist", + + # Left arm + "LeftShoulder": "Left shoulder", + "左肩": "Left shoulder", + "LeftArm": "Left arm", + "左腕": "Left arm", + "LeftElbow": "Left elbow", + "左ひじ": "Left elbow", + "LeftWrist": "Left wrist", + "左手首": "Left wrist", + + # Cancel/Helper bones (remove these) + "WaistCancelRight": None, + "WaistCancelLeft": None, + "LegIKParentRight": None, + "LegIKParentLeft": None, +} + + +# Unity humanoid bone hierarchy +# Defines parent-child relationships for Unity standard +unity_bone_hierarchy = { + "Hips": None, # Root bone + "Spine": "Hips", + "Chest": "Spine", + "Neck": "Chest", + "Head": "Neck", + + # Arms + "Left shoulder": "Chest", + "Left arm": "Left shoulder", + "Left elbow": "Left arm", + "Left wrist": "Left elbow", + + "Right shoulder": "Chest", + "Right arm": "Right shoulder", + "Right elbow": "Right arm", + "Right wrist": "Right elbow", + + # Legs + "Left leg": "Hips", + "Left knee": "Left leg", + "Left ankle": "Left knee", + "Left toe": "Left ankle", + + "Right leg": "Hips", + "Right knee": "Right leg", + "Right ankle": "Right knee", + "Right toe": "Right ankle", +} + + def detect_mmd_armature(armature: Object) -> bool: """Detect if armature uses MMD bone naming conventions""" @@ -441,3 +547,143 @@ def translate_mmd_everything(armature: Object, logger.info(f"Comprehensive MMD translation complete: {total_successful} successful, {total_failed} failed") return total_failed == 0, all_messages + + +def restructure_mmd_to_unity_bones(armature: Object) -> Tuple[bool, List[str]]: + """Restructure MMD bone hierarchy to Unity humanoid format.""" + if not armature or armature.type != 'ARMATURE': + return False, [t("MMD.error.invalid_armature")] + + logger.info(f"Starting MMD to Unity bone restructuring for: {armature.name}") + + messages = [] + renamed_count = 0 + removed_count = 0 + reparented_count = 0 + + # Store the current mode + current_mode = bpy.context.mode + if current_mode != 'EDIT': + bpy.context.view_layer.objects.active = armature + bpy.ops.object.mode_set(mode='EDIT') + + try: + edit_bones = armature.data.edit_bones + bones_to_remove = [] + bone_renames = {} + + # Step 1: Identify and map bones + for bone in edit_bones: + bone_name = bone.name + + # Check if bone should be renamed + unity_name = mmd_to_unity_bone_map.get(bone_name) + + if unity_name is None and bone_name not in mmd_to_unity_bone_map: + # Try to find a match by checking if bone name contains a key + for mmd_name, unity_target in mmd_to_unity_bone_map.items(): + if mmd_name.lower() in bone_name.lower(): + unity_name = unity_target + break + + if unity_name is None: + # Mark for removal + bones_to_remove.append(bone_name) + logger.debug(f"Marking bone for removal: {bone_name}") + elif unity_name != bone_name: + # Mark for rename + bone_renames[bone_name] = unity_name + logger.debug(f"Planning rename: {bone_name} -> {unity_name}") + + # Step 2: Handle bone merging (e.g., LowerBody + Center -> Hips) + unity_bone_sources = {} + for old_name, new_name in bone_renames.items(): + if new_name not in unity_bone_sources: + unity_bone_sources[new_name] = [] + unity_bone_sources[new_name].append(old_name) + + # For bones with multiple sources, keep the first one and remove others + for unity_name, sources in unity_bone_sources.items(): + if len(sources) > 1: + logger.info(f"Multiple bones map to '{unity_name}': {sources}") + # Keep the first, mark others for removal and reparent their children + keep_bone = sources[0] + for source in sources[1:]: + if source in edit_bones: + # Reparent children to the kept bone + bone_to_remove = edit_bones[source] + keep_bone_obj = edit_bones[keep_bone] + for child in bone_to_remove.children: + child.parent = keep_bone_obj + reparented_count += 1 + bones_to_remove.append(source) + if source in bone_renames: + del bone_renames[source] + + # Step 3: Reparent bones to be removed (move children to parent) + for bone_name in bones_to_remove: + if bone_name in edit_bones: + bone = edit_bones[bone_name] + parent_bone = bone.parent + for child in bone.children: + child.parent = parent_bone + reparented_count += 1 + logger.debug(f"Reparented {child.name} from {bone_name} to {parent_bone.name if parent_bone else 'None'}") + + # Step 4: Remove marked bones + for bone_name in bones_to_remove: + if bone_name in edit_bones: + edit_bones.remove(edit_bones[bone_name]) + removed_count += 1 + logger.info(f"Removed bone: {bone_name}") + + # Step 5: Rename bones + for old_name, new_name in bone_renames.items(): + if old_name in edit_bones: + bone = edit_bones[old_name] + try: + bone.name = new_name + renamed_count += 1 + logger.info(f"Renamed bone: {old_name} -> {new_name}") + except Exception as e: + logger.error(f"Failed to rename bone {old_name}: {e}") + + # Step 6: Fix hierarchy to match Unity standard + for bone in edit_bones: + expected_parent_name = unity_bone_hierarchy.get(bone.name) + if expected_parent_name is not None: + # This bone should have a specific parent + if expected_parent_name in edit_bones: + expected_parent = edit_bones[expected_parent_name] + if bone.parent != expected_parent: + bone.parent = expected_parent + reparented_count += 1 + logger.debug(f"Fixed hierarchy: {bone.name} -> parent: {expected_parent_name}") + elif expected_parent_name is None and unity_bone_hierarchy.get(bone.name) is not None: + # This should be a root bone + if bone.parent is not None: + bone.parent = None + reparented_count += 1 + logger.debug(f"Made {bone.name} a root bone") + + except Exception as e: + logger.error(f"Error during bone restructuring: {e}", exc_info=True) + messages.append(t("MMD.restructure_failed", error=str(e))) + return False, messages + + finally: + # Restore original mode + if current_mode != 'EDIT': + bpy.ops.object.mode_set(mode='OBJECT') + + # Generate messages + if renamed_count > 0: + messages.append(t("MMD.bones_restructured", count=renamed_count)) + if removed_count > 0: + messages.append(t("MMD.bones_removed", count=removed_count)) + if reparented_count > 0: + messages.append(t("MMD.bones_reparented", count=reparented_count)) + + logger.info(f"Bone restructuring complete: {renamed_count} renamed, {removed_count} removed, {reparented_count} reparented") + + return True, messages diff --git a/core/properties.py b/core/properties.py index 10ef5d5..0295351 100644 --- a/core/properties.py +++ b/core/properties.py @@ -745,6 +745,12 @@ class AvatarToolkitSceneProperties(PropertyGroup): description="Translate object names", default=True ) + + mmd_restructure_bones: BoolProperty( + name=t("MMD.restructure_bones"), + description="Restructure bone hierarchy to Unity humanoid format (Hips, Spine, Chest, etc.)", + default=True + ) # Translation System Properties translation_service: EnumProperty( diff --git a/functions/tools/mmd_conversion.py b/functions/tools/mmd_conversion.py index dd81254..1704392 100644 --- a/functions/tools/mmd_conversion.py +++ b/functions/tools/mmd_conversion.py @@ -7,7 +7,7 @@ from bpy.types import Operator from ...core.common import get_active_armature from ...core.translations import t from ...core.mmd_converter import (convert_mmd_armature, detect_mmd_armature, - translate_mmd_everything) + translate_mmd_everything, restructure_mmd_to_unity_bones) from ...core.logging_setup import logger @@ -47,8 +47,9 @@ class AvatarToolkit_OT_ConvertMMDArmature(Operator): translate_materials = toolkit.mmd_translate_materials translate_shapekeys = toolkit.mmd_translate_shapekeys translate_objects = toolkit.mmd_translate_objects + restructure_bones = toolkit.mmd_restructure_bones - logger.info(f"Conversion settings - Make parent: {make_parent}, Rename: {rename_armature}") + logger.info(f"Conversion settings - Make parent: {make_parent}, Rename: {rename_armature}, Restructure: {restructure_bones}") logger.info(f"Translation settings - Enabled: {translate_names}, Bones: {translate_bones}, " + f"Materials: {translate_materials}, Shapekeys: {translate_shapekeys}, Objects: {translate_objects}") @@ -86,4 +87,19 @@ class AvatarToolkit_OT_ConvertMMDArmature(Operator): for msg in trans_messages: self.report({'INFO'}, msg) + # Step 3: Restructure bones to Unity format (if enabled) + if restructure_bones: + logger.info("Starting bone restructuring to Unity format") + self.report({'INFO'}, t("MMD.restructure_starting")) + + struct_success, struct_messages = restructure_mmd_to_unity_bones(armature) + + if struct_success: + logger.info("Bone restructuring completed successfully") + else: + logger.warning("Bone restructuring completed with errors") + + for msg in struct_messages: + self.report({'INFO'}, msg) + return {'FINISHED'} diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index c8fb9a2..24ddeb0 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -616,6 +616,7 @@ "MMD.translate_materials": "Materials", "MMD.translate_shapekeys": "Shape Keys", "MMD.translate_objects": "Objects", + "MMD.restructure_bones": "Restructure to Unity Format", "MMD.translation_options": "Translation Options:", "MMD.convert_armature_button": "Convert MMD Armature", "MMD.convert_armature.label": "Convert MMD Armature", @@ -623,6 +624,7 @@ "MMD.conversion_info.title": "Conversion Info:", "MMD.conversion_info.removes_parent": "• Removes parent Empty object", "MMD.conversion_info.renames_armature": "• Renames armature to 'Armature'", + "MMD.conversion_info.restructures_bones": "• Converts to Unity bone structure (Hips/Spine/Chest)", "MMD.conversion_info.maintains_hierarchy": "• Maintains object hierarchy", "MMD.conversion_info.translates_names": "• Translates Japanese names to English", "MMD.detection_failed.title": "MMD Detection Failed:", @@ -650,6 +652,11 @@ "MMD.objects_translated": "Translated {count} objects", "MMD.objects_failed": "Failed to translate {count} objects", "MMD.translation_complete": "Translation complete: {total} items translated", + "MMD.restructure_starting": "Restructuring bones to Unity format...", + "MMD.bones_restructured": "Restructured {count} bones to Unity format", + "MMD.bones_removed": "Removed {count} unnecessary bones", + "MMD.bones_reparented": "Reparented {count} bones", + "MMD.restructure_failed": "Bone restructuring failed: {error}", "Translation.label": "Translation", "Translation.service": "Translation Service", diff --git a/ui/mmd_panel.py b/ui/mmd_panel.py index 43a9100..84e04fa 100644 --- a/ui/mmd_panel.py +++ b/ui/mmd_panel.py @@ -55,6 +55,10 @@ class AvatarToolKit_PT_MMDPanel(Panel): col.prop(toolkit, 'mmd_rename_armature', text=t("MMD.rename_to_armature")) col.separator(factor=0.2) + # Bone restructuring + col.prop(toolkit, 'mmd_restructure_bones', text=t("MMD.restructure_bones")) + col.separator(factor=0.2) + # Translation settings col.prop(toolkit, 'mmd_translate_names', text=t("MMD.translate_names")) @@ -81,6 +85,8 @@ class AvatarToolKit_PT_MMDPanel(Panel): info_col.label(text=t("MMD.conversion_info.title"), icon='INFO') info_col.label(text=t("MMD.conversion_info.removes_parent")) info_col.label(text=t("MMD.conversion_info.renames_armature")) + if toolkit.mmd_restructure_bones: + info_col.label(text=t("MMD.conversion_info.restructures_bones")) info_col.label(text=t("MMD.conversion_info.maintains_hierarchy")) if toolkit.mmd_translate_names: info_col.label(text=t("MMD.conversion_info.translates_names"))