Re-do still hate it
This commit is contained in:
+447
-443
@@ -1,498 +1,502 @@
|
||||
import bpy
|
||||
import numpy as np
|
||||
from typing import Set, Dict, List, Optional, Tuple
|
||||
from bpy.types import Operator, Context, Object, EditBone, Mesh
|
||||
from typing import Tuple, Set, Dict
|
||||
from bpy.types import Operator, Context, Object
|
||||
from mathutils import Vector
|
||||
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
|
||||
)
|
||||
from ..core.logging_setup import logger
|
||||
from ..core.translations import t
|
||||
from ..core.common import (
|
||||
get_active_armature,
|
||||
validate_armature,
|
||||
get_all_meshes,
|
||||
ProgressTracker,
|
||||
transfer_vertex_weights,
|
||||
remove_unused_shapekeys
|
||||
)
|
||||
from ..core.dictionaries import bone_names, mmd_bone_renames
|
||||
from ..core.dictionaries import bone_names
|
||||
|
||||
class AvatarToolkit_OT_FixBoneNames(Operator):
|
||||
"""Standardize and fix bone names"""
|
||||
bl_idname = "avatar_toolkit.fix_bone_names"
|
||||
bl_label = t("MMDTools.fix_bone_names")
|
||||
bl_description = t("MMDTools.fix_bone_names_desc")
|
||||
class AvatarToolkit_OT_StandardizeMMDBones(Operator):
|
||||
bl_idname = "avatar_toolkit.mmd_standardize_bones"
|
||||
bl_label = t("MMD.standardize_bones")
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
armature = get_active_armature(context)
|
||||
if not armature:
|
||||
return False
|
||||
valid, _ = validate_armature(armature)
|
||||
return valid
|
||||
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 execute(self, context: Context) -> Set[str]:
|
||||
armature = get_active_armature(context)
|
||||
|
||||
with ProgressTracker(context, 3, "Fixing Bone Names") as progress:
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
# First pass - standardize names
|
||||
for bone in armature.data.edit_bones:
|
||||
bone.name = self.standardize_bone_name(bone.name)
|
||||
progress.step("Standardized names")
|
||||
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}"
|
||||
|
||||
# Second pass - apply MMD mappings
|
||||
for bone in armature.data.edit_bones:
|
||||
if bone.name in mmd_bone_renames:
|
||||
bone.name = mmd_bone_renames[bone.name]
|
||||
progress.step("Applied MMD mappings")
|
||||
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)
|
||||
|
||||
# Third pass - fix common names
|
||||
for bone in armature.data.edit_bones:
|
||||
self.fix_common_names(bone)
|
||||
progress.step("Fixed common names")
|
||||
|
||||
self.report({'INFO'}, t("MMDTools.bones_renamed"))
|
||||
return {'FINISHED'}
|
||||
|
||||
def standardize_bone_name(self, name: str) -> str:
|
||||
"""Standardize bone naming convention"""
|
||||
prefixes = ['def-', 'def_', 'sk_', 'b_', 'bone_', 'mmd_']
|
||||
name_lower = name.lower()
|
||||
|
||||
# Remove common prefixes
|
||||
for prefix in prefixes:
|
||||
if name_lower.startswith(prefix):
|
||||
name = name[len(prefix):]
|
||||
break
|
||||
|
||||
# Fix side indicators
|
||||
name = name.replace('_l', '_L').replace('_r', '_R')
|
||||
name = name.replace('.l', '_L').replace('.r', '_R')
|
||||
name = name.replace('左', '_L').replace('右', '_R')
|
||||
|
||||
return name
|
||||
|
||||
def fix_common_names(self, bone: EditBone) -> None:
|
||||
"""Fix common bone names to standard names"""
|
||||
for standard_name, variations in bone_names.items():
|
||||
if bone.name.lower() in variations:
|
||||
bone.name = standard_name
|
||||
break
|
||||
|
||||
class AvatarToolkit_OT_FixBoneHierarchy(Operator):
|
||||
"""Fix bone parenting and hierarchy"""
|
||||
bl_idname = "avatar_toolkit.fix_bone_hierarchy"
|
||||
bl_label = t("MMDTools.fix_hierarchy")
|
||||
bl_description = t("MMDTools.fix_hierarchy_desc")
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
armature = get_active_armature(context)
|
||||
if not armature:
|
||||
return False
|
||||
valid, _ = validate_armature(armature)
|
||||
return valid
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
armature = get_active_armature(context)
|
||||
|
||||
with ProgressTracker(context, 3, "Fixing Bone Hierarchy") as progress:
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
# Fix spine chain
|
||||
self.fix_spine_chain(armature)
|
||||
progress.step("Fixed spine chain")
|
||||
|
||||
# Fix limb chains
|
||||
self.fix_limb_chains(armature)
|
||||
progress.step("Fixed limb chains")
|
||||
|
||||
# Fix bone orientations
|
||||
self.fix_bone_orientations(armature)
|
||||
progress.step("Fixed bone orientations")
|
||||
|
||||
self.report({'INFO'}, t("MMDTools.hierarchy_fixed"))
|
||||
return {'FINISHED'}
|
||||
|
||||
def fix_spine_chain(self, armature: Object) -> None:
|
||||
"""Fix the spine bone chain hierarchy"""
|
||||
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
|
||||
spine_chain = ['Hips', 'Spine', 'Chest', 'Neck', 'Head']
|
||||
previous = None
|
||||
|
||||
for bone_name in spine_chain:
|
||||
if bone_name in edit_bones:
|
||||
bone = edit_bones[bone_name]
|
||||
if previous:
|
||||
bone.parent = edit_bones[previous]
|
||||
previous = bone_name
|
||||
|
||||
def fix_limb_chains(self, armature: Object) -> None:
|
||||
"""Fix arm and leg bone chains"""
|
||||
edit_bones = armature.data.edit_bones
|
||||
limb_chains = {
|
||||
'Left': {
|
||||
'arm': ['Left shoulder', 'Left arm', 'Left elbow', 'Left wrist'],
|
||||
'leg': ['Left leg', 'Left knee', 'Left ankle', 'Left toe']
|
||||
},
|
||||
'Right': {
|
||||
'arm': ['Right shoulder', 'Right arm', 'Right elbow', 'Right wrist'],
|
||||
'leg': ['Right leg', 'Right knee', 'Right ankle', 'Right toe']
|
||||
}
|
||||
spine_bones = {
|
||||
'hips': None,
|
||||
'spine': None,
|
||||
'chest': None,
|
||||
'upper_chest': None,
|
||||
'neck': None,
|
||||
'head': None
|
||||
}
|
||||
|
||||
for side in limb_chains:
|
||||
for chain in limb_chains[side].values():
|
||||
previous = None
|
||||
for bone_name in chain:
|
||||
if bone_name in edit_bones:
|
||||
bone = edit_bones[bone_name]
|
||||
if previous:
|
||||
bone.parent = edit_bones[previous]
|
||||
previous = bone_name
|
||||
# Map existing spine bones
|
||||
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
|
||||
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 fix_bone_orientations(self, armature: Object) -> None:
|
||||
"""Fix bone roll and axis orientations"""
|
||||
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
|
||||
|
||||
# Fix spine chain orientations
|
||||
spine_bones = ['Hips', 'Spine', 'Chest']
|
||||
for name in spine_bones:
|
||||
if name in edit_bones:
|
||||
bone = edit_bones[name]
|
||||
bone.roll = 0
|
||||
bone.tail.y = bone.head.y
|
||||
|
||||
# Fix arm orientations
|
||||
arm_bones = ['Left arm', 'Right arm', 'Left elbow', 'Right elbow']
|
||||
for name in arm_bones:
|
||||
if name in edit_bones:
|
||||
bone = edit_bones[name]
|
||||
bone.roll = 0 if 'Left' in name else np.pi
|
||||
|
||||
class AvatarToolkit_OT_FixBoneWeights(Operator):
|
||||
"""Fix and clean up bone weights"""
|
||||
bl_idname = "avatar_toolkit.fix_bone_weights"
|
||||
bl_label = t("MMDTools.fix_weights")
|
||||
bl_description = t("MMDTools.fix_weights_desc")
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
# 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:
|
||||
armature = get_active_armature(context)
|
||||
if not armature:
|
||||
return False
|
||||
valid, _ = validate_armature(armature)
|
||||
return valid
|
||||
"""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]:
|
||||
armature = get_active_armature(context)
|
||||
meshes = get_all_meshes(context)
|
||||
|
||||
if not meshes:
|
||||
self.report({'WARNING'}, t("MMDTools.no_meshes"))
|
||||
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'}
|
||||
|
||||
with ProgressTracker(context, len(meshes), "Fixing Bone Weights") as progress:
|
||||
for mesh in meshes:
|
||||
# Clean weights
|
||||
self.clean_weights(mesh, context.scene.avatar_toolkit.clean_weights_threshold)
|
||||
|
||||
# Handle twist bones
|
||||
if context.scene.avatar_toolkit.merge_twist_bones:
|
||||
self.process_twist_bones(mesh)
|
||||
|
||||
# Remove empty groups
|
||||
self.remove_empty_groups(mesh)
|
||||
|
||||
# Normalize weights
|
||||
self.normalize_weights(mesh)
|
||||
|
||||
progress.step(f"Processed {mesh.name}")
|
||||
|
||||
self.report({'INFO'}, t("MMDTools.weights_fixed"))
|
||||
return {'FINISHED'}
|
||||
|
||||
def clean_weights(self, mesh: Object, threshold: float) -> None:
|
||||
"""Remove weights below threshold"""
|
||||
for vertex_group in mesh.vertex_groups:
|
||||
for vertex in mesh.data.vertices:
|
||||
try:
|
||||
weight = vertex_group.weight(vertex.index)
|
||||
if weight < threshold:
|
||||
vertex_group.remove([vertex.index])
|
||||
except RuntimeError:
|
||||
continue
|
||||
|
||||
def process_twist_bones(self, mesh: Object) -> None:
|
||||
"""Process and merge twist bone weights"""
|
||||
twist_groups = [g for g in mesh.vertex_groups if 'twist' in g.name.lower()]
|
||||
for group in twist_groups:
|
||||
base_name = group.name.lower().replace('twist', '').strip('_')
|
||||
for target in mesh.vertex_groups:
|
||||
if target.name.lower() == base_name:
|
||||
transfer_vertex_weights(mesh, group.name, target.name)
|
||||
break
|
||||
|
||||
def remove_empty_groups(self, mesh: Object) -> None:
|
||||
"""Remove vertex groups with no weights"""
|
||||
empty_groups = []
|
||||
for group in mesh.vertex_groups:
|
||||
has_weights = False
|
||||
for vert in mesh.data.vertices:
|
||||
for g in vert.groups:
|
||||
if g.group == group.index and g.weight > 0:
|
||||
has_weights = True
|
||||
break
|
||||
if has_weights:
|
||||
break
|
||||
if not has_weights:
|
||||
empty_groups.append(group)
|
||||
|
||||
for group in empty_groups:
|
||||
mesh.vertex_groups.remove(group)
|
||||
|
||||
def normalize_weights(self, mesh: Object) -> None:
|
||||
"""Normalize vertex weights"""
|
||||
for vertex in mesh.data.vertices:
|
||||
total_weight = sum(group.weight for group in vertex.groups)
|
||||
if total_weight > 0:
|
||||
for group in vertex.groups:
|
||||
group.weight /= total_weight
|
||||
|
||||
class AvatarToolkit_OT_FixMMDFeatures(Operator):
|
||||
"""Fix MMD-specific features and settings"""
|
||||
bl_idname = "avatar_toolkit.fix_mmd_features"
|
||||
bl_label = t("MMDTools.fix_mmd_features")
|
||||
bl_description = t("MMDTools.fix_mmd_features_desc")
|
||||
class AvatarToolkit_OT_ProcessMMDWeights(Operator):
|
||||
bl_idname = "avatar_toolkit.mmd_process_weights"
|
||||
bl_label = t("MMD.process_weights")
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
armature = get_active_armature(context)
|
||||
if not armature:
|
||||
return False
|
||||
valid, _ = validate_armature(armature)
|
||||
return valid
|
||||
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 execute(self, context: Context) -> Set[str]:
|
||||
armature = get_active_armature(context)
|
||||
meshes = get_all_meshes(context)
|
||||
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']
|
||||
}
|
||||
|
||||
with ProgressTracker(context, 4, "Fixing MMD Features") as progress:
|
||||
# Process shape keys
|
||||
for mesh in meshes:
|
||||
self.process_shape_keys(mesh)
|
||||
progress.step("Processed shape keys")
|
||||
|
||||
# Fix MMD shading
|
||||
self.fix_mmd_shading(meshes)
|
||||
progress.step("Fixed MMD shading")
|
||||
|
||||
# Handle physics cleanup
|
||||
self.cleanup_physics(armature)
|
||||
progress.step("Cleaned up physics")
|
||||
|
||||
# Remove unused data
|
||||
self.cleanup_unused_data(context)
|
||||
progress.step("Cleaned up unused data")
|
||||
|
||||
return {'FINISHED'}
|
||||
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_shape_keys(self, mesh: Object) -> None:
|
||||
"""Process and clean up shape keys"""
|
||||
if not mesh.data.shape_keys:
|
||||
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
|
||||
|
||||
# Clean unused shape keys
|
||||
remove_unused_shapekeys(mesh)
|
||||
twist_pairs = [
|
||||
('arm_twist_l', 'left_arm'),
|
||||
('arm_twist_r', 'right_arm'),
|
||||
('forearm_twist_l', 'left_elbow'),
|
||||
('forearm_twist_r', 'right_elbow')
|
||||
]
|
||||
|
||||
# Sort and rename shape keys
|
||||
shape_keys = mesh.data.shape_keys.key_blocks
|
||||
for key in shape_keys:
|
||||
# Handle Japanese prefixes
|
||||
if key.name.startswith('防'):
|
||||
key.name = key.name[1:]
|
||||
# Handle common MMD prefixes
|
||||
if key.name.startswith('表情'):
|
||||
key.name = key.name[2:]
|
||||
for twist, target in twist_pairs:
|
||||
if twist in mesh.vertex_groups:
|
||||
self.merge_bone_weights(context, mesh, twist, target)
|
||||
|
||||
def fix_mmd_shading(self, meshes: List[Object]) -> None:
|
||||
"""Fix MMD material shading settings"""
|
||||
for mesh in meshes:
|
||||
for material in mesh.data.materials:
|
||||
if material:
|
||||
material.use_backface_culling = True
|
||||
material.blend_method = 'HASHED'
|
||||
if material.node_tree:
|
||||
for node in material.node_tree.nodes:
|
||||
if node.type == 'BSDF_PRINCIPLED':
|
||||
node.inputs['Alpha'].default_value = 1.0
|
||||
|
||||
def cleanup_physics(self, armature: Object) -> None:
|
||||
"""Clean up MMD physics objects"""
|
||||
physics_objects = [obj for obj in bpy.data.objects
|
||||
if obj.parent == armature and
|
||||
(obj.rigid_body or obj.rigid_body_constraint)]
|
||||
def cleanup_vertex_groups(self, context: Context, mesh: Object) -> None:
|
||||
"""Remove empty and unused vertex groups"""
|
||||
threshold = context.scene.avatar_toolkit.clean_weights_threshold
|
||||
|
||||
for obj in physics_objects:
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
|
||||
def cleanup_unused_data(self, context: Context) -> None:
|
||||
"""Clean up unused MMD data"""
|
||||
# Remove unused actions
|
||||
for action in bpy.data.actions:
|
||||
if not action.users:
|
||||
bpy.data.actions.remove(action)
|
||||
# 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
|
||||
|
||||
# Remove empty vertex groups
|
||||
for mesh in get_all_meshes(context):
|
||||
self.remove_empty_groups(mesh)
|
||||
|
||||
def remove_empty_groups(self, mesh: Object) -> None:
|
||||
"""Remove empty vertex groups"""
|
||||
empty_groups = []
|
||||
for group in mesh.vertex_groups:
|
||||
# Check if group has any weights above threshold
|
||||
has_weights = False
|
||||
for vert in mesh.data.vertices:
|
||||
for g in vert.groups:
|
||||
if g.group == group.index and g.weight > 0:
|
||||
has_weights = True
|
||||
break
|
||||
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:
|
||||
empty_groups.append(group)
|
||||
|
||||
for group in empty_groups:
|
||||
mesh.vertex_groups.remove(group)
|
||||
mesh.vertex_groups.remove(group)
|
||||
|
||||
class AvatarToolkit_OT_AdvancedBoneOps(Operator):
|
||||
"""Advanced bone operations and fixes"""
|
||||
bl_idname = "avatar_toolkit.advanced_bone_ops"
|
||||
bl_label = t("MMDTools.advanced_bone_ops")
|
||||
bl_description = t("MMDTools.advanced_bone_ops_desc")
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
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]:
|
||||
armature = get_active_armature(context)
|
||||
|
||||
with ProgressTracker(context, 4, "Advanced Bone Operations") as progress:
|
||||
# Fix zero length bones
|
||||
self.fix_zero_length_bones(armature)
|
||||
progress.step("Fixed zero length bones")
|
||||
try:
|
||||
meshes = get_all_meshes(context)
|
||||
|
||||
# Connect bones with children
|
||||
self.connect_bone_chains(armature)
|
||||
progress.step("Connected bone chains")
|
||||
# Save initial state
|
||||
if context.scene.avatar_toolkit.save_backup_state:
|
||||
self.initial_states = {mesh: get_vertex_weights(mesh) for mesh in meshes}
|
||||
|
||||
# Handle bone roll values
|
||||
self.fix_bone_rolls(armature)
|
||||
progress.step("Fixed bone rolls")
|
||||
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'}
|
||||
|
||||
# Fix bone orientations
|
||||
self.fix_bone_orientations(armature)
|
||||
progress.step("Fixed bone orientations")
|
||||
|
||||
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'}
|
||||
|
||||
def fix_zero_length_bones(self, armature: Object) -> None:
|
||||
"""Fix bones with zero length by extending them"""
|
||||
min_length = 0.001
|
||||
for bone in armature.data.edit_bones:
|
||||
length = (bone.tail - bone.head).length
|
||||
if length < min_length:
|
||||
if bone.parent:
|
||||
bone.tail = bone.head + bone.parent.vector * 0.1
|
||||
else:
|
||||
bone.tail.z = bone.head.z + 0.1
|
||||
|
||||
def connect_bone_chains(self, armature: Object) -> None:
|
||||
"""Connect bones that should form chains"""
|
||||
min_distance = bpy.context.scene.avatar_toolkit.connect_bones_min_distance
|
||||
|
||||
for bone in armature.data.edit_bones:
|
||||
if len(bone.children) == 1:
|
||||
child = bone.children[0]
|
||||
distance = (bone.tail - child.head).length
|
||||
if distance < min_distance:
|
||||
child.use_connect = True
|
||||
child.head = bone.tail
|
||||
|
||||
def fix_bone_rolls(self, armature: Object) -> None:
|
||||
"""Fix bone roll values for proper orientation"""
|
||||
for bone in armature.data.edit_bones:
|
||||
if 'spine' in bone.name.lower() or 'chest' in bone.name.lower():
|
||||
bone.roll = 0
|
||||
elif 'shoulder' in bone.name.lower():
|
||||
bone.roll = 0 if 'left' in bone.name.lower() else np.pi
|
||||
|
||||
class AvatarToolkit_OT_CleanupOperations(Operator):
|
||||
"""Cleanup unused data and objects"""
|
||||
bl_idname = "avatar_toolkit.cleanup_operations"
|
||||
bl_label = t("MMDTools.cleanup_operations")
|
||||
bl_description = t("MMDTools.cleanup_operations_desc")
|
||||
class AvatarToolkit_OT_FixMMDHierarchy(Operator):
|
||||
bl_idname = "avatar_toolkit.mmd_fix_hierarchy"
|
||||
bl_label = t("MMD.fix_hierarchy")
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
armature = get_active_armature(context)
|
||||
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
|
||||
|
||||
with ProgressTracker(context, 4, "Cleanup Operations") as progress:
|
||||
# Remove rigidbodies and joints
|
||||
self.remove_physics_objects(armature)
|
||||
progress.step("Removed physics objects")
|
||||
|
||||
# Clear unused animation data
|
||||
self.clear_unused_animations(armature)
|
||||
progress.step("Cleared unused animations")
|
||||
|
||||
# Remove empty objects
|
||||
self.remove_empty_objects()
|
||||
progress.step("Removed empty objects")
|
||||
|
||||
# Clean up collections
|
||||
self.cleanup_collections(armature)
|
||||
progress.step("Cleaned up collections")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def remove_physics_objects(self, armature: Object) -> None:
|
||||
"""Remove all physics objects and constraints"""
|
||||
physics_objects = [obj for obj in bpy.data.objects
|
||||
if obj.parent == armature and
|
||||
(obj.rigid_body or obj.rigid_body_constraint)]
|
||||
# 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']
|
||||
}
|
||||
|
||||
for obj in physics_objects:
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
# 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 clear_unused_animations(self, armature: Object) -> None:
|
||||
"""Remove unused animation data"""
|
||||
if armature.animation_data:
|
||||
if armature.animation_data.action and armature.animation_data.action.users == 0:
|
||||
bpy.data.actions.remove(armature.animation_data.action)
|
||||
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
|
||||
|
||||
# Clear unused NLA tracks
|
||||
if armature.animation_data.nla_tracks:
|
||||
for track in armature.animation_data.nla_tracks:
|
||||
if not track.strips:
|
||||
armature.animation_data.nla_tracks.remove(track)
|
||||
|
||||
def remove_empty_objects(self) -> None:
|
||||
"""Remove empty objects from the scene"""
|
||||
empty_objects = [obj for obj in bpy.data.objects
|
||||
if obj.type == 'EMPTY' and not obj.children]
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
edit_bones = armature.data.edit_bones
|
||||
min_distance = context.scene.avatar_toolkit.connect_bones_min_distance
|
||||
|
||||
for obj in empty_objects:
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
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
|
||||
|
||||
def cleanup_collections(self, armature: Object) -> None:
|
||||
"""Clean up and organize collections"""
|
||||
# Remove empty collections
|
||||
for collection in bpy.data.collections:
|
||||
if not collection.objects and not collection.children:
|
||||
bpy.data.collections.remove(collection)
|
||||
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
|
||||
|
||||
# Ensure armature is in main collection
|
||||
if armature.users_collection[0] != bpy.context.scene.collection:
|
||||
bpy.context.scene.collection.objects.link(armature)
|
||||
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'}
|
||||
|
||||
Reference in New Issue
Block a user