Re-do 3rd attempt I hate MMD stuff

This commit is contained in:
Yusarina
2024-12-13 01:59:28 +00:00
parent c39f77d6d5
commit 1e0fe403aa
4 changed files with 473 additions and 577 deletions
+453 -454
View File
@@ -1,62 +1,190 @@
import bpy
from typing import Tuple, Set, Dict
from bpy.types import Operator, Context, Object
from mathutils import Vector
from typing import Dict, List, Tuple, Set, Optional
from bpy.types import Object, Armature, EditBone, Bone, Operator, Context
from ..core.logging_setup import logger
from ..core.common import (
ProgressTracker,
get_active_armature,
validate_meshes,
simplify_bonename,
duplicate_bone_chain,
save_armature_state,
restore_armature_state,
get_all_meshes,
validate_bone_hierarchy,
transfer_vertex_weights,
get_vertex_weights
validate_armature,
get_vertex_weights,
transfer_vertex_weights
)
from ..core.logging_setup import logger
from ..core.translations import t
from ..core.dictionaries import bone_names
class AvatarToolkit_OT_StandardizeMMDBones(Operator):
bl_idname = "avatar_toolkit.mmd_standardize_bones"
bl_label = t("MMD.standardize_bones")
class AVATAR_TOOLKIT_OT_StandardizeMmd(Operator):
"""MMD Bone standardization system"""
bl_idname = "avatar_toolkit.standardize_mmd"
bl_label = t("MMD.standardize")
bl_options = {'REGISTER', 'UNDO'}
def standardize_bone_names(self, armature: Object) -> None:
"""Standardize bone names using MMD to Unity/VRChat conventions"""
for bone in armature.data.bones:
simplified_name = simplify_bonename(bone.name)
for standard_name, variations in bone_names.items():
if simplified_name in variations:
bone.name = standard_name
break
def process_lr_bones(self, armature: Object) -> None:
"""Process left/right bone pairs for consistency"""
for bone in armature.data.bones:
if bone.name.endswith(('_l', '_r', '.l', '.r', 'Left', 'Right')):
base_name = bone.name.rsplit('_', 1)[0]
side = '_l' if any(s in bone.name.lower() for s in ('left', '_l', '.l')) else '_r'
bone.name = f"{base_name}{side}"
def resolve_name_conflicts(self, armature: Object) -> None:
"""Handle duplicate bone names"""
used_names = set()
for bone in armature.data.bones:
base_name = bone.name
counter = 1
while bone.name in used_names:
bone.name = f"{base_name}_{counter}"
counter += 1
used_names.add(bone.name)
def process_spine_chain(self, armature: Object) -> None:
"""Process spine bones for VRChat compatibility"""
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = armature.data.edit_bones
def __init__(self):
self.bone_mapping: Dict[str, str] = {}
self.processed_bones: Set[str] = set()
def execute(self, context: Context) -> Set[str]:
self.armature = get_active_armature(context)
if not self.armature:
self.report({'ERROR'}, t("MMD.no_armature"))
return {'CANCELLED'}
try:
with ProgressTracker(context, 5, "MMD Standardization") as progress:
# Step 1: Process bone names
self.process_bone_names(context)
progress.step("Processed bone names")
# Step 2: Fix bone structure
self.fix_bone_structure(context)
progress.step("Fixed bone structure")
# Step 3: Process weights
self.process_weights(context)
progress.step("Processed weights")
# Step 4: Clean up
self.cleanup_armature(context)
progress.step("Cleaned up armature")
# Step 5: Final validation
self.validate_results(context)
progress.step("Validated results")
self.report({'INFO'}, t("MMD.standardization_complete"))
return {'FINISHED'}
except Exception as e:
logger.error(f"MMD Standardization failed: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def standardize_armature(self) -> Tuple[bool, str]:
"""Main standardization process"""
if not self.armature:
return False, t("MMD.no_armature")
try:
with ProgressTracker(self.context, 5, "MMD Standardization") as progress:
# Step 1: Process bone names
self.process_bone_names()
progress.step("Processed bone names")
# Step 2: Fix bone structure
self.fix_bone_structure()
progress.step("Fixed bone structure")
# Step 3: Process weights
self.process_weights()
progress.step("Processed weights")
# Step 4: Clean up
self.cleanup_armature()
progress.step("Cleaned up armature")
# Step 5: Final validation
self.validate_results()
progress.step("Validated results")
return True, t("MMD.standardization_complete")
except Exception as e:
logger.error(f"MMD Standardization failed: {str(e)}")
return False, str(e)
def process_bone_names(self, context: Context) -> None:
"""Process and standardize bone names"""
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = self.armature.data.edit_bones
for bone in edit_bones:
new_name = self.standardize_bone_name(bone.name)
if new_name != bone.name:
self.bone_mapping[bone.name] = new_name
bone.name = new_name
def translate_japanese_bone_name(self, name: str) -> str:
"""Translate Japanese bone names to English standardized names"""
from ..core.dictionaries import bone_names
# Convert to lowercase for matching
name_lower = name.lower()
# Check each bone category for Japanese character matches
for bone_category, variations in bone_names.items():
for variation in variations:
if variation in name_lower:
# If Japanese characters are found, return the standardized name
return bone_category
# If no match found, return original name
return name
def standardize_bone_name(self, name: str) -> str:
"""Standardize individual bone names"""
# First translate Japanese names
result = self.translate_japanese_bone_name(name)
# Remove common prefixes
prefixes = ['ValveBiped_', 'Bip01_', 'MMD_', 'Armature|']
for prefix in prefixes:
if result.lower().startswith(prefix.lower()):
result = result[len(prefix):]
# Handle left/right conventions
if result.endswith('_L') or result.endswith('.L'):
result = f"{result[:-2]}.L"
elif result.endswith('_R') or result.endswith('.R'):
result = f"{result[:-2]}.R"
return result
def fix_bone_structure(self, context: Context) -> None:
"""Fix bone hierarchy and orientations"""
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = self.armature.data.edit_bones
# Process spine hierarchy
self.process_spine_chain(context)
# Fix bone orientations
self.fix_bone_orientations(context)
# Connect appropriate bones
self.connect_bones(context)
def process_weights(self, context: Context) -> None:
"""Process and clean up vertex weights"""
for mesh in self.get_associated_meshes(context):
# Transfer weights based on bone mapping
for old_name, new_name in self.bone_mapping.items():
if old_name != new_name:
transfer_vertex_weights(mesh, old_name, new_name)
# Clean up zero weights
self.cleanup_vertex_groups(mesh, context)
def cleanup_armature(self, context: Context) -> None:
"""Perform final cleanup operations"""
# Remove unused bones
self.remove_unused_bones(context)
# Clean up constraints
self.cleanup_constraints(context)
# Fix zero-length bones
self.fix_zero_length_bones(context)
def get_associated_meshes(self, context: Context) -> List[Object]:
"""Get all mesh objects associated with the armature"""
return [obj for obj in bpy.data.objects
if obj.type == 'MESH'
and obj.parent == self.armature]
def process_spine_chain(self, context: Context) -> None:
"""Process and fix spine bone chain hierarchy"""
edit_bones = self.armature.data.edit_bones
spine_bones = {
'hips': None,
'spine': None,
@@ -66,437 +194,308 @@ class AvatarToolkit_OT_StandardizeMMDBones(Operator):
'head': None
}
# Map existing spine bones
# Find spine bones using bone_names dictionary
for bone in edit_bones:
simplified = simplify_bonename(bone.name)
for spine_name in spine_bones.keys():
if simplified in bone_names[spine_name]:
spine_bones[spine_name] = bone
for spine_part, _ in spine_bones.items():
if any(alt_name in bone.name.lower() for alt_name in bone_names[spine_part]):
spine_bones[spine_part] = bone
break
# Create missing spine bones
if spine_bones['spine'] and not spine_bones['chest']:
chest = edit_bones.new('chest')
chest.head = spine_bones['spine'].tail
chest.tail = spine_bones['neck'].head if spine_bones['neck'] else spine_bones['head'].head
spine_bones['chest'] = chest
# Set up spine hierarchy
if spine_bones['hips']:
for i, key in enumerate(['spine', 'chest', 'upper_chest', 'neck', 'head']):
if spine_bones[key]:
prev_key = list(spine_bones.keys())[i]
if spine_bones[prev_key]:
spine_bones[key].parent = spine_bones[prev_key]
def correct_bone_orientations(self, armature: Object) -> None:
"""Automatically correct bone orientations to align with Unity's axes"""
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = armature.data.edit_bones
# Define standard orientations
orientations = {
'spine': Vector((0, 0, 1)), # Points up
'chest': Vector((0, 0, 1)),
'neck': Vector((0, 0, 1)),
'head': Vector((0, 0, 1)),
'shoulder': Vector((1, 0, 0)), # Points outward
'arm': Vector((0, -1, 0)), # Points down
'elbow': Vector((0, -1, 0)),
'leg': Vector((0, -1, 0)),
'knee': Vector((0, -1, 0)),
'foot': Vector((1, 0, 0)), # Points forward
}
for bone in edit_bones:
simplified_name = simplify_bonename(bone.name)
for bone_type, direction in orientations.items():
if bone_type in simplified_name:
# Calculate new tail position while maintaining length
length = (bone.tail - bone.head).length
bone.tail = bone.head + direction * length
break
@classmethod
def poll(cls, context: Context) -> bool:
"""Check if there is an active armature in the scene"""
return get_active_armature(context) is not None
def execute(self, context: Context) -> Set[str]:
try:
armature = get_active_armature(context)
# Save initial state if enabled
if context.scene.avatar_toolkit.save_backup_state:
self.initial_state = save_armature_state(armature)
with ProgressTracker(context, 6, "Standardizing Bones") as progress:
# Step 1: Standardize bone names
self.standardize_bone_names(armature)
progress.step("Standardized bone names")
# Step 3: Process left/right bones
self.process_lr_bones(armature)
progress.step("Processed left/right bones")
# Step 4: Handle name conflicts
self.resolve_name_conflicts(armature)
progress.step("Resolved naming conflicts")
# Step 5: Process spine chain
self.process_spine_chain(armature)
progress.step("Processed spine chain")
# Step 6: Correct bone orientations
self.correct_bone_orientations(armature)
progress.step("Corrected bone orientations")
self.report({'INFO'}, t("MMD.bones_standardized"))
return {'FINISHED'}
except Exception as e:
logger.error(f"Bone standardization failed: {str(e)}")
if hasattr(self, 'initial_state'):
restore_armature_state(armature, self.initial_state)
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
class AvatarToolkit_OT_ProcessMMDWeights(Operator):
bl_idname = "avatar_toolkit.mmd_process_weights"
bl_label = t("MMD.process_weights")
bl_options = {'REGISTER', 'UNDO'}
def merge_bone_weights(self, context: Context, mesh: Object, source: str, target: str) -> None:
"""Transfer weights from source bone to target bone"""
transfer_vertex_weights(
mesh,
source,
target,
context.scene.avatar_toolkit.merge_weights_threshold
)
def process_eye_weights(self, context: Context, mesh: Object) -> None:
"""Handle special cases for eye bone weights"""
eye_bones = {
'eye_l': ['eyel', 'lefteye', 'eye.l'],
'eye_r': ['eyer', 'righteye', 'eye.r']
}
for target, sources in eye_bones.items():
for source in sources:
if source in mesh.vertex_groups:
self.merge_bone_weights(context, mesh, source, target)
def process_twist_bones(self, context: Context, mesh: Object) -> None:
"""Process and merge twist bone weights"""
if not context.scene.avatar_toolkit.mmd_process_twist_bones:
return
twist_pairs = [
('arm_twist_l', 'left_arm'),
('arm_twist_r', 'right_arm'),
('forearm_twist_l', 'left_elbow'),
('forearm_twist_r', 'right_elbow')
hierarchy = [
('hips', 'spine'),
('spine', 'chest'),
('chest', 'neck'),
('neck', 'head')
]
for twist, target in twist_pairs:
if twist in mesh.vertex_groups:
self.merge_bone_weights(context, mesh, twist, target)
for parent_name, child_name in hierarchy:
parent = spine_bones.get(parent_name)
child = spine_bones.get(child_name)
if parent and child:
child.parent = parent
child.use_connect = True
def cleanup_vertex_groups(self, context: Context, mesh: Object) -> None:
"""Remove empty and unused vertex groups"""
threshold = context.scene.avatar_toolkit.clean_weights_threshold
def fix_bone_orientations(self, context: Context) -> None:
"""Fix bone orientations for standard pose compatibility"""
edit_bones = self.armature.data.edit_bones
# Get list of used bones from armature
armature = mesh.find_armature()
if not armature:
return
valid_bones = set(bone.name for bone in armature.data.bones)
# Process arm bones
arm_pairs = [
('upper_arm', 'forearm'),
('forearm', 'hand')
]
# Remove unused groups
for group in mesh.vertex_groups[:]:
if group.name not in valid_bones:
mesh.vertex_groups.remove(group)
for side in ['.L', '.R']:
for parent, child in arm_pairs:
parent_bone = next((b for b in edit_bones if b.name.lower().startswith(parent) and b.name.endswith(side)), None)
child_bone = next((b for b in edit_bones if b.name.lower().startswith(child) and b.name.endswith(side)), None)
if parent_bone and child_bone:
child_bone.use_connect = True
child_bone.use_inherit_rotation = True
# Process leg bones
leg_pairs = [
('thigh', 'shin'),
('shin', 'foot')
]
for side in ['.L', '.R']:
for parent, child in leg_pairs:
parent_bone = next((b for b in edit_bones if b.name.lower().startswith(parent) and b.name.endswith(side)), None)
child_bone = next((b for b in edit_bones if b.name.lower().startswith(child) and b.name.endswith(side)), None)
if parent_bone and child_bone:
child_bone.use_connect = True
child_bone.use_inherit_rotation = True
def remove_unused_bones(self, context: Context) -> None:
"""Remove unused and unnecessary bones from the armature"""
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = self.armature.data.edit_bones
# Get list of bones that have vertex weights
used_bones = set()
for mesh in self.get_associated_meshes(context):
for group in mesh.vertex_groups:
used_bones.add(group.name)
# Get list of bones to keep based on settings
toolkit = context.scene.avatar_toolkit
keep_upper_chest = toolkit.keep_upper_chest
keep_twist = toolkit.keep_twist_bones
# Remove unused bones
for bone in edit_bones:
# Skip if bone has weights
if bone.name in used_bones:
continue
# Check if group has any weights above threshold
has_weights = False
for vert in mesh.data.vertices:
for group_element in vert.groups:
if group_element.group == group.index:
if group_element.weight > threshold:
has_weights = True
break
if has_weights:
break
if not has_weights:
mesh.vertex_groups.remove(group)
# Skip if bone is upper chest and we want to keep it
if 'upper_chest' in bone.name.lower() and keep_upper_chest:
continue
# Skip if bone is twist bone and we want to keep them
if 'twist' in bone.name.lower() and keep_twist:
continue
# Remove the bone
edit_bones.remove(bone)
def merge_remaining_weights(self, context: Context, mesh: Object) -> None:
"""Process remaining weight merging cases"""
# Common MMD weight merge pairs
merge_pairs = [
# Finger weights
('pinky', 'pinkie'),
('thumb0', 'thumb_0'),
('index0', 'index_0'),
('middle0', 'middle_0'),
('ring0', 'ring_0'),
# Additional arm weights
('upperarm', 'arm'),
('lowerarm', 'elbow'),
('wrist', 'hand'),
# Leg weights
('upperleg', 'leg'),
('lowerleg', 'knee'),
('ankle', 'foot'),
# Spine weights
('spine1', 'chest'),
('spine2', 'upper_chest'),
def connect_bones(self, context: Context) -> None:
"""Connect bones that should be connected in the hierarchy"""
edit_bones = self.armature.data.edit_bones
connect_chains = [
['hips', 'spine', 'chest', 'neck', 'head'],
['shoulder.L', 'upper_arm.L', 'forearm.L', 'hand.L'],
['shoulder.R', 'upper_arm.R', 'forearm.R', 'hand.R'],
['thigh.L', 'shin.L', 'foot.L', 'toe.L'],
['thigh.R', 'shin.R', 'foot.R', 'toe.R']
]
for source, target in merge_pairs:
for suffix in ['_l', '_r', '.l', '.r']:
source_name = f"{source}{suffix}"
target_name = f"{target}{suffix}"
if source_name in mesh.vertex_groups:
self.merge_bone_weights(context, mesh, source_name, target_name)
for chain in connect_chains:
prev_bone = None
for bone_name in chain:
bone = next((b for b in edit_bones if b.name.lower().endswith(bone_name.lower())), None)
if bone and prev_bone:
bone.parent = prev_bone
bone.use_connect = True
prev_bone = bone
def cleanup_vertex_groups(self, mesh_obj: Object, context: Context) -> None:
"""Clean up vertex groups by removing zero weights and merging similar groups"""
threshold = context.scene.avatar_toolkit.merge_weights_threshold
# Get list of vertex groups
vertex_groups = mesh_obj.vertex_groups
# Track groups to remove
groups_to_remove = set()
# Check each vertex group
for group in vertex_groups:
weights = get_vertex_weights(mesh_obj, group.name)
# If no weights above threshold, mark for removal
if not any(weight > threshold for weight in weights.values()):
groups_to_remove.add(group.name)
# Remove empty groups
for group_name in groups_to_remove:
group = vertex_groups.get(group_name)
if group:
vertex_groups.remove(group)
def validate_results(self, context: Context) -> None:
"""Validate the results of standardization"""
valid, messages = validate_armature(self.armature)
if not valid:
raise ValueError("\n".join(messages))
def cleanup_constraints(self, context: Context) -> None:
"""Clean up and fix bone constraints"""
bpy.ops.object.mode_set(mode='POSE')
# Process each pose bone
for pose_bone in self.armature.pose.bones:
constraints_to_remove = []
for constraint in pose_bone.constraints:
should_remove = False
# Handle IK constraints
if constraint.type == 'IK':
if not constraint.target or constraint.target != self.armature:
should_remove = True
elif not constraint.subtarget or constraint.subtarget not in self.armature.data.bones:
should_remove = True
# Handle MMD additional rotation constraints
elif constraint.name == 'mmd_additional_rotation':
if not constraint.target or constraint.target != self.armature:
should_remove = True
elif not constraint.subtarget or constraint.subtarget not in self.armature.data.bones:
should_remove = True
# Handle transformation constraints
elif constraint.type in {'COPY_ROTATION', 'COPY_LOCATION', 'COPY_TRANSFORMS'}:
if not constraint.target or constraint.target != self.armature:
should_remove = True
elif not constraint.subtarget or constraint.subtarget not in self.armature.data.bones:
should_remove = True
if should_remove:
constraints_to_remove.append(constraint)
# Remove invalid constraints
for constraint in constraints_to_remove:
pose_bone.constraints.remove(constraint)
def fix_zero_length_bones(self, context: Context) -> None:
"""Fix zero-length bones by setting minimal length"""
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = self.armature.data.edit_bones
min_length = 0.01 # Minimum bone length in Blender units
for bone in edit_bones:
# Calculate bone length
bone_length = (bone.tail - bone.head).length
if bone_length < min_length:
# Set minimal length while preserving direction
if bone.parent:
# Use parent's orientation as reference
direction = bone.parent.tail - bone.parent.head
direction.normalize()
else:
# Default to Z-axis if no parent
direction = mathutils.Vector((0, 0, 1))
bone.tail = bone.head + (direction * min_length)
class FixUnmovableBonesOperator(bpy.types.Operator):
bl_idname = "avatar_toolkit.fix_unmovable_bones"
bl_label = t("MMD.fix_unmovable_bones")
bl_description = t("MMD.fix_unmovable_bones_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
"""Check if there is an active armature in the scene"""
return get_active_armature(context) is not None
def poll(cls, context):
armature = get_active_armature(context)
return armature is not None and armature.type == 'ARMATURE'
def execute(self, context):
armature = get_active_armature(context)
if not armature:
self.report({'ERROR'}, t("MMD.no_armature"))
return {'CANCELLED'}
def execute(self, context: Context) -> Set[str]:
try:
meshes = get_all_meshes(context)
# Save initial state
if context.scene.avatar_toolkit.save_backup_state:
self.initial_states = {mesh: get_vertex_weights(mesh) for mesh in meshes}
with ProgressTracker(context, len(meshes) * 4, "Processing Weights") as progress:
with ProgressTracker(context, 2, "Unlocking Transforms") as progress:
# Unlock armature transforms
progress.step("Unlocking armature transforms")
for attr in ('location', 'rotation', 'scale'):
for i in range(3):
setattr(armature, f"lock_{attr}", [False] * 3)
# Unlock bone transforms
progress.step("Unlocking bone transforms")
for bone in armature.pose.bones:
for attr in ('location', 'rotation', 'scale'):
setattr(bone, f"lock_{attr}", [False] * 3)
self.report({'INFO'}, t("MMD.transforms_unlocked"))
return {'FINISHED'}
except Exception as e:
logger.error(f"Error unlocking transforms: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
class ReparentMeshesOperator(bpy.types.Operator):
bl_idname = "avatar_toolkit.reparent_meshes"
bl_label = t("MMD.reparent_meshes")
bl_description = t("MMD.reparent_meshes_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
armature = get_active_armature(context)
return armature is not None and get_all_meshes(context)
def execute(self, context):
armature = get_active_armature(context)
if not armature:
self.report({'ERROR'}, t("MMD.no_armature"))
return {'CANCELLED'}
meshes = get_all_meshes(context)
if not meshes:
self.report({'ERROR'}, t("MMD.no_meshes"))
return {'CANCELLED'}
try:
with ProgressTracker(context, len(meshes) + 1, "Reparenting Meshes") as progress:
# Get or create main collection
main_collection = self._get_main_collection(context)
progress.step("Setting up collections")
# Process each mesh
for mesh in meshes:
# Step 1: Process eye weights
self.process_eye_weights(context, mesh)
progress.step(f"Processed eye weights for {mesh.name}")
# Step 2: Process twist bones
self.process_twist_bones(context, mesh)
progress.step(f"Processed twist bones for {mesh.name}")
# Step 3: Merge remaining weights
self.merge_remaining_weights(context, mesh)
progress.step(f"Merged weights for {mesh.name}")
# Step 4: Cleanup
self.cleanup_vertex_groups(context, mesh)
progress.step(f"Cleaned up weights for {mesh.name}")
progress.step(f"Processing {mesh.name}")
self._process_mesh(mesh, armature, main_collection)
self.report({'INFO'}, t("MMD.weights_processed"))
self.report({'INFO'}, t("MMD.reparenting_complete"))
return {'FINISHED'}
except Exception as e:
logger.error(f"Weight processing failed: {str(e)}")
if hasattr(self, 'initial_states'):
for mesh, state in self.initial_states.items():
restore_mesh_weights_state(mesh, state)
logger.error(f"Error reparenting meshes: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
class AvatarToolkit_OT_FixMMDHierarchy(Operator):
bl_idname = "avatar_toolkit.mmd_fix_hierarchy"
bl_label = t("MMD.fix_hierarchy")
bl_options = {'REGISTER', 'UNDO'}
def _get_main_collection(self, context) -> bpy.types.Collection:
"""Get or create the main collection for the armature"""
if hasattr(context.scene, 'collection'):
return context.scene.collection
return context.scene.collection
def fix_bone_parenting(self, armature: Object) -> None:
"""Fix bone parenting to match standard hierarchy"""
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = armature.data.edit_bones
# Define parent-child relationships
hierarchy_map = {
'hips': ['spine', 'left_leg', 'right_leg'],
'spine': ['chest'],
'chest': ['upper_chest', 'left_shoulder', 'right_shoulder'],
'upper_chest': ['neck'],
'neck': ['head'],
'head': ['left_eye', 'right_eye'],
'left_shoulder': ['left_arm'],
'right_shoulder': ['right_arm'],
'left_arm': ['left_elbow'],
'right_arm': ['right_elbow'],
'left_elbow': ['left_wrist'],
'right_elbow': ['right_wrist'],
'left_leg': ['left_knee'],
'right_leg': ['right_knee'],
'left_knee': ['left_ankle'],
'right_knee': ['right_ankle'],
'left_ankle': ['left_toe'],
'right_ankle': ['right_toe']
}
# Apply parenting
for parent_name, children in hierarchy_map.items():
parent_bone = None
for bone in edit_bones:
if simplify_bonename(bone.name) in bone_names[parent_name]:
parent_bone = bone
break
if parent_bone:
for child_name in children:
for bone in edit_bones:
if simplify_bonename(bone.name) in bone_names[child_name]:
bone.parent = parent_bone
def _process_mesh(self, mesh: bpy.types.Object,
armature: bpy.types.Object,
main_collection: bpy.types.Collection) -> None:
"""Process individual mesh parenting and collection management"""
# Unlink from other collections
for col in mesh.users_collection:
if col != main_collection:
col.objects.unlink(mesh)
def connect_bones(self, context: Context, armature: Object) -> None:
"""Connect bones to their children where appropriate"""
if not context.scene.avatar_toolkit.mmd_connect_bones:
return
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = armature.data.edit_bones
min_distance = context.scene.avatar_toolkit.connect_bones_min_distance
for bone in edit_bones:
if bone.children:
for child in bone.children:
# Check if bones are close enough to connect
distance = (bone.tail - child.head).length
if distance < min_distance:
bone.tail = child.head
child.use_connect = True
# Ensure mesh is in main collection
if mesh.name not in main_collection.objects:
main_collection.objects.link(mesh)
def validate_hierarchy(self, armature: Object) -> bool:
"""Validate final bone hierarchy"""
# Check essential parent-child relationships
essential_pairs = [
('spine', 'hips'),
('chest', 'spine'),
('neck', 'chest'),
('head', 'neck')
]
for child, parent in essential_pairs:
if not validate_bone_hierarchy(armature.data.bones, parent, child):
return False
return True
def execute(self, context: Context) -> Set[str]:
try:
armature = get_active_armature(context)
# Save initial state
if context.scene.avatar_toolkit.save_backup_state:
self.initial_state = save_armature_state(armature)
with ProgressTracker(context, 3, "Fixing Bone Hierarchy") as progress:
# Step 1: Fix bone parenting
self.fix_bone_parenting(armature)
progress.step("Fixed bone parenting")
# Step 2: Connect bones
self.connect_bones(context, armature)
progress.step("Connected bones")
# Step 3: Validate hierarchy
if not self.validate_hierarchy(armature):
self.report({'WARNING'}, t("MMD.hierarchy_validation_warning"))
progress.step("Validated hierarchy")
self.report({'INFO'}, t("MMD.hierarchy_fixed"))
return {'FINISHED'}
except Exception as e:
logger.error(f"Hierarchy fix failed: {str(e)}")
if hasattr(self, 'initial_state'):
restore_armature_state(armature, self.initial_state)
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
class AvatarToolkit_OT_CleanupMMDArmature(Operator):
bl_idname = "avatar_toolkit.mmd_cleanup_armature"
bl_label = t("MMD.cleanup_armature")
bl_options = {'REGISTER', 'UNDO'}
def remove_unused_bones(self, context: Context, armature: Object) -> None:
"""Remove bones that aren't in the standard hierarchy or affecting weights"""
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = armature.data.edit_bones
# Get all bones affecting vertex groups
used_bones = set()
for mesh in get_all_meshes(context):
used_bones.update(group.name for group in mesh.vertex_groups)
# Add essential bones from dictionary
essential_bones = set(bone_names.keys())
# Remove non-essential, unused bones
for bone in edit_bones[:]: # Slice to avoid modification during iteration
simplified_name = simplify_bonename(bone.name)
if (not any(simplified_name in variations for variations in bone_names.values()) and
bone.name not in used_bones):
edit_bones.remove(bone)
def fix_bone_orientations(self, armature: Object) -> None:
"""Fix bone orientations for Unity/VRChat compatibility"""
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = armature.data.edit_bones
# Standard bone alignments
alignments = {
'spine': (0, 0, 1), # Points up
'chest': (0, 0, 1),
'neck': (0, 0, 1),
'head': (0, 0, 1),
'shoulder': (1, 0, 0), # Points outward
'arm': (0, -1, 0), # Points down
'elbow': (0, -1, 0),
'leg': (0, -1, 0),
'knee': (0, -1, 0),
'foot': (1, 0, 0), # Points forward
}
for bone in edit_bones:
simplified_name = simplify_bonename(bone.name)
for bone_type, direction in alignments.items():
if bone_type in simplified_name:
# Calculate new tail position while maintaining length
length = (bone.tail - bone.head).length
bone.tail = bone.head + Vector(direction) * length
break
def execute(self, context: Context) -> Set[str]:
try:
armature = get_active_armature(context)
# Save initial state
if context.scene.avatar_toolkit.save_backup_state:
self.initial_state = save_armature_state(armature)
with ProgressTracker(context, 2, "Cleaning Up Armature") as progress:
# Step 1: Remove unused bones
self.remove_unused_bones(context, armature)
progress.step("Removed unused bones")
# Step 2: Fix bone orientations
self.fix_bone_orientations(armature)
progress.step("Fixed bone orientations")
self.report({'INFO'}, t("MMD.cleanup_completed"))
return {'FINISHED'}
except Exception as e:
logger.error(f"Armature cleanup failed: {str(e)}")
if hasattr(self, 'initial_state'):
restore_armature_state(armature, self.initial_state)
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
# Set parent to armature
mesh.parent = armature
if not mesh.parent_type == 'ARMATURE':
mesh.parent_type = 'ARMATURE'