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:
@@ -166,3 +166,79 @@ resonite_translations = {
|
|||||||
'thumb_2_r': "thumb2.R",
|
'thumb_2_r': "thumb2.R",
|
||||||
'thumb_3_r': "thumb3.R"
|
'thumb_3_r': "thumb3.R"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mmd_bone_renames = {
|
||||||
|
# Core body
|
||||||
|
"センター": "Center",
|
||||||
|
"グルーブ": "Groove",
|
||||||
|
"腰": "Waist",
|
||||||
|
"上半身": "Upper Body",
|
||||||
|
"上半身2": "Upper Body 2",
|
||||||
|
"下半身": "Lower Body",
|
||||||
|
|
||||||
|
# Head
|
||||||
|
"首": "Neck",
|
||||||
|
"頭": "Head",
|
||||||
|
"両目": "Eyes",
|
||||||
|
"左目": "Eye_L",
|
||||||
|
"右目": "Eye_R",
|
||||||
|
|
||||||
|
# Arms
|
||||||
|
"左肩": "Shoulder_L",
|
||||||
|
"左腕": "Arm_L",
|
||||||
|
"左ひじ": "Elbow_L",
|
||||||
|
"左手首": "Wrist_L",
|
||||||
|
"右肩": "Shoulder_R",
|
||||||
|
"右腕": "Arm_R",
|
||||||
|
"右ひじ": "Elbow_R",
|
||||||
|
"右手首": "Wrist_R",
|
||||||
|
|
||||||
|
# Fingers
|
||||||
|
"左親指1": "Thumb1_L",
|
||||||
|
"左親指2": "Thumb2_L",
|
||||||
|
"左人指1": "Index1_L",
|
||||||
|
"左人指2": "Index2_L",
|
||||||
|
"左人指3": "Index3_L",
|
||||||
|
"左中指1": "Middle1_L",
|
||||||
|
"左中指2": "Middle2_L",
|
||||||
|
"左中指3": "Middle3_L",
|
||||||
|
"左薬指1": "Ring1_L",
|
||||||
|
"左薬指2": "Ring2_L",
|
||||||
|
"左薬指3": "Ring3_L",
|
||||||
|
"左小指1": "Pinky1_L",
|
||||||
|
"左小指2": "Pinky2_L",
|
||||||
|
"左小指3": "Pinky3_L",
|
||||||
|
|
||||||
|
"右親指1": "Thumb1_R",
|
||||||
|
"右親指2": "Thumb2_R",
|
||||||
|
"右人指1": "Index1_R",
|
||||||
|
"右人指2": "Index2_R",
|
||||||
|
"右人指3": "Index3_R",
|
||||||
|
"右中指1": "Middle1_R",
|
||||||
|
"右中指2": "Middle2_R",
|
||||||
|
"右中指3": "Middle3_R",
|
||||||
|
"右薬指1": "Ring1_R",
|
||||||
|
"右薬指2": "Ring2_R",
|
||||||
|
"右薬指3": "Ring3_R",
|
||||||
|
"右小指1": "Pinky1_R",
|
||||||
|
"右小指2": "Pinky2_R",
|
||||||
|
"右小指3": "Pinky3_R",
|
||||||
|
|
||||||
|
# Legs
|
||||||
|
"左足": "Leg_L",
|
||||||
|
"左ひざ": "Knee_L",
|
||||||
|
"左足首": "Ankle_L",
|
||||||
|
"右足": "Leg_R",
|
||||||
|
"右ひざ": "Knee_R",
|
||||||
|
"右足首": "Ankle_R",
|
||||||
|
|
||||||
|
# Toes
|
||||||
|
"左つま先": "Toe_L",
|
||||||
|
"右つま先": "Toe_R",
|
||||||
|
|
||||||
|
# IK bones
|
||||||
|
"左足IK": "Leg_IK_L",
|
||||||
|
"右足IK": "Leg_IK_R",
|
||||||
|
"左つま先IK": "Toe_IK_L",
|
||||||
|
"右つま先IK": "Toe_IK_R"
|
||||||
|
}
|
||||||
|
|||||||
@@ -116,6 +116,32 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
max=1.0
|
max=1.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mmd_keep_upper_chest: BoolProperty(
|
||||||
|
name=t("MMDTools.keep_upper_chest"),
|
||||||
|
description=t("MMDTools.keep_upper_chest_desc"),
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
mmd_remove_unused_bones: BoolProperty(
|
||||||
|
name=t("MMDTools.remove_unused"),
|
||||||
|
description=t("MMDTools.remove_unused_desc"),
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
mmd_merge_distance: FloatProperty(
|
||||||
|
name=t("MMDTools.merge_distance"),
|
||||||
|
description=t("MMDTools.merge_distance_desc"),
|
||||||
|
default=0.001,
|
||||||
|
min=0.0001,
|
||||||
|
max=0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
mmd_cleanup_shapekeys: BoolProperty(
|
||||||
|
name=t("MMDTools.cleanup_shapekeys"),
|
||||||
|
description=t("MMDTools.cleanup_shapekeys_desc"),
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
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")
|
||||||
|
|||||||
+1
-1
@@ -76,7 +76,7 @@ class AvatarToolkit_PT_UpdaterPanel(bpy.types.Panel):
|
|||||||
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 = 3
|
bl_order = 4
|
||||||
|
|
||||||
def draw(self, context: bpy.types.Context) -> None:
|
def draw(self, context: bpy.types.Context) -> None:
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -188,6 +188,35 @@
|
|||||||
"Tools.shapekey_tolerance_desc": "Minimum difference to consider a shape key as used",
|
"Tools.shapekey_tolerance_desc": "Minimum difference to consider a shape key as used",
|
||||||
"Tools.shapekeys_removed": "Removed {count} unused shape keys",
|
"Tools.shapekeys_removed": "Removed {count} unused shape keys",
|
||||||
|
|
||||||
|
"MMDTools.label": "MMD Tools",
|
||||||
|
"MMDTools.basic_tools": "Basic MMD Tools",
|
||||||
|
"MMDTools.advanced_tools": "Advanced Tools",
|
||||||
|
"MMDTools.settings": "MMD Settings",
|
||||||
|
"MMDTools.cleanup": "Cleanup Tools",
|
||||||
|
"MMDTools.fix_bone_names": "Fix Bone Names",
|
||||||
|
"MMDTools.fix_bone_names_desc": "Standardize and fix bone names",
|
||||||
|
"MMDTools.fix_hierarchy": "Fix Bone Hierarchy",
|
||||||
|
"MMDTools.fix_hierarchy_desc": "Fix bone parenting and hierarchy",
|
||||||
|
"MMDTools.fix_weights": "Fix Bone Weights",
|
||||||
|
"MMDTools.fix_weights_desc": "Clean up and normalize bone weights",
|
||||||
|
"MMDTools.fix_mmd_features": "Fix MMD Features",
|
||||||
|
"MMDTools.fix_mmd_features_desc": "Fix MMD-specific features and settings",
|
||||||
|
"MMDTools.advanced_bone_ops": "Advanced Bone Operations",
|
||||||
|
"MMDTools.advanced_bone_ops_desc": "Perform advanced bone fixes and cleanup",
|
||||||
|
"MMDTools.keep_upper_chest": "Keep Upper Chest",
|
||||||
|
"MMDTools.keep_upper_chest_desc": "Keep the upper chest bone during cleanup",
|
||||||
|
"MMDTools.remove_unused": "Remove Unused Bones",
|
||||||
|
"MMDTools.remove_unused_desc": "Remove bones with no weights or influence",
|
||||||
|
"MMDTools.merge_distance": "Merge Distance",
|
||||||
|
"MMDTools.merge_distance_desc": "Distance threshold for merging vertices",
|
||||||
|
"MMDTools.cleanup_shapekeys": "Clean Shape Keys",
|
||||||
|
"MMDTools.cleanup_shapekeys_desc": "Remove unused and duplicate shape keys",
|
||||||
|
"MMDTools.bones_renamed": "Bone names standardized successfully",
|
||||||
|
"MMDTools.hierarchy_fixed": "Bone hierarchy fixed successfully",
|
||||||
|
"MMDTools.weights_fixed": "Bone weights cleaned and normalized",
|
||||||
|
"MMDTools.no_meshes": "No meshes found to process",
|
||||||
|
"MMDTools.not_mmd_model": "Selected armature is not an MMD model",
|
||||||
|
|
||||||
"Settings.label": "Settings",
|
"Settings.label": "Settings",
|
||||||
"Settings.language": "Language",
|
"Settings.language": "Language",
|
||||||
"Settings.language_desc": "Select interface language",
|
"Settings.language_desc": "Select interface language",
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import bpy
|
||||||
|
from typing import Set
|
||||||
|
from bpy.types import Panel, Context, UILayout, Operator
|
||||||
|
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
||||||
|
from ..core.translations import t
|
||||||
|
|
||||||
|
class AvatarToolKit_PT_MMDPanel(Panel):
|
||||||
|
"""Panel containing MMD-specific tools and operations"""
|
||||||
|
bl_label = t("MMDTools.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 = 3
|
||||||
|
|
||||||
|
def draw(self, context: Context) -> None:
|
||||||
|
"""Draw the MMD tools panel interface"""
|
||||||
|
layout = self.layout
|
||||||
|
|
||||||
|
# Basic MMD Tools Box
|
||||||
|
basic_box = layout.box()
|
||||||
|
col = basic_box.column(align=True)
|
||||||
|
col.label(text=t("MMDTools.basic_tools"), icon='ARMATURE_DATA')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
col.operator("avatar_toolkit.fix_bone_names", icon='SORTALPHA')
|
||||||
|
col.operator("avatar_toolkit.fix_bone_hierarchy", icon='BONE_DATA')
|
||||||
|
col.operator("avatar_toolkit.fix_bone_weights", icon='GROUP_BONE')
|
||||||
|
|
||||||
|
# Advanced MMD Tools Box
|
||||||
|
advanced_box = layout.box()
|
||||||
|
col = advanced_box.column(align=True)
|
||||||
|
col.label(text=t("MMDTools.advanced_tools"), icon='MODIFIER')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
col.operator("avatar_toolkit.fix_mmd_features", icon='SHAPEKEY_DATA')
|
||||||
|
col.operator("avatar_toolkit.advanced_bone_ops", icon='CONSTRAINT_BONE')
|
||||||
|
|
||||||
|
# Settings Box
|
||||||
|
settings_box = layout.box()
|
||||||
|
col = settings_box.column(align=True)
|
||||||
|
col.label(text=t("MMDTools.settings"), icon='PREFERENCES')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
col.prop(context.scene.avatar_toolkit, "mmd_keep_upper_chest")
|
||||||
|
col.prop(context.scene.avatar_toolkit, "mmd_remove_unused_bones")
|
||||||
|
col.prop(context.scene.avatar_toolkit, "mmd_cleanup_shapekeys")
|
||||||
|
|
||||||
|
# Cleanup Box
|
||||||
|
cleanup_box = layout.box()
|
||||||
|
col = cleanup_box.column(align=True)
|
||||||
|
col.label(text=t("MMDTools.cleanup"), icon='TRASH')
|
||||||
|
col.separator(factor=0.5)
|
||||||
|
col.operator("avatar_toolkit.cleanup_operations", icon='BRUSH_DATA')
|
||||||
@@ -36,7 +36,7 @@ class AvatarToolKit_PT_SettingsPanel(Panel):
|
|||||||
bl_region_type: str = 'UI'
|
bl_region_type: str = 'UI'
|
||||||
bl_category: str = CATEGORY_NAME
|
bl_category: str = CATEGORY_NAME
|
||||||
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
||||||
bl_order: int = 4
|
bl_order: int = 5
|
||||||
|
|
||||||
def draw(self, context: Context) -> None:
|
def draw(self, context: Context) -> None:
|
||||||
"""Draw the settings panel layout with language selection"""
|
"""Draw the settings panel layout with language selection"""
|
||||||
|
|||||||
Reference in New Issue
Block a user