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
-58
View File
@@ -485,61 +485,3 @@ def remove_unused_shapekeys(mesh_obj: Object, tolerance: float = 0.001) -> int:
removed_count += 1
return removed_count
def save_armature_state(armature: Object) -> Dict[str, Any]:
"""Save current armature state for recovery"""
state = {
'bones': {},
'pose': {},
'settings': {}
}
# Save bone data
for bone in armature.data.bones:
state['bones'][bone.name] = {
'head': bone.head_local.copy(),
'tail': bone.tail_local.copy(),
'roll': bone.roll,
'parent': bone.parent.name if bone.parent else None
}
# Save pose data if exists
if armature.pose:
for bone in armature.pose.bones:
state['pose'][bone.name] = {
'location': bone.location.copy(),
'rotation': bone.rotation_quaternion.copy(),
'scale': bone.scale.copy()
}
return state
def restore_armature_state(armature: Object, state: Dict[str, Any]) -> None:
"""Restore armature from saved state"""
bpy.ops.object.mode_set(mode='EDIT')
# Restore bones
for name, data in state['bones'].items():
if name in armature.data.edit_bones:
bone = armature.data.edit_bones[name]
bone.head = data['head']
bone.tail = data['tail']
bone.roll = data['roll']
# Restore parenting
for name, data in state['bones'].items():
if data['parent'] and name in armature.data.edit_bones:
bone = armature.data.edit_bones[name]
if data['parent'] in armature.data.edit_bones:
bone.parent = armature.data.edit_bones[data['parent']]
bpy.ops.object.mode_set(mode='POSE')
# Restore pose if exists
if 'pose' in state:
for name, data in state['pose'].items():
if name in armature.pose.bones:
bone = armature.pose.bones[name]
bone.location = data['location']
bone.rotation_quaternion = data['rotation']
bone.scale = data['scale']
+13 -35
View File
@@ -87,53 +87,31 @@ class AvatarToolkitSceneProperties(PropertyGroup):
)
merge_twist_bones: BoolProperty(
name=t("Tools.merge_twist_bones"),
description=t("Tools.merge_twist_bones_desc"),
name=t("MMD.merge_twist_bones"),
description=t("MMD.merge_twist_bones_desc"),
default=True
)
clean_weights_threshold: FloatProperty(
name=t("Tools.clean_weights_threshold"),
description=t("Tools.clean_weights_threshold_desc"),
default=0.01,
min=0.0000001,
max=0.9999999
keep_twist_bones: BoolProperty(
name=t("MMD.keep_twist_bones"),
description=t("MMD.keep_twist_bones_desc"),
default=False
)
connect_bones_min_distance: FloatProperty(
name=t("Tools.connect_bones_min_distance"),
description=t("Tools.connect_bones_min_distance_desc"),
default=0.005,
min=0.001,
max=0.1
keep_upper_chest: BoolProperty(
name=t("MMD.keep_upper_chest"),
description=t("MMD.keep_upper_chest_desc"),
default=True
)
merge_weights_threshold: FloatProperty(
name=t("Tools.merge_weights_threshold"),
description=t("Tools.merge_weights_threshold_desc"),
name=t("MMD.merge_weights_threshold"),
description=t("MMD.merge_weights_threshold_desc"),
default=0.01,
min=0.0001,
min=0.0,
max=1.0
)
mmd_process_twist_bones: BoolProperty(
name=t("MMD.process_twist_bones"),
description=t("MMD.process_twist_bones_desc"),
default=True
)
mmd_connect_bones: BoolProperty(
name=t("MMD.connect_bones"),
description=t("MMD.connect_bones_desc"),
default=True
)
save_backup_state: BoolProperty(
name="Save Backup State",
description="Save the initial state of the armature before standardizing bones",
default=False
)
def register() -> None:
"""Register the Avatar Toolkit property group"""
logger.info("Registering Avatar Toolkit properties")
+440 -441
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 __init__(self):
self.bone_mapping: Dict[str, str] = {}
self.processed_bones: Set[str] = set()
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 execute(self, context: Context) -> Set[str]:
self.armature = get_active_armature(context)
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)
if not self.armature:
self.report({'ERROR'}, t("MMD.no_armature"))
return {'CANCELLED'}
def process_spine_chain(self, armature: Object) -> None:
"""Process spine bones for VRChat compatibility"""
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 = armature.data.edit_bones
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)
def cleanup_vertex_groups(self, context: Context, mesh: Object) -> None:
"""Remove empty and unused vertex groups"""
threshold = context.scene.avatar_toolkit.clean_weights_threshold
# 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)
# Remove unused groups
for group in mesh.vertex_groups[:]:
if group.name not in valid_bones:
mesh.vertex_groups.remove(group)
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)
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'),
]
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)
@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:
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:
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}")
self.report({'INFO'}, t("MMD.weights_processed"))
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)
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 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 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
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 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')
def fix_bone_orientations(self, context: Context) -> None:
"""Fix bone orientations for standard pose compatibility"""
edit_bones = self.armature.data.edit_bones
# Process arm bones
arm_pairs = [
('upper_arm', 'forearm'),
('forearm', 'hand')
]
for child, parent in essential_pairs:
if not validate_bone_hierarchy(armature.data.bones, parent, child):
return False
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)
return True
if parent_bone and child_bone:
child_bone.use_connect = True
child_bone.use_inherit_rotation = True
def execute(self, context: Context) -> Set[str]:
try:
armature = get_active_armature(context)
# Process leg bones
leg_pairs = [
('thigh', 'shin'),
('shin', 'foot')
]
# Save initial state
if context.scene.avatar_toolkit.save_backup_state:
self.initial_state = save_armature_state(armature)
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)
with ProgressTracker(context, 3, "Fixing Bone Hierarchy") as progress:
# Step 1: Fix bone parenting
self.fix_bone_parenting(armature)
progress.step("Fixed bone parenting")
if parent_bone and child_bone:
child_bone.use_connect = True
child_bone.use_inherit_rotation = True
# 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"""
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 = armature.data.edit_bones
edit_bones = self.armature.data.edit_bones
# Get all bones affecting vertex groups
# Get list of bones that have vertex weights
used_bones = set()
for mesh in get_all_meshes(context):
used_bones.update(group.name for group in mesh.vertex_groups)
for mesh in self.get_associated_meshes(context):
for group in mesh.vertex_groups:
used_bones.add(group.name)
# Add essential bones from dictionary
essential_bones = set(bone_names.keys())
# 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 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):
# Remove unused bones
for bone in edit_bones:
# Skip if bone has weights
if bone.name in used_bones:
continue
# 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 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
def connect_bones(self, context: Context) -> None:
"""Connect bones that should be connected in the hierarchy"""
edit_bones = self.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
}
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 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:
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
# Calculate bone length
bone_length = (bone.tail - bone.head).length
def execute(self, context: Context) -> Set[str]:
try:
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):
armature = get_active_armature(context)
return armature is not None and armature.type == 'ARMATURE'
# Save initial state
if context.scene.avatar_toolkit.save_backup_state:
self.initial_state = save_armature_state(armature)
def execute(self, context):
armature = get_active_armature(context)
if not armature:
self.report({'ERROR'}, t("MMD.no_armature"))
return {'CANCELLED'}
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")
try:
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)
# Step 2: Fix bone orientations
self.fix_bone_orientations(armature)
progress.step("Fixed bone orientations")
# 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.cleanup_completed"))
self.report({'INFO'}, t("MMD.transforms_unlocked"))
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)
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:
progress.step(f"Processing {mesh.name}")
self._process_mesh(mesh, armature, main_collection)
self.report({'INFO'}, t("MMD.reparenting_complete"))
return {'FINISHED'}
except Exception as e:
logger.error(f"Error reparenting meshes: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
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 _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)
# Ensure mesh is in main collection
if mesh.name not in main_collection.objects:
main_collection.objects.link(mesh)
# Set parent to armature
mesh.parent = armature
if not mesh.parent_type == 'ARMATURE':
mesh.parent_type = 'ARMATURE'
+7 -30
View File
@@ -1,46 +1,23 @@
import bpy
from typing import Set
from bpy.types import Panel, Context, UILayout, Operator
from bpy.types import Panel, Context, UILayout
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..core.translations import t
class AvatarToolKit_PT_MMDPanel(Panel):
"""Panel containing MMD conversion and optimization tools"""
"""Panel containing MMD bone standardization tools"""
bl_label = t("MMD.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 = 2
bl_order = 3
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
toolkit = context.scene.avatar_toolkit
# Bone Standardization Box
bone_box: UILayout = layout.box()
col: UILayout = bone_box.column(align=True)
col.label(text=t("MMD.bone_standardization"), icon='ARMATURE_DATA')
col.separator(factor=0.5)
col.operator("avatar_toolkit.mmd_standardize_bones", icon='BONE_DATA')
# Weight Processing Box
weight_box: UILayout = layout.box()
col = weight_box.column(align=True)
col.label(text=t("MMD.weight_processing"), icon='GROUP_VERTEX')
col.separator(factor=0.5)
col.operator("avatar_toolkit.mmd_process_weights", icon='WPAINT_HLT')
# Hierarchy Box
hierarchy_box: UILayout = layout.box()
col = hierarchy_box.column(align=True)
col.label(text=t("MMD.hierarchy"), icon='OUTLINER')
col.separator(factor=0.5)
col.operator("avatar_toolkit.mmd_fix_hierarchy", icon='CONSTRAINT_BONE')
# Cleanup Box
cleanup_box: UILayout = layout.box()
col = cleanup_box.column(align=True)
col.label(text=t("MMD.cleanup"), icon='BRUSH_DATA')
col.separator(factor=0.5)
col.operator("avatar_toolkit.mmd_cleanup_armature", icon='MODIFIER')
# Add merge twist bones option
layout.prop(toolkit, "keep_twist_bones")
layout.operator("avatar_toolkit.standardize_mmd", icon='BONE_DATA')