diff --git a/functions/mesh_tools.py b/functions/mesh_tools.py new file mode 100644 index 0000000..c0e2a23 --- /dev/null +++ b/functions/mesh_tools.py @@ -0,0 +1,56 @@ +import numpy as np +import bpy +from bpy.types import Context +from ..core.common import get_selected_armature, get_all_meshes, is_valid_armature +from ..functions.translations import t +from ..core.register import register_wrap + +@register_wrap +class AvatarToolkit_OT_RemoveUnusedShapekeys(bpy.types.Operators): + tolerance: bpy.props.FloatProperty(name=t("Tools.remove_unused_shapekeys.tolerance.label"), default=0.001, description=t("Tools.remove_unused_shapekeys.tolerance.desc")) + bl_idname = "avatar_toolkit.remove_unused_shapekeys" + bl_label = t("Tools.remove_unused_shapekeys.label") + bl_description = t("Tools.remove_unused_shapekeys.desc") + bl_options = {'REGISTER', 'UNDO'} + + + @classmethod + def poll(cls, context: Context) -> bool: + armature = get_selected_armature(context) + return armature is not None and is_valid_armature(armature) and (len(get_all_meshes(context)) > 0) and (context.mode == "OBJECT") + + def execute(self, context: Context) -> set[str]: + #Shamefully taken from: https://blender.stackexchange.com/a/237611 + #at least I am crediting them - @989onan + for ob in get_all_meshes(context): + if not ob.data.shape_keys: continue + if not ob.data.shape_keys.use_relative: continue + + kbs = ob.data.shape_keys.key_blocks + nverts = len(ob.data.vertices) + to_delete = [] + + # Cache locs for rel keys since many keys have the same rel key + cache = {} + + locs = np.empty(3*nverts, dtype=np.float32) + + for kb in kbs: + if kb == kb.relative_key: continue + + kb.data.foreach_get("co", locs) + + if kb.relative_key.name not in cache: + rel_locs = np.empty(3*nverts, dtype=np.float32) + kb.relative_key.data.foreach_get("co", rel_locs) + cache[kb.relative_key.name] = rel_locs + rel_locs = cache[kb.relative_key.name] + + locs -= rel_locs + if (np.abs(locs) < self.tolerance).all(): + to_delete.append(kb.name) + + for kb_name in to_delete: + if ("-" in kb_name) or ("=" in kb_name) or ("~" in kb_name): #don't delete category names. - @989onan + continue + ob.shape_key_remove(ob.data.shape_keys.key_blocks[kb_name]) \ No newline at end of file diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index 67e268c..7358d95 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -145,6 +145,10 @@ "Tools.apply_transforms.desc": "Apply position, rotation, and scale to the armature and its meshes", "Tools.apply_transforms.invalid_armature": "Invalid armature selected", "Tools.apply_transforms.success": "Transforms applied successfully to armature and meshes", + "Tools.remove_unused_shapekeys.label": "Remove Unused Shapekeys", + "Tools.remove_unused_shapekeys.tolerance.desc": "Min movement for position on any coordinate\n for any vertex for a shapekey to be kept.", + "Tools.remove_unused_shapekeys.desc": "Remove shapekeys that don't move anything.\nDoesn't get rid of category shapekeys.\n(ex: has \"~\", \"-\", or \"=\" in the name.)", + "Tools.remove_unused_shapekeys.tolerance.label": "Position Tolerance", "Tools.remove_zero_weight_bones.success": "Zero weight bones removed successfully", "Tools.remove_zero_weight_bones.label": "Remove Zero Weight Bones", "Tools.remove_zero_weight_bones.desc": "Remove bones from the armature that have weights less than threshold.", diff --git a/ui/tools.py b/ui/tools.py index caec2c2..05f3fa9 100644 --- a/ui/tools.py +++ b/ui/tools.py @@ -6,6 +6,7 @@ from ..functions.digitigrade_legs import AvatarToolKit_OT_CreateDigitigradeLegs from ..functions.resonite_functions import AvatarToolKit_OT_ConvertToResonite from ..functions.translations import t from ..core.common import get_selected_armature +from ..functions.mesh_tools import AvatarToolkit_OT_RemoveUnusedShapekeys from ..functions.seperate_by import AvatarToolKit_OT_SeparateByMaterials, AvatarToolKit_OT_SeparateByLooseParts from ..functions.additional_tools import AvatarToolKit_OT_ApplyTransforms from ..functions.armature_modifying import AvatarToolkit_OT_RemoveZeroWeightBones, AvatarToolkit_OT_MergeBonesToActive, AvatarToolkit_OT_MergeBonesToParents @@ -40,6 +41,7 @@ class AvatarToolkit_PT_ToolsPanel(bpy.types.Panel): row.operator(AvatarToolKit_OT_SeparateByLooseParts.bl_idname, text=t("Tools.separate_by_loose_parts.label"), icon='OUTLINER_OB_MESH') row = layout.row(align=True) row.operator(AvatarToolKit_OT_ApplyTransforms.bl_idname, text=t("Tools.apply_transforms.label"), icon='OBJECT_ORIGIN') + row.operator(AvatarToolkit_OT_RemoveUnusedShapekeys.bl_idname, text=t("Tools.remove_unused_shapekeys.label"), icon='SHAPEKEY_DATA') row = layout.row(align=True) row.operator(AvatarToolkit_OT_RemoveZeroWeightBones.bl_idname, text=t("Tools.remove_zero_weight_bones.label"), icon='BONE_DATA') row = layout.row(align=True)