diff --git a/blender_manifest.toml b/blender_manifest.toml index 653f0c7..cd05857 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -17,6 +17,7 @@ license = [ wheels = [ "./wheels/lz4-4.4.3-cp311-cp311-macosx_11_0_arm64.whl", + "./wheels/lz4-4.3.3-cp311-cp311-macosx_10_9_x86_64.whl", "./wheels/lz4-4.4.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", "./wheels/lz4-4.4.3-cp311-cp311-win_amd64.whl" ] diff --git a/core/armature_validation.py b/core/armature_validation.py index ef8ad2a..446bac4 100644 --- a/core/armature_validation.py +++ b/core/armature_validation.py @@ -116,6 +116,15 @@ def validate_armature(armature: Object, detailed_messages: bool = False) -> Unio logger.warning(f"Found {len(non_standard_bones)} non-standard bones") non_standard_list = "\n".join([f"- {bone}" for bone in non_standard_bones]) non_standard_messages.append(t("Armature.validation.non_standard_bones", bones=non_standard_list)) + + non_standard_messages.append(t("Armature.validation.accessory_bones_note.line1")) + non_standard_messages.append(t("Armature.validation.accessory_bones_note.line2")) + non_standard_messages.append(t("Armature.validation.accessory_bones_note.line3")) + non_standard_messages.append(t("Armature.validation.accessory_bones_note.line4")) + non_standard_messages.append("") # Add a blank line for spacing + non_standard_messages.append(t("Armature.validation.standardize_note.line1")) + non_standard_messages.append(t("Armature.validation.standardize_note.line2")) + non_standard_messages.append(t("Armature.validation.standardize_note.line3")) # Combine messages in correct order messages.extend(non_standard_messages) diff --git a/core/common.py b/core/common.py index 95300d7..452760c 100644 --- a/core/common.py +++ b/core/common.py @@ -278,51 +278,67 @@ def validate_meshes(meshes: List[Object]) -> Tuple[bool, str]: return False, t("Optimization.non_mesh_objects") return True, "" -def join_mesh_objects(context: Context, meshes: List[Object], progress: Optional[ProgressTracker] = None) -> Optional[Object]: - """Combines multiple mesh objects into a single mesh with proper cleanup and UV fixing""" - try: - # Store UV maps before joining - uv_maps_data = {} - for mesh in meshes: - uv_maps_data[mesh.name] = {uv.name: uv.data.copy() for uv in mesh.data.uv_layers} +def fast_uv_fix(obj: Object) -> None: + """Fast UV coordinate fixing for joined meshes""" + if not obj or not obj.data or not obj.data.uv_layers: + return + + current_mode = bpy.context.mode + + if current_mode != 'EDIT_MESH': + bpy.ops.object.mode_set(mode='EDIT') + + bpy.ops.mesh.select_all(action='SELECT') + + # Process all UV layers at once + bpy.ops.uv.select_all(action='SELECT') + bpy.ops.uv.pack_islands(margin=0.001) + + if current_mode != 'EDIT_MESH': + bpy.ops.object.mode_set(mode=current_mode) +def join_mesh_objects(context: Context, meshes: List[Object], progress: Optional[ProgressTracker] = None) -> Optional[Object]: + """Combines multiple mesh objects into a single mesh with optimized performance""" + try: + if not meshes: + return None + bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.select_all(action='DESELECT') - for mesh in meshes: + # Create a list of valid meshes + valid_meshes = [mesh for mesh in meshes if mesh.name in bpy.data.objects] + if not valid_meshes: + return None + + for mesh in valid_meshes: mesh.select_set(True) - if context.selected_objects: - context.view_layer.objects.active = context.selected_objects[0] + context.view_layer.objects.active = valid_meshes[0] + + if progress: + progress.step(t("Optimization.joining_meshes")) - if progress: - progress.step(t("Optimization.joining_meshes")) - bpy.ops.object.join() + bpy.ops.object.join() + joined_mesh = context.active_object + + if progress: + progress.step(t("Optimization.applying_transforms")) - if progress: - progress.step(t("Optimization.applying_transforms")) - bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + + if progress: + progress.step(t("Optimization.fixing_uvs")) - if progress: - progress.step(t("Optimization.fixing_uvs")) - fix_uv_coordinates(context) - - # Restore UV maps after joining - joined_mesh = context.active_object - for uv_name, uv_data in uv_maps_data.items(): - for map_name, map_data in uv_data.items(): - if map_name not in joined_mesh.data.uv_layers: - joined_mesh.data.uv_layers.new(name=map_name) - joined_mesh.data.uv_layers[map_name].data.foreach_set("uv", map_data) - - return context.active_object - - return None + fast_uv_fix(joined_mesh) + + return joined_mesh except Exception as e: logger.error(f"Failed to join meshes: {str(e)}") return None + def fix_uv_coordinates(context: Context) -> None: """Normalizes and fixes UV coordinates for the active mesh object""" obj: Object = context.object diff --git a/core/dictionaries.py b/core/dictionaries.py index d17c4f9..d3f8df5 100644 --- a/core/dictionaries.py +++ b/core/dictionaries.py @@ -568,7 +568,15 @@ acceptable_bone_names = { 'breast_upper_1_l': ['BreastUpper1_L'], 'breast_upper_2_l': ['BreastUpper2_L'], 'breast_upper_1_r': ['BreastUpper1_R'], - 'breast_upper_2_r': ['BreastUpper2_R'] + 'breast_upper_2_r': ['BreastUpper2_R'], + + 'ear_upper_l': ['UpperEar.L', 'Upper Ear.L', 'Upper Ear_L'], + 'ear_upper_r': ['UpperEar.R', 'Upper Ear.R', 'Upper Ear_R'], + 'ear_lower_l': ['LowerEar.L', 'Lower Ear.L', 'Lower Ear_L'], + 'ear_lower_r': ['LowerEar.R', 'Lower Ear.R', 'Lower Ear_R'], + + 'ears_upper': ['Ears Upper', 'EarsUpper', 'ears_upper'], + 'ears_lower': ['Ears Lower', 'EarsLower', 'ears_lower'] } rigify_unity_names = { diff --git a/functions/custom_tools/armature_merging.py b/functions/custom_tools/armature_merging.py index adb8d88..cfe197a 100644 --- a/functions/custom_tools/armature_merging.py +++ b/functions/custom_tools/armature_merging.py @@ -146,6 +146,9 @@ def merge_armatures( operator.report({'ERROR'}, t('MergeArmature.error.notFound', name=merge_armature_name)) return + # Store meshes that need to be reparented + meshes_to_reparent = [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == merge_armature] + # Check transforms early if not validate_merge_armature_transforms(base_armature, merge_armature, None, tolerance): if not bpy.context.scene.avatar_toolkit.apply_transforms: @@ -212,7 +215,6 @@ def merge_armatures( else: merge_armature_data.edit_bones.remove(merge_armature_data.edit_bones[bone_name]) - # Return to object mode bpy.ops.object.mode_set(mode='OBJECT') @@ -223,6 +225,8 @@ def merge_armatures( bpy.context.view_layer.objects.active = base_armature bpy.ops.object.join() + # Explicitly set active object after join + bpy.context.view_layer.objects.active = base_armature base_armature_data: bpy.types.Armature = base_armature.data # Restore parent relationships @@ -237,10 +241,12 @@ def merge_armatures( bpy.ops.object.mode_set(mode='OBJECT') - # Update mesh parenting - for obj in bpy.data.objects: - if obj.type == 'MESH' and obj.parent == merge_armature: - obj.parent = base_armature + for mesh_obj in meshes_to_reparent: + if mesh_obj and mesh_obj.name in bpy.data.objects: + mesh_obj.parent = base_armature + for mod in mesh_obj.modifiers: + if mod.type == 'ARMATURE': + mod.object = base_armature # Process vertex groups if not mesh_only if not mesh_only: @@ -261,6 +267,8 @@ def merge_armatures( joined_mesh: Optional[Object] = join_mesh_objects(bpy.context, meshes_to_join) if joined_mesh: logger.info(f"Joined meshes into {joined_mesh.name}") + # Ensure the joined mesh is properly parented + joined_mesh.parent = base_armature # Clean up shape keys if enabled if bpy.context.scene.avatar_toolkit.cleanup_shape_keys: diff --git a/functions/eye_tracking.py b/functions/eye_tracking.py index 8a6b69d..e720286 100644 --- a/functions/eye_tracking.py +++ b/functions/eye_tracking.py @@ -406,6 +406,14 @@ def set_rotation(self, context): StartTestingButton.execute(StartTestingButton, context) return None + # Check if rotation data is available + if not eye_left_rot or len(eye_left_rot) < 3 or not eye_right_rot or len(eye_right_rot) < 3: + # Initialize rotation data if missing + eye_left.rotation_mode = 'XYZ' + eye_left_rot = list(eye_left.rotation_euler) + eye_right.rotation_mode = 'XYZ' + eye_right_rot = list(eye_right.rotation_euler) + eye_left.rotation_mode = 'XYZ' eye_right.rotation_mode = 'XYZ' @@ -898,9 +906,24 @@ class ResetEyeTrackingButton(Operator): def execute(self, context): global eye_left, eye_right, eye_left_data, eye_right_data, eye_left_rot, eye_right_rot - eye_left = eye_right = eye_left_data = eye_right_data = None - eye_left_rot = eye_right_rot = [] + context.scene.avatar_toolkit.eye_mode = 'CREATION' + context.scene.avatar_toolkit.eye_rotation_x = 0 + context.scene.avatar_toolkit.eye_rotation_y = 0 + + eye_left = None + eye_right = None + eye_left_data = None + eye_right_data = None + eye_left_rot = [] + eye_right_rot = [] + + mesh_name = context.scene.avatar_toolkit.mesh_name_eye + mesh = bpy.data.objects.get(mesh_name) + if mesh and mesh.data.shape_keys: + for shape_key in mesh.data.shape_keys.key_blocks: + shape_key.value = 0 + return {'FINISHED'} def validate_weights(mesh_obj: Object, vertex_group: str) -> bool: diff --git a/functions/mmd_tools.py b/functions/mmd_tools.py deleted file mode 100644 index c480aff..0000000 --- a/functions/mmd_tools.py +++ /dev/null @@ -1,792 +0,0 @@ -import bpy -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, - get_vertex_weights, - transfer_vertex_weights, - get_all_meshes -) -from ..core.translations import t -from ..core.dictionaries import bone_names, dont_delete_these_main_bones -from ..core.armature_validation import validate_armature, validate_bone_hierarchy - -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 __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 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 - - # First pass - handle IK bones - ik_bones = [bone for bone in edit_bones if 'IK' in bone.name or 'IK' in bone.name] - for bone in ik_bones: - new_name = f"ik_{self.standardize_bone_name(bone.name.replace('IK', '').replace('IK', ''))}" - self.bone_mapping[bone.name] = new_name - bone.name = new_name - - # Second pass - standard bones - for bone in edit_bones: - if bone not in ik_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""" - name_lower = name.lower() - - for bone_category, variations in bone_names.items(): - for variation in variations: - if variation in name_lower: - return bone_category - - return name - - def standardize_bone_name(self, name: str) -> str: - """Standardize individual bone names""" - result = self.translate_japanese_bone_name(name) - - prefixes = ['ValveBiped_', 'Bip01_', 'MMD_', 'Armature|'] - for prefix in prefixes: - if result.lower().startswith(prefix.lower()): - result = result[len(prefix):] - - 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 - 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 - - self.process_spine_chain(context) - self.fix_bone_orientations(context) - 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""" - self.remove_unused_bones(context) - self.cleanup_constraints(context) - 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""" - bpy.ops.object.mode_set(mode='EDIT') - edit_bones = self.armature.data.edit_bones - spine_bones = { - 'hips': None, - 'spine': None, - 'chest': None, - 'upper_chest': None, - 'neck': None, - 'head': None - } - - # Find spine bones using bone_names dictionary - for bone in edit_bones: - 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 - - # Set up spine hierarchy - hierarchy = [ - ('hips', 'spine'), - ('spine', 'chest'), - ('chest', 'neck'), - ('neck', 'head') - ] - - 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 fix_bone_orientations(self, context: Context) -> None: - """Fix bone orientations for standard pose compatibility""" - edit_bones = self.armature.data.edit_bones - - # Define standardized roll values for key bones - roll_values = { - 'upper_arm.L': -0.1, - 'upper_arm.R': 0.1, - 'forearm.L': -0.1, - 'forearm.R': 0.1, - 'thigh.L': 0.0, - 'thigh.R': 0.0, - 'shin.L': 0.0, - 'shin.R': 0.0, - 'foot.L': 0.0, - 'foot.R': 0.0, - 'spine': 0.0, - 'chest': 0.0, - 'neck': 0.0 - } - - # Apply roll corrections - for bone in edit_bones: - if bone.name.lower() in roll_values: - bone.roll = roll_values[bone.name.lower()] - - # Process arm chains - arm_pairs = [ - ('upper_arm', 'forearm'), - ('forearm', 'hand') - ] - - 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 chains - 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 - - # Align twist bones if present - twist_bones = [b for b in edit_bones if 'twist' in b.name.lower()] - for twist_bone in twist_bones: - if twist_bone.parent: - twist_bone.roll = twist_bone.parent.roll - - 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 essential bones to always keep - essential_bones = { - 'hips', 'spine', 'chest', 'upper_chest', 'neck', 'head', - 'left_leg', 'right_leg', 'left_knee', 'right_knee', - 'left_ankle', 'right_ankle', 'left_toe', 'right_toe' - } - - # Add any additional bones you want to preserve - essential_bones.update(dont_delete_these_main_bones) - - # Remove unused bones - for bone in edit_bones: - # Skip if bone is essential - if bone.name.lower() in essential_bones: - continue - - # Skip if bone has weights - if bone.name in used_bones: - continue - - # Remove the bone - edit_bones.remove(bone) - - - 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 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 - - vertex_groups = mesh_obj.vertex_groups - - groups_to_remove = set() - - for group in vertex_groups: - weights = get_vertex_weights(mesh_obj, group.name) - - if not any(weight > threshold for weight in weights.values()): - groups_to_remove.add(group.name) - - 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: - """Remove all constraints from the armature.""" - bpy.ops.object.mode_set(mode='POSE') - - for pose_bone in self.armature.pose.bones: - constraints_to_remove = [constraint for constraint in pose_bone.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: - bone_length = (bone.tail - bone.head).length - - if bone_length < min_length: - if bone.parent: - direction = bone.parent.tail - bone.parent.head - direction.normalize() - else: - direction = Vector((0, 0, 1)) - - bone.tail = bone.head + (direction * min_length) - - -class ReparentMeshesOperator(bpy.types.Operator): - bl_idname = "avatar_toolkit.reparent_meshes" - bl_label = t("MMD.reparent_meshes") - bl_description = t("MMD.reparent_meshes_desc") - bl_options = {'REGISTER', 'UNDO'} - - @classmethod - def poll(cls, context): - armature = get_active_armature(context) - return armature is not None and get_all_meshes(context) - - def execute(self, context): - armature = get_active_armature(context) - if not armature: - self.report({'ERROR'}, t("MMD.no_armature")) - return {'CANCELLED'} - - meshes = get_all_meshes(context) - if not meshes: - self.report({'ERROR'}, t("MMD.no_meshes")) - return {'CANCELLED'} - - try: - with ProgressTracker(context, len(meshes) + 1, "Reparenting Meshes") as progress: - # Get or create main collection - main_collection = self._get_main_collection(context) - progress.step("Setting up collections") - - # Process each mesh - for mesh in meshes: - progress.step(f"Processing {mesh.name}") - self._process_mesh(mesh, armature, main_collection) - - self.report({'INFO'}, t("MMD.reparenting_complete")) - return {'FINISHED'} - - except Exception as e: - logger.error(f"Error reparenting meshes: {str(e)}") - self.report({'ERROR'}, str(e)) - return {'CANCELLED'} - - def _get_main_collection(self, context) -> bpy.types.Collection: - """Get or create the main collection for the armature""" - if hasattr(context.scene, 'collection'): - return context.scene.collection - return context.scene.collection - - def _process_mesh(self, mesh: bpy.types.Object, - armature: bpy.types.Object, - main_collection: bpy.types.Collection) -> None: - """Process individual mesh parenting and collection management""" - # Unlink from other collections - for col in mesh.users_collection: - if col != main_collection: - col.objects.unlink(mesh) - - # Ensure mesh is in main collection - if mesh.name not in main_collection.objects: - main_collection.objects.link(mesh) - - # Set parent to armature - mesh.parent = armature - if not mesh.parent_type == 'ARMATURE': - mesh.parent_type = 'ARMATURE' - -class AVATAR_TOOLKIT_OT_ConvertMmdMorphs(Operator): - """Convert MMD morph data to shape keys""" - bl_idname = "avatar_toolkit.convert_mmd_morphs" - bl_label = t("MMD.convert_morphs") - 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'} - - try: - with ProgressTracker(context, 3, "Converting MMD Morphs") as progress: - # Convert bone morphs to shape keys - if hasattr(armature, 'mmd_root') and armature.mmd_root.bone_morphs: - self.process_bone_morphs(context, armature, progress) - - progress.step("Processed bone morphs") - - # Clean up unused data - self.cleanup_unused_data(context) - progress.step("Cleaned up data") - - # Validate results - self.validate_results(context) - progress.step("Validated results") - - self.report({'INFO'}, t("MMD.conversion_complete")) - return {'FINISHED'} - - except Exception as e: - logger.error(f"Error converting MMD morphs: {str(e)}") - self.report({'ERROR'}, str(e)) - return {'CANCELLED'} - - def process_bone_morphs(self, context, armature, progress): - """Process bone morphs into shape keys""" - for morph in armature.mmd_root.bone_morphs: - for mesh in get_all_meshes(context): - # Create armature modifier - mod = mesh.modifiers.new(morph.name, 'ARMATURE') - mod.object = armature - - # Apply as shape key - with context.temp_override(object=mesh): - bpy.ops.object.modifier_apply(modifier=mod.name) - -class AVATAR_TOOLKIT_OT_CleanupMmdModel(Operator): - """Clean up MMD model by removing unused data and fixing display settings""" - bl_idname = "avatar_toolkit.cleanup_mmd" - bl_label = t("MMD.cleanup") - bl_options = {'REGISTER', 'UNDO'} - - def execute(self, context): - armature = get_active_armature(context) - if not armature: - self.report({'ERROR'}, t("MMD.no_armature")) - return {'CANCELLED'} - - try: - with ProgressTracker(context, 4, "Cleaning MMD Model") as progress: - # Remove rigid bodies and joints - self.remove_physics_objects(armature) - progress.step("Removed physics objects") - - # Clean up collections and hierarchy - self.cleanup_hierarchy(context, armature) - progress.step("Cleaned hierarchy") - - # Fix viewport settings - self.fix_viewport_settings(context) - progress.step("Fixed viewport") - - # Final cleanup - clear_unused_data_blocks() - progress.step("Cleared unused data") - - self.report({'INFO'}, t("MMD.cleanup_complete")) - return {'FINISHED'} - - except Exception as e: - logger.error(f"Error cleaning MMD model: {str(e)}") - self.report({'ERROR'}, str(e)) - return {'CANCELLED'} - - def remove_physics_objects(self, armature): - """Remove physics-related objects""" - to_delete = [] - for child in armature.children: - if any(x in child.name.lower() for x in ['rigidbodies', 'joints', 'physics']): - to_delete.append(child) - - for obj in to_delete: - bpy.data.objects.remove(obj, do_unlink=True) - - def cleanup_hierarchy(self, context, armature): - """Clean up object hierarchy and collections""" - meshes = get_all_meshes(context) - for mesh in meshes: - # Ensure proper parenting - mesh.parent = armature - mesh.parent_type = 'ARMATURE' - - # Clean up collections - for col in mesh.users_collection: - if col != context.scene.collection: - col.objects.unlink(mesh) - - if mesh.name not in context.scene.collection.objects: - context.scene.collection.objects.link(mesh) - - def fix_viewport_settings(self, context): - """Fix viewport display settings""" - # Set armature display - armature = get_active_armature(context) - armature.data.display_type = 'OCTAHEDRAL' - armature.show_in_front = True - - # Set viewport shading - for area in context.screen.areas: - if area.type == 'VIEW_3D': - space = area.spaces[0] - space.shading.type = 'MATERIAL' - space.clip_start = 0.01 - space.clip_end = 300 - -class AVATAR_TOOLKIT_OT_FixMeshes(Operator): - """Clean up and optimize mesh materials, shading, and shape keys""" - bl_idname = "avatar_toolkit.fix_meshes" - bl_label = t("Optimization.fix_meshes") - 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): - try: - meshes = get_all_meshes(context) - if not meshes: - self.report({'ERROR'}, t("Optimization.no_meshes")) - return {'CANCELLED'} - - with ProgressTracker(context, len(meshes), "Fixing Meshes") as progress: - for mesh in meshes: - self.process_mesh(context, mesh) - progress.step(f"Processed {mesh.name}") - - self.report({'INFO'}, t("Optimization.meshes_fixed")) - return {'FINISHED'} - - except Exception as e: - logger.error(f"Error fixing meshes: {str(e)}") - self.report({'ERROR'}, str(e)) - return {'CANCELLED'} - - def process_mesh(self, context: Context, mesh: Object) -> None: - """Process and fix individual mesh""" - # Unlock transforms - for i in range(3): - mesh.lock_location[i] = False - mesh.lock_rotation[i] = False - mesh.lock_scale[i] = False - - # Process shape keys - if mesh.data.shape_keys: - self.fix_shape_keys(mesh) - - # Process materials - self.fix_materials(context, mesh) - - def fix_shape_keys(self, mesh: Object) -> None: - """Fix and clean up shape keys""" - if not mesh.data.shape_keys: - return - - shape_keys = mesh.data.shape_keys.key_blocks - - # Rename basis - if shape_keys[0].name != "Basis": - shape_keys[0].name = "Basis" - - # Clean up names - for key in shape_keys: - # Remove common prefixes/suffixes - clean_name = key.name - for prefix in ['Face.M F00 000 Fcl ', 'Face.M F00 000 00 Fcl ']: - clean_name = clean_name.replace(prefix, '') - - # Replace underscores with spaces - clean_name = clean_name.replace('_', ' ') - key.name = clean_name - - # Sort shape keys by category - categories = ['MTH', 'EYE', 'BRW', 'ALL'] - - # Create sorted list of shape key names - ordered_names = [] - - # Add categorized keys first - for category in categories: - category_keys = [key.name for key in shape_keys if key.name.startswith(category)] - ordered_names.extend(sorted(category_keys)) - - # Add remaining keys - remaining = [key.name for key in shape_keys if not any(key.name.startswith(c) for c in categories)] - ordered_names.extend(sorted(remaining)) - - # Reorder using context override - with bpy.context.temp_override(active_object=mesh, selected_objects=[mesh]): - for idx, name in enumerate(ordered_names): - mesh.active_shape_key_index = shape_keys.find(name) - while mesh.active_shape_key_index > idx: - bpy.ops.object.shape_key_move(type='UP') - - - def fix_materials(self, context: Context, mesh: Object) -> None: - """Fix and optimize materials""" - for slot in mesh.material_slots: - if not slot.material: - continue - - material = slot.material - - # Set up basic material properties - material.use_backface_culling = True - material.blend_method = 'HASHED' - material.shadow_method = 'HASHED' - - # Clean up material name - material.name = self.clean_material_name(material.name) - - # Consolidate similar materials - for other_slot in mesh.material_slots: - if other_slot.material and other_slot.material != material: - if materials_match(material, other_slot.material): - other_slot.material = material - - def clean_material_name(self, name: str) -> str: - """Clean up material name""" - # Remove common prefixes/suffixes - prefixes = ['material', 'mat', 'mtl', 'material.'] - for prefix in prefixes: - if name.lower().startswith(prefix): - name = name[len(prefix):] - - # Remove numbers at end - while name and name[-1].isdigit(): - name = name[:-1] - - return name.strip() - -class AVATAR_TOOLKIT_OT_ValidateMeshes(Operator): - """Validate meshes and UV maps for common issues""" - bl_idname = "avatar_toolkit.validate_meshes" - bl_label = t("Validation.check_meshes") - bl_options = {'REGISTER', 'UNDO'} - - def execute(self, context): - armature = get_active_armature(context) - if not armature: - self.report({'ERROR'}, t("Validation.no_armature")) - return {'CANCELLED'} - - try: - with ProgressTracker(context, 3, "Validating Meshes") as progress: - # Check bone hierarchy - hierarchy_issues = self.validate_bone_hierarchy(armature) - progress.step("Checked bone hierarchy") - - # Check UV coordinates - uv_issues = self.validate_uv_maps(context) - progress.step("Checked UV maps") - - # Generate report - self.generate_validation_report(context, hierarchy_issues, uv_issues) - progress.step("Generated report") - - return {'FINISHED'} - - except Exception as e: - logger.error(f"Error validating meshes: {str(e)}") - self.report({'ERROR'}, str(e)) - return {'CANCELLED'} - - def validate_bone_hierarchy(self, armature: Object) -> List[str]: - """Validate bone hierarchy against standard structure""" - issues = [] - - # Define expected hierarchy - hierarchy = [ - ['hips', 'spine', 'chest', 'neck', 'head'], - ['hips', 'left_leg', 'left_knee', 'left_ankle'], - ['hips', 'right_leg', 'right_knee', 'right_ankle'], - ['chest', 'left_shoulder', 'left_arm', 'left_elbow', 'left_wrist'], - ['chest', 'right_shoulder', 'right_arm', 'right_elbow', 'right_wrist'] - ] - - for chain in hierarchy: - previous = None - for bone_name in chain: - # Check if bone exists - bone = None - for alt_name in bone_names[bone_name]: - if alt_name in armature.data.bones: - bone = armature.data.bones[alt_name] - break - - if not bone: - issues.append(t("Validation.missing_bone", bone=bone_name)) - continue - - # Check parent relationship - if previous: - if not bone.parent: - issues.append(t("Validation.no_parent", bone=bone.name)) - elif bone.parent.name != previous.name: - issues.append(t("Validation.wrong_parent", - bone=bone.name, - expected=previous.name, - actual=bone.parent.name)) - previous = bone - - return issues - - def validate_uv_maps(self, context: Context) -> Dict[str, int]: - """Check UV maps for issues""" - issues = {'nan_coords': 0, 'missing_uvs': 0} - - for mesh in get_all_meshes(context): - if not mesh.data.uv_layers: - issues['missing_uvs'] += 1 - continue - - for uv_layer in mesh.data.uv_layers: - for uv in uv_layer.data: - if math.isnan(uv.uv.x): - uv.uv.x = 0 - issues['nan_coords'] += 1 - if math.isnan(uv.uv.y): - uv.uv.y = 0 - issues['nan_coords'] += 1 - - return issues - - def generate_validation_report(self, context: Context, - hierarchy_issues: List[str], - uv_issues: Dict[str, int]) -> None: - """Generate and display validation report""" - report_lines = [] - - # Add hierarchy issues - if hierarchy_issues: - report_lines.append(t("Validation.hierarchy_issues")) - report_lines.extend(hierarchy_issues) - - # Add UV issues - if uv_issues['nan_coords'] > 0: - report_lines.append(t("Validation.uv_nan_coords", - count=uv_issues['nan_coords'])) - - if uv_issues['missing_uvs'] > 0: - report_lines.append(t("Validation.missing_uvs", - count=uv_issues['missing_uvs'])) - - # Show report - if report_lines: - self.report({'WARNING'}, "\n".join(report_lines)) - else: - self.report({'INFO'}, t("Validation.no_issues")) diff --git a/functions/visemes.py b/functions/visemes.py index 3492f0e..da332bc 100644 --- a/functions/visemes.py +++ b/functions/visemes.py @@ -35,6 +35,7 @@ class VisemePreview: _preview_data: Dict[str, float] = {} _active: bool = False _preview_shapes: Optional[OrderedDict] = None + _mesh_name: str = "" @classmethod def start_preview(cls, context: Context, mesh: Object, shapes: List[str]) -> bool: @@ -43,6 +44,7 @@ class VisemePreview: cls._active = True cls._preview_data = {} + cls._mesh_name = mesh.name # Store original values for shape_key in mesh.data.shape_keys.key_blocks: @@ -79,7 +81,11 @@ class VisemePreview: if not cls._active or not cls._preview_shapes: return - mesh = context.active_object + # Get the mesh by name instead of using active object + mesh = bpy.data.objects.get(cls._mesh_name) + if not mesh: + return + props = context.scene.avatar_toolkit viseme_data = cls._preview_shapes.get(props.viseme_preview_selection) if viseme_data: @@ -116,6 +122,7 @@ class VisemePreview: cls._active = False cls._preview_data.clear() cls._preview_shapes = None + cls._mesh_name = "" class ATOOLKIT_OT_preview_visemes(Operator): """Operator for previewing viseme shapes in real-time""" @@ -126,7 +133,6 @@ class ATOOLKIT_OT_preview_visemes(Operator): @classmethod def poll(cls, context: Context) -> bool: - # Check if we're in object mode if context.mode != 'OBJECT': return False @@ -143,13 +149,13 @@ class ATOOLKIT_OT_preview_visemes(Operator): def execute(self, context: Context) -> Set[str]: props = context.scene.avatar_toolkit - mesh = context.active_object + mesh = bpy.data.objects.get(props.viseme_mesh) if props.viseme_preview_mode: VisemePreview.end_preview(mesh) props.viseme_preview_mode = False else: - if not mesh.data.shape_keys: + if not mesh or not mesh.data.shape_keys: self.report({'ERROR'}, t("Visemes.error.no_shapekeys")) return {'CANCELLED'} @@ -199,12 +205,11 @@ class ATOOLKIT_OT_create_visemes(Operator): valid, _, _ = validate_armature(armature) return valid and mesh_obj and mesh_obj.type == 'MESH' - def execute(self, context: Context) -> Set[str]: props = context.scene.avatar_toolkit - mesh = context.active_object + mesh = bpy.data.objects.get(props.viseme_mesh) # Changed from context.active_object - if not mesh.data.shape_keys: + if not mesh or not mesh.data.shape_keys: self.report({'ERROR'}, t("Visemes.error.no_shapekeys")) return {'CANCELLED'} @@ -279,7 +284,7 @@ class ATOOLKIT_OT_create_visemes(Operator): continue # Create new shape key - self.mix_shapekey(context, renamed_shapes, data['mix'], key) + self.mix_shapekey(context, renamed_shapes, data['mix'], key, mesh) # Added mesh parameter # Cache the new shape key data shape_data = [v.co.copy() for v in mesh.data.shape_keys.key_blocks[key].data] @@ -292,14 +297,16 @@ class ATOOLKIT_OT_create_visemes(Operator): mesh.active_shape_key_index = 0 wm.progress_end() - def mix_shapekey(self, context: Context, shapes: List[str], mix_data: List, new_name: str) -> None: + def mix_shapekey(self, context: Context, shapes: List[str], mix_data: List, new_name: str, mesh: Object) -> None: # Added mesh parameter """Creates a new shape key by mixing existing ones""" - mesh = context.active_object # Remove existing shape key if it exists if new_name in mesh.data.shape_keys.key_blocks: mesh.active_shape_key_index = mesh.data.shape_keys.key_blocks.find(new_name) + old_active = context.view_layer.objects.active + context.view_layer.objects.active = mesh bpy.ops.object.shape_key_remove() + context.view_layer.objects.active = old_active # Reset all shape keys for shapekey in mesh.data.shape_keys.key_blocks: @@ -312,7 +319,10 @@ class ATOOLKIT_OT_create_visemes(Operator): shapekey.value = value # Create mixed shape key + old_active = context.view_layer.objects.active + context.view_layer.objects.active = mesh mesh.shape_key_add(name=new_name, from_mix=True) + context.view_layer.objects.active = old_active # Reset values and restore shape key settings for shapekey in mesh.data.shape_keys.key_blocks: @@ -355,3 +365,4 @@ class ATOOLKIT_OT_create_visemes(Operator): props.mouth_a = current_names[0] props.mouth_o = current_names[1] props.mouth_ch = current_names[2] + diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index 388e8bd..6721c80 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -1,7 +1,7 @@ { "authors": ["Avatar Toolkit Team"], "messages": { - "AvatarToolkit.label": "Avatar Toolkit (Alpha 0.1.1)", + "AvatarToolkit.label": "Avatar Toolkit (Alpha 0.2.0)", "AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there", "AvatarToolkit.desc2": "will be issues, if you find any issues,", "AvatarToolkit.desc3": "please report it on our Github.", @@ -72,6 +72,13 @@ "Armature.validation.asymmetric_hand_wrist": "Missing symmetric bones for hands/wrists", "Armature.validation.found_bones": "Found bones in armature:\n- {bones}", "Armature.validation.non_standard_bones": "Non-standard bones found:\n- {bones}", + "Armature.validation.accessory_bones_note.line1": "If you have hair bones, skirt bones, or other", + "Armature.validation.accessory_bones_note.line2": "accessorybones named similarly to main armature", + "Armature.validation.accessory_bones_note.line3": "bones (e.g., Head1, Head2), please rename them to", + "Armature.validation.accessory_bones_note.line4": "more descriptive names like Hair_1, Skirt_1.", + "Armature.validation.standardize_note.line1": "You can standardize your armature", + "Armature.validation.standardize_note.line2": "automatically by using the 'Standardize Armature'", + "Armature.validation.standardize_note.line3": "button in the Tools section.", "Validation.section.found_bones": "Found Bones", "Validation.section.non_standard": "Non-Standard Bones", "Validation.section.hierarchy": "Hierarchy Issues", @@ -392,10 +399,15 @@ "EyeTracking.sdk_version": "SDK Version", "EyeTracking.type.av3": "Avatar 3.0", "EyeTracking.type.av3_desc": "VRChat Avatar 3.0 eye tracking setup", - "EyeTracking.type.sdk2": "SDK2 (Legacy)", - "EyeTracking.type.sdk2_desc": "VRChat SDK2 eye tracking setup", + "EyeTracking.type.sdk2": "Legacy (ChilloutVR", + "EyeTracking.type.sdk2_desc": "Legacy (SDK2) eye tracking setup", "EyeTracking.adjust.label": "Adjust Eye Position", "EyeTracking.adjust.desc": "Adjust the position of eye bones based on vertex groups", + "EyeTracking.sdk2_warning": "Legacy (SDK2) Eye Tracking Notice", + "EyeTracking.sdk2_warning_detail1": "This system SHOULD NOT BE USED FOR VRChat,", + "EyeTracking.sdk2_warning_detail2": "as eye tracking is now configured directly", + "EyeTracking.sdk2_warning_detail3": "in Unity. It remains for other platforms.", + "EyeTracking.sdk2_warning_detail4": "like ChilloutVR.", "CustomPanel.label": "Custom Avatar Tools", "CustomPanel.merge_mode": "Merge Mode", diff --git a/ui/eye_tracking_panel.py b/ui/eye_tracking_panel.py index 1178bc9..cf7b6c4 100644 --- a/ui/eye_tracking_panel.py +++ b/ui/eye_tracking_panel.py @@ -43,6 +43,16 @@ class AvatarToolKit_PT_EyeTrackingPanel(Panel): row.prop(toolkit, "eye_tracking_type", expand=True) if toolkit.eye_tracking_type == 'SDK2': + # SDK2 Warning Box + warning_box: UILayout = layout.box() + col: UILayout = warning_box.column(align=True) + col.label(text=t("EyeTracking.sdk2_warning"), icon='INFO') + col.separator(factor=0.5) + col.label(text=t("EyeTracking.sdk2_warning_detail1")) + col.label(text=t("EyeTracking.sdk2_warning_detail2")) + col.label(text=t("EyeTracking.sdk2_warning_detail3")) + col.label(text=t("EyeTracking.sdk2_warning_detail4")) + # Mode Selection Box mode_box: UILayout = layout.box() col: UILayout = mode_box.column(align=True) diff --git a/ui/mmd_panel.py b/ui/mmd_panel.py deleted file mode 100644 index d7333dc..0000000 --- a/ui/mmd_panel.py +++ /dev/null @@ -1,49 +0,0 @@ -# MMD Tools disabled for the time being unto it can be fixed. - -# import bpy -# from typing import Set -# 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 bone standardization and cleanup 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 = 3 -# bl_options = {'DEFAULT_CLOSED'} - -# def draw(self, context: Context) -> None: -# layout: UILayout = self.layout -# toolkit = context.scene.avatar_toolkit - - # Bone Settings Box -# bone_box: UILayout = layout.box() -# col: UILayout = bone_box.column(align=True) -# col.label(text=t("MMD.bone_settings"), icon='BONE_DATA') -# col.separator(factor=0.5) -# col.prop(toolkit, "keep_twist_bones") -# col.prop(toolkit, "keep_upper_chest") -# col.operator("avatar_toolkit.standardize_mmd", icon='BONE_DATA') - - # Mesh Tools Box -# mesh_box: UILayout = layout.box() -# col = mesh_box.column(align=True) -# col.label(text=t("MMD.mesh_tools"), icon='MESH_DATA') -# col.separator(factor=0.5) -# row: UILayout = col.row(align=True) -# row.operator("avatar_toolkit.fix_meshes", icon='MODIFIER') -# row.operator("avatar_toolkit.validate_meshes", icon='CHECKMARK') - - # 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.cleanup_mmd", icon='SHADERFX') -# col.operator("avatar_toolkit.convert_mmd_morphs", icon='SHAPEKEY_DATA') -# col.operator("avatar_toolkit.reparent_meshes", icon='OUTLINER_OB_ARMATURE') diff --git a/wheels/lz4-4.3.3-cp311-cp311-macosx_10_9_x86_64.whl b/wheels/lz4-4.3.3-cp311-cp311-macosx_10_9_x86_64.whl new file mode 100644 index 0000000..992aee7 Binary files /dev/null and b/wheels/lz4-4.3.3-cp311-cp311-macosx_10_9_x86_64.whl differ diff --git a/wheels/lz4-4.3.3-cp311-cp311-macosx_11_0_arm64.whl b/wheels/lz4-4.3.3-cp311-cp311-macosx_11_0_arm64.whl new file mode 100644 index 0000000..373420e Binary files /dev/null and b/wheels/lz4-4.3.3-cp311-cp311-macosx_11_0_arm64.whl differ diff --git a/wheels/lz4-4.3.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl b/wheels/lz4-4.3.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl new file mode 100644 index 0000000..cdc8e27 Binary files /dev/null and b/wheels/lz4-4.3.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl differ diff --git a/wheels/lz4-4.3.3-cp311-cp311-win_amd64.whl b/wheels/lz4-4.3.3-cp311-cp311-win_amd64.whl new file mode 100644 index 0000000..a2d06a8 Binary files /dev/null and b/wheels/lz4-4.3.3-cp311-cp311-win_amd64.whl differ diff --git a/wheels/lz4-4.4.3-cp313-cp313-macosx_11_0_arm64.whl b/wheels/lz4-4.4.3-cp313-cp313-macosx_11_0_arm64.whl deleted file mode 100644 index f2674c5..0000000 Binary files a/wheels/lz4-4.4.3-cp313-cp313-macosx_11_0_arm64.whl and /dev/null differ diff --git a/wheels/lz4-4.4.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl b/wheels/lz4-4.4.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl deleted file mode 100644 index f1e3494..0000000 Binary files a/wheels/lz4-4.4.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl and /dev/null differ diff --git a/wheels/lz4-4.4.3-cp313-cp313-win_amd64.whl b/wheels/lz4-4.4.3-cp313-cp313-win_amd64.whl deleted file mode 100644 index cd35104..0000000 Binary files a/wheels/lz4-4.4.3-cp313-cp313-win_amd64.whl and /dev/null differ