Start of the MMD Converter

This commit is contained in:
Yusarina
2025-11-22 16:39:28 +00:00
parent aedd83e078
commit 95cb726485
7 changed files with 381 additions and 1 deletions
+20
View File
@@ -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] = {}
+166
View File
@@ -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
+13
View File
@@ -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"),
+58
View File
@@ -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'}
+33
View File
@@ -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",
+87
View File
@@ -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"))
+4 -1
View File
@@ -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)