From 95cb7264855a3324b75af657e622d23cbb0c7888 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Sat, 22 Nov 2025 16:39:28 +0000 Subject: [PATCH 1/8] Start of the MMD Converter --- core/enhanced_dictionaries.py | 20 ++++ core/mmd_converter.py | 166 ++++++++++++++++++++++++++++++ core/properties.py | 13 +++ functions/tools/mmd_conversion.py | 58 +++++++++++ resources/translations/en_US.json | 33 ++++++ ui/mmd_panel.py | 87 ++++++++++++++++ ui/panel_layout.py | 5 +- 7 files changed, 381 insertions(+), 1 deletion(-) create mode 100644 core/mmd_converter.py create mode 100644 functions/tools/mmd_conversion.py create mode 100644 ui/mmd_panel.py diff --git a/core/enhanced_dictionaries.py b/core/enhanced_dictionaries.py index e35c9a7..889ffe9 100644 --- a/core/enhanced_dictionaries.py +++ b/core/enhanced_dictionaries.py @@ -174,6 +174,26 @@ physics_names: Dict[str, List[str]] = { "breast_tip": ["胸先", "むねさき", "ブレストティップ", "breasttip"], } +# MMD bone name patterns (for detection) +mmd_bone_patterns: List[str] = [ + # Japanese bone names + '全ての親', 'センター', '上半身', '下半身', '首', '頭', + '右腕', '左腕', '右ひじ', '左ひじ', '右手首', '左手首', + '右足', '左足', '右ひざ', '左ひざ', '右足首', '左足首', + '両目', '左目', '右目', '右肩', '左肩', + # English bone names (common in MMD exports) + 'center', 'groove', 'waist', 'upperbody', 'upperbody2', 'lowerbody', + 'neck', 'head', + 'shoulder_r', 'shoulder_l', 'arm_r', 'arm_l', + 'elbow_r', 'elbow_l', 'wrist_r', 'wrist_l', + 'leg_r', 'leg_l', 'knee_r', 'knee_l', + 'ankle_r', 'ankle_l', 'toe_r', 'toe_l', + # Mixed/Romanized patterns + '센터', 'グルーブ', 'ウエスト', + # Common MMD suffixes + '_r', '_l', '.r', '.l' +] + # Create reverse lookup dictionaries reverse_shapekey_lookup: Dict[str, str] = {} reverse_material_lookup: Dict[str, str] = {} diff --git a/core/mmd_converter.py b/core/mmd_converter.py new file mode 100644 index 0000000..d260097 --- /dev/null +++ b/core/mmd_converter.py @@ -0,0 +1,166 @@ +""" +MMD Converter - Core conversion logic for MMD models +Handles armature hierarchy and naming conventions +""" +import bpy +from typing import Dict, List, Optional, Tuple, Set +from bpy.types import Object, Bone, Collection +from .common import get_active_armature +from .dictionaries import simplify_bonename +from .enhanced_dictionaries import mmd_bone_patterns +from .logging_setup import logger +from .translations import t + + +def detect_mmd_armature(armature: Object) -> bool: + """Detect if armature uses MMD bone naming conventions""" + + if not armature or armature.type != 'ARMATURE': + return False + + found_mmd_bones = 0 + for bone in armature.data.bones: + bone_name_lower = bone.name.lower() + if any(pattern.lower() in bone_name_lower for pattern in mmd_bone_patterns): + found_mmd_bones += 1 + logger.debug(f"Found MMD bone: {bone.name}") + + # Consider it MMD if we find at least 5 MMD bones + logger.debug(f"Found {found_mmd_bones} MMD bones in armature {armature.name}") + return found_mmd_bones >= 5 + + +def get_armature_parent_object(armature: Object) -> Optional[Object]: + """Get the parent object of the armature (typically an Empty in MMD imports)""" + if armature and armature.parent: + return armature.parent + return None + + +def make_armature_main_parent(armature: Object) -> Tuple[bool, str]: + """Make the armature the main parent object by removing any parent empties + and reparenting all children to the armature.""" + + if not armature or armature.type != 'ARMATURE': + return False, t("MMD.error.invalid_armature") + + logger.info(f"Making armature '{armature.name}' the main parent") + + # Store original parent + original_parent = armature.parent + + if not original_parent: + logger.info("Armature already has no parent") + return True, t("MMD.armature_already_root") + + parent_name = original_parent.name + parent_type = original_parent.type + + logger.info(f"Found parent: {parent_name} (type: {parent_type})") + + # Get all children of the parent + siblings = [child for child in original_parent.children if child != armature] + + armature.parent = None + + # Reparent siblings to the armature + reparented_count = 0 + for sibling in siblings: + sibling.parent = armature + reparented_count += 1 + logger.debug(f"Reparented {sibling.name} to armature") + + # If the parent was an Empty and now has no children, remove it + if parent_type == 'EMPTY' and len(original_parent.children) == 0: + try: + bpy.data.objects.remove(original_parent, do_unlink=True) + logger.info(f"Removed empty parent object: {parent_name}") + message = t("MMD.parent_removed_and_reparented", + parent_name=parent_name, + count=reparented_count) + except Exception as e: + logger.warning(f"Could not remove parent empty: {str(e)}") + message = t("MMD.parent_unlinked_and_reparented", + parent_name=parent_name, + count=reparented_count) + else: + message = t("MMD.parent_unlinked", parent_name=parent_name) + + logger.info(f"Successfully made armature the main parent. Reparented {reparented_count} objects") + return True, message + + +def rename_armature_to_standard(armature: Object) -> Tuple[bool, str]: + """Rename the armature object to 'Armature' (standard Blender convention)""" + if not armature or armature.type != 'ARMATURE': + return False, t("MMD.error.invalid_armature") + + old_name = armature.name + + # Check if already named 'Armature' + if old_name == 'Armature': + logger.info("Armature already named 'Armature'") + return True, t("MMD.armature_already_named") + + logger.info(f"Renaming armature from '{old_name}' to 'Armature'") + + try: + armature.name = 'Armature' + # Blender might append .001 if name exists, check actual result (Wonder if needed) + actual_name = armature.name + + if actual_name == 'Armature': + message = t("MMD.armature_renamed", old_name=old_name, new_name='Armature') + else: + message = t("MMD.armature_renamed_with_suffix", + old_name=old_name, + new_name=actual_name) + logger.warning(f"Name collision, armature named: {actual_name}") + + logger.info(f"Successfully renamed armature to: {actual_name}") + return True, message + + except Exception as e: + logger.error(f"Failed to rename armature: {str(e)}") + return False, t("MMD.error.rename_failed", error=str(e)) + + +def convert_mmd_armature(armature: Object, + make_parent: bool = True, + rename_armature: bool = True) -> Tuple[bool, List[str]]: + """Convert MMD armature to standard Blender format""" + if not armature or armature.type != 'ARMATURE': + return False, [t("MMD.error.invalid_armature")] + + logger.info(f"Starting MMD armature conversion for: {armature.name}") + + # Check if this is an MMD armature + if not detect_mmd_armature(armature): + return False, [t("MMD.error.not_mmd_armature")] + + messages = [] + overall_success = True + + # Step 1: Make armature the main parent + if make_parent: + success, message = make_armature_main_parent(armature) + messages.append(message) + if not success: + overall_success = False + logger.warning("Failed to make armature main parent") + + # Step 2: Rename armature + if rename_armature: + success, message = rename_armature_to_standard(armature) + messages.append(message) + if not success: + overall_success = False + logger.warning("Failed to rename armature") + + if overall_success: + logger.info("MMD armature conversion completed successfully") + messages.append(t("MMD.conversion_complete")) + else: + logger.warning("MMD armature conversion completed with errors") + + return overall_success, messages diff --git a/core/properties.py b/core/properties.py index 7c14f56..5c9dbab 100644 --- a/core/properties.py +++ b/core/properties.py @@ -703,6 +703,19 @@ class AvatarToolkitSceneProperties(PropertyGroup): default=True ) + # MMD Conversion Properties + mmd_make_parent: BoolProperty( + name=t("MMD.make_armature_parent"), + description="Remove parent Empty object and make armature the main parent", + default=True + ) + + mmd_rename_armature: BoolProperty( + name=t("MMD.rename_to_armature"), + description="Rename the armature object to 'Armature'", + default=True + ) + # Translation System Properties translation_service: EnumProperty( name=t("Translation.service"), diff --git a/functions/tools/mmd_conversion.py b/functions/tools/mmd_conversion.py new file mode 100644 index 0000000..0f238d6 --- /dev/null +++ b/functions/tools/mmd_conversion.py @@ -0,0 +1,58 @@ +""" +MMD Conversion Operator +Converts MMD armatures to standard Blender format +""" +import bpy +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 +from ...core.logging_setup import logger + + +class AvatarToolkit_OT_ConvertMMDArmature(Operator): + """Convert MMD armature to standard Blender format""" + bl_idname = "avatar_toolkit.convert_mmd_armature" + bl_label = t("MMD.convert_armature.label") + bl_description = t("MMD.convert_armature.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 MMD conversion") + self.report({'ERROR'}, t("MMD.no_armature_selected")) + return {'CANCELLED'} + + logger.info(f"Starting MMD conversion for armature: {armature.name}") + + # Check if it's an MMD armature + if not detect_mmd_armature(armature): + logger.warning(f"Armature '{armature.name}' does not appear to be an MMD armature") + self.report({'WARNING'}, t("MMD.not_mmd_armature")) + return {'CANCELLED'} + + # conversion settings + toolkit = context.scene.avatar_toolkit + make_parent = toolkit.mmd_make_parent + rename_armature = toolkit.mmd_rename_armature + + logger.info(f"Conversion settings - Make parent: {make_parent}, Rename: {rename_armature}") + success, messages = convert_mmd_armature(armature, make_parent, rename_armature) + + if not success: + logger.warning(f"MMD conversion failed: {messages}") + for msg in messages: + self.report({'WARNING'}, msg) + return {'CANCELLED'} + + logger.info(f"MMD conversion completed successfully") + for msg in messages: + self.report({'INFO'}, msg) + + return {'FINISHED'} diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index bcb8ec5..80a1f3b 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -601,6 +601,39 @@ "VRM.remove_root": "Remove Root Bone", "VRM.remove_root_desc": "Remove unnecessary VRM root bone and make Hips the root bone", + "MMD.panel.label": "MMD Converter", + "MMD.converter.title": "MMD Armature Converter", + "MMD.no_armature_selected": "No armature selected", + "MMD.select_armature_to_convert": "Select an armature to convert", + "MMD.armature_name": "Armature: {name}", + "MMD.armature_detected": "MMD armature detected", + "MMD.no_mmd_bones_detected": "No MMD bones detected", + "MMD.not_mmd_armature": "Selected armature does not appear to be MMD format", + "MMD.make_armature_parent": "Make Armature Main Parent", + "MMD.rename_to_armature": "Rename to 'Armature'", + "MMD.convert_armature_button": "Convert MMD Armature", + "MMD.convert_armature.label": "Convert MMD Armature", + "MMD.convert_armature.desc": "Convert MMD armature to standard Blender format", + "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.maintains_hierarchy": "• Maintains object hierarchy", + "MMD.detection_failed.title": "MMD Detection Failed:", + "MMD.detection_failed.not_mmd_format": "• Selected armature is not MMD format", + "MMD.detection_failed.need_mmd_bones": "• Need at least 5 MMD bones detected", + "MMD.detection_failed.check_bone_names": "• Check armature bone names", + "MMD.error.invalid_armature": "Invalid armature object", + "MMD.error.not_mmd_armature": "Armature does not appear to be MMD format", + "MMD.error.rename_failed": "Failed to rename armature: {error}", + "MMD.armature_already_root": "Armature already has no parent", + "MMD.armature_already_named": "Armature is already named 'Armature'", + "MMD.parent_removed_and_reparented": "Removed parent '{parent_name}' and reparented {count} objects to armature", + "MMD.parent_unlinked_and_reparented": "Unlinked from parent '{parent_name}' and reparented {count} objects", + "MMD.parent_unlinked": "Unlinked armature from parent '{parent_name}'", + "MMD.armature_renamed": "Renamed armature from '{old_name}' to '{new_name}'", + "MMD.armature_renamed_with_suffix": "Renamed armature from '{old_name}' to '{new_name}' (name collision)", + "MMD.conversion_complete": "MMD armature conversion completed successfully", + "Translation.label": "Translation", "Translation.service": "Translation Service", "Translation.service_desc": "Choose the translation service to use", diff --git a/ui/mmd_panel.py b/ui/mmd_panel.py new file mode 100644 index 0000000..39baf4f --- /dev/null +++ b/ui/mmd_panel.py @@ -0,0 +1,87 @@ +""" +MMD Converter Panel - UI for MMD conversion tools +""" +import bpy +from bpy.types import Panel, Context, UILayout +from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME +from .panel_layout import get_panel_order, should_open_by_default +from ..core.translations import t +from ..core.common import get_active_armature +from ..core.mmd_converter import detect_mmd_armature +from ..functions.tools.mmd_conversion import AvatarToolkit_OT_ConvertMMDArmature + + +class AvatarToolKit_PT_MMDPanel(Panel): + """Panel for MMD conversion tools""" + bl_label = t("MMD.panel.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 = get_panel_order('mmd') + bl_options = set() if not should_open_by_default('MMD') else {'DEFAULT_CLOSED'} + + def draw(self, context: Context) -> None: + """Draw the MMD conversion panel interface""" + layout: UILayout = self.layout + + # MMD Conversion Tools + mmd_box: UILayout = layout.box() + col: UILayout = mmd_box.column(align=True) + col.label(text=t("MMD.converter.title"), icon='ARMATURE_DATA') + col.separator(factor=0.5) + + # Check if we have an active armature + armature = get_active_armature(context) + + if not armature: + col.label(text=t("MMD.no_armature_selected"), icon='ERROR') + col.label(text=t("MMD.select_armature_to_convert")) + return + + # Check if the armature appears to be MMD + is_mmd = detect_mmd_armature(armature) + + if is_mmd: + col.label(text=t("MMD.armature_name", name=armature.name), icon='CHECKMARK') + col.label(text=t("MMD.armature_detected"), icon='INFO') + col.separator(factor=0.3) + + toolkit = context.scene.avatar_toolkit + col.prop(toolkit, 'mmd_make_parent', text=t("MMD.make_armature_parent")) + col.prop(toolkit, 'mmd_rename_armature', text=t("MMD.rename_to_armature")) + col.separator(factor=0.2) + + col.operator( + AvatarToolkit_OT_ConvertMMDArmature.bl_idname, + text=t("MMD.convert_armature_button"), + icon='EXPORT' + ) + + info_box = mmd_box.box() + info_col = info_box.column(align=True) + 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")) + info_col.label(text=t("MMD.conversion_info.maintains_hierarchy")) + + else: + col.label(text=t("MMD.armature_name", name=armature.name), icon='ERROR') + col.label(text=t("MMD.no_mmd_bones_detected"), icon='CANCEL') + col.separator(factor=0.3) + + row = col.row() + row.enabled = False + row.operator( + AvatarToolkit_OT_ConvertMMDArmature.bl_idname, + text=t("MMD.convert_armature_button"), + icon='CANCEL' + ) + + help_box = mmd_box.box() + help_col = help_box.column(align=True) + help_col.label(text=t("MMD.detection_failed.title"), icon='QUESTION') + help_col.label(text=t("MMD.detection_failed.not_mmd_format")) + help_col.label(text=t("MMD.detection_failed.need_mmd_bones")) + help_col.label(text=t("MMD.detection_failed.check_bone_names")) diff --git a/ui/panel_layout.py b/ui/panel_layout.py index 90ccc32..b6e36cf 100644 --- a/ui/panel_layout.py +++ b/ui/panel_layout.py @@ -14,7 +14,8 @@ VISEMES_ORDER = 6 EYE_TRACKING_ORDER = 7 TEXTURE_ATLAS_ORDER = 8 VRM_UNITY_ORDER = 9 -SETTINGS_ORDER = 10 +MMD_ORDER = 10 +SETTINGS_ORDER = 11 # Panel open/closed by default PANELS_OPEN_BY_DEFAULT = { @@ -27,6 +28,7 @@ PANELS_OPEN_BY_DEFAULT = { 'EYE_TRACKING': True, 'TEXTURE_ATLAS': True, 'VRM_UNITY': True, + 'MMD': True, 'SETTINGS': True, 'TRANSLATION': True, } @@ -44,6 +46,7 @@ def get_panel_order(panel_name: str) -> int: 'eye_tracking': EYE_TRACKING_ORDER, 'texture_atlas': TEXTURE_ATLAS_ORDER, 'vrm_unity': VRM_UNITY_ORDER, + 'mmd': MMD_ORDER, 'settings': SETTINGS_ORDER, } return order_map.get(panel_name.lower(), 99) From 53d2ac10b70d72e713839497777e45b5079a9111 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Sat, 22 Nov 2025 16:57:26 +0000 Subject: [PATCH 2/8] Add bone Translation - Uses MMD Tools Dictionary to convert things into English then uses Translation service to do the rest This i useful for the rest of our converter, it's better to have standard english names then trying to use a service first as each service translate things differnetly my orignal approach was bad due to this. --- core/mmd_converter.py | 279 +++++++++++++++++++++++++++++- core/properties.py | 30 ++++ functions/tools/mmd_conversion.py | 37 +++- resources/translations/en_US.json | 17 ++ ui/mmd_panel.py | 19 ++ 5 files changed, 378 insertions(+), 4 deletions(-) diff --git a/core/mmd_converter.py b/core/mmd_converter.py index d260097..dd4c44e 100644 --- a/core/mmd_converter.py +++ b/core/mmd_converter.py @@ -4,12 +4,13 @@ Handles armature hierarchy and naming conventions """ import bpy from typing import Dict, List, Optional, Tuple, Set -from bpy.types import Object, Bone, Collection +from bpy.types import Object, Bone, Collection, Material, ShapeKey from .common import get_active_armature from .dictionaries import simplify_bonename from .enhanced_dictionaries import mmd_bone_patterns from .logging_setup import logger from .translations import t +from .mmd.translations import jp_to_en_tuples, translateFromJp def detect_mmd_armature(armature: Object) -> bool: @@ -164,3 +165,279 @@ def convert_mmd_armature(armature: Object, logger.warning("MMD armature conversion completed with errors") return overall_success, messages + + +def translate_mmd_name(name: str, category: str = "auto") -> Tuple[str, str]: + """Translate MMD name using MMD dictionary first, then translation services""" + if not name or not name.strip(): + return name, "unchanged" + + original_name = name.strip() + + # Step 1: Try MMD built-in dictionary translation + mmd_translated = translateFromJp(original_name) + + # Check if MMD dictionary actually translated something + if mmd_translated != original_name and mmd_translated: + logger.debug(f"MMD dictionary translated: '{original_name}' -> '{mmd_translated}'") + return mmd_translated, "mmd_dictionary" + + # Step 2: If MMD dictionary didn't translate or only partially translated, + # use Avatar Toolkit translation services + try: + from .translation_manager import get_avatar_translation_manager + + manager = get_avatar_translation_manager() + result = manager.translate_single(original_name, category=category, source_lang="ja", target_lang="en") + + if result.translated != original_name: + logger.debug(f"API translated: '{original_name}' -> '{result.translated}' (method: {result.method})") + return result.translated, "api_translation" + except Exception as e: + logger.warning(f"Translation service failed for '{original_name}': {e}") + + # Step 3: No translation available + logger.debug(f"No translation available for: '{original_name}'") + return original_name, "unchanged" + + +def translate_mmd_armature_bones(armature: Object, apply_translation: bool = True) -> Tuple[int, int, List[str]]: + """Translate all bone names in an MMD armature""" + if not armature or armature.type != 'ARMATURE': + return 0, 0, [t("MMD.error.invalid_armature")] + + logger.info(f"Starting bone translation for armature: {armature.name}") + + successful = 0 + failed = 0 + messages = [] + bone_translations = {} + + # 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: + for bone in armature.data.edit_bones: + original_name = bone.name + translated_name, method = translate_mmd_name(original_name, category="bones") + + if translated_name != original_name: + bone_translations[original_name] = (translated_name, method) + + if apply_translation: + try: + bone.name = translated_name + logger.info(f"Translated bone: '{original_name}' -> '{translated_name}' ({method})") + successful += 1 + except Exception as e: + logger.error(f"Failed to rename bone '{original_name}': {e}") + failed += 1 + else: + successful += 1 + else: + logger.debug(f"Bone '{original_name}' not translated") + + finally: + # Restore original mode + if current_mode != 'EDIT': + bpy.ops.object.mode_set(mode='OBJECT') + + # Generate summary messages + if successful > 0: + messages.append(t("MMD.bones_translated", count=successful)) + if failed > 0: + messages.append(t("MMD.bones_failed", count=failed)) + + mmd_dict_count = sum(1 for _, (_, method) in bone_translations.items() if method == "mmd_dictionary") + api_count = sum(1 for _, (_, method) in bone_translations.items() if method == "api_translation") + + logger.info(f"Bone translation complete: {successful} successful, {failed} failed") + logger.info(f"Translation methods: MMD Dictionary: {mmd_dict_count}, API: {api_count}") + + return successful, failed, messages + + +def translate_mmd_materials(armature: Object, apply_translation: bool = True) -> Tuple[int, int, List[str]]: + """Translate all material names for meshes parented to the armature""" + if not armature or armature.type != 'ARMATURE': + return 0, 0, [t("MMD.error.invalid_armature")] + + logger.info(f"Starting material translation for armature: {armature.name}") + + successful = 0 + failed = 0 + messages = [] + processed_materials = set() + + # Get all mesh objects parented to this armature + for obj in bpy.data.objects: + if obj.type == 'MESH' and obj.parent == armature and obj.data.materials: + for mat in obj.data.materials: + if mat and mat.name not in processed_materials: + processed_materials.add(mat.name) + original_name = mat.name + translated_name, method = translate_mmd_name(original_name, category="materials") + + if translated_name != original_name and apply_translation: + try: + mat.name = translated_name + logger.info(f"Translated material: '{original_name}' -> '{translated_name}' ({method})") + successful += 1 + except Exception as e: + logger.error(f"Failed to rename material '{original_name}': {e}") + failed += 1 + elif translated_name != original_name: + successful += 1 + + if successful > 0: + messages.append(t("MMD.materials_translated", count=successful)) + if failed > 0: + messages.append(t("MMD.materials_failed", count=failed)) + + logger.info(f"Material translation complete: {successful} successful, {failed} failed") + + return successful, failed, messages + + +def translate_mmd_shapekeys(armature: Object, apply_translation: bool = True) -> Tuple[int, int, List[str]]: + """Translate all shape key names for meshes parented to the armature""" + if not armature or armature.type != 'ARMATURE': + return 0, 0, [t("MMD.error.invalid_armature")] + + logger.info(f"Starting shape key translation for armature: {armature.name}") + + successful = 0 + failed = 0 + messages = [] + + # Get all mesh objects parented to this armature + for obj in bpy.data.objects: + if obj.type == 'MESH' and obj.parent == armature and obj.data.shape_keys: + for shape_key in obj.data.shape_keys.key_blocks: + original_name = shape_key.name + translated_name, method = translate_mmd_name(original_name, category="shapekeys") + + if translated_name != original_name and apply_translation: + try: + shape_key.name = translated_name + logger.info(f"Translated shape key: '{original_name}' -> '{translated_name}' ({method})") + successful += 1 + except Exception as e: + logger.error(f"Failed to rename shape key '{original_name}': {e}") + failed += 1 + elif translated_name != original_name: + successful += 1 + + if successful > 0: + messages.append(t("MMD.shapekeys_translated", count=successful)) + if failed > 0: + messages.append(t("MMD.shapekeys_failed", count=failed)) + + logger.info(f"Shape key translation complete: {successful} successful, {failed} failed") + + return successful, failed, messages + + +def translate_mmd_objects(armature: Object, apply_translation: bool = True) -> Tuple[int, int, List[str]]: + """Translate object names parented to the armature""" + if not armature or armature.type != 'ARMATURE': + return 0, 0, [t("MMD.error.invalid_armature")] + + logger.info(f"Starting object name translation for armature: {armature.name}") + + successful = 0 + failed = 0 + messages = [] + + # Get all objects parented to this armature + for obj in bpy.data.objects: + if obj.parent == armature: + original_name = obj.name + translated_name, method = translate_mmd_name(original_name, category="objects") + + if translated_name != original_name and apply_translation: + try: + obj.name = translated_name + logger.info(f"Translated object: '{original_name}' -> '{translated_name}' ({method})") + successful += 1 + except Exception as e: + logger.error(f"Failed to rename object '{original_name}': {e}") + failed += 1 + elif translated_name != original_name: + successful += 1 + + if successful > 0: + messages.append(t("MMD.objects_translated", count=successful)) + if failed > 0: + messages.append(t("MMD.objects_failed", count=failed)) + + logger.info(f"Object translation complete: {successful} successful, {failed} failed") + + return successful, failed, messages + + +def translate_mmd_everything(armature: Object, + translate_bones: bool = True, + translate_materials: bool = True, + translate_shapekeys: bool = True, + translate_objects: bool = True) -> Tuple[bool, List[str]]: + """ + Translate all MMD names (bones, materials, shape keys, objects) + + Args: + armature: The armature object + translate_bones: Whether to translate bone names + translate_materials: Whether to translate material names + translate_shapekeys: Whether to translate shape key names + translate_objects: Whether to translate object names + + Returns: + Tuple of (success, messages) + """ + if not armature or armature.type != 'ARMATURE': + return False, [t("MMD.error.invalid_armature")] + + logger.info(f"Starting comprehensive MMD translation for: {armature.name}") + + all_messages = [] + total_successful = 0 + total_failed = 0 + + # Translate bones + if translate_bones: + success, failed, messages = translate_mmd_armature_bones(armature, apply_translation=True) + total_successful += success + total_failed += failed + all_messages.extend(messages) + + # Translate materials + if translate_materials: + success, failed, messages = translate_mmd_materials(armature, apply_translation=True) + total_successful += success + total_failed += failed + all_messages.extend(messages) + + # Translate shape keys + if translate_shapekeys: + success, failed, messages = translate_mmd_shapekeys(armature, apply_translation=True) + total_successful += success + total_failed += failed + all_messages.extend(messages) + + # Translate objects + if translate_objects: + success, failed, messages = translate_mmd_objects(armature, apply_translation=True) + total_successful += success + total_failed += failed + all_messages.extend(messages) + + # Summary + if total_successful > 0: + all_messages.append(t("MMD.translation_complete", total=total_successful)) + + logger.info(f"Comprehensive MMD translation complete: {total_successful} successful, {total_failed} failed") + + return total_failed == 0, all_messages diff --git a/core/properties.py b/core/properties.py index 5c9dbab..10ef5d5 100644 --- a/core/properties.py +++ b/core/properties.py @@ -715,6 +715,36 @@ class AvatarToolkitSceneProperties(PropertyGroup): description="Rename the armature object to 'Armature'", default=True ) + + mmd_translate_names: BoolProperty( + name=t("MMD.translate_names"), + description="Translate Japanese names to English using MMD dictionary and translation services", + default=True + ) + + mmd_translate_bones: BoolProperty( + name=t("MMD.translate_bones"), + description="Translate bone names", + default=True + ) + + mmd_translate_materials: BoolProperty( + name=t("MMD.translate_materials"), + description="Translate material names", + default=True + ) + + mmd_translate_shapekeys: BoolProperty( + name=t("MMD.translate_shapekeys"), + description="Translate shape key names", + default=True + ) + + mmd_translate_objects: BoolProperty( + name=t("MMD.translate_objects"), + description="Translate object names", + default=True + ) # Translation System Properties translation_service: EnumProperty( diff --git a/functions/tools/mmd_conversion.py b/functions/tools/mmd_conversion.py index 0f238d6..dd81254 100644 --- a/functions/tools/mmd_conversion.py +++ b/functions/tools/mmd_conversion.py @@ -6,7 +6,8 @@ import bpy 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 +from ...core.mmd_converter import (convert_mmd_armature, detect_mmd_armature, + translate_mmd_everything) from ...core.logging_setup import logger @@ -37,12 +38,21 @@ class AvatarToolkit_OT_ConvertMMDArmature(Operator): self.report({'WARNING'}, t("MMD.not_mmd_armature")) return {'CANCELLED'} - # conversion settings + # Get conversion settings toolkit = context.scene.avatar_toolkit make_parent = toolkit.mmd_make_parent rename_armature = toolkit.mmd_rename_armature + translate_names = toolkit.mmd_translate_names + translate_bones = toolkit.mmd_translate_bones + translate_materials = toolkit.mmd_translate_materials + translate_shapekeys = toolkit.mmd_translate_shapekeys + translate_objects = toolkit.mmd_translate_objects logger.info(f"Conversion settings - Make parent: {make_parent}, Rename: {rename_armature}") + logger.info(f"Translation settings - Enabled: {translate_names}, Bones: {translate_bones}, " + + f"Materials: {translate_materials}, Shapekeys: {translate_shapekeys}, Objects: {translate_objects}") + + # Step 1: Basic conversion (parent and rename) success, messages = convert_mmd_armature(armature, make_parent, rename_armature) if not success: @@ -51,8 +61,29 @@ class AvatarToolkit_OT_ConvertMMDArmature(Operator): self.report({'WARNING'}, msg) return {'CANCELLED'} - logger.info(f"MMD conversion completed successfully") + logger.info(f"MMD basic conversion completed successfully") for msg in messages: self.report({'INFO'}, msg) + # Step 2: Translation (if enabled) + if translate_names: + logger.info("Starting MMD name translation") + self.report({'INFO'}, t("MMD.translation_starting")) + + trans_success, trans_messages = translate_mmd_everything( + armature, + translate_bones=translate_bones, + translate_materials=translate_materials, + translate_shapekeys=translate_shapekeys, + translate_objects=translate_objects + ) + + if trans_success: + logger.info("MMD name translation completed successfully") + else: + logger.warning("MMD name translation completed with some failures") + + for msg in trans_messages: + self.report({'INFO'}, msg) + return {'FINISHED'} diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index 80a1f3b..c8fb9a2 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -611,6 +611,12 @@ "MMD.not_mmd_armature": "Selected armature does not appear to be MMD format", "MMD.make_armature_parent": "Make Armature Main Parent", "MMD.rename_to_armature": "Rename to 'Armature'", + "MMD.translate_names": "Translate Names to English", + "MMD.translate_bones": "Bones", + "MMD.translate_materials": "Materials", + "MMD.translate_shapekeys": "Shape Keys", + "MMD.translate_objects": "Objects", + "MMD.translation_options": "Translation Options:", "MMD.convert_armature_button": "Convert MMD Armature", "MMD.convert_armature.label": "Convert MMD Armature", "MMD.convert_armature.desc": "Convert MMD armature to standard Blender format", @@ -618,6 +624,7 @@ "MMD.conversion_info.removes_parent": "• Removes parent Empty object", "MMD.conversion_info.renames_armature": "• Renames armature to 'Armature'", "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:", "MMD.detection_failed.not_mmd_format": "• Selected armature is not MMD format", "MMD.detection_failed.need_mmd_bones": "• Need at least 5 MMD bones detected", @@ -633,6 +640,16 @@ "MMD.armature_renamed": "Renamed armature from '{old_name}' to '{new_name}'", "MMD.armature_renamed_with_suffix": "Renamed armature from '{old_name}' to '{new_name}' (name collision)", "MMD.conversion_complete": "MMD armature conversion completed successfully", + "MMD.translation_starting": "Starting name translation...", + "MMD.bones_translated": "Translated {count} bones", + "MMD.bones_failed": "Failed to translate {count} bones", + "MMD.materials_translated": "Translated {count} materials", + "MMD.materials_failed": "Failed to translate {count} materials", + "MMD.shapekeys_translated": "Translated {count} shape keys", + "MMD.shapekeys_failed": "Failed to translate {count} shape keys", + "MMD.objects_translated": "Translated {count} objects", + "MMD.objects_failed": "Failed to translate {count} objects", + "MMD.translation_complete": "Translation complete: {total} items translated", "Translation.label": "Translation", "Translation.service": "Translation Service", diff --git a/ui/mmd_panel.py b/ui/mmd_panel.py index 39baf4f..43a9100 100644 --- a/ui/mmd_panel.py +++ b/ui/mmd_panel.py @@ -49,10 +49,27 @@ class AvatarToolKit_PT_MMDPanel(Panel): col.separator(factor=0.3) toolkit = context.scene.avatar_toolkit + + # Basic conversion settings col.prop(toolkit, 'mmd_make_parent', text=t("MMD.make_armature_parent")) col.prop(toolkit, 'mmd_rename_armature', text=t("MMD.rename_to_armature")) col.separator(factor=0.2) + # Translation settings + col.prop(toolkit, 'mmd_translate_names', text=t("MMD.translate_names")) + + # Translation sub-options (only show if translation is enabled) + if toolkit.mmd_translate_names: + trans_box = col.box() + trans_col = trans_box.column(align=True) + trans_col.label(text=t("MMD.translation_options"), icon='WORLD') + trans_col.prop(toolkit, 'mmd_translate_bones', text=t("MMD.translate_bones")) + trans_col.prop(toolkit, 'mmd_translate_materials', text=t("MMD.translate_materials")) + trans_col.prop(toolkit, 'mmd_translate_shapekeys', text=t("MMD.translate_shapekeys")) + trans_col.prop(toolkit, 'mmd_translate_objects', text=t("MMD.translate_objects")) + + col.separator(factor=0.2) + col.operator( AvatarToolkit_OT_ConvertMMDArmature.bl_idname, text=t("MMD.convert_armature_button"), @@ -65,6 +82,8 @@ class AvatarToolKit_PT_MMDPanel(Panel): info_col.label(text=t("MMD.conversion_info.removes_parent")) info_col.label(text=t("MMD.conversion_info.renames_armature")) 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")) else: col.label(text=t("MMD.armature_name", name=armature.name), icon='ERROR') From 84bacca92363ab58c83768ad47211aaa15bc2b35 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Sat, 22 Nov 2025 17:49:09 +0000 Subject: [PATCH 3/8] Armature Re-strutring and etc --- core/mmd_converter.py | 246 ++++++++++++++++++++++++++++++ core/properties.py | 6 + functions/tools/mmd_conversion.py | 20 ++- resources/translations/en_US.json | 7 + ui/mmd_panel.py | 6 + 5 files changed, 283 insertions(+), 2 deletions(-) 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")) From b13ca15eced0f253cb2ce435bcfc6e05c33d1737 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Sun, 23 Nov 2025 02:21:30 +0000 Subject: [PATCH 4/8] Removal of IK bones and etc, zero weight bones and more --- core/enhanced_dictionaries.py | 104 +++++ core/mmd_converter.py | 642 ++++++++++++++++++++++++------ core/properties.py | 12 + functions/tools/mmd_conversion.py | 57 ++- resources/translations/en_US.json | 16 + ui/mmd_panel.py | 13 + 6 files changed, 720 insertions(+), 124 deletions(-) diff --git a/core/enhanced_dictionaries.py b/core/enhanced_dictionaries.py index 889ffe9..f641a4c 100644 --- a/core/enhanced_dictionaries.py +++ b/core/enhanced_dictionaries.py @@ -194,6 +194,110 @@ mmd_bone_patterns: List[str] = [ '_r', '_l', '.r', '.l' ] +# MMD to Unity bone mapping +# Maps MMD bone names (after English translation) to Unity humanoid bone names +mmd_to_unity_bone_map: Dict[str, Optional[str]] = { + # 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: Dict[str, Optional[str]] = { + "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", +} + # Create reverse lookup dictionaries reverse_shapekey_lookup: Dict[str, str] = {} reverse_material_lookup: Dict[str, str] = {} diff --git a/core/mmd_converter.py b/core/mmd_converter.py index 018820b..81d6b53 100644 --- a/core/mmd_converter.py +++ b/core/mmd_converter.py @@ -3,122 +3,17 @@ MMD Converter - Core conversion logic for MMD models Handles armature hierarchy and naming conventions """ import bpy +import re from typing import Dict, List, Optional, Tuple, Set from bpy.types import Object, Bone, Collection, Material, ShapeKey from .common import get_active_armature from .dictionaries import simplify_bonename -from .enhanced_dictionaries import mmd_bone_patterns +from .enhanced_dictionaries import mmd_bone_patterns, mmd_to_unity_bone_map, unity_bone_hierarchy from .logging_setup import logger 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""" @@ -572,26 +467,50 @@ def restructure_mmd_to_unity_bones(armature: Object) -> Tuple[bool, List[str]]: bones_to_remove = [] bone_renames = {} - # Step 1: Identify and map bones + # Protected bone name patterns (never remove or modify these) + protected_patterns = [ + r'.*[bB]reast.*', r'.*[bB]ust.*', r'.*[tT]its.*', # Breast bones + r'.*[sS]kirt.*', # Skirt bones + r'.*[hH]air.*', # Hair bones + r'.*[bB]ag.*', # Bag/accessory bones + r'.*[rR]ibbon.*', # Ribbon bones + r'.*[tT]ail.*', # Tail bones + r'.*[wW]ing.*', # Wing bones + r'.*[eE]ar.*', # Ear bones + r'.*[sS]leeve.*', # Sleeve bones + r'.*[cC]ape.*', r'.*[sS]carf.*', # Cape/Scarf bones + r'.*[cC]oat.*', r'.*[dD]ress.*', # Coat/Dress bones + r'.*[fF]inger.*', r'.*[tT]humb.*', # Finger bones + r'.*[aA]ccessor.*', # Accessory bones + r'.*[jJ]oint.*', # Joint bones + r'.*[cC]loth.*', r'.*[pP]hys.*', # Cloth/Physics bones + r'^tf_.*', # tf_ prefixed bones (clothing/accessories) + r'^\+.*', # + prefixed bones (accessories) + r'.*[tT]ooth.*', # Tooth bones + ] + compiled_protected = [re.compile(pattern) for pattern in protected_patterns] + + # Step 1: Identify and map bones (but only rename/remove bones explicitly in the map) for bone in edit_bones: bone_name = bone.name - # Check if bone should be renamed + # Check if bone is protected - never touch these + is_protected = any(pattern.match(bone_name) for pattern in compiled_protected) + if is_protected: + logger.debug(f"Protected bone (keeping): {bone_name}") + continue + + # Only process bones that are EXPLICITLY in the map 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}") + # Only mark for removal if explicitly mapped to None + if bone_name in mmd_to_unity_bone_map: + bones_to_remove.append(bone_name) + logger.debug(f"Marking bone for removal: {bone_name}") + # Otherwise, keep the bone as-is elif unity_name != bone_name: - # Mark for rename + # Mark for rename only if explicitly mapped bone_renames[bone_name] = unity_name logger.debug(f"Planning rename: {bone_name} -> {unity_name}") @@ -687,3 +606,486 @@ def restructure_mmd_to_unity_bones(armature: Object) -> Tuple[bool, List[str]]: logger.info(f"Bone restructuring complete: {renamed_count} renamed, {removed_count} removed, {reparented_count} reparented") return True, messages + + +def remove_mmd_ik_bones(armature: Object) -> Tuple[bool, List[str]]: + """ + Remove MMD IK (Inverse Kinematics) and helper bones. + + This identifies bones that have zero vertex weights AND match IK/helper patterns. + Similar to CATS approach: remove bones with no mesh influence that are control/helper bones. + """ + if not armature or armature.type != 'ARMATURE': + return False, [t("MMD.error.invalid_armature")] + + logger.info(f"Starting MMD IK bone removal for: {armature.name}") + + messages = [] + removed_count = 0 + reparented_count = 0 + + # Store the current mode + current_mode = bpy.context.mode + + try: + # Switch to object mode to check weights + if bpy.context.mode != 'OBJECT': + bpy.context.view_layer.objects.active = armature + bpy.ops.object.mode_set(mode='OBJECT') + + # Get all meshes using this armature + meshes = [obj for obj in bpy.data.objects + if obj.type == 'MESH' and obj.parent == armature] + + # Track bones with weights + bones_with_weights = set() + + for mesh in meshes: + for vertex_group in mesh.vertex_groups: + # Check if any vertices have non-zero weights + has_weight = False + for vert in mesh.data.vertices: + try: + weight = vertex_group.weight(vert.index) + if weight > 0.001: # Threshold for "zero" weight + has_weight = True + break + except: + continue + + if has_weight: + bones_with_weights.add(vertex_group.name) + + logger.info(f"Found {len(bones_with_weights)} bones with vertex weights") + + # Set armature as active object before switching modes + bpy.context.view_layer.objects.active = armature + + # Patterns to identify IK and helper bones (Japanese and English) + # These are control/helper bones that typically have zero weights + ik_helper_patterns = [ + r'.*[iI][kK].*', # Contains IK (IK, ik, etc.) + r'.*IK.*', # Japanese fullwidth IK + r'.*親.*', # Japanese "parent" + r'.*[DCd]$', # D/C suffix (D-bones, control bones like RightKneeD, RightAnkleD) + r'.*[Ee][Xx]$', # EX suffix (extra bones like RightLegTipEX) + r'.*[Pp]arent$', # Ends with Parent + r'.*[Pp]$', # P suffix (helper bones like ShoulderP) + r'.*[Cc]$', # C suffix (control bones like ShoulderC) + r'^_dummy_.*', # Dummy bones + r'^_shadow_.*', # Shadow bones + r'.*ダミー.*', # Japanese "dummy" + r'.*補助.*', # Japanese "auxiliary/helper" + r'.*操作.*', # Japanese "control/operation" + r'.*[Tt]arget$', # Target bones + r'.*[Gg]roup$', # Group bones + ] + + # Compile patterns + compiled_patterns = [re.compile(pattern) for pattern in ik_helper_patterns] + + # Protected bone names (main skeleton - never remove these even with zero weights) + protected_bones = { + "Hips", "Spine", "Chest", "UpperChest", "Upper Chest", "Neck", "Head", + "Left shoulder", "Right shoulder", "Shoulder_L", "Shoulder_R", + "Left arm", "Right arm", "UpperArm_L", "UpperArm_R", + "Left elbow", "Right elbow", "LowerArm_L", "LowerArm_R", + "Left wrist", "Right wrist", "Hand_L", "Hand_R", + "Left leg", "Right leg", "UpperLeg_L", "UpperLeg_R", + "Left knee", "Right knee", "LowerLeg_L", "LowerLeg_R", + "Left ankle", "Right ankle", "Foot_L", "Foot_R", + "Left toe", "Right toe", "Toe_L", "Toe_R", + "Left eye", "Right eye", "Eye_L", "Eye_R" + } + + # Protected bone name patterns (never remove these) + protected_patterns = [ + r'.*[bB]reast.*', # Breast bones + r'.*[bB]ust.*', # Bust bones + r'.*[sS]kirt.*', # Skirt bones + r'.*[hH]air.*', # Hair bones + r'.*[bB]ag.*', # Bag/accessory bones + r'.*[rR]ibbon.*', # Ribbon bones + r'.*[tT]ail.*', # Tail bones + r'.*[wW]ing.*', # Wing bones + r'.*[sS]leeve.*', # Sleeve bones + r'.*[cC]ape.*', # Cape bones + r'.*[sS]carf.*', # Scarf bones + r'.*[cC]oat.*', # Coat bones + r'.*[dD]ress.*', # Dress bones + r'.*[fF]inger.*', # Finger bones + r'.*[tT]humb.*', # Thumb bones + r'.*[aA]ccessor.*', # Accessory bones + r'.*[cC]loth.*', # Cloth bones + r'.*[pP]hys.*', # Physics bones + ] + compiled_protected = [re.compile(pattern) for pattern in protected_patterns] + + # Switch to pose mode to remove constraints first + bpy.ops.object.mode_set(mode='POSE') + pose_bones = armature.pose.bones + + # Identify IK/helper bones to remove (zero weight + matches pattern) + bones_to_remove = [] + + for bone in armature.data.bones: + bone_name = bone.name + + # Check if it matches IK/helper pattern FIRST (before protection checks) + matches_pattern = any(pattern.match(bone_name) for pattern in compiled_patterns) + + # Skip if bone has weights (it's actually used by the mesh) + if bone_name in bones_with_weights: + if matches_pattern: + logger.debug(f"IK pattern match but has weights (keeping): {bone_name}") + continue + + # If matches IK pattern, remove regardless of other checks (except weights) + if matches_pattern: + bones_to_remove.append(bone_name) + logger.debug(f"IK/helper bone identified (zero weight): {bone_name}") + + # Remove constraints from this bone + if bone_name in pose_bones: + pose_bone = pose_bones[bone_name] + for constraint in list(pose_bone.constraints): + constraint_name = constraint.name + pose_bone.constraints.remove(constraint) + logger.debug(f"Removed constraint '{constraint_name}' from {bone_name}") + continue + + # Skip if in protected set + if bone_name in protected_bones: + continue + + # Skip if matches protected pattern + is_protected = any(pattern.match(bone_name) for pattern in compiled_protected) + if is_protected: + logger.debug(f"Protected bone (keeping): {bone_name}") + continue + + # Remove constraints that reference bones we're about to delete + for pose_bone in pose_bones: + for constraint in list(pose_bone.constraints): + if hasattr(constraint, 'target') and constraint.target: + if hasattr(constraint, 'subtarget') and constraint.subtarget in bones_to_remove: + constraint_name = constraint.name + pose_bone_name = pose_bone.name + pose_bone.constraints.remove(constraint) + logger.debug(f"Removed constraint '{constraint_name}' from {pose_bone_name} (referenced deleted bone)") + + # Switch to edit mode for bone removal + bpy.ops.object.mode_set(mode='EDIT') + edit_bones = armature.data.edit_bones + + # Reparent children before removing + 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'}") + + # Remove IK/helper 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 IK/helper bone: {bone_name}") + + except Exception as e: + logger.error(f"Error during IK bone removal: {e}", exc_info=True) + messages.append(t("MMD.ik_removal_failed", error=str(e))) + return False, messages + + finally: + # Restore original mode + if current_mode != 'OBJECT': + bpy.ops.object.mode_set(mode='OBJECT') + + # Generate messages + if removed_count > 0: + messages.append(t("MMD.ik_bones_removed", count=removed_count)) + else: + messages.append(t("MMD.no_ik_bones_found")) + + if reparented_count > 0: + messages.append(t("MMD.bones_reparented", count=reparented_count)) + + logger.info(f"IK bone removal complete: {removed_count} removed, {reparented_count} reparented") + + return True, messages + + +def remove_mmd_twist_bones(armature: Object) -> Tuple[bool, List[str]]: + """ + Remove MMD twist bones. + + Twist bone patterns: + - Contains 'twist', 'Twist' + - Ends with '_twist' + - Contains '捩' (Japanese for twist) + """ + if not armature or armature.type != 'ARMATURE': + return False, [t("MMD.error.invalid_armature")] + + logger.info(f"Starting MMD twist bone removal for: {armature.name}") + + messages = [] + 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 = [] + + # Patterns to identify twist bones - be specific about twist markers + twist_patterns = [ + r'.*_[tT]wist$', # Ends with _twist or _Twist + r'^[tT]wist_', # Starts with twist_ or Twist_ + r'.*\.[tT]wist$', # Ends with .twist or .Twist + r'^[tT]wist\.', # Starts with twist. or Twist. + r'.*[tT]wist$', # Ends with Twist (no underscore/dot required) + r'.*捩.*', # Contains Japanese twist character + ] + + # Compile patterns + compiled_patterns = [re.compile(pattern) for pattern in twist_patterns] + + # Protected bone name patterns (never remove these) + protected_patterns = [ + r'.*[bB]reast.*', # Breast bones + r'.*[bB]ust.*', # Bust bones + r'.*[sS]kirt.*', # Skirt bones + r'.*[hH]air.*', # Hair bones + r'.*[bB]ag.*', # Bag/accessory bones + r'.*[rR]ibbon.*', # Ribbon bones + r'.*[tT]ail.*', # Tail bones + r'.*[wW]ing.*', # Wing bones + r'.*[eE]ar.*', # Ear bones + r'.*[sS]leeve.*', # Sleeve bones + r'.*[cC]ape.*', # Cape bones + r'.*[sS]carf.*', # Scarf bones + r'.*[cC]oat.*', # Coat bones + r'.*[dD]ress.*', # Dress bones + ] + compiled_protected = [re.compile(pattern) for pattern in protected_patterns] + + # Identify twist bones (but exclude protected bones) + for bone in edit_bones: + bone_name = bone.name + + # Check if bone is protected + is_protected = any(pattern.match(bone_name) for pattern in compiled_protected) + if is_protected: + continue + + # Check if it's a twist bone + for pattern in compiled_patterns: + if pattern.match(bone_name): + bones_to_remove.append(bone_name) + logger.debug(f"Twist bone identified: {bone_name}") + break + + # Reparent children before removing + 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'}") + + # Remove twist 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 twist bone: {bone_name}") + + except Exception as e: + logger.error(f"Error during twist bone removal: {e}", exc_info=True) + messages.append(t("MMD.twist_removal_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 removed_count > 0: + messages.append(t("MMD.twist_bones_removed", count=removed_count)) + else: + messages.append(t("MMD.no_twist_bones_found")) + + if reparented_count > 0: + messages.append(t("MMD.bones_reparented", count=reparented_count)) + + logger.info(f"Twist bone removal complete: {removed_count} removed, {reparented_count} reparented") + + return True, messages + + +def remove_mmd_zero_weight_bones(armature: Object) -> Tuple[bool, List[str]]: + """ + Remove bones with zero or near-zero vertex weights. + Protects main skeleton bones from removal. + """ + if not armature or armature.type != 'ARMATURE': + return False, [t("MMD.error.invalid_armature")] + + logger.info(f"Starting zero weight bone removal for: {armature.name}") + + messages = [] + removed_count = 0 + reparented_count = 0 + + # Store the current mode + current_mode = bpy.context.mode + + try: + # Switch to object mode to check weights + if bpy.context.mode != 'OBJECT': + bpy.context.view_layer.objects.active = armature + bpy.ops.object.mode_set(mode='OBJECT') + + # Get all meshes using this armature + meshes = [obj for obj in bpy.data.objects + if obj.type == 'MESH' and obj.parent == armature] + + # Track bones with weights + bones_with_weights = set() + + for mesh in meshes: + for vertex_group in mesh.vertex_groups: + # Check if any vertices have non-zero weights + has_weight = False + for vert in mesh.data.vertices: + try: + weight = vertex_group.weight(vert.index) + if weight > 0.001: # Threshold for "zero" weight + has_weight = True + break + except: + continue + + if has_weight: + bones_with_weights.add(vertex_group.name) + + # Switch to edit mode + bpy.ops.object.mode_set(mode='EDIT') + edit_bones = armature.data.edit_bones + + # Protected bone names (main skeleton) + protected_bones = { + "Hips", "Spine", "Chest", "UpperChest", "Neck", "Head", + "Left shoulder", "Right shoulder", "Shoulder_L", "Shoulder_R", + "Left arm", "Right arm", "UpperArm_L", "UpperArm_R", + "Left elbow", "Right elbow", "LowerArm_L", "LowerArm_R", + "Left wrist", "Right wrist", "Hand_L", "Hand_R", + "Left leg", "Right leg", "UpperLeg_L", "UpperLeg_R", + "Left knee", "Right knee", "LowerLeg_L", "LowerLeg_R", + "Left ankle", "Right ankle", "Foot_L", "Foot_R", + "Left toe", "Right toe", "Toe_L", "Toe_R", + "Left eye", "Right eye", "Eye_L", "Eye_R" + } + + # Protected bone name patterns (never remove these even with zero weights) + protected_patterns = [ + r'.*[bB]reast.*', # Breast bones + r'.*[bB]ust.*', # Bust bones + r'.*[sS]kirt.*', # Skirt bones + r'.*[hH]air.*', # Hair bones + r'.*[bB]ag.*', # Bag/accessory bones + r'.*[rR]ibbon.*', # Ribbon bones + r'.*[tT]ail.*', # Tail bones + r'.*[wW]ing.*', # Wing bones + r'.*[eE]ar.*', # Ear bones (not Eye) + r'.*[sS]leeve.*', # Sleeve bones + r'.*[cC]ape.*', # Cape bones + r'.*[sS]carf.*', # Scarf bones + r'.*[cC]oat.*', # Coat bones + r'.*[dD]ress.*', # Dress bones + r'.*[fF]inger.*', # Finger bones + r'.*[tT]humb.*', # Thumb bones + r'.*[iI]ndex.*', # Index finger bones + r'.*[mM]iddle.*', # Middle finger bones + r'.*[rR]ing.*', # Ring finger bones + r'.*[pP]ink.*', # Pinky bones + r'.*[tT]oe.*', # Toe bones + r'.*[aA]ccessor.*', # Accessory bones + r'.*[jJ]oint.*', # Joint bones + r'.*[cC]loth.*', # Cloth bones + r'.*[pP]hys.*', # Physics bones + ] + compiled_protected = [re.compile(pattern) for pattern in protected_patterns] + + # Identify zero weight bones (but exclude protected bones) + bones_to_remove = [] + for bone in edit_bones: + # Skip if bone has weights + if bone.name in bones_with_weights: + continue + + # Skip if in protected set + if bone.name in protected_bones: + continue + + # Skip if matches protected pattern + is_protected = any(pattern.match(bone.name) for pattern in compiled_protected) + if is_protected: + logger.debug(f"Protected bone (zero weight but keeping): {bone.name}") + continue + + bones_to_remove.append(bone.name) + logger.debug(f"Zero weight bone identified: {bone.name}") + + # Reparent children before removing + 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'}") + + # Remove zero weight 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 zero weight bone: {bone_name}") + + except Exception as e: + logger.error(f"Error during zero weight bone removal: {e}", exc_info=True) + messages.append(t("MMD.zero_weight_removal_failed", error=str(e))) + return False, messages + + finally: + # Restore original mode + if current_mode != 'OBJECT': + bpy.ops.object.mode_set(mode='OBJECT') + + # Generate messages + if removed_count > 0: + messages.append(t("MMD.zero_weight_bones_removed", count=removed_count)) + else: + messages.append(t("MMD.no_zero_weight_bones_found")) + + if reparented_count > 0: + messages.append(t("MMD.bones_reparented", count=reparented_count)) + + logger.info(f"Zero weight bone removal complete: {removed_count} removed, {reparented_count} reparented") + + return True, messages diff --git a/core/properties.py b/core/properties.py index 0295351..2c2e3f5 100644 --- a/core/properties.py +++ b/core/properties.py @@ -751,6 +751,18 @@ class AvatarToolkitSceneProperties(PropertyGroup): description="Restructure bone hierarchy to Unity humanoid format (Hips, Spine, Chest, etc.)", default=True ) + + mmd_remove_twist_bones: BoolProperty( + name=t("MMD.remove_twist_bones"), + description="Remove twist bones", + default=True + ) + + mmd_remove_zero_weight_bones: BoolProperty( + name=t("MMD.remove_zero_weight_bones"), + description="Remove bones with zero or near-zero vertex weights", + default=False + ) # Translation System Properties translation_service: EnumProperty( diff --git a/functions/tools/mmd_conversion.py b/functions/tools/mmd_conversion.py index 1704392..c0d2047 100644 --- a/functions/tools/mmd_conversion.py +++ b/functions/tools/mmd_conversion.py @@ -7,7 +7,8 @@ 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, restructure_mmd_to_unity_bones) + translate_mmd_everything, restructure_mmd_to_unity_bones, + remove_mmd_ik_bones, remove_mmd_twist_bones, remove_mmd_zero_weight_bones) from ...core.logging_setup import logger @@ -48,8 +49,12 @@ class AvatarToolkit_OT_ConvertMMDArmature(Operator): translate_shapekeys = toolkit.mmd_translate_shapekeys translate_objects = toolkit.mmd_translate_objects restructure_bones = toolkit.mmd_restructure_bones + remove_twist_bones = toolkit.mmd_remove_twist_bones + remove_zero_weight_bones = toolkit.mmd_remove_zero_weight_bones - logger.info(f"Conversion settings - Make parent: {make_parent}, Rename: {rename_armature}, Restructure: {restructure_bones}") + logger.info(f"Conversion settings - Make parent: {make_parent}, Rename: {rename_armature}, " + + f"Restructure: {restructure_bones}") + logger.info(f"Bone cleanup - IK: True (automatic), Twist: {remove_twist_bones}, Zero weight: {remove_zero_weight_bones}") logger.info(f"Translation settings - Enabled: {translate_names}, Bones: {translate_bones}, " + f"Materials: {translate_materials}, Shapekeys: {translate_shapekeys}, Objects: {translate_objects}") @@ -66,7 +71,36 @@ class AvatarToolkit_OT_ConvertMMDArmature(Operator): for msg in messages: self.report({'INFO'}, msg) - # Step 2: Translation (if enabled) + # Step 2: Remove IK bones BEFORE translation (always automatic) + logger.info("Starting IK bone removal (before translation)") + self.report({'INFO'}, "Removing IK bones...") + + ik_success, ik_messages = remove_mmd_ik_bones(armature) + + if ik_success: + logger.info("IK bone removal completed successfully") + else: + logger.warning("IK bone removal completed with errors") + + for msg in ik_messages: + self.report({'INFO'}, msg) + + # Step 3: Remove twist bones BEFORE translation (if enabled) + if remove_twist_bones: + logger.info("Starting twist bone removal (before translation)") + self.report({'INFO'}, "Removing twist bones...") + + twist_success, twist_messages = remove_mmd_twist_bones(armature) + + if twist_success: + logger.info("Twist bone removal completed successfully") + else: + logger.warning("Twist bone removal completed with errors") + + for msg in twist_messages: + self.report({'INFO'}, msg) + + # Step 4: Translation (if enabled) if translate_names: logger.info("Starting MMD name translation") self.report({'INFO'}, t("MMD.translation_starting")) @@ -87,7 +121,7 @@ class AvatarToolkit_OT_ConvertMMDArmature(Operator): for msg in trans_messages: self.report({'INFO'}, msg) - # Step 3: Restructure bones to Unity format (if enabled) + # Step 5: 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")) @@ -102,4 +136,19 @@ class AvatarToolkit_OT_ConvertMMDArmature(Operator): for msg in struct_messages: self.report({'INFO'}, msg) + # Step 6: Remove zero weight bones (if enabled) + if remove_zero_weight_bones: + logger.info("Starting zero weight bone removal") + self.report({'INFO'}, "Removing zero weight bones...") + + zero_success, zero_messages = remove_mmd_zero_weight_bones(armature) + + if zero_success: + logger.info("Zero weight bone removal completed successfully") + else: + logger.warning("Zero weight bone removal completed with errors") + + for msg in zero_messages: + self.report({'INFO'}, msg) + return {'FINISHED'} diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index 24ddeb0..8789feb 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -617,6 +617,10 @@ "MMD.translate_shapekeys": "Shape Keys", "MMD.translate_objects": "Objects", "MMD.restructure_bones": "Restructure to Unity Format", + "MMD.bone_cleanup": "Bone Cleanup Options:", + "MMD.remove_ik_bones": "Remove IK Bones", + "MMD.remove_twist_bones": "Remove Twist Bones", + "MMD.remove_zero_weight_bones": "Remove Zero Weight Bones", "MMD.translation_options": "Translation Options:", "MMD.convert_armature_button": "Convert MMD Armature", "MMD.convert_armature.label": "Convert MMD Armature", @@ -625,6 +629,9 @@ "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.removes_ik_bones": "• Removes IK (Inverse Kinematics) bones", + "MMD.conversion_info.removes_twist_bones": "• Removes twist bones", + "MMD.conversion_info.removes_zero_weight_bones": "• Removes bones with zero vertex weights", "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:", @@ -657,6 +664,15 @@ "MMD.bones_removed": "Removed {count} unnecessary bones", "MMD.bones_reparented": "Reparented {count} bones", "MMD.restructure_failed": "Bone restructuring failed: {error}", + "MMD.ik_bones_removed": "Removed {count} IK bones", + "MMD.no_ik_bones_found": "No IK bones found to remove", + "MMD.ik_removal_failed": "IK bone removal failed: {error}", + "MMD.twist_bones_removed": "Removed {count} twist bones", + "MMD.no_twist_bones_found": "No twist bones found to remove", + "MMD.twist_removal_failed": "Twist bone removal failed: {error}", + "MMD.zero_weight_bones_removed": "Removed {count} zero weight bones", + "MMD.no_zero_weight_bones_found": "No zero weight bones found to remove", + "MMD.zero_weight_removal_failed": "Zero weight bone removal failed: {error}", "Translation.label": "Translation", "Translation.service": "Translation Service", diff --git a/ui/mmd_panel.py b/ui/mmd_panel.py index 84e04fa..7c1397e 100644 --- a/ui/mmd_panel.py +++ b/ui/mmd_panel.py @@ -59,6 +59,14 @@ class AvatarToolKit_PT_MMDPanel(Panel): col.prop(toolkit, 'mmd_restructure_bones', text=t("MMD.restructure_bones")) col.separator(factor=0.2) + # Bone cleanup options + col.label(text=t("MMD.bone_cleanup"), icon='BONE_DATA') + cleanup_box = col.box() + cleanup_col = cleanup_box.column(align=True) + cleanup_col.prop(toolkit, 'mmd_remove_twist_bones', text=t("MMD.remove_twist_bones")) + cleanup_col.prop(toolkit, 'mmd_remove_zero_weight_bones', text=t("MMD.remove_zero_weight_bones")) + col.separator(factor=0.2) + # Translation settings col.prop(toolkit, 'mmd_translate_names', text=t("MMD.translate_names")) @@ -87,6 +95,11 @@ class AvatarToolKit_PT_MMDPanel(Panel): 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.removes_ik_bones")) + if toolkit.mmd_remove_twist_bones: + info_col.label(text=t("MMD.conversion_info.removes_twist_bones")) + if toolkit.mmd_remove_zero_weight_bones: + info_col.label(text=t("MMD.conversion_info.removes_zero_weight_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")) From d85231b62b4ff6ec701373baad0a03837cd8d0c2 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Sun, 23 Nov 2025 02:50:19 +0000 Subject: [PATCH 5/8] meh --- core/mmd_converter.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/core/mmd_converter.py b/core/mmd_converter.py index 81d6b53..dbf07e0 100644 --- a/core/mmd_converter.py +++ b/core/mmd_converter.py @@ -609,12 +609,7 @@ def restructure_mmd_to_unity_bones(armature: Object) -> Tuple[bool, List[str]]: def remove_mmd_ik_bones(armature: Object) -> Tuple[bool, List[str]]: - """ - Remove MMD IK (Inverse Kinematics) and helper bones. - - This identifies bones that have zero vertex weights AND match IK/helper patterns. - Similar to CATS approach: remove bones with no mesh influence that are control/helper bones. - """ + """Remove MMD IK (Inverse Kinematics) and helper bones.""" if not armature or armature.type != 'ARMATURE': return False, [t("MMD.error.invalid_armature")] @@ -820,14 +815,7 @@ def remove_mmd_ik_bones(armature: Object) -> Tuple[bool, List[str]]: def remove_mmd_twist_bones(armature: Object) -> Tuple[bool, List[str]]: - """ - Remove MMD twist bones. - - Twist bone patterns: - - Contains 'twist', 'Twist' - - Ends with '_twist' - - Contains '捩' (Japanese for twist) - """ + """Remove MMD twist bones.""" if not armature or armature.type != 'ARMATURE': return False, [t("MMD.error.invalid_armature")] From 4b538cb8b25dda2b673fc6be078ee18044184007 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Wed, 26 Nov 2025 19:35:20 +0000 Subject: [PATCH 6/8] Restart --- core/mmd_converter.py | 1079 ----------------------------- functions/tools/mmd_conversion.py | 154 ---- ui/mmd_panel.py | 125 ---- 3 files changed, 1358 deletions(-) delete mode 100644 core/mmd_converter.py delete mode 100644 functions/tools/mmd_conversion.py delete mode 100644 ui/mmd_panel.py diff --git a/core/mmd_converter.py b/core/mmd_converter.py deleted file mode 100644 index dbf07e0..0000000 --- a/core/mmd_converter.py +++ /dev/null @@ -1,1079 +0,0 @@ -""" -MMD Converter - Core conversion logic for MMD models -Handles armature hierarchy and naming conventions -""" -import bpy -import re -from typing import Dict, List, Optional, Tuple, Set -from bpy.types import Object, Bone, Collection, Material, ShapeKey -from .common import get_active_armature -from .dictionaries import simplify_bonename -from .enhanced_dictionaries import mmd_bone_patterns, mmd_to_unity_bone_map, unity_bone_hierarchy -from .logging_setup import logger -from .translations import t -from .mmd.translations import jp_to_en_tuples, translateFromJp - - -def detect_mmd_armature(armature: Object) -> bool: - """Detect if armature uses MMD bone naming conventions""" - - if not armature or armature.type != 'ARMATURE': - return False - - found_mmd_bones = 0 - for bone in armature.data.bones: - bone_name_lower = bone.name.lower() - if any(pattern.lower() in bone_name_lower for pattern in mmd_bone_patterns): - found_mmd_bones += 1 - logger.debug(f"Found MMD bone: {bone.name}") - - # Consider it MMD if we find at least 5 MMD bones - logger.debug(f"Found {found_mmd_bones} MMD bones in armature {armature.name}") - return found_mmd_bones >= 5 - - -def get_armature_parent_object(armature: Object) -> Optional[Object]: - """Get the parent object of the armature (typically an Empty in MMD imports)""" - if armature and armature.parent: - return armature.parent - return None - - -def make_armature_main_parent(armature: Object) -> Tuple[bool, str]: - """Make the armature the main parent object by removing any parent empties - and reparenting all children to the armature.""" - - if not armature or armature.type != 'ARMATURE': - return False, t("MMD.error.invalid_armature") - - logger.info(f"Making armature '{armature.name}' the main parent") - - # Store original parent - original_parent = armature.parent - - if not original_parent: - logger.info("Armature already has no parent") - return True, t("MMD.armature_already_root") - - parent_name = original_parent.name - parent_type = original_parent.type - - logger.info(f"Found parent: {parent_name} (type: {parent_type})") - - # Get all children of the parent - siblings = [child for child in original_parent.children if child != armature] - - armature.parent = None - - # Reparent siblings to the armature - reparented_count = 0 - for sibling in siblings: - sibling.parent = armature - reparented_count += 1 - logger.debug(f"Reparented {sibling.name} to armature") - - # If the parent was an Empty and now has no children, remove it - if parent_type == 'EMPTY' and len(original_parent.children) == 0: - try: - bpy.data.objects.remove(original_parent, do_unlink=True) - logger.info(f"Removed empty parent object: {parent_name}") - message = t("MMD.parent_removed_and_reparented", - parent_name=parent_name, - count=reparented_count) - except Exception as e: - logger.warning(f"Could not remove parent empty: {str(e)}") - message = t("MMD.parent_unlinked_and_reparented", - parent_name=parent_name, - count=reparented_count) - else: - message = t("MMD.parent_unlinked", parent_name=parent_name) - - logger.info(f"Successfully made armature the main parent. Reparented {reparented_count} objects") - return True, message - - -def rename_armature_to_standard(armature: Object) -> Tuple[bool, str]: - """Rename the armature object to 'Armature' (standard Blender convention)""" - if not armature or armature.type != 'ARMATURE': - return False, t("MMD.error.invalid_armature") - - old_name = armature.name - - # Check if already named 'Armature' - if old_name == 'Armature': - logger.info("Armature already named 'Armature'") - return True, t("MMD.armature_already_named") - - logger.info(f"Renaming armature from '{old_name}' to 'Armature'") - - try: - armature.name = 'Armature' - # Blender might append .001 if name exists, check actual result (Wonder if needed) - actual_name = armature.name - - if actual_name == 'Armature': - message = t("MMD.armature_renamed", old_name=old_name, new_name='Armature') - else: - message = t("MMD.armature_renamed_with_suffix", - old_name=old_name, - new_name=actual_name) - logger.warning(f"Name collision, armature named: {actual_name}") - - logger.info(f"Successfully renamed armature to: {actual_name}") - return True, message - - except Exception as e: - logger.error(f"Failed to rename armature: {str(e)}") - return False, t("MMD.error.rename_failed", error=str(e)) - - -def convert_mmd_armature(armature: Object, - make_parent: bool = True, - rename_armature: bool = True) -> Tuple[bool, List[str]]: - """Convert MMD armature to standard Blender format""" - if not armature or armature.type != 'ARMATURE': - return False, [t("MMD.error.invalid_armature")] - - logger.info(f"Starting MMD armature conversion for: {armature.name}") - - # Check if this is an MMD armature - if not detect_mmd_armature(armature): - return False, [t("MMD.error.not_mmd_armature")] - - messages = [] - overall_success = True - - # Step 1: Make armature the main parent - if make_parent: - success, message = make_armature_main_parent(armature) - messages.append(message) - if not success: - overall_success = False - logger.warning("Failed to make armature main parent") - - # Step 2: Rename armature - if rename_armature: - success, message = rename_armature_to_standard(armature) - messages.append(message) - if not success: - overall_success = False - logger.warning("Failed to rename armature") - - if overall_success: - logger.info("MMD armature conversion completed successfully") - messages.append(t("MMD.conversion_complete")) - else: - logger.warning("MMD armature conversion completed with errors") - - return overall_success, messages - - -def translate_mmd_name(name: str, category: str = "auto") -> Tuple[str, str]: - """Translate MMD name using MMD dictionary first, then translation services""" - if not name or not name.strip(): - return name, "unchanged" - - original_name = name.strip() - - # Step 1: Try MMD built-in dictionary translation - mmd_translated = translateFromJp(original_name) - - # Check if MMD dictionary actually translated something - if mmd_translated != original_name and mmd_translated: - logger.debug(f"MMD dictionary translated: '{original_name}' -> '{mmd_translated}'") - return mmd_translated, "mmd_dictionary" - - # Step 2: If MMD dictionary didn't translate or only partially translated, - # use Avatar Toolkit translation services - try: - from .translation_manager import get_avatar_translation_manager - - manager = get_avatar_translation_manager() - result = manager.translate_single(original_name, category=category, source_lang="ja", target_lang="en") - - if result.translated != original_name: - logger.debug(f"API translated: '{original_name}' -> '{result.translated}' (method: {result.method})") - return result.translated, "api_translation" - except Exception as e: - logger.warning(f"Translation service failed for '{original_name}': {e}") - - # Step 3: No translation available - logger.debug(f"No translation available for: '{original_name}'") - return original_name, "unchanged" - - -def translate_mmd_armature_bones(armature: Object, apply_translation: bool = True) -> Tuple[int, int, List[str]]: - """Translate all bone names in an MMD armature""" - if not armature or armature.type != 'ARMATURE': - return 0, 0, [t("MMD.error.invalid_armature")] - - logger.info(f"Starting bone translation for armature: {armature.name}") - - successful = 0 - failed = 0 - messages = [] - bone_translations = {} - - # 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: - for bone in armature.data.edit_bones: - original_name = bone.name - translated_name, method = translate_mmd_name(original_name, category="bones") - - if translated_name != original_name: - bone_translations[original_name] = (translated_name, method) - - if apply_translation: - try: - bone.name = translated_name - logger.info(f"Translated bone: '{original_name}' -> '{translated_name}' ({method})") - successful += 1 - except Exception as e: - logger.error(f"Failed to rename bone '{original_name}': {e}") - failed += 1 - else: - successful += 1 - else: - logger.debug(f"Bone '{original_name}' not translated") - - finally: - # Restore original mode - if current_mode != 'EDIT': - bpy.ops.object.mode_set(mode='OBJECT') - - # Generate summary messages - if successful > 0: - messages.append(t("MMD.bones_translated", count=successful)) - if failed > 0: - messages.append(t("MMD.bones_failed", count=failed)) - - mmd_dict_count = sum(1 for _, (_, method) in bone_translations.items() if method == "mmd_dictionary") - api_count = sum(1 for _, (_, method) in bone_translations.items() if method == "api_translation") - - logger.info(f"Bone translation complete: {successful} successful, {failed} failed") - logger.info(f"Translation methods: MMD Dictionary: {mmd_dict_count}, API: {api_count}") - - return successful, failed, messages - - -def translate_mmd_materials(armature: Object, apply_translation: bool = True) -> Tuple[int, int, List[str]]: - """Translate all material names for meshes parented to the armature""" - if not armature or armature.type != 'ARMATURE': - return 0, 0, [t("MMD.error.invalid_armature")] - - logger.info(f"Starting material translation for armature: {armature.name}") - - successful = 0 - failed = 0 - messages = [] - processed_materials = set() - - # Get all mesh objects parented to this armature - for obj in bpy.data.objects: - if obj.type == 'MESH' and obj.parent == armature and obj.data.materials: - for mat in obj.data.materials: - if mat and mat.name not in processed_materials: - processed_materials.add(mat.name) - original_name = mat.name - translated_name, method = translate_mmd_name(original_name, category="materials") - - if translated_name != original_name and apply_translation: - try: - mat.name = translated_name - logger.info(f"Translated material: '{original_name}' -> '{translated_name}' ({method})") - successful += 1 - except Exception as e: - logger.error(f"Failed to rename material '{original_name}': {e}") - failed += 1 - elif translated_name != original_name: - successful += 1 - - if successful > 0: - messages.append(t("MMD.materials_translated", count=successful)) - if failed > 0: - messages.append(t("MMD.materials_failed", count=failed)) - - logger.info(f"Material translation complete: {successful} successful, {failed} failed") - - return successful, failed, messages - - -def translate_mmd_shapekeys(armature: Object, apply_translation: bool = True) -> Tuple[int, int, List[str]]: - """Translate all shape key names for meshes parented to the armature""" - if not armature or armature.type != 'ARMATURE': - return 0, 0, [t("MMD.error.invalid_armature")] - - logger.info(f"Starting shape key translation for armature: {armature.name}") - - successful = 0 - failed = 0 - messages = [] - - # Get all mesh objects parented to this armature - for obj in bpy.data.objects: - if obj.type == 'MESH' and obj.parent == armature and obj.data.shape_keys: - for shape_key in obj.data.shape_keys.key_blocks: - original_name = shape_key.name - translated_name, method = translate_mmd_name(original_name, category="shapekeys") - - if translated_name != original_name and apply_translation: - try: - shape_key.name = translated_name - logger.info(f"Translated shape key: '{original_name}' -> '{translated_name}' ({method})") - successful += 1 - except Exception as e: - logger.error(f"Failed to rename shape key '{original_name}': {e}") - failed += 1 - elif translated_name != original_name: - successful += 1 - - if successful > 0: - messages.append(t("MMD.shapekeys_translated", count=successful)) - if failed > 0: - messages.append(t("MMD.shapekeys_failed", count=failed)) - - logger.info(f"Shape key translation complete: {successful} successful, {failed} failed") - - return successful, failed, messages - - -def translate_mmd_objects(armature: Object, apply_translation: bool = True) -> Tuple[int, int, List[str]]: - """Translate object names parented to the armature""" - if not armature or armature.type != 'ARMATURE': - return 0, 0, [t("MMD.error.invalid_armature")] - - logger.info(f"Starting object name translation for armature: {armature.name}") - - successful = 0 - failed = 0 - messages = [] - - # Get all objects parented to this armature - for obj in bpy.data.objects: - if obj.parent == armature: - original_name = obj.name - translated_name, method = translate_mmd_name(original_name, category="objects") - - if translated_name != original_name and apply_translation: - try: - obj.name = translated_name - logger.info(f"Translated object: '{original_name}' -> '{translated_name}' ({method})") - successful += 1 - except Exception as e: - logger.error(f"Failed to rename object '{original_name}': {e}") - failed += 1 - elif translated_name != original_name: - successful += 1 - - if successful > 0: - messages.append(t("MMD.objects_translated", count=successful)) - if failed > 0: - messages.append(t("MMD.objects_failed", count=failed)) - - logger.info(f"Object translation complete: {successful} successful, {failed} failed") - - return successful, failed, messages - - -def translate_mmd_everything(armature: Object, - translate_bones: bool = True, - translate_materials: bool = True, - translate_shapekeys: bool = True, - translate_objects: bool = True) -> Tuple[bool, List[str]]: - """ - Translate all MMD names (bones, materials, shape keys, objects) - - Args: - armature: The armature object - translate_bones: Whether to translate bone names - translate_materials: Whether to translate material names - translate_shapekeys: Whether to translate shape key names - translate_objects: Whether to translate object names - - Returns: - Tuple of (success, messages) - """ - if not armature or armature.type != 'ARMATURE': - return False, [t("MMD.error.invalid_armature")] - - logger.info(f"Starting comprehensive MMD translation for: {armature.name}") - - all_messages = [] - total_successful = 0 - total_failed = 0 - - # Translate bones - if translate_bones: - success, failed, messages = translate_mmd_armature_bones(armature, apply_translation=True) - total_successful += success - total_failed += failed - all_messages.extend(messages) - - # Translate materials - if translate_materials: - success, failed, messages = translate_mmd_materials(armature, apply_translation=True) - total_successful += success - total_failed += failed - all_messages.extend(messages) - - # Translate shape keys - if translate_shapekeys: - success, failed, messages = translate_mmd_shapekeys(armature, apply_translation=True) - total_successful += success - total_failed += failed - all_messages.extend(messages) - - # Translate objects - if translate_objects: - success, failed, messages = translate_mmd_objects(armature, apply_translation=True) - total_successful += success - total_failed += failed - all_messages.extend(messages) - - # Summary - if total_successful > 0: - all_messages.append(t("MMD.translation_complete", total=total_successful)) - - 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 = {} - - # Protected bone name patterns (never remove or modify these) - protected_patterns = [ - r'.*[bB]reast.*', r'.*[bB]ust.*', r'.*[tT]its.*', # Breast bones - r'.*[sS]kirt.*', # Skirt bones - r'.*[hH]air.*', # Hair bones - r'.*[bB]ag.*', # Bag/accessory bones - r'.*[rR]ibbon.*', # Ribbon bones - r'.*[tT]ail.*', # Tail bones - r'.*[wW]ing.*', # Wing bones - r'.*[eE]ar.*', # Ear bones - r'.*[sS]leeve.*', # Sleeve bones - r'.*[cC]ape.*', r'.*[sS]carf.*', # Cape/Scarf bones - r'.*[cC]oat.*', r'.*[dD]ress.*', # Coat/Dress bones - r'.*[fF]inger.*', r'.*[tT]humb.*', # Finger bones - r'.*[aA]ccessor.*', # Accessory bones - r'.*[jJ]oint.*', # Joint bones - r'.*[cC]loth.*', r'.*[pP]hys.*', # Cloth/Physics bones - r'^tf_.*', # tf_ prefixed bones (clothing/accessories) - r'^\+.*', # + prefixed bones (accessories) - r'.*[tT]ooth.*', # Tooth bones - ] - compiled_protected = [re.compile(pattern) for pattern in protected_patterns] - - # Step 1: Identify and map bones (but only rename/remove bones explicitly in the map) - for bone in edit_bones: - bone_name = bone.name - - # Check if bone is protected - never touch these - is_protected = any(pattern.match(bone_name) for pattern in compiled_protected) - if is_protected: - logger.debug(f"Protected bone (keeping): {bone_name}") - continue - - # Only process bones that are EXPLICITLY in the map - unity_name = mmd_to_unity_bone_map.get(bone_name) - - if unity_name is None: - # Only mark for removal if explicitly mapped to None - if bone_name in mmd_to_unity_bone_map: - bones_to_remove.append(bone_name) - logger.debug(f"Marking bone for removal: {bone_name}") - # Otherwise, keep the bone as-is - elif unity_name != bone_name: - # Mark for rename only if explicitly mapped - 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 - - -def remove_mmd_ik_bones(armature: Object) -> Tuple[bool, List[str]]: - """Remove MMD IK (Inverse Kinematics) and helper bones.""" - if not armature or armature.type != 'ARMATURE': - return False, [t("MMD.error.invalid_armature")] - - logger.info(f"Starting MMD IK bone removal for: {armature.name}") - - messages = [] - removed_count = 0 - reparented_count = 0 - - # Store the current mode - current_mode = bpy.context.mode - - try: - # Switch to object mode to check weights - if bpy.context.mode != 'OBJECT': - bpy.context.view_layer.objects.active = armature - bpy.ops.object.mode_set(mode='OBJECT') - - # Get all meshes using this armature - meshes = [obj for obj in bpy.data.objects - if obj.type == 'MESH' and obj.parent == armature] - - # Track bones with weights - bones_with_weights = set() - - for mesh in meshes: - for vertex_group in mesh.vertex_groups: - # Check if any vertices have non-zero weights - has_weight = False - for vert in mesh.data.vertices: - try: - weight = vertex_group.weight(vert.index) - if weight > 0.001: # Threshold for "zero" weight - has_weight = True - break - except: - continue - - if has_weight: - bones_with_weights.add(vertex_group.name) - - logger.info(f"Found {len(bones_with_weights)} bones with vertex weights") - - # Set armature as active object before switching modes - bpy.context.view_layer.objects.active = armature - - # Patterns to identify IK and helper bones (Japanese and English) - # These are control/helper bones that typically have zero weights - ik_helper_patterns = [ - r'.*[iI][kK].*', # Contains IK (IK, ik, etc.) - r'.*IK.*', # Japanese fullwidth IK - r'.*親.*', # Japanese "parent" - r'.*[DCd]$', # D/C suffix (D-bones, control bones like RightKneeD, RightAnkleD) - r'.*[Ee][Xx]$', # EX suffix (extra bones like RightLegTipEX) - r'.*[Pp]arent$', # Ends with Parent - r'.*[Pp]$', # P suffix (helper bones like ShoulderP) - r'.*[Cc]$', # C suffix (control bones like ShoulderC) - r'^_dummy_.*', # Dummy bones - r'^_shadow_.*', # Shadow bones - r'.*ダミー.*', # Japanese "dummy" - r'.*補助.*', # Japanese "auxiliary/helper" - r'.*操作.*', # Japanese "control/operation" - r'.*[Tt]arget$', # Target bones - r'.*[Gg]roup$', # Group bones - ] - - # Compile patterns - compiled_patterns = [re.compile(pattern) for pattern in ik_helper_patterns] - - # Protected bone names (main skeleton - never remove these even with zero weights) - protected_bones = { - "Hips", "Spine", "Chest", "UpperChest", "Upper Chest", "Neck", "Head", - "Left shoulder", "Right shoulder", "Shoulder_L", "Shoulder_R", - "Left arm", "Right arm", "UpperArm_L", "UpperArm_R", - "Left elbow", "Right elbow", "LowerArm_L", "LowerArm_R", - "Left wrist", "Right wrist", "Hand_L", "Hand_R", - "Left leg", "Right leg", "UpperLeg_L", "UpperLeg_R", - "Left knee", "Right knee", "LowerLeg_L", "LowerLeg_R", - "Left ankle", "Right ankle", "Foot_L", "Foot_R", - "Left toe", "Right toe", "Toe_L", "Toe_R", - "Left eye", "Right eye", "Eye_L", "Eye_R" - } - - # Protected bone name patterns (never remove these) - protected_patterns = [ - r'.*[bB]reast.*', # Breast bones - r'.*[bB]ust.*', # Bust bones - r'.*[sS]kirt.*', # Skirt bones - r'.*[hH]air.*', # Hair bones - r'.*[bB]ag.*', # Bag/accessory bones - r'.*[rR]ibbon.*', # Ribbon bones - r'.*[tT]ail.*', # Tail bones - r'.*[wW]ing.*', # Wing bones - r'.*[sS]leeve.*', # Sleeve bones - r'.*[cC]ape.*', # Cape bones - r'.*[sS]carf.*', # Scarf bones - r'.*[cC]oat.*', # Coat bones - r'.*[dD]ress.*', # Dress bones - r'.*[fF]inger.*', # Finger bones - r'.*[tT]humb.*', # Thumb bones - r'.*[aA]ccessor.*', # Accessory bones - r'.*[cC]loth.*', # Cloth bones - r'.*[pP]hys.*', # Physics bones - ] - compiled_protected = [re.compile(pattern) for pattern in protected_patterns] - - # Switch to pose mode to remove constraints first - bpy.ops.object.mode_set(mode='POSE') - pose_bones = armature.pose.bones - - # Identify IK/helper bones to remove (zero weight + matches pattern) - bones_to_remove = [] - - for bone in armature.data.bones: - bone_name = bone.name - - # Check if it matches IK/helper pattern FIRST (before protection checks) - matches_pattern = any(pattern.match(bone_name) for pattern in compiled_patterns) - - # Skip if bone has weights (it's actually used by the mesh) - if bone_name in bones_with_weights: - if matches_pattern: - logger.debug(f"IK pattern match but has weights (keeping): {bone_name}") - continue - - # If matches IK pattern, remove regardless of other checks (except weights) - if matches_pattern: - bones_to_remove.append(bone_name) - logger.debug(f"IK/helper bone identified (zero weight): {bone_name}") - - # Remove constraints from this bone - if bone_name in pose_bones: - pose_bone = pose_bones[bone_name] - for constraint in list(pose_bone.constraints): - constraint_name = constraint.name - pose_bone.constraints.remove(constraint) - logger.debug(f"Removed constraint '{constraint_name}' from {bone_name}") - continue - - # Skip if in protected set - if bone_name in protected_bones: - continue - - # Skip if matches protected pattern - is_protected = any(pattern.match(bone_name) for pattern in compiled_protected) - if is_protected: - logger.debug(f"Protected bone (keeping): {bone_name}") - continue - - # Remove constraints that reference bones we're about to delete - for pose_bone in pose_bones: - for constraint in list(pose_bone.constraints): - if hasattr(constraint, 'target') and constraint.target: - if hasattr(constraint, 'subtarget') and constraint.subtarget in bones_to_remove: - constraint_name = constraint.name - pose_bone_name = pose_bone.name - pose_bone.constraints.remove(constraint) - logger.debug(f"Removed constraint '{constraint_name}' from {pose_bone_name} (referenced deleted bone)") - - # Switch to edit mode for bone removal - bpy.ops.object.mode_set(mode='EDIT') - edit_bones = armature.data.edit_bones - - # Reparent children before removing - 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'}") - - # Remove IK/helper 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 IK/helper bone: {bone_name}") - - except Exception as e: - logger.error(f"Error during IK bone removal: {e}", exc_info=True) - messages.append(t("MMD.ik_removal_failed", error=str(e))) - return False, messages - - finally: - # Restore original mode - if current_mode != 'OBJECT': - bpy.ops.object.mode_set(mode='OBJECT') - - # Generate messages - if removed_count > 0: - messages.append(t("MMD.ik_bones_removed", count=removed_count)) - else: - messages.append(t("MMD.no_ik_bones_found")) - - if reparented_count > 0: - messages.append(t("MMD.bones_reparented", count=reparented_count)) - - logger.info(f"IK bone removal complete: {removed_count} removed, {reparented_count} reparented") - - return True, messages - - -def remove_mmd_twist_bones(armature: Object) -> Tuple[bool, List[str]]: - """Remove MMD twist bones.""" - if not armature or armature.type != 'ARMATURE': - return False, [t("MMD.error.invalid_armature")] - - logger.info(f"Starting MMD twist bone removal for: {armature.name}") - - messages = [] - 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 = [] - - # Patterns to identify twist bones - be specific about twist markers - twist_patterns = [ - r'.*_[tT]wist$', # Ends with _twist or _Twist - r'^[tT]wist_', # Starts with twist_ or Twist_ - r'.*\.[tT]wist$', # Ends with .twist or .Twist - r'^[tT]wist\.', # Starts with twist. or Twist. - r'.*[tT]wist$', # Ends with Twist (no underscore/dot required) - r'.*捩.*', # Contains Japanese twist character - ] - - # Compile patterns - compiled_patterns = [re.compile(pattern) for pattern in twist_patterns] - - # Protected bone name patterns (never remove these) - protected_patterns = [ - r'.*[bB]reast.*', # Breast bones - r'.*[bB]ust.*', # Bust bones - r'.*[sS]kirt.*', # Skirt bones - r'.*[hH]air.*', # Hair bones - r'.*[bB]ag.*', # Bag/accessory bones - r'.*[rR]ibbon.*', # Ribbon bones - r'.*[tT]ail.*', # Tail bones - r'.*[wW]ing.*', # Wing bones - r'.*[eE]ar.*', # Ear bones - r'.*[sS]leeve.*', # Sleeve bones - r'.*[cC]ape.*', # Cape bones - r'.*[sS]carf.*', # Scarf bones - r'.*[cC]oat.*', # Coat bones - r'.*[dD]ress.*', # Dress bones - ] - compiled_protected = [re.compile(pattern) for pattern in protected_patterns] - - # Identify twist bones (but exclude protected bones) - for bone in edit_bones: - bone_name = bone.name - - # Check if bone is protected - is_protected = any(pattern.match(bone_name) for pattern in compiled_protected) - if is_protected: - continue - - # Check if it's a twist bone - for pattern in compiled_patterns: - if pattern.match(bone_name): - bones_to_remove.append(bone_name) - logger.debug(f"Twist bone identified: {bone_name}") - break - - # Reparent children before removing - 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'}") - - # Remove twist 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 twist bone: {bone_name}") - - except Exception as e: - logger.error(f"Error during twist bone removal: {e}", exc_info=True) - messages.append(t("MMD.twist_removal_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 removed_count > 0: - messages.append(t("MMD.twist_bones_removed", count=removed_count)) - else: - messages.append(t("MMD.no_twist_bones_found")) - - if reparented_count > 0: - messages.append(t("MMD.bones_reparented", count=reparented_count)) - - logger.info(f"Twist bone removal complete: {removed_count} removed, {reparented_count} reparented") - - return True, messages - - -def remove_mmd_zero_weight_bones(armature: Object) -> Tuple[bool, List[str]]: - """ - Remove bones with zero or near-zero vertex weights. - Protects main skeleton bones from removal. - """ - if not armature or armature.type != 'ARMATURE': - return False, [t("MMD.error.invalid_armature")] - - logger.info(f"Starting zero weight bone removal for: {armature.name}") - - messages = [] - removed_count = 0 - reparented_count = 0 - - # Store the current mode - current_mode = bpy.context.mode - - try: - # Switch to object mode to check weights - if bpy.context.mode != 'OBJECT': - bpy.context.view_layer.objects.active = armature - bpy.ops.object.mode_set(mode='OBJECT') - - # Get all meshes using this armature - meshes = [obj for obj in bpy.data.objects - if obj.type == 'MESH' and obj.parent == armature] - - # Track bones with weights - bones_with_weights = set() - - for mesh in meshes: - for vertex_group in mesh.vertex_groups: - # Check if any vertices have non-zero weights - has_weight = False - for vert in mesh.data.vertices: - try: - weight = vertex_group.weight(vert.index) - if weight > 0.001: # Threshold for "zero" weight - has_weight = True - break - except: - continue - - if has_weight: - bones_with_weights.add(vertex_group.name) - - # Switch to edit mode - bpy.ops.object.mode_set(mode='EDIT') - edit_bones = armature.data.edit_bones - - # Protected bone names (main skeleton) - protected_bones = { - "Hips", "Spine", "Chest", "UpperChest", "Neck", "Head", - "Left shoulder", "Right shoulder", "Shoulder_L", "Shoulder_R", - "Left arm", "Right arm", "UpperArm_L", "UpperArm_R", - "Left elbow", "Right elbow", "LowerArm_L", "LowerArm_R", - "Left wrist", "Right wrist", "Hand_L", "Hand_R", - "Left leg", "Right leg", "UpperLeg_L", "UpperLeg_R", - "Left knee", "Right knee", "LowerLeg_L", "LowerLeg_R", - "Left ankle", "Right ankle", "Foot_L", "Foot_R", - "Left toe", "Right toe", "Toe_L", "Toe_R", - "Left eye", "Right eye", "Eye_L", "Eye_R" - } - - # Protected bone name patterns (never remove these even with zero weights) - protected_patterns = [ - r'.*[bB]reast.*', # Breast bones - r'.*[bB]ust.*', # Bust bones - r'.*[sS]kirt.*', # Skirt bones - r'.*[hH]air.*', # Hair bones - r'.*[bB]ag.*', # Bag/accessory bones - r'.*[rR]ibbon.*', # Ribbon bones - r'.*[tT]ail.*', # Tail bones - r'.*[wW]ing.*', # Wing bones - r'.*[eE]ar.*', # Ear bones (not Eye) - r'.*[sS]leeve.*', # Sleeve bones - r'.*[cC]ape.*', # Cape bones - r'.*[sS]carf.*', # Scarf bones - r'.*[cC]oat.*', # Coat bones - r'.*[dD]ress.*', # Dress bones - r'.*[fF]inger.*', # Finger bones - r'.*[tT]humb.*', # Thumb bones - r'.*[iI]ndex.*', # Index finger bones - r'.*[mM]iddle.*', # Middle finger bones - r'.*[rR]ing.*', # Ring finger bones - r'.*[pP]ink.*', # Pinky bones - r'.*[tT]oe.*', # Toe bones - r'.*[aA]ccessor.*', # Accessory bones - r'.*[jJ]oint.*', # Joint bones - r'.*[cC]loth.*', # Cloth bones - r'.*[pP]hys.*', # Physics bones - ] - compiled_protected = [re.compile(pattern) for pattern in protected_patterns] - - # Identify zero weight bones (but exclude protected bones) - bones_to_remove = [] - for bone in edit_bones: - # Skip if bone has weights - if bone.name in bones_with_weights: - continue - - # Skip if in protected set - if bone.name in protected_bones: - continue - - # Skip if matches protected pattern - is_protected = any(pattern.match(bone.name) for pattern in compiled_protected) - if is_protected: - logger.debug(f"Protected bone (zero weight but keeping): {bone.name}") - continue - - bones_to_remove.append(bone.name) - logger.debug(f"Zero weight bone identified: {bone.name}") - - # Reparent children before removing - 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'}") - - # Remove zero weight 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 zero weight bone: {bone_name}") - - except Exception as e: - logger.error(f"Error during zero weight bone removal: {e}", exc_info=True) - messages.append(t("MMD.zero_weight_removal_failed", error=str(e))) - return False, messages - - finally: - # Restore original mode - if current_mode != 'OBJECT': - bpy.ops.object.mode_set(mode='OBJECT') - - # Generate messages - if removed_count > 0: - messages.append(t("MMD.zero_weight_bones_removed", count=removed_count)) - else: - messages.append(t("MMD.no_zero_weight_bones_found")) - - if reparented_count > 0: - messages.append(t("MMD.bones_reparented", count=reparented_count)) - - logger.info(f"Zero weight bone removal complete: {removed_count} removed, {reparented_count} reparented") - - return True, messages diff --git a/functions/tools/mmd_conversion.py b/functions/tools/mmd_conversion.py deleted file mode 100644 index c0d2047..0000000 --- a/functions/tools/mmd_conversion.py +++ /dev/null @@ -1,154 +0,0 @@ -""" -MMD Conversion Operator -Converts MMD armatures to standard Blender format -""" -import bpy -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, restructure_mmd_to_unity_bones, - remove_mmd_ik_bones, remove_mmd_twist_bones, remove_mmd_zero_weight_bones) -from ...core.logging_setup import logger - - -class AvatarToolkit_OT_ConvertMMDArmature(Operator): - """Convert MMD armature to standard Blender format""" - bl_idname = "avatar_toolkit.convert_mmd_armature" - bl_label = t("MMD.convert_armature.label") - bl_description = t("MMD.convert_armature.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 MMD conversion") - self.report({'ERROR'}, t("MMD.no_armature_selected")) - return {'CANCELLED'} - - logger.info(f"Starting MMD conversion for armature: {armature.name}") - - # Check if it's an MMD armature - if not detect_mmd_armature(armature): - logger.warning(f"Armature '{armature.name}' does not appear to be an MMD armature") - self.report({'WARNING'}, t("MMD.not_mmd_armature")) - return {'CANCELLED'} - - # Get conversion settings - toolkit = context.scene.avatar_toolkit - make_parent = toolkit.mmd_make_parent - rename_armature = toolkit.mmd_rename_armature - translate_names = toolkit.mmd_translate_names - translate_bones = toolkit.mmd_translate_bones - translate_materials = toolkit.mmd_translate_materials - translate_shapekeys = toolkit.mmd_translate_shapekeys - translate_objects = toolkit.mmd_translate_objects - restructure_bones = toolkit.mmd_restructure_bones - remove_twist_bones = toolkit.mmd_remove_twist_bones - remove_zero_weight_bones = toolkit.mmd_remove_zero_weight_bones - - logger.info(f"Conversion settings - Make parent: {make_parent}, Rename: {rename_armature}, " + - f"Restructure: {restructure_bones}") - logger.info(f"Bone cleanup - IK: True (automatic), Twist: {remove_twist_bones}, Zero weight: {remove_zero_weight_bones}") - logger.info(f"Translation settings - Enabled: {translate_names}, Bones: {translate_bones}, " + - f"Materials: {translate_materials}, Shapekeys: {translate_shapekeys}, Objects: {translate_objects}") - - # Step 1: Basic conversion (parent and rename) - success, messages = convert_mmd_armature(armature, make_parent, rename_armature) - - if not success: - logger.warning(f"MMD conversion failed: {messages}") - for msg in messages: - self.report({'WARNING'}, msg) - return {'CANCELLED'} - - logger.info(f"MMD basic conversion completed successfully") - for msg in messages: - self.report({'INFO'}, msg) - - # Step 2: Remove IK bones BEFORE translation (always automatic) - logger.info("Starting IK bone removal (before translation)") - self.report({'INFO'}, "Removing IK bones...") - - ik_success, ik_messages = remove_mmd_ik_bones(armature) - - if ik_success: - logger.info("IK bone removal completed successfully") - else: - logger.warning("IK bone removal completed with errors") - - for msg in ik_messages: - self.report({'INFO'}, msg) - - # Step 3: Remove twist bones BEFORE translation (if enabled) - if remove_twist_bones: - logger.info("Starting twist bone removal (before translation)") - self.report({'INFO'}, "Removing twist bones...") - - twist_success, twist_messages = remove_mmd_twist_bones(armature) - - if twist_success: - logger.info("Twist bone removal completed successfully") - else: - logger.warning("Twist bone removal completed with errors") - - for msg in twist_messages: - self.report({'INFO'}, msg) - - # Step 4: Translation (if enabled) - if translate_names: - logger.info("Starting MMD name translation") - self.report({'INFO'}, t("MMD.translation_starting")) - - trans_success, trans_messages = translate_mmd_everything( - armature, - translate_bones=translate_bones, - translate_materials=translate_materials, - translate_shapekeys=translate_shapekeys, - translate_objects=translate_objects - ) - - if trans_success: - logger.info("MMD name translation completed successfully") - else: - logger.warning("MMD name translation completed with some failures") - - for msg in trans_messages: - self.report({'INFO'}, msg) - - # Step 5: 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) - - # Step 6: Remove zero weight bones (if enabled) - if remove_zero_weight_bones: - logger.info("Starting zero weight bone removal") - self.report({'INFO'}, "Removing zero weight bones...") - - zero_success, zero_messages = remove_mmd_zero_weight_bones(armature) - - if zero_success: - logger.info("Zero weight bone removal completed successfully") - else: - logger.warning("Zero weight bone removal completed with errors") - - for msg in zero_messages: - self.report({'INFO'}, msg) - - return {'FINISHED'} diff --git a/ui/mmd_panel.py b/ui/mmd_panel.py deleted file mode 100644 index 7c1397e..0000000 --- a/ui/mmd_panel.py +++ /dev/null @@ -1,125 +0,0 @@ -""" -MMD Converter Panel - UI for MMD conversion tools -""" -import bpy -from bpy.types import Panel, Context, UILayout -from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME -from .panel_layout import get_panel_order, should_open_by_default -from ..core.translations import t -from ..core.common import get_active_armature -from ..core.mmd_converter import detect_mmd_armature -from ..functions.tools.mmd_conversion import AvatarToolkit_OT_ConvertMMDArmature - - -class AvatarToolKit_PT_MMDPanel(Panel): - """Panel for MMD conversion tools""" - bl_label = t("MMD.panel.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 = get_panel_order('mmd') - bl_options = set() if not should_open_by_default('MMD') else {'DEFAULT_CLOSED'} - - def draw(self, context: Context) -> None: - """Draw the MMD conversion panel interface""" - layout: UILayout = self.layout - - # MMD Conversion Tools - mmd_box: UILayout = layout.box() - col: UILayout = mmd_box.column(align=True) - col.label(text=t("MMD.converter.title"), icon='ARMATURE_DATA') - col.separator(factor=0.5) - - # Check if we have an active armature - armature = get_active_armature(context) - - if not armature: - col.label(text=t("MMD.no_armature_selected"), icon='ERROR') - col.label(text=t("MMD.select_armature_to_convert")) - return - - # Check if the armature appears to be MMD - is_mmd = detect_mmd_armature(armature) - - if is_mmd: - col.label(text=t("MMD.armature_name", name=armature.name), icon='CHECKMARK') - col.label(text=t("MMD.armature_detected"), icon='INFO') - col.separator(factor=0.3) - - toolkit = context.scene.avatar_toolkit - - # Basic conversion settings - col.prop(toolkit, 'mmd_make_parent', text=t("MMD.make_armature_parent")) - 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) - - # Bone cleanup options - col.label(text=t("MMD.bone_cleanup"), icon='BONE_DATA') - cleanup_box = col.box() - cleanup_col = cleanup_box.column(align=True) - cleanup_col.prop(toolkit, 'mmd_remove_twist_bones', text=t("MMD.remove_twist_bones")) - cleanup_col.prop(toolkit, 'mmd_remove_zero_weight_bones', text=t("MMD.remove_zero_weight_bones")) - col.separator(factor=0.2) - - # Translation settings - col.prop(toolkit, 'mmd_translate_names', text=t("MMD.translate_names")) - - # Translation sub-options (only show if translation is enabled) - if toolkit.mmd_translate_names: - trans_box = col.box() - trans_col = trans_box.column(align=True) - trans_col.label(text=t("MMD.translation_options"), icon='WORLD') - trans_col.prop(toolkit, 'mmd_translate_bones', text=t("MMD.translate_bones")) - trans_col.prop(toolkit, 'mmd_translate_materials', text=t("MMD.translate_materials")) - trans_col.prop(toolkit, 'mmd_translate_shapekeys', text=t("MMD.translate_shapekeys")) - trans_col.prop(toolkit, 'mmd_translate_objects', text=t("MMD.translate_objects")) - - col.separator(factor=0.2) - - col.operator( - AvatarToolkit_OT_ConvertMMDArmature.bl_idname, - text=t("MMD.convert_armature_button"), - icon='EXPORT' - ) - - info_box = mmd_box.box() - info_col = info_box.column(align=True) - 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.removes_ik_bones")) - if toolkit.mmd_remove_twist_bones: - info_col.label(text=t("MMD.conversion_info.removes_twist_bones")) - if toolkit.mmd_remove_zero_weight_bones: - info_col.label(text=t("MMD.conversion_info.removes_zero_weight_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")) - - else: - col.label(text=t("MMD.armature_name", name=armature.name), icon='ERROR') - col.label(text=t("MMD.no_mmd_bones_detected"), icon='CANCEL') - col.separator(factor=0.3) - - row = col.row() - row.enabled = False - row.operator( - AvatarToolkit_OT_ConvertMMDArmature.bl_idname, - text=t("MMD.convert_armature_button"), - icon='CANCEL' - ) - - help_box = mmd_box.box() - help_col = help_box.column(align=True) - help_col.label(text=t("MMD.detection_failed.title"), icon='QUESTION') - help_col.label(text=t("MMD.detection_failed.not_mmd_format")) - help_col.label(text=t("MMD.detection_failed.need_mmd_bones")) - help_col.label(text=t("MMD.detection_failed.check_bone_names")) From 1e734a518e4d291cb9771af94bdd60e263233eaa Mon Sep 17 00:00:00 2001 From: Yusarina Date: Sat, 29 Nov 2025 22:48:25 +0000 Subject: [PATCH 7/8] Fix garbled Japanese/Unicode text in armature and mesh dropdowns - Add proper caching to EnumProperty callbacks to prevent encoding corruption - Use ASCII-safe identifiers (ARM_/MESH_ + pointer) with Unicode display names - Add get_mesh_from_identifier() helper for safe mesh retrieval - Update visemes panel to use new mesh identifier system - Ensure stable string objects prevent Blender RNA encoding issues --- core/common.py | 70 +++++++++++++++++++++++++++++++++++++++----- core/properties.py | 38 ++++++++++++++++++++++-- functions/visemes.py | 12 +++++--- ui/visemes_panel.py | 5 ++-- 4 files changed, 108 insertions(+), 17 deletions(-) diff --git a/core/common.py b/core/common.py index 8299b30..f63163e 100644 --- a/core/common.py +++ b/core/common.py @@ -142,6 +142,41 @@ def set_active_armature(context: Context, armature: Object) -> None: else: context.scene.avatar_toolkit.active_armature = 'NONE' +def get_mesh_from_identifier(mesh_id: str) -> Optional[Object]: + """Get mesh object from safe identifier + + Args: + mesh_id: Safe identifier in format "MESH_{pointer}" or direct object name + + Returns: + Mesh object or None if not found + """ + if not mesh_id or mesh_id == 'NONE': + return None + + # Handle new-style identifiers (MESH_{pointer}) + if mesh_id.startswith('MESH_'): + try: + pointer_str = mesh_id[5:] # Remove "MESH_" prefix + target_pointer = int(pointer_str) + + # Search for object with matching pointer + for obj in bpy.data.objects: + if obj.type == 'MESH' and obj.as_pointer() == target_pointer: + return obj + except (ValueError, AttributeError): + pass + + # Fallback for old-style identifiers (direct name) + return bpy.data.objects.get(mesh_id) + +def clear_enum_caches() -> None: + """Clear all enum property caches to force refresh of dropdown lists""" + if hasattr(get_armature_list, '_cache_key'): + delattr(get_armature_list, '_cache_key') + if hasattr(get_armature_list, '_cached_items'): + delattr(get_armature_list, '_cached_items') + def get_armature_list(self: Optional[Any] = None, context: Optional[Context] = None) -> List[Tuple[str, str, str]]: """Get list of all armature objects in the scene @@ -149,21 +184,40 @@ def get_armature_list(self: Optional[Any] = None, context: Optional[Context] = N - identifier: ASCII-safe unique ID (uses object's memory address) - display_name: The actual object name (can contain Japanese characters) - description: Empty string + + Uses caching to prevent encoding issues with Blender's EnumProperty system """ if context is None: context = bpy.context - # Use object's as_pointer() value as a safe ASCII identifier + # Create a cache key based on armature objects in scene + armature_objects = [obj for obj in context.scene.objects if obj.type == 'ARMATURE'] + cache_key = tuple((obj.name, obj.as_pointer()) for obj in armature_objects) + + # Check if we have a cached result + if hasattr(get_armature_list, '_cache_key') and get_armature_list._cache_key == cache_key: + if hasattr(get_armature_list, '_cached_items'): + return get_armature_list._cached_items + + # Build the list armatures = [] - for obj in context.scene.objects: - if obj.type == 'ARMATURE': - # Create a safe ASCII identifier using the object pointer - safe_id = f"ARM_{obj.as_pointer()}" - armatures.append((safe_id, obj.name, "")) + for obj in armature_objects: + # Create a safe ASCII identifier using the object pointer + safe_id = f"ARM_{obj.as_pointer()}" + # Use the name directly - Blender should handle Unicode in display names + display_name = obj.name + armatures.append((safe_id, display_name, "")) if not armatures: - return [('NONE', t("Armature.validation.no_armature"), '')] - return armatures + result = [('NONE', t("Armature.validation.no_armature"), '')] + else: + result = armatures + + # Cache the result + get_armature_list._cache_key = cache_key + get_armature_list._cached_items = result + + return result def auto_select_single_armature(context: Context) -> None: """Automatically select armature if only one exists in scene""" diff --git a/core/properties.py b/core/properties.py index 2c2e3f5..6b666fb 100644 --- a/core/properties.py +++ b/core/properties.py @@ -67,10 +67,42 @@ def highlight_problem_bones(self: PropertyGroup, context: Context) -> None: save_preference("highlight_problem_bones", self.highlight_problem_bones) def get_mesh_objects(self, context): - meshes = [(obj.name, obj.name, "") for obj in bpy.data.objects if obj.type == 'MESH'] + """Get list of all mesh objects with ASCII-safe identifiers + + Returns tuples of (identifier, display_name, description) where: + - identifier: ASCII-safe unique ID (uses object's memory address) + - display_name: The actual object name (can contain Japanese/non-ASCII characters) + - description: Empty string + + Uses caching to prevent encoding issues with Blender's EnumProperty system + """ + # Create a cache key based on mesh objects + mesh_objects = [obj for obj in bpy.data.objects if obj.type == 'MESH'] + cache_key = tuple((obj.name, obj.as_pointer()) for obj in mesh_objects) + + # Check if we have a cached result + if hasattr(get_mesh_objects, '_cache_key') and get_mesh_objects._cache_key == cache_key: + if hasattr(get_mesh_objects, '_cached_items'): + return get_mesh_objects._cached_items + + # Build the list + meshes = [] + for obj in mesh_objects: + safe_id = f"MESH_{obj.as_pointer()}" + # Use the name directly - Blender should handle Unicode in display names + display_name = obj.name + meshes.append((safe_id, display_name, "")) + if not meshes: - return [('NONE', t("Visemes.no_meshes"), '')] - return meshes + result = [('NONE', t("Visemes.no_meshes"), '')] + else: + result = meshes + + # Cache the result + get_mesh_objects._cache_key = cache_key + get_mesh_objects._cached_items = result + + return result def auto_populate_merge_armatures(context: Context) -> None: """Auto-populate merge armature fields when there are 2+ armatures""" diff --git a/functions/visemes.py b/functions/visemes.py index 008bd3c..da22101 100644 --- a/functions/visemes.py +++ b/functions/visemes.py @@ -137,15 +137,17 @@ class AvatarToolkit_OT_PreviewVisemes(Operator): return False # Get mesh from UI selection + from ..core.common import get_mesh_from_identifier props = context.scene.avatar_toolkit - mesh_obj = bpy.data.objects.get(props.viseme_mesh) + mesh_obj = get_mesh_from_identifier(props.viseme_mesh) # Validate mesh return mesh_obj and mesh_obj.type == 'MESH' def execute(self, context: Context) -> Set[str]: + from ..core.common import get_mesh_from_identifier props = context.scene.avatar_toolkit - mesh = bpy.data.objects.get(props.viseme_mesh) + mesh = get_mesh_from_identifier(props.viseme_mesh) if props.viseme_preview_mode: VisemePreview.end_preview(mesh) @@ -191,15 +193,17 @@ class AvatarToolkit_OT_CreateVisemes(Operator): return False # Get mesh from UI selection + from ..core.common import get_mesh_from_identifier props = context.scene.avatar_toolkit - mesh_obj = bpy.data.objects.get(props.viseme_mesh) + mesh_obj = get_mesh_from_identifier(props.viseme_mesh) # Validate mesh return mesh_obj and mesh_obj.type == 'MESH' def execute(self, context: Context) -> Set[str]: + from ..core.common import get_mesh_from_identifier props = context.scene.avatar_toolkit - mesh = bpy.data.objects.get(props.viseme_mesh) # Changed from context.active_object + mesh = get_mesh_from_identifier(props.viseme_mesh) if not mesh or not mesh.data.shape_keys: self.report({'ERROR'}, t("Visemes.error.no_shapekeys")) diff --git a/ui/visemes_panel.py b/ui/visemes_panel.py index b5563dd..544cde8 100644 --- a/ui/visemes_panel.py +++ b/ui/visemes_panel.py @@ -34,8 +34,9 @@ class AvatarToolKit_PT_VisemesPanel(Panel): else: col.label(text=t("Visemes.no_armature"), icon='ERROR') - # Get selected mesh - mesh_obj = bpy.data.objects.get(props.viseme_mesh) + # Get selected mesh using safe identifier + from ..core.common import get_mesh_from_identifier + mesh_obj = get_mesh_from_identifier(props.viseme_mesh) if not mesh_obj or not mesh_obj.data or not mesh_obj.data.shape_keys: layout.label(text=t("Visemes.no_shapekeys")) return From 24b489f7a2c4138c1ddf3844fc93f24acfe07d09 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Sat, 29 Nov 2025 22:49:18 +0000 Subject: [PATCH 8/8] Fix swapped operator IDs for Apply Pose as Rest/Shapekey buttons --- functions/pose_mode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/functions/pose_mode.py b/functions/pose_mode.py index b55fea1..f0815c8 100644 --- a/functions/pose_mode.py +++ b/functions/pose_mode.py @@ -92,7 +92,7 @@ class AvatarToolkit_OT_StopPoseMode(Operator): self.report({'ERROR'}, t("PoseMode.error.stop", error=traceback.format_exc())) return {'CANCELLED'} -class AvatarToolkit_OT_ApplyPoseAsRest(Operator, BatchPoseOperationMixin): +class AvatarToolkit_OT_ApplyPoseAsShapekey(Operator, BatchPoseOperationMixin): bl_idname = 'avatar_toolkit.apply_pose_as_shapekey' bl_label = t("QuickAccess.apply_pose_as_shapekey.label") bl_description = t("QuickAccess.apply_pose_as_shapekey.desc") @@ -136,7 +136,7 @@ class AvatarToolkit_OT_ApplyPoseAsRest(Operator, BatchPoseOperationMixin): self.report({'ERROR'}, t("PoseMode.error.shapekey", error=traceback.format_exc())) return {'CANCELLED'} -class AvatarToolkit_OT_ApplyPoseAsShapekey(Operator, BatchPoseOperationMixin): +class AvatarToolkit_OT_ApplyPoseAsRest(Operator, BatchPoseOperationMixin): bl_idname = 'avatar_toolkit.apply_pose_as_rest' bl_label = t("QuickAccess.apply_pose_as_rest.label") bl_description = t("QuickAccess.apply_pose_as_rest.desc")