Re-do 3rd attempt I hate MMD stuff
This commit is contained in:
@@ -485,61 +485,3 @@ def remove_unused_shapekeys(mesh_obj: Object, tolerance: float = 0.001) -> int:
|
||||
removed_count += 1
|
||||
|
||||
return removed_count
|
||||
|
||||
def save_armature_state(armature: Object) -> Dict[str, Any]:
|
||||
"""Save current armature state for recovery"""
|
||||
state = {
|
||||
'bones': {},
|
||||
'pose': {},
|
||||
'settings': {}
|
||||
}
|
||||
|
||||
# Save bone data
|
||||
for bone in armature.data.bones:
|
||||
state['bones'][bone.name] = {
|
||||
'head': bone.head_local.copy(),
|
||||
'tail': bone.tail_local.copy(),
|
||||
'roll': bone.roll,
|
||||
'parent': bone.parent.name if bone.parent else None
|
||||
}
|
||||
|
||||
# Save pose data if exists
|
||||
if armature.pose:
|
||||
for bone in armature.pose.bones:
|
||||
state['pose'][bone.name] = {
|
||||
'location': bone.location.copy(),
|
||||
'rotation': bone.rotation_quaternion.copy(),
|
||||
'scale': bone.scale.copy()
|
||||
}
|
||||
|
||||
return state
|
||||
|
||||
def restore_armature_state(armature: Object, state: Dict[str, Any]) -> None:
|
||||
"""Restore armature from saved state"""
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
# Restore bones
|
||||
for name, data in state['bones'].items():
|
||||
if name in armature.data.edit_bones:
|
||||
bone = armature.data.edit_bones[name]
|
||||
bone.head = data['head']
|
||||
bone.tail = data['tail']
|
||||
bone.roll = data['roll']
|
||||
|
||||
# Restore parenting
|
||||
for name, data in state['bones'].items():
|
||||
if data['parent'] and name in armature.data.edit_bones:
|
||||
bone = armature.data.edit_bones[name]
|
||||
if data['parent'] in armature.data.edit_bones:
|
||||
bone.parent = armature.data.edit_bones[data['parent']]
|
||||
|
||||
bpy.ops.object.mode_set(mode='POSE')
|
||||
|
||||
# Restore pose if exists
|
||||
if 'pose' in state:
|
||||
for name, data in state['pose'].items():
|
||||
if name in armature.pose.bones:
|
||||
bone = armature.pose.bones[name]
|
||||
bone.location = data['location']
|
||||
bone.rotation_quaternion = data['rotation']
|
||||
bone.scale = data['scale']
|
||||
|
||||
+13
-35
@@ -87,53 +87,31 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
||||
)
|
||||
|
||||
merge_twist_bones: BoolProperty(
|
||||
name=t("Tools.merge_twist_bones"),
|
||||
description=t("Tools.merge_twist_bones_desc"),
|
||||
name=t("MMD.merge_twist_bones"),
|
||||
description=t("MMD.merge_twist_bones_desc"),
|
||||
default=True
|
||||
)
|
||||
|
||||
clean_weights_threshold: FloatProperty(
|
||||
name=t("Tools.clean_weights_threshold"),
|
||||
description=t("Tools.clean_weights_threshold_desc"),
|
||||
default=0.01,
|
||||
min=0.0000001,
|
||||
max=0.9999999
|
||||
keep_twist_bones: BoolProperty(
|
||||
name=t("MMD.keep_twist_bones"),
|
||||
description=t("MMD.keep_twist_bones_desc"),
|
||||
default=False
|
||||
)
|
||||
|
||||
connect_bones_min_distance: FloatProperty(
|
||||
name=t("Tools.connect_bones_min_distance"),
|
||||
description=t("Tools.connect_bones_min_distance_desc"),
|
||||
default=0.005,
|
||||
min=0.001,
|
||||
max=0.1
|
||||
keep_upper_chest: BoolProperty(
|
||||
name=t("MMD.keep_upper_chest"),
|
||||
description=t("MMD.keep_upper_chest_desc"),
|
||||
default=True
|
||||
)
|
||||
|
||||
merge_weights_threshold: FloatProperty(
|
||||
name=t("Tools.merge_weights_threshold"),
|
||||
description=t("Tools.merge_weights_threshold_desc"),
|
||||
name=t("MMD.merge_weights_threshold"),
|
||||
description=t("MMD.merge_weights_threshold_desc"),
|
||||
default=0.01,
|
||||
min=0.0001,
|
||||
min=0.0,
|
||||
max=1.0
|
||||
)
|
||||
|
||||
mmd_process_twist_bones: BoolProperty(
|
||||
name=t("MMD.process_twist_bones"),
|
||||
description=t("MMD.process_twist_bones_desc"),
|
||||
default=True
|
||||
)
|
||||
|
||||
mmd_connect_bones: BoolProperty(
|
||||
name=t("MMD.connect_bones"),
|
||||
description=t("MMD.connect_bones_desc"),
|
||||
default=True
|
||||
)
|
||||
|
||||
save_backup_state: BoolProperty(
|
||||
name="Save Backup State",
|
||||
description="Save the initial state of the armature before standardizing bones",
|
||||
default=False
|
||||
)
|
||||
|
||||
def register() -> None:
|
||||
"""Register the Avatar Toolkit property group"""
|
||||
logger.info("Registering Avatar Toolkit properties")
|
||||
|
||||
+453
-454
@@ -1,62 +1,190 @@
|
||||
import bpy
|
||||
from typing import Tuple, Set, Dict
|
||||
from bpy.types import Operator, Context, Object
|
||||
from mathutils import Vector
|
||||
from typing import Dict, List, Tuple, Set, Optional
|
||||
from bpy.types import Object, Armature, EditBone, Bone, Operator, Context
|
||||
from ..core.logging_setup import logger
|
||||
from ..core.common import (
|
||||
ProgressTracker,
|
||||
get_active_armature,
|
||||
validate_meshes,
|
||||
simplify_bonename,
|
||||
duplicate_bone_chain,
|
||||
save_armature_state,
|
||||
restore_armature_state,
|
||||
get_all_meshes,
|
||||
validate_bone_hierarchy,
|
||||
transfer_vertex_weights,
|
||||
get_vertex_weights
|
||||
validate_armature,
|
||||
get_vertex_weights,
|
||||
transfer_vertex_weights
|
||||
)
|
||||
from ..core.logging_setup import logger
|
||||
from ..core.translations import t
|
||||
from ..core.dictionaries import bone_names
|
||||
|
||||
class AvatarToolkit_OT_StandardizeMMDBones(Operator):
|
||||
bl_idname = "avatar_toolkit.mmd_standardize_bones"
|
||||
bl_label = t("MMD.standardize_bones")
|
||||
class AVATAR_TOOLKIT_OT_StandardizeMmd(Operator):
|
||||
"""MMD Bone standardization system"""
|
||||
bl_idname = "avatar_toolkit.standardize_mmd"
|
||||
bl_label = t("MMD.standardize")
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def standardize_bone_names(self, armature: Object) -> None:
|
||||
"""Standardize bone names using MMD to Unity/VRChat conventions"""
|
||||
for bone in armature.data.bones:
|
||||
simplified_name = simplify_bonename(bone.name)
|
||||
for standard_name, variations in bone_names.items():
|
||||
if simplified_name in variations:
|
||||
bone.name = standard_name
|
||||
break
|
||||
|
||||
def process_lr_bones(self, armature: Object) -> None:
|
||||
"""Process left/right bone pairs for consistency"""
|
||||
for bone in armature.data.bones:
|
||||
if bone.name.endswith(('_l', '_r', '.l', '.r', 'Left', 'Right')):
|
||||
base_name = bone.name.rsplit('_', 1)[0]
|
||||
side = '_l' if any(s in bone.name.lower() for s in ('left', '_l', '.l')) else '_r'
|
||||
bone.name = f"{base_name}{side}"
|
||||
|
||||
def 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)
|
||||
|
||||
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
|
||||
|
||||
def __init__(self):
|
||||
self.bone_mapping: Dict[str, str] = {}
|
||||
self.processed_bones: Set[str] = set()
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
self.armature = get_active_armature(context)
|
||||
|
||||
if not self.armature:
|
||||
self.report({'ERROR'}, t("MMD.no_armature"))
|
||||
return {'CANCELLED'}
|
||||
|
||||
try:
|
||||
with ProgressTracker(context, 5, "MMD Standardization") as progress:
|
||||
# Step 1: Process bone names
|
||||
self.process_bone_names(context)
|
||||
progress.step("Processed bone names")
|
||||
|
||||
# Step 2: Fix bone structure
|
||||
self.fix_bone_structure(context)
|
||||
progress.step("Fixed bone structure")
|
||||
|
||||
# Step 3: Process weights
|
||||
self.process_weights(context)
|
||||
progress.step("Processed weights")
|
||||
|
||||
# Step 4: Clean up
|
||||
self.cleanup_armature(context)
|
||||
progress.step("Cleaned up armature")
|
||||
|
||||
# Step 5: Final validation
|
||||
self.validate_results(context)
|
||||
progress.step("Validated results")
|
||||
|
||||
self.report({'INFO'}, t("MMD.standardization_complete"))
|
||||
return {'FINISHED'}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"MMD Standardization failed: {str(e)}")
|
||||
self.report({'ERROR'}, str(e))
|
||||
return {'CANCELLED'}
|
||||
|
||||
def standardize_armature(self) -> Tuple[bool, str]:
|
||||
"""Main standardization process"""
|
||||
if not self.armature:
|
||||
return False, t("MMD.no_armature")
|
||||
|
||||
try:
|
||||
with ProgressTracker(self.context, 5, "MMD Standardization") as progress:
|
||||
# Step 1: Process bone names
|
||||
self.process_bone_names()
|
||||
progress.step("Processed bone names")
|
||||
|
||||
# Step 2: Fix bone structure
|
||||
self.fix_bone_structure()
|
||||
progress.step("Fixed bone structure")
|
||||
|
||||
# Step 3: Process weights
|
||||
self.process_weights()
|
||||
progress.step("Processed weights")
|
||||
|
||||
# Step 4: Clean up
|
||||
self.cleanup_armature()
|
||||
progress.step("Cleaned up armature")
|
||||
|
||||
# Step 5: Final validation
|
||||
self.validate_results()
|
||||
progress.step("Validated results")
|
||||
|
||||
return True, t("MMD.standardization_complete")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"MMD Standardization failed: {str(e)}")
|
||||
return False, str(e)
|
||||
|
||||
def process_bone_names(self, context: Context) -> None:
|
||||
"""Process and standardize bone names"""
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
edit_bones = self.armature.data.edit_bones
|
||||
|
||||
for bone in edit_bones:
|
||||
new_name = self.standardize_bone_name(bone.name)
|
||||
if new_name != bone.name:
|
||||
self.bone_mapping[bone.name] = new_name
|
||||
bone.name = new_name
|
||||
|
||||
def translate_japanese_bone_name(self, name: str) -> str:
|
||||
"""Translate Japanese bone names to English standardized names"""
|
||||
from ..core.dictionaries import bone_names
|
||||
|
||||
# Convert to lowercase for matching
|
||||
name_lower = name.lower()
|
||||
|
||||
# Check each bone category for Japanese character matches
|
||||
for bone_category, variations in bone_names.items():
|
||||
for variation in variations:
|
||||
if variation in name_lower:
|
||||
# If Japanese characters are found, return the standardized name
|
||||
return bone_category
|
||||
|
||||
# If no match found, return original name
|
||||
return name
|
||||
|
||||
def standardize_bone_name(self, name: str) -> str:
|
||||
"""Standardize individual bone names"""
|
||||
# First translate Japanese names
|
||||
result = self.translate_japanese_bone_name(name)
|
||||
|
||||
# Remove common prefixes
|
||||
prefixes = ['ValveBiped_', 'Bip01_', 'MMD_', 'Armature|']
|
||||
for prefix in prefixes:
|
||||
if result.lower().startswith(prefix.lower()):
|
||||
result = result[len(prefix):]
|
||||
|
||||
# Handle left/right conventions
|
||||
if result.endswith('_L') or result.endswith('.L'):
|
||||
result = f"{result[:-2]}.L"
|
||||
elif result.endswith('_R') or result.endswith('.R'):
|
||||
result = f"{result[:-2]}.R"
|
||||
|
||||
return result
|
||||
|
||||
def fix_bone_structure(self, context: Context) -> None:
|
||||
"""Fix bone hierarchy and orientations"""
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
edit_bones = self.armature.data.edit_bones
|
||||
|
||||
# Process spine hierarchy
|
||||
self.process_spine_chain(context)
|
||||
|
||||
# Fix bone orientations
|
||||
self.fix_bone_orientations(context)
|
||||
|
||||
# Connect appropriate bones
|
||||
self.connect_bones(context)
|
||||
|
||||
def process_weights(self, context: Context) -> None:
|
||||
"""Process and clean up vertex weights"""
|
||||
for mesh in self.get_associated_meshes(context):
|
||||
# Transfer weights based on bone mapping
|
||||
for old_name, new_name in self.bone_mapping.items():
|
||||
if old_name != new_name:
|
||||
transfer_vertex_weights(mesh, old_name, new_name)
|
||||
|
||||
# Clean up zero weights
|
||||
self.cleanup_vertex_groups(mesh, context)
|
||||
|
||||
def cleanup_armature(self, context: Context) -> None:
|
||||
"""Perform final cleanup operations"""
|
||||
# Remove unused bones
|
||||
self.remove_unused_bones(context)
|
||||
|
||||
# Clean up constraints
|
||||
self.cleanup_constraints(context)
|
||||
|
||||
# Fix zero-length bones
|
||||
self.fix_zero_length_bones(context)
|
||||
|
||||
def get_associated_meshes(self, context: Context) -> List[Object]:
|
||||
"""Get all mesh objects associated with the armature"""
|
||||
return [obj for obj in bpy.data.objects
|
||||
if obj.type == 'MESH'
|
||||
and obj.parent == self.armature]
|
||||
|
||||
def process_spine_chain(self, context: Context) -> None:
|
||||
"""Process and fix spine bone chain hierarchy"""
|
||||
edit_bones = self.armature.data.edit_bones
|
||||
spine_bones = {
|
||||
'hips': None,
|
||||
'spine': None,
|
||||
@@ -66,437 +194,308 @@ class AvatarToolkit_OT_StandardizeMMDBones(Operator):
|
||||
'head': None
|
||||
}
|
||||
|
||||
# Map existing spine bones
|
||||
# Find spine bones using bone_names dictionary
|
||||
for bone in edit_bones:
|
||||
simplified = simplify_bonename(bone.name)
|
||||
for spine_name in spine_bones.keys():
|
||||
if simplified in bone_names[spine_name]:
|
||||
spine_bones[spine_name] = bone
|
||||
for spine_part, _ in spine_bones.items():
|
||||
if any(alt_name in bone.name.lower() for alt_name in bone_names[spine_part]):
|
||||
spine_bones[spine_part] = bone
|
||||
break
|
||||
|
||||
# Create missing spine bones
|
||||
if spine_bones['spine'] and not spine_bones['chest']:
|
||||
chest = edit_bones.new('chest')
|
||||
chest.head = spine_bones['spine'].tail
|
||||
chest.tail = spine_bones['neck'].head if spine_bones['neck'] else spine_bones['head'].head
|
||||
spine_bones['chest'] = chest
|
||||
|
||||
# Set up spine hierarchy
|
||||
if spine_bones['hips']:
|
||||
for i, key in enumerate(['spine', 'chest', 'upper_chest', 'neck', 'head']):
|
||||
if spine_bones[key]:
|
||||
prev_key = list(spine_bones.keys())[i]
|
||||
if spine_bones[prev_key]:
|
||||
spine_bones[key].parent = spine_bones[prev_key]
|
||||
|
||||
def correct_bone_orientations(self, armature: Object) -> None:
|
||||
"""Automatically correct bone orientations to align with Unity's axes"""
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
edit_bones = armature.data.edit_bones
|
||||
|
||||
# Define standard orientations
|
||||
orientations = {
|
||||
'spine': Vector((0, 0, 1)), # Points up
|
||||
'chest': Vector((0, 0, 1)),
|
||||
'neck': Vector((0, 0, 1)),
|
||||
'head': Vector((0, 0, 1)),
|
||||
'shoulder': Vector((1, 0, 0)), # Points outward
|
||||
'arm': Vector((0, -1, 0)), # Points down
|
||||
'elbow': Vector((0, -1, 0)),
|
||||
'leg': Vector((0, -1, 0)),
|
||||
'knee': Vector((0, -1, 0)),
|
||||
'foot': Vector((1, 0, 0)), # Points forward
|
||||
}
|
||||
|
||||
for bone in edit_bones:
|
||||
simplified_name = simplify_bonename(bone.name)
|
||||
for bone_type, direction in orientations.items():
|
||||
if bone_type in simplified_name:
|
||||
# Calculate new tail position while maintaining length
|
||||
length = (bone.tail - bone.head).length
|
||||
bone.tail = bone.head + direction * length
|
||||
break
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
"""Check if there is an active armature in the scene"""
|
||||
return get_active_armature(context) is not None
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
try:
|
||||
armature = get_active_armature(context)
|
||||
|
||||
# Save initial state if enabled
|
||||
if context.scene.avatar_toolkit.save_backup_state:
|
||||
self.initial_state = save_armature_state(armature)
|
||||
|
||||
with ProgressTracker(context, 6, "Standardizing Bones") as progress:
|
||||
# Step 1: Standardize bone names
|
||||
self.standardize_bone_names(armature)
|
||||
progress.step("Standardized bone names")
|
||||
|
||||
# Step 3: Process left/right bones
|
||||
self.process_lr_bones(armature)
|
||||
progress.step("Processed left/right bones")
|
||||
|
||||
# Step 4: Handle name conflicts
|
||||
self.resolve_name_conflicts(armature)
|
||||
progress.step("Resolved naming conflicts")
|
||||
|
||||
# Step 5: Process spine chain
|
||||
self.process_spine_chain(armature)
|
||||
progress.step("Processed spine chain")
|
||||
|
||||
# Step 6: Correct bone orientations
|
||||
self.correct_bone_orientations(armature)
|
||||
progress.step("Corrected bone orientations")
|
||||
|
||||
self.report({'INFO'}, t("MMD.bones_standardized"))
|
||||
return {'FINISHED'}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Bone standardization failed: {str(e)}")
|
||||
if hasattr(self, 'initial_state'):
|
||||
restore_armature_state(armature, self.initial_state)
|
||||
self.report({'ERROR'}, str(e))
|
||||
return {'CANCELLED'}
|
||||
|
||||
class AvatarToolkit_OT_ProcessMMDWeights(Operator):
|
||||
bl_idname = "avatar_toolkit.mmd_process_weights"
|
||||
bl_label = t("MMD.process_weights")
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def merge_bone_weights(self, context: Context, mesh: Object, source: str, target: str) -> None:
|
||||
"""Transfer weights from source bone to target bone"""
|
||||
transfer_vertex_weights(
|
||||
mesh,
|
||||
source,
|
||||
target,
|
||||
context.scene.avatar_toolkit.merge_weights_threshold
|
||||
)
|
||||
|
||||
def process_eye_weights(self, context: Context, mesh: Object) -> None:
|
||||
"""Handle special cases for eye bone weights"""
|
||||
eye_bones = {
|
||||
'eye_l': ['eyel', 'lefteye', 'eye.l'],
|
||||
'eye_r': ['eyer', 'righteye', 'eye.r']
|
||||
}
|
||||
|
||||
for target, sources in eye_bones.items():
|
||||
for source in sources:
|
||||
if source in mesh.vertex_groups:
|
||||
self.merge_bone_weights(context, mesh, source, target)
|
||||
|
||||
def process_twist_bones(self, context: Context, mesh: Object) -> None:
|
||||
"""Process and merge twist bone weights"""
|
||||
if not context.scene.avatar_toolkit.mmd_process_twist_bones:
|
||||
return
|
||||
|
||||
twist_pairs = [
|
||||
('arm_twist_l', 'left_arm'),
|
||||
('arm_twist_r', 'right_arm'),
|
||||
('forearm_twist_l', 'left_elbow'),
|
||||
('forearm_twist_r', 'right_elbow')
|
||||
hierarchy = [
|
||||
('hips', 'spine'),
|
||||
('spine', 'chest'),
|
||||
('chest', 'neck'),
|
||||
('neck', 'head')
|
||||
]
|
||||
|
||||
for twist, target in twist_pairs:
|
||||
if twist in mesh.vertex_groups:
|
||||
self.merge_bone_weights(context, mesh, twist, target)
|
||||
for parent_name, child_name in hierarchy:
|
||||
parent = spine_bones.get(parent_name)
|
||||
child = spine_bones.get(child_name)
|
||||
if parent and child:
|
||||
child.parent = parent
|
||||
child.use_connect = True
|
||||
|
||||
def cleanup_vertex_groups(self, context: Context, mesh: Object) -> None:
|
||||
"""Remove empty and unused vertex groups"""
|
||||
threshold = context.scene.avatar_toolkit.clean_weights_threshold
|
||||
def fix_bone_orientations(self, context: Context) -> None:
|
||||
"""Fix bone orientations for standard pose compatibility"""
|
||||
edit_bones = self.armature.data.edit_bones
|
||||
|
||||
# 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)
|
||||
# Process arm bones
|
||||
arm_pairs = [
|
||||
('upper_arm', 'forearm'),
|
||||
('forearm', 'hand')
|
||||
]
|
||||
|
||||
# Remove unused groups
|
||||
for group in mesh.vertex_groups[:]:
|
||||
if group.name not in valid_bones:
|
||||
mesh.vertex_groups.remove(group)
|
||||
for side in ['.L', '.R']:
|
||||
for parent, child in arm_pairs:
|
||||
parent_bone = next((b for b in edit_bones if b.name.lower().startswith(parent) and b.name.endswith(side)), None)
|
||||
child_bone = next((b for b in edit_bones if b.name.lower().startswith(child) and b.name.endswith(side)), None)
|
||||
|
||||
if parent_bone and child_bone:
|
||||
child_bone.use_connect = True
|
||||
child_bone.use_inherit_rotation = True
|
||||
|
||||
# Process leg bones
|
||||
leg_pairs = [
|
||||
('thigh', 'shin'),
|
||||
('shin', 'foot')
|
||||
]
|
||||
|
||||
for side in ['.L', '.R']:
|
||||
for parent, child in leg_pairs:
|
||||
parent_bone = next((b for b in edit_bones if b.name.lower().startswith(parent) and b.name.endswith(side)), None)
|
||||
child_bone = next((b for b in edit_bones if b.name.lower().startswith(child) and b.name.endswith(side)), None)
|
||||
|
||||
if parent_bone and child_bone:
|
||||
child_bone.use_connect = True
|
||||
child_bone.use_inherit_rotation = True
|
||||
|
||||
def remove_unused_bones(self, context: Context) -> None:
|
||||
"""Remove unused and unnecessary bones from the armature"""
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
edit_bones = self.armature.data.edit_bones
|
||||
|
||||
# Get list of bones that have vertex weights
|
||||
used_bones = set()
|
||||
for mesh in self.get_associated_meshes(context):
|
||||
for group in mesh.vertex_groups:
|
||||
used_bones.add(group.name)
|
||||
|
||||
# Get list of bones to keep based on settings
|
||||
toolkit = context.scene.avatar_toolkit
|
||||
keep_upper_chest = toolkit.keep_upper_chest
|
||||
keep_twist = toolkit.keep_twist_bones
|
||||
|
||||
# Remove unused bones
|
||||
for bone in edit_bones:
|
||||
# Skip if bone has weights
|
||||
if bone.name in used_bones:
|
||||
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)
|
||||
# Skip if bone is upper chest and we want to keep it
|
||||
if 'upper_chest' in bone.name.lower() and keep_upper_chest:
|
||||
continue
|
||||
|
||||
# Skip if bone is twist bone and we want to keep them
|
||||
if 'twist' in bone.name.lower() and keep_twist:
|
||||
continue
|
||||
|
||||
# Remove the bone
|
||||
edit_bones.remove(bone)
|
||||
|
||||
def 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'),
|
||||
def connect_bones(self, context: Context) -> None:
|
||||
"""Connect bones that should be connected in the hierarchy"""
|
||||
edit_bones = self.armature.data.edit_bones
|
||||
|
||||
connect_chains = [
|
||||
['hips', 'spine', 'chest', 'neck', 'head'],
|
||||
['shoulder.L', 'upper_arm.L', 'forearm.L', 'hand.L'],
|
||||
['shoulder.R', 'upper_arm.R', 'forearm.R', 'hand.R'],
|
||||
['thigh.L', 'shin.L', 'foot.L', 'toe.L'],
|
||||
['thigh.R', 'shin.R', 'foot.R', 'toe.R']
|
||||
]
|
||||
|
||||
for 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)
|
||||
for chain in connect_chains:
|
||||
prev_bone = None
|
||||
for bone_name in chain:
|
||||
bone = next((b for b in edit_bones if b.name.lower().endswith(bone_name.lower())), None)
|
||||
if bone and prev_bone:
|
||||
bone.parent = prev_bone
|
||||
bone.use_connect = True
|
||||
prev_bone = bone
|
||||
|
||||
def cleanup_vertex_groups(self, mesh_obj: Object, context: Context) -> None:
|
||||
"""Clean up vertex groups by removing zero weights and merging similar groups"""
|
||||
threshold = context.scene.avatar_toolkit.merge_weights_threshold
|
||||
|
||||
# Get list of vertex groups
|
||||
vertex_groups = mesh_obj.vertex_groups
|
||||
|
||||
# Track groups to remove
|
||||
groups_to_remove = set()
|
||||
|
||||
# Check each vertex group
|
||||
for group in vertex_groups:
|
||||
weights = get_vertex_weights(mesh_obj, group.name)
|
||||
|
||||
# If no weights above threshold, mark for removal
|
||||
if not any(weight > threshold for weight in weights.values()):
|
||||
groups_to_remove.add(group.name)
|
||||
|
||||
# Remove empty groups
|
||||
for group_name in groups_to_remove:
|
||||
group = vertex_groups.get(group_name)
|
||||
if group:
|
||||
vertex_groups.remove(group)
|
||||
|
||||
def validate_results(self, context: Context) -> None:
|
||||
"""Validate the results of standardization"""
|
||||
valid, messages = validate_armature(self.armature)
|
||||
if not valid:
|
||||
raise ValueError("\n".join(messages))
|
||||
|
||||
def cleanup_constraints(self, context: Context) -> None:
|
||||
"""Clean up and fix bone constraints"""
|
||||
bpy.ops.object.mode_set(mode='POSE')
|
||||
|
||||
# Process each pose bone
|
||||
for pose_bone in self.armature.pose.bones:
|
||||
constraints_to_remove = []
|
||||
|
||||
for constraint in pose_bone.constraints:
|
||||
should_remove = False
|
||||
|
||||
# Handle IK constraints
|
||||
if constraint.type == 'IK':
|
||||
if not constraint.target or constraint.target != self.armature:
|
||||
should_remove = True
|
||||
elif not constraint.subtarget or constraint.subtarget not in self.armature.data.bones:
|
||||
should_remove = True
|
||||
|
||||
# Handle MMD additional rotation constraints
|
||||
elif constraint.name == 'mmd_additional_rotation':
|
||||
if not constraint.target or constraint.target != self.armature:
|
||||
should_remove = True
|
||||
elif not constraint.subtarget or constraint.subtarget not in self.armature.data.bones:
|
||||
should_remove = True
|
||||
|
||||
# Handle transformation constraints
|
||||
elif constraint.type in {'COPY_ROTATION', 'COPY_LOCATION', 'COPY_TRANSFORMS'}:
|
||||
if not constraint.target or constraint.target != self.armature:
|
||||
should_remove = True
|
||||
elif not constraint.subtarget or constraint.subtarget not in self.armature.data.bones:
|
||||
should_remove = True
|
||||
|
||||
if should_remove:
|
||||
constraints_to_remove.append(constraint)
|
||||
|
||||
# Remove invalid constraints
|
||||
for constraint in constraints_to_remove:
|
||||
pose_bone.constraints.remove(constraint)
|
||||
|
||||
def fix_zero_length_bones(self, context: Context) -> None:
|
||||
"""Fix zero-length bones by setting minimal length"""
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
edit_bones = self.armature.data.edit_bones
|
||||
|
||||
min_length = 0.01 # Minimum bone length in Blender units
|
||||
|
||||
for bone in edit_bones:
|
||||
# Calculate bone length
|
||||
bone_length = (bone.tail - bone.head).length
|
||||
|
||||
if bone_length < min_length:
|
||||
# Set minimal length while preserving direction
|
||||
if bone.parent:
|
||||
# Use parent's orientation as reference
|
||||
direction = bone.parent.tail - bone.parent.head
|
||||
direction.normalize()
|
||||
else:
|
||||
# Default to Z-axis if no parent
|
||||
direction = mathutils.Vector((0, 0, 1))
|
||||
|
||||
bone.tail = bone.head + (direction * min_length)
|
||||
|
||||
class FixUnmovableBonesOperator(bpy.types.Operator):
|
||||
bl_idname = "avatar_toolkit.fix_unmovable_bones"
|
||||
bl_label = t("MMD.fix_unmovable_bones")
|
||||
bl_description = t("MMD.fix_unmovable_bones_desc")
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
"""Check if there is an active armature in the scene"""
|
||||
return get_active_armature(context) is not None
|
||||
def poll(cls, context):
|
||||
armature = get_active_armature(context)
|
||||
return armature is not None and armature.type == 'ARMATURE'
|
||||
|
||||
def execute(self, context):
|
||||
armature = get_active_armature(context)
|
||||
if not armature:
|
||||
self.report({'ERROR'}, t("MMD.no_armature"))
|
||||
return {'CANCELLED'}
|
||||
|
||||
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:
|
||||
with ProgressTracker(context, 2, "Unlocking Transforms") as progress:
|
||||
# Unlock armature transforms
|
||||
progress.step("Unlocking armature transforms")
|
||||
for attr in ('location', 'rotation', 'scale'):
|
||||
for i in range(3):
|
||||
setattr(armature, f"lock_{attr}", [False] * 3)
|
||||
|
||||
# Unlock bone transforms
|
||||
progress.step("Unlocking bone transforms")
|
||||
for bone in armature.pose.bones:
|
||||
for attr in ('location', 'rotation', 'scale'):
|
||||
setattr(bone, f"lock_{attr}", [False] * 3)
|
||||
|
||||
self.report({'INFO'}, t("MMD.transforms_unlocked"))
|
||||
return {'FINISHED'}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error unlocking transforms: {str(e)}")
|
||||
self.report({'ERROR'}, str(e))
|
||||
return {'CANCELLED'}
|
||||
|
||||
class ReparentMeshesOperator(bpy.types.Operator):
|
||||
bl_idname = "avatar_toolkit.reparent_meshes"
|
||||
bl_label = t("MMD.reparent_meshes")
|
||||
bl_description = t("MMD.reparent_meshes_desc")
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
armature = get_active_armature(context)
|
||||
return armature is not None and get_all_meshes(context)
|
||||
|
||||
def execute(self, context):
|
||||
armature = get_active_armature(context)
|
||||
if not armature:
|
||||
self.report({'ERROR'}, t("MMD.no_armature"))
|
||||
return {'CANCELLED'}
|
||||
|
||||
meshes = get_all_meshes(context)
|
||||
if not meshes:
|
||||
self.report({'ERROR'}, t("MMD.no_meshes"))
|
||||
return {'CANCELLED'}
|
||||
|
||||
try:
|
||||
with ProgressTracker(context, len(meshes) + 1, "Reparenting Meshes") as progress:
|
||||
# Get or create main collection
|
||||
main_collection = self._get_main_collection(context)
|
||||
progress.step("Setting up collections")
|
||||
|
||||
# Process each mesh
|
||||
for mesh in meshes:
|
||||
# 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}")
|
||||
progress.step(f"Processing {mesh.name}")
|
||||
self._process_mesh(mesh, armature, main_collection)
|
||||
|
||||
self.report({'INFO'}, t("MMD.weights_processed"))
|
||||
self.report({'INFO'}, t("MMD.reparenting_complete"))
|
||||
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)
|
||||
logger.error(f"Error reparenting meshes: {str(e)}")
|
||||
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 _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 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 _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)
|
||||
|
||||
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
|
||||
# Ensure mesh is in main collection
|
||||
if mesh.name not in main_collection.objects:
|
||||
main_collection.objects.link(mesh)
|
||||
|
||||
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
|
||||
|
||||
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'}
|
||||
# Set parent to armature
|
||||
mesh.parent = armature
|
||||
if not mesh.parent_type == 'ARMATURE':
|
||||
mesh.parent_type = 'ARMATURE'
|
||||
+7
-30
@@ -1,46 +1,23 @@
|
||||
import bpy
|
||||
from typing import Set
|
||||
from bpy.types import Panel, Context, UILayout, Operator
|
||||
from bpy.types import Panel, Context, UILayout
|
||||
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
||||
from ..core.translations import t
|
||||
|
||||
class AvatarToolKit_PT_MMDPanel(Panel):
|
||||
"""Panel containing MMD conversion and optimization tools"""
|
||||
"""Panel containing MMD bone standardization tools"""
|
||||
bl_label = t("MMD.label")
|
||||
bl_idname = "OBJECT_PT_avatar_toolkit_mmd"
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'UI'
|
||||
bl_category = CATEGORY_NAME
|
||||
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
||||
bl_order = 2
|
||||
bl_order = 3
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout: UILayout = self.layout
|
||||
toolkit = context.scene.avatar_toolkit
|
||||
|
||||
# Bone Standardization Box
|
||||
bone_box: UILayout = layout.box()
|
||||
col: UILayout = bone_box.column(align=True)
|
||||
col.label(text=t("MMD.bone_standardization"), icon='ARMATURE_DATA')
|
||||
col.separator(factor=0.5)
|
||||
col.operator("avatar_toolkit.mmd_standardize_bones", icon='BONE_DATA')
|
||||
|
||||
# Weight Processing Box
|
||||
weight_box: UILayout = layout.box()
|
||||
col = weight_box.column(align=True)
|
||||
col.label(text=t("MMD.weight_processing"), icon='GROUP_VERTEX')
|
||||
col.separator(factor=0.5)
|
||||
col.operator("avatar_toolkit.mmd_process_weights", icon='WPAINT_HLT')
|
||||
|
||||
# Hierarchy Box
|
||||
hierarchy_box: UILayout = layout.box()
|
||||
col = hierarchy_box.column(align=True)
|
||||
col.label(text=t("MMD.hierarchy"), icon='OUTLINER')
|
||||
col.separator(factor=0.5)
|
||||
col.operator("avatar_toolkit.mmd_fix_hierarchy", icon='CONSTRAINT_BONE')
|
||||
|
||||
# Cleanup Box
|
||||
cleanup_box: UILayout = layout.box()
|
||||
col = cleanup_box.column(align=True)
|
||||
col.label(text=t("MMD.cleanup"), icon='BRUSH_DATA')
|
||||
col.separator(factor=0.5)
|
||||
col.operator("avatar_toolkit.mmd_cleanup_armature", icon='MODIFIER')
|
||||
# Add merge twist bones option
|
||||
layout.prop(toolkit, "keep_twist_bones")
|
||||
layout.operator("avatar_toolkit.standardize_mmd", icon='BONE_DATA')
|
||||
|
||||
Reference in New Issue
Block a user