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/enhanced_dictionaries.py b/core/enhanced_dictionaries.py index e35c9a7..f641a4c 100644 --- a/core/enhanced_dictionaries.py +++ b/core/enhanced_dictionaries.py @@ -174,6 +174,130 @@ 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' +] + +# 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/properties.py b/core/properties.py index 7c14f56..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""" @@ -703,6 +735,67 @@ 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 + ) + + 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 + ) + + mmd_restructure_bones: BoolProperty( + name=t("MMD.restructure_bones"), + 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( name=t("Translation.service"), 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") 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/resources/translations/en_US.json b/resources/translations/en_US.json index bcb8ec5..8789feb 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -601,6 +601,79 @@ "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.translate_names": "Translate Names to English", + "MMD.translate_bones": "Bones", + "MMD.translate_materials": "Materials", + "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", + "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.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:", + "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", + "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", + "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}", + "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", "Translation.service_desc": "Choose the translation service to use", 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) 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