From f28e1866a9655e6afb93073df1347299141bc295 Mon Sep 17 00:00:00 2001 From: 989onan Date: Thu, 3 Apr 2025 02:45:39 -0400 Subject: [PATCH] Add AMFOWSH Add apply modifier for object with shapekeys tool --- core/common.py | 12 ++ .../custom_tools/force_apply_modifier.py | 139 ++++++++++++++++++ resources/translations/en_US.json | 2 + ui/tools_panel.py | 2 + 4 files changed, 155 insertions(+) create mode 100644 functions/custom_tools/force_apply_modifier.py diff --git a/core/common.py b/core/common.py index 4e563d5..e0093e9 100644 --- a/core/common.py +++ b/core/common.py @@ -535,6 +535,18 @@ def add_armature_modifier(mesh: Object, armature: Object) -> None: modifier: Modifier = mesh.modifiers.new('Armature', 'ARMATURE') modifier.object = armature +def get_modifiers(self: Optional[Any] = None, context: Optional[Context] = None) -> List[Tuple[str, str, str]]: + returned: List[Tuple[str, str, str]] = [] + if context.active_object == None: + return returned + if context.active_object.type != "MESH": + return returned + for mod in context.active_object.modifiers: + returned.append((mod.name,mod.name,"")) + + return returned + + def get_shapekeys(context: Context, names: List[str], is_mouth: bool, diff --git a/functions/custom_tools/force_apply_modifier.py b/functions/custom_tools/force_apply_modifier.py new file mode 100644 index 0000000..2d478ab --- /dev/null +++ b/functions/custom_tools/force_apply_modifier.py @@ -0,0 +1,139 @@ +import traceback +import bpy +import re +from typing import Any, Set, Dict, List, Optional, Tuple +from bpy.types import ( + Operator, + Context, + Object, + Material, + NodeTree, + ShaderNodeTexImage +) +import mathutils +import bmesh +from ...core.logging_setup import logger +from ...core.translations import t +from ...core.common import ( + get_active_armature, + get_all_meshes, + ProgressTracker, + calculate_bone_orientation, + add_armature_modifier, + get_modifiers, + has_shapekeys +) +from ...core.armature_validation import validate_armature + +class AvatarToolkit_OT_ApplyModifierForShapkeyObj(bpy.types.Operator): + """Operator for forcing the application of a modifier. A shortened way of saying \"Apply modifier for object with shapekeys\"""" + bl_idname: str = 'avatar_toolkit.merge_armatures' + bl_label: str = t('Tools.apply_modifier_on_shapekey_obj') + bl_description: str = t('Tools.apply_modifier_on_shapekey_obj_desc') + bl_options: Set[str] = {'REGISTER', 'UNDO'} + + modifier: bpy.props.EnumProperty(items=get_modifiers,name="Modifier To Apply") + + + def draw(self, context: Context) -> None: + """Draw the operator's UI""" + layout = self.layout + layout.prop(self, "modifier") + + def invoke(self, context: Context, event: bpy.types.Event) -> set[str]: + """Initialize the operator""" + return context.window_manager.invoke_props_dialog(self) + + @classmethod + def poll(cls, context: Context) -> bool: + if context.active_object != None: + return context.active_object.type == "MESH" + return False + + def execute(self, context: Context) -> Set[str]: + + obj: bpy.types.Object = context.active_object + mesh: bpy.types.Mesh = obj.data + + shapes: list[bpy.types.Object] = [] + + bpy.ops.object.mode_set(mode="OBJECT") + + if has_shapekeys(obj): + #reset shapekeys + for idx,key in enumerate(mesh.shape_keys.key_blocks): + obj.active_shape_key_index = idx + obj.active_shape_key.value = 0 + + for idx,key in enumerate(mesh.shape_keys.key_blocks): + # duplicate object for shapekey + bpy.ops.object.select_all(action="DESELECT") + context.view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.object.duplicate() + + # name new object after shapekey + new_obj = context.view_layer.objects.active + new_obj.select_set(True) + new_obj.active_shape_key_index = idx + new_obj.name = new_obj.active_shape_key.name + + #add to cleanup list + shapes.append(new_obj) + + #make basis the same shape as shapekey + for idx,point in enumerate(new_obj.active_shape_key.points): + new_obj.data.vertices[idx].co.xyz = point.co.xyz + + #remove all shaoekeys on new object and then apply modifier + bpy.ops.object.shape_key_remove(all=True,apply_mix=False) + try: + bpy.ops.object.modifier_apply(modifier=self.modifier) + except Exception as e: + self.report({'ERROR'}, f"Shapekey modifier apply for shapekey \"{new_obj.name}\" failed!!") + print(f"Shapekey modifier apply for shapekey \"{new_obj.name}\" failed!!") + print(traceback.format_exc(e)) + #clean up after critical failure + for shape in shapes: + bpy.data.objects.remove(shape)#faster than ops delete + bpy.ops.object.select_all(action="DESELECT") + + + + try: + #remove shapekeys on original object + bpy.ops.object.select_all(action="DESELECT") + obj.select_set(True) + context.view_layer.objects.active = obj + bpy.ops.object.shape_key_remove(all=True,apply_mix=False) + bpy.ops.object.modifier_apply(modifier=self.modifier) + bpy.ops.object.select_all(action="DESELECT") + #delete first shapekey object aka basis + bpy.data.objects.remove(shapes.pop(0)) + + #join all objects with applied modifiers back together as shapes + for shape in shapes: + shape.select_set(True) + obj.select_set(True) + context.view_layer.objects.active = obj + bpy.ops.object.join_shapes() + except Exception as e: + + self.report({'ERROR'}, f"Shapekey joining failed!!") + print(f"Shapekey joining failed!!") + print(traceback.format_exc(e)) + #clean up after critical failure + for shape in shapes: + bpy.data.objects.remove(shape)#faster than ops delete + + #final clean up + for shape in shapes: + bpy.data.objects.remove(shape)#faster than ops delete + + else: + #mesh has no shapekeys, just apply normally. + bpy.ops.object.modifier_apply(modifier=self.modifier) + + + + return {'FINISHED'} \ No newline at end of file diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index 46771cd..2c642d6 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -219,6 +219,8 @@ "Tools.clean_weights_threshold_desc": "Minimum weight value to consider a bone as weighted", "Tools.find_shortest_seam_path": "Find Shortest Seam Path", "Tools.find_shortest_seam_path_desc": "Find shortest path of seams between two selected vertices connected to seams.", + "Tools.apply_modifier_on_shapekey_obj":"Apply Modifier on Shapekey Object", + "Tools.apply_modifier_on_shapekey_obj_desc":"Applies a modifier on an object regardless of it having shapekeys.", "Tools.merge_title": "Merge Tools", "Tools.merge_to_active": "Merge to Active", "Tools.merge_to_active_desc": "Merge selected bones to active bone", diff --git a/ui/tools_panel.py b/ui/tools_panel.py index 901bc9c..b8aa933 100644 --- a/ui/tools_panel.py +++ b/ui/tools_panel.py @@ -19,6 +19,7 @@ from ..functions.tools.standardize_armature import AvatarToolkit_OT_StandardizeA from ..functions.tools.merge_tools import AvatarToolkit_OT_MergeToActive, AvatarToolkit_OT_MergeToParent, AvatarToolkit_OT_ConnectBones from ..functions.tools.rigify_converter import AvatarToolkit_OT_ConvertRigifyToUnity from ..functions.tools.general_mesh_tools import AvatarToolkit_OT_SelectShortestSeamPath +from ..functions.custom_tools.force_apply_modifier import AvatarToolkit_OT_ApplyModifierForShapkeyObj class AvatarToolKit_PT_ToolsPanel(Panel): """Panel containing various tools for avatar customization and optimization""" @@ -66,6 +67,7 @@ class AvatarToolKit_PT_ToolsPanel(Panel): col.label(text=t("Tools.mesh_title"), icon='MESH_DATA') col.separator(factor=0.5) col.operator(AvatarToolkit_OT_SelectShortestSeamPath.bl_idname,text=t("Tools.find_shortest_seam_path"),icon="MESH_DATA") + col.operator(AvatarToolkit_OT_ApplyModifierForShapkeyObj.bl_idname,text=t("Tools.apply_modifier_on_shapekey_obj"),icon="SHAPEKEY_DATA") # Standardization Tools