From 9bdaa1ef017d2a5371a899eabf886426509bc7fd Mon Sep 17 00:00:00 2001 From: 989onan Date: Mon, 9 Sep 2024 21:57:13 -0400 Subject: [PATCH] Add Remove unused shapekeys removes shapekeys that don't do anything --- functions/mesh_tools.py | 56 +++++++++++++++++++++++++++++++ resources/translations/en_US.json | 4 +++ ui/tools.py | 2 ++ 3 files changed, 62 insertions(+) create mode 100644 functions/mesh_tools.py 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 0317753..1af48d9 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -132,6 +132,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", "VisemePanel.create_visemes": "Create Visemes", "VisemePanel.creating_viseme": "Creating viseme: {viseme_name}", "VisemePanel.creating_viseme_detail": "Creating viseme: {viseme_name}", diff --git a/ui/tools.py b/ui/tools.py index 62dde6c..4016d06 100644 --- a/ui/tools.py +++ b/ui/tools.py @@ -7,6 +7,7 @@ from ..functions.translations import t from ..core.common import get_selected_armature from ..functions.seperate_by import SeparateByMaterials, SeparateByLooseParts from ..functions.additional_tools import ApplyTransforms +from ..functions.mesh_tools import AvatarToolkit_OT_RemoveUnusedShapekeys @register_wrap class AvatarToolkitToolsPanel(bpy.types.Panel): @@ -38,5 +39,6 @@ class AvatarToolkitToolsPanel(bpy.types.Panel): row.operator(SeparateByLooseParts.bl_idname, text=t("Tools.separate_by_loose_parts.label"), icon='OUTLINER_OB_MESH') row = layout.row(align=True) row.operator(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') else: layout.label(text=t("Tools.select_armature"), icon='ERROR')