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 removed_count += 1
return removed_count 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( merge_twist_bones: BoolProperty(
name=t("Tools.merge_twist_bones"), name=t("MMD.merge_twist_bones"),
description=t("Tools.merge_twist_bones_desc"), description=t("MMD.merge_twist_bones_desc"),
default=True default=True
) )
clean_weights_threshold: FloatProperty( keep_twist_bones: BoolProperty(
name=t("Tools.clean_weights_threshold"), name=t("MMD.keep_twist_bones"),
description=t("Tools.clean_weights_threshold_desc"), description=t("MMD.keep_twist_bones_desc"),
default=0.01, default=False
min=0.0000001,
max=0.9999999
) )
connect_bones_min_distance: FloatProperty( keep_upper_chest: BoolProperty(
name=t("Tools.connect_bones_min_distance"), name=t("MMD.keep_upper_chest"),
description=t("Tools.connect_bones_min_distance_desc"), description=t("MMD.keep_upper_chest_desc"),
default=0.005, default=True
min=0.001,
max=0.1
) )
merge_weights_threshold: FloatProperty( merge_weights_threshold: FloatProperty(
name=t("Tools.merge_weights_threshold"), name=t("MMD.merge_weights_threshold"),
description=t("Tools.merge_weights_threshold_desc"), description=t("MMD.merge_weights_threshold_desc"),
default=0.01, default=0.01,
min=0.0001, min=0.0,
max=1.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: def register() -> None:
"""Register the Avatar Toolkit property group""" """Register the Avatar Toolkit property group"""
logger.info("Registering Avatar Toolkit properties") logger.info("Registering Avatar Toolkit properties")
+440 -441
View File
@@ -1,62 +1,190 @@
import bpy import bpy
from typing import Tuple, Set, Dict
from bpy.types import Operator, Context, Object
from mathutils import Vector 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 ( from ..core.common import (
ProgressTracker, ProgressTracker,
get_active_armature, get_active_armature,
validate_meshes, validate_armature,
simplify_bonename, get_vertex_weights,
duplicate_bone_chain, transfer_vertex_weights
save_armature_state,
restore_armature_state,
get_all_meshes,
validate_bone_hierarchy,
transfer_vertex_weights,
get_vertex_weights
) )
from ..core.logging_setup import logger
from ..core.translations import t from ..core.translations import t
from ..core.dictionaries import bone_names from ..core.dictionaries import bone_names
class AvatarToolkit_OT_StandardizeMMDBones(Operator): class AVATAR_TOOLKIT_OT_StandardizeMmd(Operator):
bl_idname = "avatar_toolkit.mmd_standardize_bones" """MMD Bone standardization system"""
bl_label = t("MMD.standardize_bones") bl_idname = "avatar_toolkit.standardize_mmd"
bl_label = t("MMD.standardize")
bl_options = {'REGISTER', 'UNDO'} bl_options = {'REGISTER', 'UNDO'}
def standardize_bone_names(self, armature: Object) -> None: def __init__(self):
"""Standardize bone names using MMD to Unity/VRChat conventions""" self.bone_mapping: Dict[str, str] = {}
for bone in armature.data.bones: self.processed_bones: Set[str] = set()
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: def execute(self, context: Context) -> Set[str]:
"""Process left/right bone pairs for consistency""" self.armature = get_active_armature(context)
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: if not self.armature:
"""Handle duplicate bone names""" self.report({'ERROR'}, t("MMD.no_armature"))
used_names = set() return {'CANCELLED'}
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: try:
"""Process spine bones for VRChat compatibility""" 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') 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 = { spine_bones = {
'hips': None, 'hips': None,
'spine': None, 'spine': None,
@@ -66,437 +194,308 @@ class AvatarToolkit_OT_StandardizeMMDBones(Operator):
'head': None 'head': None
} }
# Map existing spine bones # Find spine bones using bone_names dictionary
for bone in edit_bones: for bone in edit_bones:
simplified = simplify_bonename(bone.name) for spine_part, _ in spine_bones.items():
for spine_name in spine_bones.keys(): if any(alt_name in bone.name.lower() for alt_name in bone_names[spine_part]):
if simplified in bone_names[spine_name]: spine_bones[spine_part] = bone
spine_bones[spine_name] = bone
break 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 # Set up spine hierarchy
if spine_bones['hips']: hierarchy = [
for i, key in enumerate(['spine', 'chest', 'upper_chest', 'neck', 'head']): ('hips', 'spine'),
if spine_bones[key]: ('spine', 'chest'),
prev_key = list(spine_bones.keys())[i] ('chest', 'neck'),
if spine_bones[prev_key]: ('neck', 'head')
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')
] ]
for twist, target in twist_pairs: for parent_name, child_name in hierarchy:
if twist in mesh.vertex_groups: parent = spine_bones.get(parent_name)
self.merge_bone_weights(context, mesh, twist, target) child = spine_bones.get(child_name)
if parent and child:
def cleanup_vertex_groups(self, context: Context, mesh: Object) -> None: child.parent = parent
"""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
child.use_connect = True child.use_connect = True
def validate_hierarchy(self, armature: Object) -> bool: def fix_bone_orientations(self, context: Context) -> None:
"""Validate final bone hierarchy""" """Fix bone orientations for standard pose compatibility"""
# Check essential parent-child relationships edit_bones = self.armature.data.edit_bones
essential_pairs = [
('spine', 'hips'), # Process arm bones
('chest', 'spine'), arm_pairs = [
('neck', 'chest'), ('upper_arm', 'forearm'),
('head', 'neck') ('forearm', 'hand')
] ]
for child, parent in essential_pairs: for side in ['.L', '.R']:
if not validate_bone_hierarchy(armature.data.bones, parent, child): for parent, child in arm_pairs:
return False 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]: # Process leg bones
try: leg_pairs = [
armature = get_active_armature(context) ('thigh', 'shin'),
('shin', 'foot')
]
# Save initial state for side in ['.L', '.R']:
if context.scene.avatar_toolkit.save_backup_state: for parent, child in leg_pairs:
self.initial_state = save_armature_state(armature) 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: if parent_bone and child_bone:
# Step 1: Fix bone parenting child_bone.use_connect = True
self.fix_bone_parenting(armature) child_bone.use_inherit_rotation = True
progress.step("Fixed bone parenting")
# Step 2: Connect bones def remove_unused_bones(self, context: Context) -> None:
self.connect_bones(context, armature) """Remove unused and unnecessary bones from the 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') 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() used_bones = set()
for mesh in get_all_meshes(context): for mesh in self.get_associated_meshes(context):
used_bones.update(group.name for group in mesh.vertex_groups) for group in mesh.vertex_groups:
used_bones.add(group.name)
# Add essential bones from dictionary # Get list of bones to keep based on settings
essential_bones = set(bone_names.keys()) toolkit = context.scene.avatar_toolkit
keep_upper_chest = toolkit.keep_upper_chest
keep_twist = toolkit.keep_twist_bones
# Remove non-essential, unused bones # Remove unused bones
for bone in edit_bones[:]: # Slice to avoid modification during iteration for bone in edit_bones:
simplified_name = simplify_bonename(bone.name) # Skip if bone has weights
if (not any(simplified_name in variations for variations in bone_names.values()) and if bone.name in used_bones:
bone.name not 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) edit_bones.remove(bone)
def fix_bone_orientations(self, armature: Object) -> None: def connect_bones(self, context: Context) -> None:
"""Fix bone orientations for Unity/VRChat compatibility""" """Connect bones that should be connected in the hierarchy"""
bpy.ops.object.mode_set(mode='EDIT') edit_bones = self.armature.data.edit_bones
edit_bones = armature.data.edit_bones
# Standard bone alignments connect_chains = [
alignments = { ['hips', 'spine', 'chest', 'neck', 'head'],
'spine': (0, 0, 1), # Points up ['shoulder.L', 'upper_arm.L', 'forearm.L', 'hand.L'],
'chest': (0, 0, 1), ['shoulder.R', 'upper_arm.R', 'forearm.R', 'hand.R'],
'neck': (0, 0, 1), ['thigh.L', 'shin.L', 'foot.L', 'toe.L'],
'head': (0, 0, 1), ['thigh.R', 'shin.R', 'foot.R', 'toe.R']
'shoulder': (1, 0, 0), # Points outward ]
'arm': (0, -1, 0), # Points down
'elbow': (0, -1, 0), for chain in connect_chains:
'leg': (0, -1, 0), prev_bone = None
'knee': (0, -1, 0), for bone_name in chain:
'foot': (1, 0, 0), # Points forward 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: for bone in edit_bones:
simplified_name = simplify_bonename(bone.name) # Calculate bone length
for bone_type, direction in alignments.items(): bone_length = (bone.tail - bone.head).length
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]: if bone_length < min_length:
try: # 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) armature = get_active_armature(context)
return armature is not None and armature.type == 'ARMATURE'
# Save initial state def execute(self, context):
if context.scene.avatar_toolkit.save_backup_state: armature = get_active_armature(context)
self.initial_state = save_armature_state(armature) if not armature:
self.report({'ERROR'}, t("MMD.no_armature"))
return {'CANCELLED'}
with ProgressTracker(context, 2, "Cleaning Up Armature") as progress: try:
# Step 1: Remove unused bones with ProgressTracker(context, 2, "Unlocking Transforms") as progress:
self.remove_unused_bones(context, armature) # Unlock armature transforms
progress.step("Removed unused bones") 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 # Unlock bone transforms
self.fix_bone_orientations(armature) progress.step("Unlocking bone transforms")
progress.step("Fixed bone orientations") 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'} return {'FINISHED'}
except Exception as e: except Exception as e:
logger.error(f"Armature cleanup failed: {str(e)}") logger.error(f"Error unlocking transforms: {str(e)}")
if hasattr(self, 'initial_state'):
restore_armature_state(armature, self.initial_state)
self.report({'ERROR'}, str(e)) self.report({'ERROR'}, str(e))
return {'CANCELLED'} 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 import bpy
from typing import Set 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 .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..core.translations import t from ..core.translations import t
class AvatarToolKit_PT_MMDPanel(Panel): class AvatarToolKit_PT_MMDPanel(Panel):
"""Panel containing MMD conversion and optimization tools""" """Panel containing MMD bone standardization tools"""
bl_label = t("MMD.label") bl_label = t("MMD.label")
bl_idname = "OBJECT_PT_avatar_toolkit_mmd" bl_idname = "OBJECT_PT_avatar_toolkit_mmd"
bl_space_type = 'VIEW_3D' bl_space_type = 'VIEW_3D'
bl_region_type = 'UI' bl_region_type = 'UI'
bl_category = CATEGORY_NAME bl_category = CATEGORY_NAME
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 2 bl_order = 3
def draw(self, context: Context) -> None: def draw(self, context: Context) -> None:
layout: UILayout = self.layout layout: UILayout = self.layout
toolkit = context.scene.avatar_toolkit
# Bone Standardization Box # Add merge twist bones option
bone_box: UILayout = layout.box() layout.prop(toolkit, "keep_twist_bones")
col: UILayout = bone_box.column(align=True) layout.operator("avatar_toolkit.standardize_mmd", icon='BONE_DATA')
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')