Start of MMD Tools

- The idea is to have several buttons which kinda mimic what Cats used to do.
- These are very basic, don't work very well, will improve before Alpha 1.
This commit is contained in:
Yusarina
2024-12-05 15:09:40 +00:00
parent b1631b2868
commit 3e187bd18a
7 changed files with 683 additions and 2 deletions
+498
View File
@@ -0,0 +1,498 @@
import bpy
import numpy as np
from typing import Set, Dict, List, Optional, Tuple
from bpy.types import Operator, Context, Object, EditBone, Mesh
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
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")
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 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")
# 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")
# 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"""
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']
}
}
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
def fix_bone_orientations(self, armature: Object) -> None:
"""Fix bone roll and axis orientations"""
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'}
@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)
meshes = get_all_meshes(context)
if not meshes:
self.report({'WARNING'}, t("MMDTools.no_meshes"))
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")
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)
meshes = get_all_meshes(context)
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'}
def process_shape_keys(self, mesh: Object) -> None:
"""Process and clean up shape keys"""
if not mesh.data.shape_keys:
return
# Clean unused shape keys
remove_unused_shapekeys(mesh)
# 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:]
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)]
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)
# 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:
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)
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 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")
# Connect bones with children
self.connect_bone_chains(armature)
progress.step("Connected bone chains")
# Handle bone roll values
self.fix_bone_rolls(armature)
progress.step("Fixed bone rolls")
# Fix bone orientations
self.fix_bone_orientations(armature)
progress.step("Fixed bone orientations")
return {'FINISHED'}
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")
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context: Context) -> Set[str]:
armature = get_active_armature(context)
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)]
for obj in physics_objects:
bpy.data.objects.remove(obj, do_unlink=True)
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)
# 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]
for obj in empty_objects:
bpy.data.objects.remove(obj, do_unlink=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)
# Ensure armature is in main collection
if armature.users_collection[0] != bpy.context.scene.collection:
bpy.context.scene.collection.objects.link(armature)