From f60f8f930e9344efd22ad9549299238390cc8dd4 Mon Sep 17 00:00:00 2001 From: Demeter Dzadik Date: Wed, 26 Jun 2024 21:21:56 +0200 Subject: [PATCH 01/10] Pose Shape Keys: Update to an Extension - Add extension .toml file. - Remove changelog.md. - Cleanup short variable names. - Add poll messages. - Replace reset_rig.py with reset_rig() from cloudrig.py. - Use PEP 585 for typing where there was any typing. --- .../addons/easy_weight/force_apply_mirror.py | 4 +- .../addons/easy_weight/toggle_weight_paint.py | 2 +- .../addons/pose_shape_keys/CHANGELOG.md | 19 - .../addons/pose_shape_keys/__init__.py | 25 +- .../pose_shape_keys/blender_manifest.toml | 19 + .../addons/pose_shape_keys/pose_key.py | 362 +++++++++++------- .../addons/pose_shape_keys/prefs.py | 13 +- .../addons/pose_shape_keys/reset_rig.py | 76 ---- .../pose_shape_keys/symmetrize_shape_key.py | 29 +- scripts-blender/addons/pose_shape_keys/ui.py | 35 +- 10 files changed, 285 insertions(+), 299 deletions(-) delete mode 100644 scripts-blender/addons/pose_shape_keys/CHANGELOG.md create mode 100644 scripts-blender/addons/pose_shape_keys/blender_manifest.toml delete mode 100644 scripts-blender/addons/pose_shape_keys/reset_rig.py diff --git a/scripts-blender/addons/easy_weight/force_apply_mirror.py b/scripts-blender/addons/easy_weight/force_apply_mirror.py index d4a428ab..fb000311 100644 --- a/scripts-blender/addons/easy_weight/force_apply_mirror.py +++ b/scripts-blender/addons/easy_weight/force_apply_mirror.py @@ -46,13 +46,13 @@ class EASYWEIGHT_OT_force_apply_mirror(bpy.types.Operator): def poll(cls, context): obj = context.active_object if not obj or obj.type != 'MESH': - cls.set_poll_message("There must be an active mesh object deformed by an Armature.") + cls.poll_message_set("There must be an active mesh object deformed by an Armature.") return False for mod in obj.modifiers: if mod.type == 'MIRROR': return True - cls.set_poll_message("This mesh is not deformed by an Armature modifier.") + cls.poll_message_set("This mesh is not deformed by an Armature modifier.") return False def execute(self, context): diff --git a/scripts-blender/addons/easy_weight/toggle_weight_paint.py b/scripts-blender/addons/easy_weight/toggle_weight_paint.py index 5883859c..113a7103 100644 --- a/scripts-blender/addons/easy_weight/toggle_weight_paint.py +++ b/scripts-blender/addons/easy_weight/toggle_weight_paint.py @@ -139,7 +139,7 @@ class EASYWEIGHT_OT_toggle_weight_paint(Operator): def poll(cls, context): obj = context.active_object if not obj and obj.type == 'MESH': - cls.set_poll_message("Active object must be a mesh.") + cls.poll_message_set("Active object must be a mesh.") return False return True diff --git a/scripts-blender/addons/pose_shape_keys/CHANGELOG.md b/scripts-blender/addons/pose_shape_keys/CHANGELOG.md deleted file mode 100644 index f4cc7673..00000000 --- a/scripts-blender/addons/pose_shape_keys/CHANGELOG.md +++ /dev/null @@ -1,19 +0,0 @@ -## 0.0.4 - 2024-02-23 - - -### CHANGED -- Change name separator from . to - -- Format w/ Black + an icon API breakage -- Use consistent registration pattern - -## 0.0.3 - 2023-08-02 - -### FIXED -- Fix Changelog Rendering (#125) -- Fix Typo in README -- Fix line ends from DOS to UNIX (#68) - -## 0.0.2 - 2023-06-02 - -## DOCUMENTED -- Initial release \ No newline at end of file diff --git a/scripts-blender/addons/pose_shape_keys/__init__.py b/scripts-blender/addons/pose_shape_keys/__init__.py index 34f7885b..163defab 100644 --- a/scripts-blender/addons/pose_shape_keys/__init__.py +++ b/scripts-blender/addons/pose_shape_keys/__init__.py @@ -1,29 +1,13 @@ -# Pose Shape Keys addon for Blender -# Copyright (C) 2022 Demeter Dzadik -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - bl_info = { "name": "Pose Shape Keys", "author": "Demeter Dzadik", - "version": (0, 0, 4), + "version": (1, 0, 0), "blender": (3, 1, 0), "location": "Properties -> Mesh Data -> Shape Keys -> Pose Keys", "description": "Create shape keys that blend deformed meshes into a desired shape", "category": "Rigging", - "doc_url": "", - "tracker_url": "", + "doc_url": "https://studio.blender.org/pipeline/addons/pose_shape_keys", + "tracker_url": "https://projects.blender.org/studio/blender-studio-pipeline/src/branch/main/scripts-blender/addons/pose_shape_keys", } import importlib @@ -32,12 +16,11 @@ import bpy from . import ui from . import pose_key from . import ui_list -from . import reset_rig from . import symmetrize_shape_key from . import prefs # Each module can have register() and unregister() functions and a list of classes to register called "registry". -modules = [prefs, ui, pose_key, ui_list, reset_rig, symmetrize_shape_key] +modules = [prefs, ui, pose_key, ui_list, symmetrize_shape_key] def register_unregister_modules(modules, register: bool): diff --git a/scripts-blender/addons/pose_shape_keys/blender_manifest.toml b/scripts-blender/addons/pose_shape_keys/blender_manifest.toml new file mode 100644 index 00000000..95847ebf --- /dev/null +++ b/scripts-blender/addons/pose_shape_keys/blender_manifest.toml @@ -0,0 +1,19 @@ +schema_version = "1.0.0" + +id = "pose_shape_keys" +version = "1.0.0" +name = "Pose Shape Keys" +tagline = "Continue iterating on your weights and constraints without destroying your shape keys" +maintainer = "Demeter Dzadik " +type = "add-on" +website = "https://studio.blender.org/pipeline/addons/pose_shape_keys" +tags = ["Rigging"] + +blender_version_min = "4.2.0" + +license = [ + "SPDX:GPL-3.0-or-later", +] +copyright = [ + "2019-2024 Demeter Dzadik & Blender Studio", +] \ No newline at end of file diff --git a/scripts-blender/addons/pose_shape_keys/pose_key.py b/scripts-blender/addons/pose_shape_keys/pose_key.py index c69533fd..50b1a4ef 100644 --- a/scripts-blender/addons/pose_shape_keys/pose_key.py +++ b/scripts-blender/addons/pose_shape_keys/pose_key.py @@ -1,10 +1,10 @@ import bpy -from typing import List from bpy.types import PropertyGroup, Object, Operator, Action, ShapeKey, VertexGroup, MeshVertex from bpy.props import PointerProperty, IntProperty, CollectionProperty, StringProperty, BoolProperty from mathutils import Vector from math import sqrt from .symmetrize_shape_key import mirror_mesh +from .prefs import get_addon_prefs # When saving or pushing shapes, disable any modifier NOT in this list. DEFORM_MODIFIERS = [ @@ -28,18 +28,14 @@ DEFORM_MODIFIERS = [ GOOD_MODIFIERS = ['ARMATURE'] -def get_addon_prefs(context): - return context.preferences.addons[__package__].preferences - - class PoseShapeKeyTarget(PropertyGroup): def update_name(self, context): if self.block_name_update: return - ob = context.object - if not ob.data.shape_keys: + obj = context.object + if not obj.data.shape_keys: return - sk = ob.data.shape_keys.key_blocks.get(self.shape_key_name) + sk = obj.data.shape_keys.key_blocks.get(self.shape_key_name) if sk: sk.name = self.name self.shape_key_name = self.name @@ -70,7 +66,7 @@ class PoseShapeKeyTarget(PropertyGroup): ) @property - def key_block(self) -> List[ShapeKey]: + def key_block(self) -> list[ShapeKey]: mesh = self.id_data if not mesh.shape_keys: return @@ -81,25 +77,25 @@ class PoseShapeKey(PropertyGroup): target_shapes: CollectionProperty(type=PoseShapeKeyTarget) def update_active_sk_index(self, context): - ob = context.object - if not ob.data.shape_keys: + obj = context.object + if not obj.data.shape_keys: return try: sk_name = self.target_shapes[self.active_target_shape_index].shape_key_name except IndexError: - ob.active_shape_key_index = len(ob.data.shape_keys.key_blocks) - 1 + obj.active_shape_key_index = len(obj.data.shape_keys.key_blocks) - 1 return - key_block_idx = ob.data.shape_keys.key_blocks.find(sk_name) + key_block_idx = obj.data.shape_keys.key_blocks.find(sk_name) if key_block_idx > -1: - ob.active_shape_key_index = key_block_idx + obj.active_shape_key_index = key_block_idx # If in weight paint mode and there is a mask vertex group, # also set that vertex group as active. if context.mode == 'PAINT_WEIGHT': - key_block = ob.data.shape_keys.key_blocks[key_block_idx] - vg_idx = ob.vertex_groups.find(key_block.vertex_group) + key_block = obj.data.shape_keys.key_blocks[key_block_idx] + vg_idx = obj.vertex_groups.find(key_block.vertex_group) if vg_idx > -1: - ob.vertex_groups.active_index = vg_idx + obj.vertex_groups.active_index = vg_idx active_target_shape_index: IntProperty(update=update_active_sk_index) @@ -121,12 +117,12 @@ class PoseShapeKey(PropertyGroup): def get_deforming_armature(mesh_ob: Object) -> Object: - for m in mesh_ob.modifiers: - if m.type == 'ARMATURE': - return m.object + for mod in mesh_ob.modifiers: + if mod.type == 'ARMATURE': + return mod.object -class OBJECT_OT_Create_ShapeKey_For_Pose(Operator): +class OBJECT_OT_create_shape_key_for_pose(Operator): """Create and assign a Shape Key""" bl_idname = "object.create_shape_key_for_pose" @@ -136,13 +132,13 @@ class OBJECT_OT_Create_ShapeKey_For_Pose(Operator): def update_sk_name(self, context): def set_vg(vg_name): - ob = context.object - vg = ob.vertex_groups.get(vg_name) + obj = context.object + vg = obj.vertex_groups.get(vg_name) if vg: self.vg_name = vg.name return vg - ob = context.object + obj = context.object vg = set_vg(self.sk_name) if not vg and self.sk_name.endswith(".L"): vg = set_vg("Side.L") @@ -162,13 +158,13 @@ class OBJECT_OT_Create_ShapeKey_For_Pose(Operator): ) def invoke(self, context, event): - ob = context.object - if ob.data.shape_keys: - self.sk_name = f"Key {len(ob.data.shape_keys.key_blocks)}" + obj = context.object + if obj.data.shape_keys: + self.sk_name = f"Key {len(obj.data.shape_keys.key_blocks)}" else: self.sk_name = "Key" - pose_key = ob.data.pose_keys[ob.data.active_pose_key_index] + pose_key = obj.data.pose_keys[obj.data.active_pose_key_index] if pose_key.name: self.sk_name = pose_key.name @@ -177,26 +173,26 @@ class OBJECT_OT_Create_ShapeKey_For_Pose(Operator): def draw(self, context): layout = self.layout layout.prop(self, 'sk_name') - ob = context.object - layout.prop_search(self, 'vg_name', ob, "vertex_groups") + obj = context.object + layout.prop_search(self, 'vg_name', obj, "vertex_groups") def execute(self, context): - ob = context.object + obj = context.object # Ensure Basis shape key - if not ob.data.shape_keys: - basis = ob.shape_key_add() + if not obj.data.shape_keys: + basis = obj.shape_key_add() basis.name = "Basis" - ob.data.update() + obj.data.update() # Add new shape key - new_sk = ob.shape_key_add() + new_sk = obj.shape_key_add() new_sk.name = self.sk_name new_sk.value = 1 if self.vg_name: new_sk.vertex_group = self.vg_name - pose_key = ob.data.pose_keys[ob.data.active_pose_key_index] + pose_key = obj.data.pose_keys[obj.data.active_pose_key_index] target = pose_key.target_shapes[pose_key.active_target_shape_index] target.name = new_sk.name @@ -210,34 +206,34 @@ class SaveAndRestoreState: self.disabled_mods_storage = [] self.disabled_mods_rigged = [] self.disabled_fcurves = [] - for ob, lst in zip( + for obj, lst in zip( [storage_ob, rigged_ob], [self.disabled_mods_storage, self.disabled_mods_rigged] ): - if not ob: + if not obj: continue - for m in ob.modifiers: - if m.type not in GOOD_MODIFIERS and m.show_viewport: - lst.append(m.name) - m.show_viewport = False - if m.show_viewport: - data_path = f'modifiers["{m.name}"].show_viewport' - fc = ob.animation_data.drivers.find(data_path) + for mod in obj.modifiers: + if mod.type not in GOOD_MODIFIERS and mod.show_viewport: + lst.append(mod.name) + mod.show_viewport = False + if mod.show_viewport: + data_path = f'modifiers["{mod.name}"].show_viewport' + fc = obj.animation_data.drivers.find(data_path) if fc: fc.mute = True self.disabled_fcurves.append(data_path) - m.show_viewport = False + mod.show_viewport = False def restore_non_deform_modifiers(self, storage_ob: Object, rigged_ob: Object): # Re-enable non-deforming modifiers - for ob, m_list in zip( + for obj, mod_list in zip( [storage_ob, rigged_ob], [self.disabled_mods_storage, self.disabled_mods_rigged] ): - if not ob: + if not obj: continue - for m_name in m_list: - ob.modifiers[m_name].show_viewport = True + for mod_namee in mod_list: + obj.modifiers[mod_namee].show_viewport = True for data_path in self.disabled_fcurves: - fc = ob.animation_data.drivers.find(data_path) + fc = obj.animation_data.drivers.find(data_path) if fc: fc.mute = False @@ -300,20 +296,61 @@ class OperatorWithWarning: raise NotImplemented -def set_pose_of_active_pose_key(context): - bpy.ops.object.posekey_reset_rig() +def reset_rig(rig, *, reset_transforms=True, reset_props=True, pbones=[]): + if not pbones: + pbones = rig.pose.bones + for pb in pbones: + if reset_transforms: + pb.location = (0, 0, 0) + pb.rotation_euler = (0, 0, 0) + pb.rotation_quaternion = (1, 0, 0, 0) + pb.scale = (1, 1, 1) + if not reset_props or len(pb.keys()) == 0: + continue + + rna_properties = [prop.identifier for prop in pb.bl_rna.properties if prop.is_runtime] + + # Reset custom property values to their default value + for key in pb.keys(): + if key.startswith("$"): + continue + if key in rna_properties: + continue # Addon defined property. + + property_settings = None + try: + property_settings = pb.id_properties_ui(key) + if not property_settings: + continue + property_settings = property_settings.as_dict() + if not 'default' in property_settings: + continue + except TypeError: + # Some properties don't support UI data, and so don't have a default value. (like addon PropertyGroups) + pass + + if not property_settings: + continue + + if type(pb[key]) not in (float, int, bool): + continue + pb[key] = property_settings['default'] + + +def set_pose_of_active_pose_key(context): rigged_ob = context.object pose_key = rigged_ob.data.pose_keys[rigged_ob.data.active_pose_key_index] arm_ob = get_deforming_armature(rigged_ob) + reset_rig(arm_ob) if pose_key.action: # Set Action and Frame to get the right pose arm_ob.animation_data.action = pose_key.action context.scene.frame_current = pose_key.frame -class OBJECT_OT_PoseKey_Set_Pose(Operator): +class OBJECT_OT_pose_key_set_pose(Operator): """Set the rig pose to the specified action and frame (Reset any other posing)""" bl_idname = "object.posekey_set_pose" @@ -322,16 +359,23 @@ class OBJECT_OT_PoseKey_Set_Pose(Operator): @classmethod def poll(cls, context): - rigged_ob = context.object - arm_ob = get_deforming_armature(rigged_ob) + obj = context.object + if not obj or obj.type != 'MESH': + cls.poll_message_set("Active object must be a mesh.") + return False + arm_ob = get_deforming_armature(obj) if not arm_ob: + cls.poll_message_set("Mesh must be deformed by an Armature modifier.") return False - if rigged_ob.type != 'MESH' or not rigged_ob.data.shape_keys: + if not obj.data.shape_keys: + cls.poll_message_set("Mesh must have shape keys.") return False - if len(rigged_ob.data.pose_keys) == 0: + if len(obj.data.pose_keys) == 0: + cls.poll_message_set("Mesh must have pose keys.") return True - pose_key = rigged_ob.data.pose_keys[rigged_ob.data.active_pose_key_index] + pose_key = obj.data.pose_keys[obj.data.active_pose_key_index] if not pose_key.action: + cls.poll_message_set("Pose Key must have an Action assigned.") return False return True @@ -341,40 +385,57 @@ class OBJECT_OT_PoseKey_Set_Pose(Operator): return {'FINISHED'} -def get_active_pose_key(ob): - if ob.type != 'MESH': +def get_active_pose_key(obj): + if obj.type != 'MESH': return - if len(ob.data.pose_keys) == 0: + if len(obj.data.pose_keys) == 0: return - return ob.data.pose_keys[ob.data.active_pose_key_index] + return obj.data.pose_keys[obj.data.active_pose_key_index] -def verify_pose(context): +def poll_correct_pose_key_pose(operator, context): """To make these operators foolproof, there are a lot of checks to make sure that the user gets to see the effect of the operator. The "Set Pose" operator can be used first to set the correct state and pass all the checks here. """ - ob = context.object + obj = context.object - pose_key = get_active_pose_key(ob) + if not obj: + operator.poll_message_set("There must be an active mesh object.") + + pose_key = get_active_pose_key(obj) if not pose_key: + operator.poll_message_set("A Pose Shape Key must be selected.") return False - arm_ob = get_deforming_armature(ob) + arm_ob = get_deforming_armature(obj) + + if not arm_ob: + operator.poll_message_set("This mesh object is not deformed by any Armature modifier.") # Action must exist and match. - if not pose_key.action: - return False - if not arm_ob.animation_data or arm_ob.animation_data.action != pose_key.action: + if not ( + pose_key.action + and arm_ob.animation_data + and arm_ob.animation_data.action + and arm_ob.animation_data.action == pose_key.action + ): + operator.poll_message_set( + "The armature must have the Pose Shape Key's action assigned. Use the Set Pose button." + ) return False + if pose_key.frame != context.scene.frame_current: + operator.poll_message_set( + "The Pose Shape Key's frame must be the same as the current scene frame. Use the Set Pose button." + ) return False return True -class OBJECT_OT_PoseKey_Save(Operator, OperatorWithWarning, SaveAndRestoreState): +class OBJECT_OT_pose_key_save(Operator, OperatorWithWarning, SaveAndRestoreState): """Save the current evaluated mesh vertex positions into the Storage Object""" bl_idname = "object.posekey_save" @@ -383,31 +444,22 @@ class OBJECT_OT_PoseKey_Save(Operator, OperatorWithWarning, SaveAndRestoreState) @classmethod def poll(cls, context): - ob = context.object - # We can guess the action and frame number - arm_ob = get_deforming_armature(ob) - pose_key = get_active_pose_key(ob) - if ( - pose_key - and not pose_key.storage_object - and not pose_key.action - and arm_ob.animation_data - and arm_ob.animation_data.action - ): - return True - return verify_pose(context) + if not poll_correct_pose_key_pose(cls, context): + return False + + return True def invoke(self, context, event): - ob = context.object - pose_key = ob.data.pose_keys[ob.data.active_pose_key_index] + obj = context.object + pose_key = obj.data.pose_keys[obj.data.active_pose_key_index] if pose_key.storage_object: return super().invoke(context, event) return self.execute(context) def get_warning_text(self, context): - ob = context.object - pose_key = ob.data.pose_keys[ob.data.active_pose_key_index] - return f'This will overwrite "{pose_key.storage_object.name}".\n Are you sure?' + obj = context.object + pose_key = obj.data.pose_keys[obj.data.active_pose_key_index] + return f'Overwrite storage object "{pose_key.storage_object.name}"?' def execute(self, context): rigged_ob = context.object @@ -490,7 +542,7 @@ class OBJECT_OT_PoseKey_Save(Operator, OperatorWithWarning, SaveAndRestoreState) return {'FINISHED'} -class OBJECT_OT_PoseKey_Push(Operator, OperatorWithWarning, SaveAndRestoreState): +class OBJECT_OT_pose_key_push(Operator, OperatorWithWarning, SaveAndRestoreState): """Let the below shape keys blend the current deformed shape into the shape of the Storage Object""" bl_idname = "object.posekey_push" @@ -499,22 +551,24 @@ class OBJECT_OT_PoseKey_Push(Operator, OperatorWithWarning, SaveAndRestoreState) @classmethod def poll(cls, context): - pose_matches = verify_pose(context) - if not pose_matches: + if not poll_correct_pose_key_pose(cls, context): return False - # No shape keys to push into - ob = context.object - pose_key = get_active_pose_key(ob) + # No shape keys to push into. + obj = context.object + pose_key = get_active_pose_key(obj) for target_shape in pose_key.target_shapes: if target_shape.key_block: return True + cls.poll_message_set( + "This Pose Key doesn't have any target shape keys to push into. Add some in the Shape Key Slots list below." + ) return False def get_warning_text(self, context): - ob = context.object - pose_key = ob.data.pose_keys[ob.data.active_pose_key_index] + obj = context.object + pose_key = obj.data.pose_keys[obj.data.active_pose_key_index] target_shape_names = [target.name for target in pose_key.target_shapes if target] return ( "This will overwrite the following Shape Keys: \n " @@ -612,7 +666,7 @@ class OBJECT_OT_PoseKey_Push(Operator, OperatorWithWarning, SaveAndRestoreState) ) -class OBJECT_OT_PoseKey_Push_All(Operator, OperatorWithWarning, SaveAndRestoreState): +class OBJECT_OT_pose_key_push_all(Operator, OperatorWithWarning, SaveAndRestoreState): """Go through all Pose Keys, set their pose and overwrite the shape keys to match the storage object shapes""" bl_idname = "object.posekey_push_all" @@ -621,15 +675,19 @@ class OBJECT_OT_PoseKey_Push_All(Operator, OperatorWithWarning, SaveAndRestoreSt @classmethod def poll(cls, context): - ob = context.object - if not ob or ob.type != 'MESH': + obj = context.object + if not obj or obj.type != 'MESH': + cls.poll_message_set("No active mesh object.") return False - return len(ob.data.pose_keys) > 0 + if len(obj.data.pose_keys) == 0: + cls.poll_message_set("No Pose Shape Keys to push.") + return False + return True def get_warning_text(self, context): - ob = context.object + obj = context.object target_shape_names = [] - for pk in ob.data.pose_keys: + for pk in obj.data.pose_keys: target_shape_names.extend([t.name for t in pk.target_shapes if t]) return ( "This will overwrite the following Shape Keys: \n " @@ -647,7 +705,7 @@ class OBJECT_OT_PoseKey_Push_All(Operator, OperatorWithWarning, SaveAndRestoreSt return {'FINISHED'} -class OBJECT_OT_PoseKey_Clamp_Influence(Operator): +class OBJECT_OT_pose_key_clamp_influence(Operator): """Clamp the influence of this pose key's shape keys to 1.0 for each vertex, by normalizing the vertex weight mask values of vertices where the total influence is greater than 1""" bl_idname = "object.posekey_clamp_influence" @@ -655,7 +713,7 @@ class OBJECT_OT_PoseKey_Clamp_Influence(Operator): bl_options = {'UNDO', 'REGISTER', 'INTERNAL'} @staticmethod - def get_affected_vertex_group_names(object: Object) -> List[str]: + def get_affected_vertex_group_names(object: Object) -> list[str]: pose_key = object.data.pose_keys[object.data.active_pose_key_index] vg_names = [] @@ -670,36 +728,38 @@ class OBJECT_OT_PoseKey_Clamp_Influence(Operator): @classmethod def poll(cls, context): - return cls.get_affected_vertex_group_names(context.object) + if not cls.get_affected_vertex_group_names(context.object): + cls.poll_message_set("No shape keys of this pose shape key use vertex masks. There is nothing to clamp.") + return False + return True - def normalize_vgroups(self, o, vgroups): + def normalize_vgroups(self, obj, vgroups): """Normalize a set of vertex groups in isolation""" """ Used for creating mask vertex groups for splitting shape keys """ - for v in o.data.vertices: + for vert in obj.data.vertices: # Find sum of weights in specified vgroups # set weight to original/sum sum_weights = 0 - for vg in vgroups: - w = 0 + for vgroup in vgroups: try: - sum_weights += vg.weight(v.index) + sum_weights += vgroup.weight(vert.index) except: pass - for vg in vgroups: + for vgroup in vgroups: if sum_weights > 1.0: try: - vg.add([v.index], vg.weight(v.index) / sum_weights, 'REPLACE') + vgroup.add([vert.index], vgroup.weight(vert.index) / sum_weights, 'REPLACE') except: pass def execute(self, context): - ob = context.object - vg_names = self.get_affected_vertex_group_names(ob) - self.normalize_vgroups(ob, [ob.vertex_groups[vg_name] for vg_name in vg_names]) + obj = context.object + vg_names = self.get_affected_vertex_group_names(obj) + self.normalize_vgroups(obj, [obj.vertex_groups[vg_name] for vg_name in vg_names]) return {'FINISHED'} -class OBJECT_OT_PoseKey_Place_Objects_In_Grid(Operator): +class OBJECT_OT_pose_key_place_objects_in_grid(Operator): """Place the storage objects in a grid above this object""" bl_idname = "object.posekey_object_grid" @@ -707,30 +767,35 @@ class OBJECT_OT_PoseKey_Place_Objects_In_Grid(Operator): bl_options = {'UNDO', 'REGISTER', 'INTERNAL'} @staticmethod - def get_storage_objects(context) -> List[Object]: - ob = context.object - pose_keys = ob.data.pose_keys + def get_storage_objects(context) -> list[Object]: + obj = context.object + pose_keys = obj.data.pose_keys return [pk.storage_object for pk in pose_keys if pk.storage_object] @classmethod def poll(cls, context): """Only available if there are any storage objects in any of the pose keys.""" - return cls.get_storage_objects(context) + if not cls.get_storage_objects(context): + cls.poll_message_set("This pose key has no storage objects, so there is nothing to sort into a grid.") + return False + return True @staticmethod - def place_objects_in_grid(context, objs: List[Object]): - x = max([o.dimensions.x for o in objs]) - y = max([o.dimensions.y for o in objs]) - z = max([o.dimensions.z for o in objs]) + def place_objects_in_grid(context, objs: list[Object]): + if not objs: + return + x = max([obj.dimensions.x for obj in objs]) + y = max([obj.dimensions.y for obj in objs]) + z = max([obj.dimensions.z for obj in objs]) scalar = 1.2 dimensions = Vector((x * scalar, y * scalar, z * scalar)) grid_rows = round(sqrt(len(objs))) - for i, ob in enumerate(objs): + for i, obj in enumerate(objs): col_i = (i % grid_rows) - int(grid_rows / 2) row_i = int(i / grid_rows) + scalar offset = Vector((col_i * dimensions.x, 0, row_i * dimensions.z)) - ob.location = context.object.location + offset + obj.location = context.object.location + offset def execute(self, context): storage_objects = self.get_storage_objects(context) @@ -739,7 +804,7 @@ class OBJECT_OT_PoseKey_Place_Objects_In_Grid(Operator): return {'FINISHED'} -class OBJECT_OT_PoseKey_Jump_To_Shape(Operator): +class OBJECT_OT_pose_key_jump_to_shape(Operator): """Place the storage object next to this object and select it""" bl_idname = "object.posekey_jump_to_storage" @@ -748,14 +813,17 @@ class OBJECT_OT_PoseKey_Jump_To_Shape(Operator): @staticmethod def get_storage_object(context): - ob = context.object - pose_key = ob.data.pose_keys[ob.data.active_pose_key_index] + obj = context.object + pose_key = obj.data.pose_keys[obj.data.active_pose_key_index] return pose_key.storage_object @classmethod def poll(cls, context): """Only available if there is a storage object in the pose key.""" - return cls.get_storage_object(context) + if not cls.get_storage_object(context): + cls.poll_message_set("This pose key doesn't have a storage object to jump to.") + return False + return True def execute(self, context): storage_object = self.get_storage_object(context) @@ -774,14 +842,14 @@ class OBJECT_OT_PoseKey_Jump_To_Shape(Operator): # Put the other storage objects in a grid prefs = get_addon_prefs(context) if prefs.grid_objects_on_jump: - storage_objects = OBJECT_OT_PoseKey_Place_Objects_In_Grid.get_storage_objects(context) + storage_objects = OBJECT_OT_pose_key_place_objects_in_grid.get_storage_objects(context) storage_objects.remove(storage_object) - OBJECT_OT_PoseKey_Place_Objects_In_Grid.place_objects_in_grid(context, storage_objects) + OBJECT_OT_pose_key_place_objects_in_grid.place_objects_in_grid(context, storage_objects) return {'FINISHED'} -class OBJECT_OT_PoseKey_Copy_Data(Operator): +class OBJECT_OT_pose_key_copy_data(Operator): """Copy Pose Key data from active object to selected ones""" bl_idname = "object.posekey_copy_data" @@ -791,16 +859,20 @@ class OBJECT_OT_PoseKey_Copy_Data(Operator): @classmethod def poll(cls, context): """Only available if there is a selected mesh and the active mesh has pose key data.""" - selected_meshes = [ob for ob in context.selected_objects if ob.type == 'MESH'] + selected_meshes = [obj for obj in context.selected_objects if obj.type == 'MESH'] if len(selected_meshes) < 2: + cls.poll_message_set("No other meshes are selected to copy pose key data to.") return False if context.object.type != 'MESH' or not context.object.data.pose_keys: + cls.poll_message_set("No active mesh object with pose keys to copy.") return False return True def execute(self, context): source_ob = context.object - targets = [ob for ob in context.selected_objects if ob.type == 'MESH' and ob != source_ob] + targets = [ + obj for obj in context.selected_objects if obj.type == 'MESH' and obj != source_ob + ] for target_ob in targets: target_ob.data.pose_keys.clear() @@ -822,15 +894,15 @@ class OBJECT_OT_PoseKey_Copy_Data(Operator): registry = [ PoseShapeKeyTarget, PoseShapeKey, - OBJECT_OT_PoseKey_Save, - OBJECT_OT_PoseKey_Set_Pose, - OBJECT_OT_PoseKey_Push, - OBJECT_OT_PoseKey_Push_All, - OBJECT_OT_Create_ShapeKey_For_Pose, - OBJECT_OT_PoseKey_Clamp_Influence, - OBJECT_OT_PoseKey_Place_Objects_In_Grid, - OBJECT_OT_PoseKey_Jump_To_Shape, - OBJECT_OT_PoseKey_Copy_Data, + OBJECT_OT_pose_key_save, + OBJECT_OT_pose_key_set_pose, + OBJECT_OT_pose_key_push, + OBJECT_OT_pose_key_push_all, + OBJECT_OT_create_shape_key_for_pose, + OBJECT_OT_pose_key_clamp_influence, + OBJECT_OT_pose_key_place_objects_in_grid, + OBJECT_OT_pose_key_jump_to_shape, + OBJECT_OT_pose_key_copy_data, ] diff --git a/scripts-blender/addons/pose_shape_keys/prefs.py b/scripts-blender/addons/pose_shape_keys/prefs.py index f121dd45..36b1225d 100644 --- a/scripts-blender/addons/pose_shape_keys/prefs.py +++ b/scripts-blender/addons/pose_shape_keys/prefs.py @@ -1,5 +1,16 @@ from bpy.types import AddonPreferences from bpy.props import BoolProperty +from . import __package__ as base_package + + +def get_addon_prefs(context=None): + if not context: + context = bpy.context + if base_package.startswith('bl_ext'): + # 4.2 + return context.preferences.addons[base_package].preferences + else: + return context.preferences.addons[base_package.split(".")[0]].preferences class PoseShapeKeysPrefs(AddonPreferences): @@ -11,7 +22,7 @@ class PoseShapeKeysPrefs(AddonPreferences): default=True, ) no_warning: BoolProperty( - name="No Warning", + name="No Danger Warning", description="Do not show a pop-up warning for dangerous operations", ) grid_objects_on_jump: BoolProperty( diff --git a/scripts-blender/addons/pose_shape_keys/reset_rig.py b/scripts-blender/addons/pose_shape_keys/reset_rig.py deleted file mode 100644 index e485e6cc..00000000 --- a/scripts-blender/addons/pose_shape_keys/reset_rig.py +++ /dev/null @@ -1,76 +0,0 @@ -import bpy -from bpy.props import BoolProperty -from .pose_key import get_deforming_armature - - -class CK_OT_reset_rig(bpy.types.Operator): - """Reset all bone transforms and custom properties to their default values""" - - bl_idname = "object.posekey_reset_rig" - bl_label = "Reset Rig" - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - reset_transforms: BoolProperty( - name="Transforms", default=True, description="Reset bone transforms" - ) - reset_props: BoolProperty( - name="Properties", default=True, description="Reset custom properties" - ) - selection_only: BoolProperty( - name="Selected Only", - default=False, - description="Affect selected bones rather than all bones", - ) - - def invoke(self, context, event): - wm = context.window_manager - return wm.invoke_props_dialog(self) - - def execute(self, context): - rigged_ob = context.object - rig = get_deforming_armature(rigged_ob) - bones = rig.pose.bones - if self.selection_only: - bones = context.selected_pose_bones - for pb in bones: - if self.reset_transforms: - pb.location = (0, 0, 0) - pb.rotation_euler = (0, 0, 0) - pb.rotation_quaternion = (1, 0, 0, 0) - pb.scale = (1, 1, 1) - - if self.reset_props and len(pb.keys()) > 0: - rna_properties = [ - prop.identifier for prop in pb.bl_rna.properties if prop.is_runtime - ] - - # Reset custom property values to their default value - for key in pb.keys(): - if key.startswith("$"): - continue - if key in rna_properties: - continue # Addon defined property. - - ui_data = None - try: - ui_data = pb.id_properties_ui(key) - if not ui_data: - continue - ui_data = ui_data.as_dict() - if not 'default' in ui_data: - continue - except TypeError: - # Some properties don't support UI data, and so don't have a default value. (like addon PropertyGroups) - pass - - if not ui_data: - continue - - if type(pb[key]) not in (float, int): - continue - pb[key] = ui_data['default'] - - return {'FINISHED'} - - -registry = [CK_OT_reset_rig] diff --git a/scripts-blender/addons/pose_shape_keys/symmetrize_shape_key.py b/scripts-blender/addons/pose_shape_keys/symmetrize_shape_key.py index 53a84f44..0cdf7c13 100644 --- a/scripts-blender/addons/pose_shape_keys/symmetrize_shape_key.py +++ b/scripts-blender/addons/pose_shape_keys/symmetrize_shape_key.py @@ -1,7 +1,4 @@ -# This script expects a mesh whose base shape is symmetrical, and symmetrize the -# active shape key based on the symmetry of the base mesh. - -from typing import List, Tuple +from bpy.types import Operator import bpy from bpy.props import BoolProperty, EnumProperty, FloatProperty from mathutils.kdtree import KDTree @@ -9,12 +6,12 @@ from mathutils.kdtree import KDTree def mirror_mesh( *, - reference_verts: List, - vertices: List, + reference_verts: list, + vertices: list, axis: str, symmetrize=False, symmetrize_pos_to_neg=True, -) -> Tuple[int, int]: +) -> tuple[int, int]: """ Symmetrize vertices around any axis in any direction based on a set of reference vertices which share the same vertex order and are known to be @@ -95,8 +92,10 @@ def mirror_mesh( return good_counter, bad_counter -class OBJECT_OT_Symmetrize_Shape_Key(bpy.types.Operator): +class OBJECT_OT_symmetrize_shape_key(Operator): """Symmetrize shape key by matching vertex pairs by proximity in the original mesh""" + # NOTE: This script expects a mesh whose base shape is symmetrical, and symmetrize the + # active shape key based on the symmetry of the base mesh. bl_idname = "object.symmetrize_shape_key" bl_label = "Symmetrize Shape Key" @@ -133,8 +132,8 @@ class OBJECT_OT_Symmetrize_Shape_Key(bpy.types.Operator): layout.prop(self, 'threshold', slider=True) def execute(self, context): - ob = context.object - mesh = ob.data + obj = context.object + mesh = obj.data if 'X' in self.direction: axis = 'X' @@ -145,10 +144,10 @@ class OBJECT_OT_Symmetrize_Shape_Key(bpy.types.Operator): pos_to_neg = not self.direction.startswith('NEG') - key_blocks = [ob.active_shape_key] + key_blocks = [obj.active_shape_key] if self.all_keys: # TODO: This could be more optimized, right now we re-build the kdtree for each key block unneccessarily. - key_blocks = ob.data.shape_keys.key_blocks[:] + key_blocks = obj.data.shape_keys.key_blocks[:] for kb in key_blocks: good_counter, bad_counter = mirror_mesh( @@ -175,12 +174,12 @@ class OBJECT_OT_Symmetrize_Shape_Key(bpy.types.Operator): def draw_symmetrize_buttons(self, context): layout = self.layout layout.separator() - op = layout.operator(OBJECT_OT_Symmetrize_Shape_Key.bl_idname, text="Symmetrize Active") - op = layout.operator(OBJECT_OT_Symmetrize_Shape_Key.bl_idname, text="Symmetrize All") + op = layout.operator(OBJECT_OT_symmetrize_shape_key.bl_idname, text="Symmetrize Active") + op = layout.operator(OBJECT_OT_symmetrize_shape_key.bl_idname, text="Symmetrize All") op.all_keys = True -registry = [OBJECT_OT_Symmetrize_Shape_Key] +registry = [OBJECT_OT_symmetrize_shape_key] def register(): diff --git a/scripts-blender/addons/pose_shape_keys/ui.py b/scripts-blender/addons/pose_shape_keys/ui.py index b4d489d7..f92d0264 100644 --- a/scripts-blender/addons/pose_shape_keys/ui.py +++ b/scripts-blender/addons/pose_shape_keys/ui.py @@ -1,12 +1,10 @@ import bpy from bpy.types import Object, Panel, UIList, Menu +from bl_ui.properties_data_mesh import DATA_PT_shape_keys from .ui_list import draw_ui_list from bpy.props import EnumProperty -from bl_ui.properties_data_mesh import DATA_PT_shape_keys - -def get_addon_prefs(context): - return context.preferences.addons[__package__].preferences +from .prefs import get_addon_prefs class CK_UL_pose_keys(UIList): @@ -27,7 +25,6 @@ class CK_UL_pose_keys(UIList): class CK_UL_target_keys(UIList): def draw_item(self, context, layout, data, item, _icon, _active_data, _active_propname): obj = context.object - pose_key = data # I think? pose_key_target = item key_block = pose_key_target.key_block @@ -58,9 +55,9 @@ class CK_UL_target_keys(UIList): mute_row.prop(key_block, 'mute', emboss=False, text="") -def ob_has_armature_mod(ob: Object) -> bool: - for m in ob.modifiers: - if m.type == 'ARMATURE': +def obj_has_armature_mod(obj: Object) -> bool: + for mod in obj.modifiers: + if mod.type == 'ARMATURE': return True return False @@ -77,8 +74,8 @@ class MESH_PT_pose_keys(Panel): return context.object and context.object.type == 'MESH' def draw(self, context): - ob = context.object - mesh = ob.data + obj = context.object + mesh = obj.data layout = self.layout layout.prop(mesh, 'shape_key_ui_type', text="List Type: ", expand=True) @@ -86,7 +83,7 @@ class MESH_PT_pose_keys(Panel): if mesh.shape_key_ui_type == 'DEFAULT': return DATA_PT_shape_keys.draw(self, context) - if not ob_has_armature_mod(ob): + if not obj_has_armature_mod(obj): layout.alert = True layout.label(text="Object must have an Armature modifier to use Pose Keys.") return @@ -153,17 +150,17 @@ class MESH_PT_shape_key_subpanel(Panel): @classmethod def poll(cls, context): - ob = context.object + obj = context.object return ( - ob.data.shape_key_ui_type == 'POSE_KEYS' - and len(ob.data.pose_keys) > 0 - and ob.data.pose_keys[ob.data.active_pose_key_index].storage_object - and ob_has_armature_mod(ob) + obj.data.shape_key_ui_type == 'POSE_KEYS' + and len(obj.data.pose_keys) > 0 + and obj.data.pose_keys[obj.data.active_pose_key_index].storage_object + and obj_has_armature_mod(obj) ) def draw(self, context): - ob = context.object - mesh = ob.data + obj = context.object + mesh = obj.data layout = self.layout layout.use_property_split = True @@ -206,7 +203,7 @@ class MESH_PT_shape_key_subpanel(Panel): row = col.row(align=True) row.prop(sk, 'slider_min', text="Range") row.prop(sk, 'slider_max', text="") - col.prop_search(sk, "vertex_group", ob, "vertex_groups", text="Vertex Mask") + col.prop_search(sk, "vertex_group", obj, "vertex_groups", text="Vertex Mask") col.row().prop(sk, 'relative_key') -- 2.30.2 From 778ace5c581d6e3d6c829d12971190f69a9e4bf6 Mon Sep 17 00:00:00 2001 From: Demeter Dzadik Date: Mon, 1 Jul 2024 18:09:03 +0200 Subject: [PATCH 02/10] ReadMe & Docs --- docs/addons/pose_shape_keys.md | 42 +++++++++++++++- .../addons/pose_shape_keys/README.md | 50 +------------------ 2 files changed, 43 insertions(+), 49 deletions(-) diff --git a/docs/addons/pose_shape_keys.md b/docs/addons/pose_shape_keys.md index c165d00f..5efd97bf 100644 --- a/docs/addons/pose_shape_keys.md +++ b/docs/addons/pose_shape_keys.md @@ -1 +1,41 @@ - \ No newline at end of file +# Pose Shape Keys + +This add-on enables a workflow where you can continue iterating on your vertex weights and bone constraints after you've already created your shape keys, without having to re-sculpt those shape keys. To put another way, you can think of shape keys as a final shape rather than as deltas on some deformation. + +The only limitation is that there is some precision loss when using bendy bone deformations. + +It also lets you manage multiple copies of a shape key together. Each copy can have a different vertex group mask, or be applied mirrored around the X axis. + +You can find a video tutorial and more detailed explanation of how it works [here](https://studio.blender.org/blog/rig-with-shape-keys-like-never-before/). + +## Basic Workflow: +- Create a pose whose deformation you want to correct. A pose is defined as an Action and a frame number. +- Create a Pose Key on the deformed mesh. Assign the action and the frame number. +- Press "Store Evaluated Mesh". This will create a copy of your mesh with all deformations applied. +- Sculpt this mesh into the desired shape. +- Go back to the deformed mesh, and assign one or more Shape Keys to the Pose Key. +- Press "Set Pose" to ensure that the rig is in the pose you created and specified earlier. +- Press "Overwrite Shape Keys". +- When you activate your shape key, your deformed mesh should now look identical to your sculpted shape. +- If you have more than one shape key, the same data will be pushed into each. +The purpose of this is that each copy of the shape key can have a different mask assigned to it. +This can streamline symmetrical workflows, since you can push to a left and a right-side shape key in a single click. + +# Example use cases: + +### 1. Sculpted facial expressions applied directly on top of a bone deformation based rig: +- A character artist can sculpt facial expressions to great quality and detail +- You pose the rig to be as close to this sculpted shape as possible, and create a rig control that blends into this pose using Action Constraints. +- Using the add-on, create corrective shape keys that blend your posed mesh into the shape of the sculpt. +- Hook up those corrective shape keys to the rig via drivers +- You now have the precise result of the sculpted facial expression, while retaining the freedom of bone-based controls that can move, scale and rotate! + +### 2. Author finger correctives 24-at-a-time: +- Create a fist pose where all finger bones (4x2x3=24) are bent by around 90 degrees. +- Create a Pose Key and a storage object, and sculpt the desired deformation result. +- On the rigged mesh, create the 24 shape keys within the PoseKey; One for each section of each finger. +- Assign vertex groups to them that mask the affected areas. +- Normalize the vertex masks. +- Now you can push the sculpted fist shape into all 24 shape keys at the same time. +- Create drivers so each shape key is driven by the corresponding finger bone. +- You can now tweak and iterate on the sculpted shape, and update all 24 shape keys with the click of a single button. \ No newline at end of file diff --git a/scripts-blender/addons/pose_shape_keys/README.md b/scripts-blender/addons/pose_shape_keys/README.md index ea140f84..1ce27a64 100644 --- a/scripts-blender/addons/pose_shape_keys/README.md +++ b/scripts-blender/addons/pose_shape_keys/README.md @@ -1,51 +1,5 @@ # Pose Shape Keys -## Table of Contents -- [Installation](#installation) -- [Basic Workflow](#basic-workflow) -- [Example Use Cases](#example-use-cases) - -This addon lets you create shape keys that blend already deformed meshes into a previously stored shape. -It also lets you manage multiple copies of a shape key together. Each copy can have a different vertex group mask, or be applied mirrored around the X axis. - -You can find a detailed video tutorial on how to download, install and use the addon [here](https://studio.blender.org/training/blender-studio-rigging-tools/pose-shape-keys/). - -## Installation -Find installation instructions [here](https://studio.blender.org/pipeline/addons/overview). - -## Basic Workflow: -- Create a pose whose deformation you want to correct. A pose is defined as an Action and a frame number. -- Create a Pose Key on the deformed mesh. Assign the action and the frame number. -- Press "Store Evaluated Mesh". This will create a copy of your mesh with all deformations applied. -- Sculpt this mesh into the desired shape. -- Go back to the deformed mesh, and assign one or more Shape Keys to the Pose Key. -- Press "Set Pose" to ensure that the rig is in the pose you created and specified earlier. -- Press "Overwrite Shape Keys". -- When you activate your shape key, your deformed mesh should now look identical to your sculpted shape. -- If you have more than one shape key, the same data will be pushed into each. -The purpose of this is that each copy of the shape key have a different mask assigned to it. -This can streamline symmetrical workflows, since you can push to a left and a right-side shape key in a single click. - -# Example use cases: - -### 1. Sculpted facial expressions applied directly on top of a bone deformation based rig: -- A character artist can sculpt facial expressions to great quality and detail -- You pose the rig to be as close to this sculpted shape as possible, and create -a rig control that blends into this pose using Action Constraints. -- Using the addon, create corrective shape keys that blend your posed mesh into -the shape of the sculpt. -- Hook up those corrective shape keys to the rig via drivers -- You now have the precise result of the sculpted facial expression, while retaining -the freedom of bone-based controls that can move, scale and rotate! - -### 2. Author finger correctives 24 at a time: -- Create a pose where all fingers are bent by 90 degrees at the first joint. -- Create a Pose Key and a storage object, and sculpt the desired deformation result. -- On the rigged mesh, create 24 shape keys within the PoseKey; One for each section of each finger. -- Assign vertex groups to them that mask each finger. -- Normalize the vertex masks. -- Now you can push the sculpted hand shape into all 24 shape keys at the same time. -- Create drivers so each shape key is driven by the corresponding finger bone. -- You can now tweak and iterate on the sculpted shape, and update all 24 shape keys -with the click of a single button. +This add-on enables a workflow where you can think of shape keys as a final shape rather than as deltas on some deformation. This means you can continue iterating on your vertex weights and bone constraints after you've already created your shape keys, without having to re-sculpt those shape keys. +You can find the documentation [here](https://studio.blender.org/pipeline/addons/pose_shape_keys). \ No newline at end of file -- 2.30.2 From f044a3270e3c23c66127042eb4d5a75f9e857991 Mon Sep 17 00:00:00 2001 From: Demeter Dzadik Date: Tue, 2 Jul 2024 09:39:45 +0200 Subject: [PATCH 03/10] Improve polls+wording & prevent unnamed PoseKeys --- .../addons/pose_shape_keys/pose_key.py | 119 ++++++++++++++---- scripts-blender/addons/pose_shape_keys/ui.py | 11 +- .../addons/pose_shape_keys/ui_list.py | 14 ++- 3 files changed, 114 insertions(+), 30 deletions(-) diff --git a/scripts-blender/addons/pose_shape_keys/pose_key.py b/scripts-blender/addons/pose_shape_keys/pose_key.py index 50b1a4ef..07adca1c 100644 --- a/scripts-blender/addons/pose_shape_keys/pose_key.py +++ b/scripts-blender/addons/pose_shape_keys/pose_key.py @@ -5,6 +5,7 @@ from mathutils import Vector from math import sqrt from .symmetrize_shape_key import mirror_mesh from .prefs import get_addon_prefs +from .ui_list import UILIST_OT_Entry_Add # When saving or pushing shapes, disable any modifier NOT in this list. DEFORM_MODIFIERS = [ @@ -99,6 +100,12 @@ class PoseShapeKey(PropertyGroup): active_target_shape_index: IntProperty(update=update_active_sk_index) + def update_name(self, context): + if self.name == "": + self.name = "Pose Key" + + name: StringProperty(name="Name", update=update_name) + action: PointerProperty( name="Action", type=Action, @@ -359,26 +366,7 @@ class OBJECT_OT_pose_key_set_pose(Operator): @classmethod def poll(cls, context): - obj = context.object - if not obj or obj.type != 'MESH': - cls.poll_message_set("Active object must be a mesh.") - return False - arm_ob = get_deforming_armature(obj) - if not arm_ob: - cls.poll_message_set("Mesh must be deformed by an Armature modifier.") - return False - if not obj.data.shape_keys: - cls.poll_message_set("Mesh must have shape keys.") - return False - if len(obj.data.pose_keys) == 0: - cls.poll_message_set("Mesh must have pose keys.") - return True - pose_key = obj.data.pose_keys[obj.data.active_pose_key_index] - if not pose_key.action: - cls.poll_message_set("Pose Key must have an Action assigned.") - return False - - return True + return poll_has_psk_and_deformed(cls, context) def execute(self, context): set_pose_of_active_pose_key(context) @@ -394,25 +382,41 @@ def get_active_pose_key(obj): return obj.data.pose_keys[obj.data.active_pose_key_index] -def poll_correct_pose_key_pose(operator, context): - """To make these operators foolproof, there are a lot of checks to make sure - that the user gets to see the effect of the operator. The "Set Pose" operator - can be used first to set the correct state and pass all the checks here. - """ +def poll_has_psk_and_deformed(operator, context): obj = context.object if not obj: operator.poll_message_set("There must be an active mesh object.") + return False pose_key = get_active_pose_key(obj) if not pose_key: operator.poll_message_set("A Pose Shape Key must be selected.") return False + if not pose_key.name: + operator.poll_message_set("The Pose Shape Key must be named.") arm_ob = get_deforming_armature(obj) if not arm_ob: operator.poll_message_set("This mesh object is not deformed by any Armature modifier.") + return False + + return obj, pose_key, arm_ob + + +def poll_correct_pose_key_pose(operator, context): + """To make these operators foolproof, there are a lot of checks to make sure + that the user gets to see the effect of the operator. The "Set Pose" operator + can be used first to set the correct state and pass all the checks here. + """ + + obj_psk_arm = poll_has_psk_and_deformed(operator, context) + + if not obj_psk_arm: + return False + + obj, pose_key, arm_ob = obj_psk_arm # Action must exist and match. if not ( @@ -435,6 +439,47 @@ def poll_correct_pose_key_pose(operator, context): return True +class OBJECT_OT_pose_key_add(UILIST_OT_Entry_Add, Operator): + """Add Pose Shape Key""" + + bl_idname = "object.posekey_add" + bl_label = "Add Pose Shape Key" + bl_options = {'UNDO', 'REGISTER', 'INTERNAL'} + + list_context_path: StringProperty() + active_idx_context_path: StringProperty() + + pose_key_name: StringProperty(name="Name", default="Pose Key") + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + self.layout.prop(self, 'pose_key_name') + if not self.pose_key_name: + self.layout.alert = True + self.layout.label(text="Name cannot be empty.", icon='ERROR') + + def execute(self, context): + if not self.pose_key_name: + self.report({'ERROR'}, "Must specify a name.") + return {'CANCELLED'} + + my_list = self.get_list(context) + active_index = self.get_active_index(context) + + to_index = active_index + 1 + if len(my_list) == 0: + to_index = 0 + + psk = my_list.add() + psk.name = self.pose_key_name + my_list.move(len(my_list) - 1, to_index) + self.set_active_index(context, to_index) + + return {'FINISHED'} + + class OBJECT_OT_pose_key_save(Operator, OperatorWithWarning, SaveAndRestoreState): """Save the current evaluated mesh vertex positions into the Storage Object""" @@ -444,6 +489,21 @@ class OBJECT_OT_pose_key_save(Operator, OperatorWithWarning, SaveAndRestoreState @classmethod def poll(cls, context): + obj_psk_arm = poll_has_psk_and_deformed(cls, context) + if not obj_psk_arm: + return False + obj, pose_key, arm_ob = obj_psk_arm + if ( + not pose_key.storage_object + and not pose_key.action + and arm_ob.animation_data + and arm_ob.animation_data.action + ): + # If we can guess the action, allow the operator to run. + # Let's call this "Initialize mode", since we're allowing the + # user to 1-click initialize some variables. + return True + if not poll_correct_pose_key_pose(cls, context): return False @@ -729,7 +789,9 @@ class OBJECT_OT_pose_key_clamp_influence(Operator): @classmethod def poll(cls, context): if not cls.get_affected_vertex_group_names(context.object): - cls.poll_message_set("No shape keys of this pose shape key use vertex masks. There is nothing to clamp.") + cls.poll_message_set( + "No shape keys of this pose shape key use vertex masks. There is nothing to clamp." + ) return False return True @@ -776,7 +838,9 @@ class OBJECT_OT_pose_key_place_objects_in_grid(Operator): def poll(cls, context): """Only available if there are any storage objects in any of the pose keys.""" if not cls.get_storage_objects(context): - cls.poll_message_set("This pose key has no storage objects, so there is nothing to sort into a grid.") + cls.poll_message_set( + "This pose key has no storage objects, so there is nothing to sort into a grid." + ) return False return True @@ -894,6 +958,7 @@ class OBJECT_OT_pose_key_copy_data(Operator): registry = [ PoseShapeKeyTarget, PoseShapeKey, + OBJECT_OT_pose_key_add, OBJECT_OT_pose_key_save, OBJECT_OT_pose_key_set_pose, OBJECT_OT_pose_key_push, diff --git a/scripts-blender/addons/pose_shape_keys/ui.py b/scripts-blender/addons/pose_shape_keys/ui.py index f92d0264..3a18771e 100644 --- a/scripts-blender/addons/pose_shape_keys/ui.py +++ b/scripts-blender/addons/pose_shape_keys/ui.py @@ -19,6 +19,11 @@ class CK_UL_pose_keys(UIList): icon = 'SURFACE_NCIRCLE' if pose_key.storage_object else 'CURVE_NCIRCLE' name_row = split.row() + if not pose_key.name: + name_row.alert = True + split = name_row.split() + name_row = split.row() + split.label(text="Unnamed!", icon='ERROR') name_row.prop(pose_key, 'name', text="", emboss=False, icon=icon) @@ -103,6 +108,7 @@ class MESH_PT_pose_keys(Panel): list_context_path='object.data.pose_keys', active_idx_context_path='object.data.active_pose_key_index', menu_class_name='MESH_MT_pose_key_utils', + add_op_name='object.posekey_add', ) layout.use_property_split = True @@ -126,7 +132,10 @@ class MESH_PT_pose_keys(Panel): else: layout.operator('object.posekey_set_pose', text="Set Pose", icon="ARMATURE_DATA") row = layout.row() - row.operator('object.posekey_save', text="Store Evaluated Mesh", icon="FILE_TICK") + text = "Store Posed Mesh" + if not active_posekey.storage_object and not active_posekey.action: + text = "Store Mesh & Init Pose Key" + row.operator('object.posekey_save', text=text, icon="FILE_TICK") row.prop(active_posekey, 'storage_object', text="") return diff --git a/scripts-blender/addons/pose_shape_keys/ui_list.py b/scripts-blender/addons/pose_shape_keys/ui_list.py index 71aac1d3..d581e521 100644 --- a/scripts-blender/addons/pose_shape_keys/ui_list.py +++ b/scripts-blender/addons/pose_shape_keys/ui_list.py @@ -118,6 +118,10 @@ def draw_ui_list( list_context_path='object.data.vertex_groups', active_idx_context_path='object.data.vertex_groups.active_index', insertion_operators=True, + add_op_name=None, + add_kwargs={}, + remove_op_name=None, + remove_kwargs={}, move_operators=True, menu_class_name='', **kwargs, @@ -148,13 +152,19 @@ def draw_ui_list( col = row.column() if insertion_operators: - add_op = col.operator('ui.list_entry_add', text="", icon='ADD') + op_name = add_op_name or 'ui.list_entry_add' + add_op = col.operator(op_name, text="", icon='ADD') + for key, value in add_kwargs.items(): + setattr(add_op, key, value) add_op.list_context_path = list_context_path add_op.active_idx_context_path = active_idx_context_path row = col.row() row.enabled = len(my_list) > 0 - remove_op = row.operator('ui.list_entry_remove', text="", icon='REMOVE') + op_name = remove_op_name or 'ui.list_entry_remove' + remove_op = row.operator(op_name, text="", icon='REMOVE') + for key, value in remove_kwargs.items(): + setattr(remove_op, key, value) remove_op.list_context_path = list_context_path remove_op.active_idx_context_path = active_idx_context_path -- 2.30.2 From e7cf029f5a16442c1f9042f1956e863528001d84 Mon Sep 17 00:00:00 2001 From: Demeter Dzadik Date: Tue, 2 Jul 2024 11:23:41 +0200 Subject: [PATCH 04/10] Cleanup: Split up into usual add-on structure --- .../addons/pose_shape_keys/__init__.py | 15 ++- .../pose_shape_keys/{pose_key.py => ops.py} | 119 +---------------- .../addons/pose_shape_keys/props.py | 123 ++++++++++++++++++ 3 files changed, 134 insertions(+), 123 deletions(-) rename scripts-blender/addons/pose_shape_keys/{pose_key.py => ops.py} (88%) create mode 100644 scripts-blender/addons/pose_shape_keys/props.py diff --git a/scripts-blender/addons/pose_shape_keys/__init__.py b/scripts-blender/addons/pose_shape_keys/__init__.py index 163defab..ef7d48b8 100644 --- a/scripts-blender/addons/pose_shape_keys/__init__.py +++ b/scripts-blender/addons/pose_shape_keys/__init__.py @@ -13,14 +13,17 @@ bl_info = { import importlib import bpy -from . import ui -from . import pose_key -from . import ui_list -from . import symmetrize_shape_key -from . import prefs +from . import ( + props, + ui, + ops, + ui_list, + symmetrize_shape_key, + prefs, +) # Each module can have register() and unregister() functions and a list of classes to register called "registry". -modules = [prefs, ui, pose_key, ui_list, symmetrize_shape_key] +modules = [props, prefs, ui, ops, ui_list, symmetrize_shape_key] def register_unregister_modules(modules, register: bool): diff --git a/scripts-blender/addons/pose_shape_keys/pose_key.py b/scripts-blender/addons/pose_shape_keys/ops.py similarity index 88% rename from scripts-blender/addons/pose_shape_keys/pose_key.py rename to scripts-blender/addons/pose_shape_keys/ops.py index 07adca1c..8cba6ca2 100644 --- a/scripts-blender/addons/pose_shape_keys/pose_key.py +++ b/scripts-blender/addons/pose_shape_keys/ops.py @@ -1,6 +1,6 @@ import bpy -from bpy.types import PropertyGroup, Object, Operator, Action, ShapeKey, VertexGroup, MeshVertex -from bpy.props import PointerProperty, IntProperty, CollectionProperty, StringProperty, BoolProperty +from bpy.types import Object, Operator +from bpy.props import StringProperty from mathutils import Vector from math import sqrt from .symmetrize_shape_key import mirror_mesh @@ -29,99 +29,6 @@ DEFORM_MODIFIERS = [ GOOD_MODIFIERS = ['ARMATURE'] -class PoseShapeKeyTarget(PropertyGroup): - def update_name(self, context): - if self.block_name_update: - return - obj = context.object - if not obj.data.shape_keys: - return - sk = obj.data.shape_keys.key_blocks.get(self.shape_key_name) - if sk: - sk.name = self.name - self.shape_key_name = self.name - - def update_shape_key_name(self, context): - self.block_name_update = True - self.name = self.shape_key_name - self.block_name_update = False - - name: StringProperty( - name="Shape Key Target", - description="Name of this shape key target. Should stay in sync with the displayed name and the shape key name, unless the shape key is renamed outside of our UI", - update=update_name, - ) - mirror_x: BoolProperty( - name="Mirror X", - description="Mirror the shape key on the X axis when applying the stored shape to this shape key", - default=False, - ) - - block_name_update: BoolProperty( - description="Flag to help keep shape key names in sync", default=False - ) - shape_key_name: StringProperty( - name="Shape Key", - description="Name of the shape key to push data to", - update=update_shape_key_name, - ) - - @property - def key_block(self) -> list[ShapeKey]: - mesh = self.id_data - if not mesh.shape_keys: - return - return mesh.shape_keys.key_blocks.get(self.name) - - -class PoseShapeKey(PropertyGroup): - target_shapes: CollectionProperty(type=PoseShapeKeyTarget) - - def update_active_sk_index(self, context): - obj = context.object - if not obj.data.shape_keys: - return - try: - sk_name = self.target_shapes[self.active_target_shape_index].shape_key_name - except IndexError: - obj.active_shape_key_index = len(obj.data.shape_keys.key_blocks) - 1 - return - key_block_idx = obj.data.shape_keys.key_blocks.find(sk_name) - if key_block_idx > -1: - obj.active_shape_key_index = key_block_idx - - # If in weight paint mode and there is a mask vertex group, - # also set that vertex group as active. - if context.mode == 'PAINT_WEIGHT': - key_block = obj.data.shape_keys.key_blocks[key_block_idx] - vg_idx = obj.vertex_groups.find(key_block.vertex_group) - if vg_idx > -1: - obj.vertex_groups.active_index = vg_idx - - active_target_shape_index: IntProperty(update=update_active_sk_index) - - def update_name(self, context): - if self.name == "": - self.name = "Pose Key" - - name: StringProperty(name="Name", update=update_name) - - action: PointerProperty( - name="Action", - type=Action, - description="Action that contains the frame that should be used when applying the stored shape as a shape key", - ) - frame: IntProperty( - name="Frame", - description="Frame that should be used within the selected action when applying the stored shape as a shape key", - default=0, - ) - storage_object: PointerProperty( - type=Object, - name="Storage Object", - description="Specify an object that stores the vertex position data", - ) - def get_deforming_armature(mesh_ob: Object) -> Object: for mod in mesh_ob.modifiers: @@ -956,8 +863,6 @@ class OBJECT_OT_pose_key_copy_data(Operator): registry = [ - PoseShapeKeyTarget, - PoseShapeKey, OBJECT_OT_pose_key_add, OBJECT_OT_pose_key_save, OBJECT_OT_pose_key_set_pose, @@ -969,23 +874,3 @@ registry = [ OBJECT_OT_pose_key_jump_to_shape, OBJECT_OT_pose_key_copy_data, ] - - -def update_posekey_index(self, context): - # Want to piggyback on update_active_sk_index() to also update the active - # shape key index when switching pose keys. - mesh = context.object.data - if mesh.pose_keys: - pk = mesh.pose_keys[mesh.active_pose_key_index] - # We just want to fire the update func. - pk.active_target_shape_index = pk.active_target_shape_index - - -def register(): - bpy.types.Mesh.pose_keys = CollectionProperty(type=PoseShapeKey) - bpy.types.Mesh.active_pose_key_index = IntProperty(update=update_posekey_index) - - -def unregister(): - del bpy.types.Mesh.pose_keys - del bpy.types.Mesh.active_pose_key_index diff --git a/scripts-blender/addons/pose_shape_keys/props.py b/scripts-blender/addons/pose_shape_keys/props.py new file mode 100644 index 00000000..ebb355d1 --- /dev/null +++ b/scripts-blender/addons/pose_shape_keys/props.py @@ -0,0 +1,123 @@ +import bpy +from bpy.types import PropertyGroup, Object, Action, ShapeKey +from bpy.props import PointerProperty, IntProperty, CollectionProperty, StringProperty, BoolProperty + + +class PoseShapeKeyTarget(PropertyGroup): + def update_name(self, context): + if self.block_name_update: + return + obj = context.object + if not obj.data.shape_keys: + return + sk = obj.data.shape_keys.key_blocks.get(self.shape_key_name) + if sk: + sk.name = self.name + self.shape_key_name = self.name + + def update_shape_key_name(self, context): + self.block_name_update = True + self.name = self.shape_key_name + self.block_name_update = False + + name: StringProperty( + name="Shape Key Target", + description="Name of this shape key target. Should stay in sync with the displayed name and the shape key name, unless the shape key is renamed outside of our UI", + update=update_name, + ) + mirror_x: BoolProperty( + name="Mirror X", + description="Mirror the shape key on the X axis when applying the stored shape to this shape key", + default=False, + ) + + block_name_update: BoolProperty( + description="Flag to help keep shape key names in sync", default=False + ) + shape_key_name: StringProperty( + name="Shape Key", + description="Name of the shape key to push data to", + update=update_shape_key_name, + ) + + @property + def key_block(self) -> list[ShapeKey]: + mesh = self.id_data + if not mesh.shape_keys: + return + return mesh.shape_keys.key_blocks.get(self.name) + + +class PoseShapeKey(PropertyGroup): + target_shapes: CollectionProperty(type=PoseShapeKeyTarget) + + def update_active_sk_index(self, context): + obj = context.object + if not obj.data.shape_keys: + return + try: + sk_name = self.target_shapes[self.active_target_shape_index].shape_key_name + except IndexError: + obj.active_shape_key_index = len(obj.data.shape_keys.key_blocks) - 1 + return + key_block_idx = obj.data.shape_keys.key_blocks.find(sk_name) + if key_block_idx > -1: + obj.active_shape_key_index = key_block_idx + + # If in weight paint mode and there is a mask vertex group, + # also set that vertex group as active. + if context.mode == 'PAINT_WEIGHT': + key_block = obj.data.shape_keys.key_blocks[key_block_idx] + vg_idx = obj.vertex_groups.find(key_block.vertex_group) + if vg_idx > -1: + obj.vertex_groups.active_index = vg_idx + + active_target_shape_index: IntProperty(update=update_active_sk_index) + + def update_name(self, context): + if self.name == "": + self.name = "Pose Key" + + name: StringProperty(name="Name", update=update_name) + + action: PointerProperty( + name="Action", + type=Action, + description="Action that contains the frame that should be used when applying the stored shape as a shape key", + ) + frame: IntProperty( + name="Frame", + description="Frame that should be used within the selected action when applying the stored shape as a shape key", + default=0, + ) + storage_object: PointerProperty( + type=Object, + name="Storage Object", + description="Specify an object that stores the vertex position data", + ) + + +registry = [ + PoseShapeKeyTarget, + PoseShapeKey, +] + + +def update_posekey_index(self, context): + # Want to piggyback on update_active_sk_index() to also update the active + # shape key index when switching pose keys. + mesh = context.object.data + if mesh.pose_keys: + pk = mesh.pose_keys[mesh.active_pose_key_index] + # We just want to fire the update func. + pk.active_target_shape_index = pk.active_target_shape_index + + +def register(): + bpy.types.Mesh.pose_keys = CollectionProperty(type=PoseShapeKey) + bpy.types.Mesh.active_pose_key_index = IntProperty(update=update_posekey_index) + + +def unregister(): + del bpy.types.Mesh.pose_keys + del bpy.types.Mesh.active_pose_key_index -- 2.30.2 From a0f32b4d0d9430a6292c07d4edaa3acde2e0f20a Mon Sep 17 00:00:00 2001 From: Demeter Dzadik Date: Tue, 2 Jul 2024 16:46:12 +0200 Subject: [PATCH 05/10] Revamp UX flow by splitting Init&Save into 2 operators --- scripts-blender/addons/pose_shape_keys/ops.py | 142 +++++++++--------- scripts-blender/addons/pose_shape_keys/ui.py | 61 ++++---- 2 files changed, 104 insertions(+), 99 deletions(-) diff --git a/scripts-blender/addons/pose_shape_keys/ops.py b/scripts-blender/addons/pose_shape_keys/ops.py index 8cba6ca2..a8c79179 100644 --- a/scripts-blender/addons/pose_shape_keys/ops.py +++ b/scripts-blender/addons/pose_shape_keys/ops.py @@ -29,7 +29,6 @@ DEFORM_MODIFIERS = [ GOOD_MODIFIERS = ['ARMATURE'] - def get_deforming_armature(mesh_ob: Object) -> Object: for mod in mesh_ob.modifiers: if mod.type == 'ARMATURE': @@ -78,7 +77,7 @@ class OBJECT_OT_create_shape_key_for_pose(Operator): else: self.sk_name = "Key" - pose_key = obj.data.pose_keys[obj.data.active_pose_key_index] + pose_key = get_active_pose_key(obj) if pose_key.name: self.sk_name = pose_key.name @@ -106,7 +105,7 @@ class OBJECT_OT_create_shape_key_for_pose(Operator): if self.vg_name: new_sk.vertex_group = self.vg_name - pose_key = obj.data.pose_keys[obj.data.active_pose_key_index] + pose_key = get_active_pose_key(obj) target = pose_key.target_shapes[pose_key.active_target_shape_index] target.name = new_sk.name @@ -265,7 +264,7 @@ def set_pose_of_active_pose_key(context): class OBJECT_OT_pose_key_set_pose(Operator): - """Set the rig pose to the specified action and frame (Reset any other posing)""" + """Reset the rig, then set the above Action and frame number""" bl_idname = "object.posekey_set_pose" bl_label = "Set Pose" @@ -273,7 +272,7 @@ class OBJECT_OT_pose_key_set_pose(Operator): @classmethod def poll(cls, context): - return poll_has_psk_and_deformed(cls, context) + return poll_correct_pose_key_pose(cls, context, demand_pose=False) def execute(self, context): set_pose_of_active_pose_key(context) @@ -289,7 +288,12 @@ def get_active_pose_key(obj): return obj.data.pose_keys[obj.data.active_pose_key_index] -def poll_has_psk_and_deformed(operator, context): +def poll_correct_pose_key_pose(operator, context, demand_pose=True): + """To make these operators foolproof, there are a lot of checks to make sure + that the user gets to see the effect of the operator. The "Set Pose" operator + can be used first to set the correct state and pass all the checks here. + """ + obj = context.object if not obj: @@ -309,39 +313,27 @@ def poll_has_psk_and_deformed(operator, context): operator.poll_message_set("This mesh object is not deformed by any Armature modifier.") return False - return obj, pose_key, arm_ob - - -def poll_correct_pose_key_pose(operator, context): - """To make these operators foolproof, there are a lot of checks to make sure - that the user gets to see the effect of the operator. The "Set Pose" operator - can be used first to set the correct state and pass all the checks here. - """ - - obj_psk_arm = poll_has_psk_and_deformed(operator, context) - - if not obj_psk_arm: + if not pose_key.action: + operator.poll_message_set("An Action must be associated with the Pose Shape Key.") return False - obj, pose_key, arm_ob = obj_psk_arm + if demand_pose: + # Action must exist and match. + if not ( + arm_ob.animation_data + and arm_ob.animation_data.action + and arm_ob.animation_data.action == pose_key.action + ): + operator.poll_message_set( + "The armature must have the Pose Shape Key's action assigned. Use the Set Pose button." + ) + return False - # Action must exist and match. - if not ( - pose_key.action - and arm_ob.animation_data - and arm_ob.animation_data.action - and arm_ob.animation_data.action == pose_key.action - ): - operator.poll_message_set( - "The armature must have the Pose Shape Key's action assigned. Use the Set Pose button." - ) - return False - - if pose_key.frame != context.scene.frame_current: - operator.poll_message_set( - "The Pose Shape Key's frame must be the same as the current scene frame. Use the Set Pose button." - ) - return False + if pose_key.frame != context.scene.frame_current: + operator.poll_message_set( + "The Pose Shape Key's frame must be the same as the current scene frame. Use the Set Pose button." + ) + return False return True @@ -352,6 +344,7 @@ class OBJECT_OT_pose_key_add(UILIST_OT_Entry_Add, Operator): bl_idname = "object.posekey_add" bl_label = "Add Pose Shape Key" bl_options = {'UNDO', 'REGISTER', 'INTERNAL'} + bl_property = "pose_key_name" # Focus the text input box list_context_path: StringProperty() active_idx_context_path: StringProperty() @@ -387,8 +380,43 @@ class OBJECT_OT_pose_key_add(UILIST_OT_Entry_Add, Operator): return {'FINISHED'} +class OBJECT_OT_pose_key_auto_init(Operator): + """Assign the current Action and scene frame number to this pose key""" + + bl_idname = "object.posekey_auto_init" + bl_label = "Initialize From Context" + bl_options = {'UNDO', 'REGISTER', 'INTERNAL'} + + @classmethod + def poll(cls, context): + obj = context.object + arm_ob = get_deforming_armature(obj) + if not arm_ob: + cls.poll_message_set("No deforming armature.") + return False + if not (arm_ob.animation_data and arm_ob.animation_data.action): + cls.poll_message_set("Armature has no Action assigned.") + return False + obj = context.object + pose_key = get_active_pose_key(obj) + if pose_key.action == arm_ob.animation_data.action and pose_key.frame == context.scene.frame_current: + cls.poll_message_set("Action and frame number are already set.") + return False + return True + + def execute(self, context): + # Set action and frame number to the current ones. + obj = context.object + pose_key = get_active_pose_key(obj) + arm_ob = get_deforming_armature(obj) + pose_key.action = arm_ob.animation_data.action + pose_key.frame = context.scene.frame_current + self.report({'INFO'}, "Initialized Pose Key data.") + return {'FINISHED'} + + class OBJECT_OT_pose_key_save(Operator, OperatorWithWarning, SaveAndRestoreState): - """Save the current evaluated mesh vertex positions into the Storage Object""" + """Save the deformed mesh vertex positions of the current pose into the Storage Object""" bl_idname = "object.posekey_save" bl_label = "Overwrite Storage Object" @@ -396,36 +424,18 @@ class OBJECT_OT_pose_key_save(Operator, OperatorWithWarning, SaveAndRestoreState @classmethod def poll(cls, context): - obj_psk_arm = poll_has_psk_and_deformed(cls, context) - if not obj_psk_arm: - return False - obj, pose_key, arm_ob = obj_psk_arm - if ( - not pose_key.storage_object - and not pose_key.action - and arm_ob.animation_data - and arm_ob.animation_data.action - ): - # If we can guess the action, allow the operator to run. - # Let's call this "Initialize mode", since we're allowing the - # user to 1-click initialize some variables. - return True - - if not poll_correct_pose_key_pose(cls, context): - return False - - return True + return poll_correct_pose_key_pose(cls, context) def invoke(self, context, event): obj = context.object - pose_key = obj.data.pose_keys[obj.data.active_pose_key_index] + pose_key = get_active_pose_key(obj) if pose_key.storage_object: return super().invoke(context, event) return self.execute(context) def get_warning_text(self, context): obj = context.object - pose_key = obj.data.pose_keys[obj.data.active_pose_key_index] + pose_key = get_active_pose_key(obj) return f'Overwrite storage object "{pose_key.storage_object.name}"?' def execute(self, context): @@ -450,13 +460,6 @@ class OBJECT_OT_pose_key_save(Operator, OperatorWithWarning, SaveAndRestoreState pose_key.storage_object = storage_ob storage_ob.location = rigged_ob.location storage_ob.location.x -= rigged_ob.dimensions.x * 1.1 - - # Set action and frame number to the current ones, in case the user - # is already in the desired pose for this pose key. - arm_ob = get_deforming_armature(rigged_ob) - if arm_ob and arm_ob.animation_data and arm_ob.animation_data.action: - pose_key.action = arm_ob.animation_data.action - pose_key.frame = context.scene.frame_current else: old_mesh = storage_ob.data storage_ob.data = storage_ob_mesh @@ -535,7 +538,7 @@ class OBJECT_OT_pose_key_push(Operator, OperatorWithWarning, SaveAndRestoreState def get_warning_text(self, context): obj = context.object - pose_key = obj.data.pose_keys[obj.data.active_pose_key_index] + pose_key = get_active_pose_key(obj) target_shape_names = [target.name for target in pose_key.target_shapes if target] return ( "This will overwrite the following Shape Keys: \n " @@ -775,7 +778,7 @@ class OBJECT_OT_pose_key_place_objects_in_grid(Operator): return {'FINISHED'} -class OBJECT_OT_pose_key_jump_to_shape(Operator): +class OBJECT_OT_pose_key_jump_to_storage(Operator): """Place the storage object next to this object and select it""" bl_idname = "object.posekey_jump_to_storage" @@ -785,7 +788,7 @@ class OBJECT_OT_pose_key_jump_to_shape(Operator): @staticmethod def get_storage_object(context): obj = context.object - pose_key = obj.data.pose_keys[obj.data.active_pose_key_index] + pose_key = get_active_pose_key(obj) return pose_key.storage_object @classmethod @@ -863,6 +866,7 @@ class OBJECT_OT_pose_key_copy_data(Operator): registry = [ + OBJECT_OT_pose_key_auto_init, OBJECT_OT_pose_key_add, OBJECT_OT_pose_key_save, OBJECT_OT_pose_key_set_pose, @@ -871,6 +875,6 @@ registry = [ OBJECT_OT_create_shape_key_for_pose, OBJECT_OT_pose_key_clamp_influence, OBJECT_OT_pose_key_place_objects_in_grid, - OBJECT_OT_pose_key_jump_to_shape, + OBJECT_OT_pose_key_jump_to_storage, OBJECT_OT_pose_key_copy_data, ] diff --git a/scripts-blender/addons/pose_shape_keys/ui.py b/scripts-blender/addons/pose_shape_keys/ui.py index 3a18771e..705408c0 100644 --- a/scripts-blender/addons/pose_shape_keys/ui.py +++ b/scripts-blender/addons/pose_shape_keys/ui.py @@ -1,9 +1,10 @@ import bpy from bpy.types import Object, Panel, UIList, Menu from bl_ui.properties_data_mesh import DATA_PT_shape_keys -from .ui_list import draw_ui_list from bpy.props import EnumProperty +from .ui_list import draw_ui_list +from .ops import get_deforming_armature from .prefs import get_addon_prefs @@ -72,7 +73,7 @@ class MESH_PT_pose_keys(Panel): bl_region_type = 'WINDOW' bl_context = 'data' bl_options = {'DEFAULT_CLOSED'} - bl_label = "Shape/Pose Keys" + bl_label = "Pose Shape Keys" @classmethod def poll(cls, context): @@ -81,16 +82,17 @@ class MESH_PT_pose_keys(Panel): def draw(self, context): obj = context.object mesh = obj.data - layout = self.layout + layout = self.layout.column() - layout.prop(mesh, 'shape_key_ui_type', text="List Type: ", expand=True) + layout.row().prop(mesh, 'shape_key_ui_type', expand=True) if mesh.shape_key_ui_type == 'DEFAULT': return DATA_PT_shape_keys.draw(self, context) - if not obj_has_armature_mod(obj): + arm_ob = get_deforming_armature(obj) + if not arm_ob: layout.alert = True - layout.label(text="Object must have an Armature modifier to use Pose Keys.") + layout.label(text="Object must be deformed by an Armature to use Pose Keys.") return if mesh.shape_keys and not mesh.shape_keys.use_relative: @@ -120,33 +122,30 @@ class MESH_PT_pose_keys(Panel): idx = context.object.data.active_pose_key_index active_posekey = context.object.data.pose_keys[idx] - col = layout.column(align=True) - col.prop(active_posekey, 'action') + action_split = layout.row().split(factor=0.4, align=True) + action_split.alignment='RIGHT' + action_split.label(text="Action") + row = action_split.row(align=True) + icon = 'FORWARD' if active_posekey.action: - col.prop(active_posekey, 'frame') - - if active_posekey.storage_object: - row = layout.row() - row.prop(active_posekey, 'storage_object') - row.operator('object.posekey_jump_to_storage', text="", icon='RESTRICT_SELECT_OFF') - else: - layout.operator('object.posekey_set_pose', text="Set Pose", icon="ARMATURE_DATA") - row = layout.row() - text = "Store Posed Mesh" - if not active_posekey.storage_object and not active_posekey.action: - text = "Store Mesh & Init Pose Key" - row.operator('object.posekey_save', text=text, icon="FILE_TICK") - row.prop(active_posekey, 'storage_object', text="") - return + icon = 'FILE_REFRESH' + row.operator('object.posekey_auto_init', text="", icon=icon) + row.prop(active_posekey, 'action', text="") + layout.prop(active_posekey, 'frame') layout.separator() - col = layout.column(align=True) - col.operator('object.posekey_set_pose', text="Set Pose", icon="ARMATURE_DATA") - col.separator() - row = col.row() - row.operator('object.posekey_save', text="Overwrite Storage Object", icon="FILE_TICK") - row.operator('object.posekey_push', text="Overwrite Shape Keys", icon="IMPORT") + layout.operator('object.posekey_set_pose', text="Set Pose", icon="ARMATURE_DATA") + + layout.separator() + + row = layout.row(align=True) + text = "Save Posed Mesh" + if active_posekey.storage_object: + text="Overwrite Posed Mesh" + row.operator('object.posekey_save', text=text, icon="FILE_TICK") + row.prop(active_posekey, 'storage_object', text="") + row.operator('object.posekey_jump_to_storage', text="", icon='RESTRICT_SELECT_OFF') class MESH_PT_shape_key_subpanel(Panel): @@ -178,6 +177,8 @@ class MESH_PT_shape_key_subpanel(Panel): idx = context.object.data.active_pose_key_index active_posekey = context.object.data.pose_keys[idx] + layout.operator('object.posekey_push', text="Overwrite Shape Keys", icon="IMPORT") + draw_ui_list( layout, context, @@ -250,7 +251,7 @@ def register(): ('DEFAULT', 'Shape Keys', "Show a flat list of shape keys"), ( 'POSE_KEYS', - 'Pose Keys', + 'Pose Shape Keys', "Organize shape keys into a higher-level concept called Pose Keys. These can store vertex positions and push one shape to multiple shape keys at once, relative to existing deformation", ), ], -- 2.30.2 From 6ca9d82ebcf10d8c414d58fe7e4d7964b54c51d0 Mon Sep 17 00:00:00 2001 From: Demeter Dzadik Date: Tue, 2 Jul 2024 16:55:04 +0200 Subject: [PATCH 06/10] Cleanup: More sensible code order --- scripts-blender/addons/pose_shape_keys/ops.py | 550 +++++++++--------- scripts-blender/addons/pose_shape_keys/ui.py | 158 +++-- 2 files changed, 352 insertions(+), 356 deletions(-) diff --git a/scripts-blender/addons/pose_shape_keys/ops.py b/scripts-blender/addons/pose_shape_keys/ops.py index a8c79179..e89bb0f7 100644 --- a/scripts-blender/addons/pose_shape_keys/ops.py +++ b/scripts-blender/addons/pose_shape_keys/ops.py @@ -3,6 +3,7 @@ from bpy.types import Object, Operator from bpy.props import StringProperty from mathutils import Vector from math import sqrt + from .symmetrize_shape_key import mirror_mesh from .prefs import get_addon_prefs from .ui_list import UILIST_OT_Entry_Add @@ -29,87 +30,99 @@ DEFORM_MODIFIERS = [ GOOD_MODIFIERS = ['ARMATURE'] -def get_deforming_armature(mesh_ob: Object) -> Object: - for mod in mesh_ob.modifiers: - if mod.type == 'ARMATURE': - return mod.object +class OBJECT_OT_pose_key_add(UILIST_OT_Entry_Add, Operator): + """Add Pose Shape Key""" - -class OBJECT_OT_create_shape_key_for_pose(Operator): - """Create and assign a Shape Key""" - - bl_idname = "object.create_shape_key_for_pose" - bl_label = "Create Shape Key" + bl_idname = "object.posekey_add" + bl_label = "Add Pose Shape Key" bl_options = {'UNDO', 'REGISTER', 'INTERNAL'} - bl_property = "sk_name" + bl_property = "pose_key_name" # Focus the text input box - def update_sk_name(self, context): - def set_vg(vg_name): - obj = context.object - vg = obj.vertex_groups.get(vg_name) - if vg: - self.vg_name = vg.name - return vg + list_context_path: StringProperty() + active_idx_context_path: StringProperty() - obj = context.object - vg = set_vg(self.sk_name) - if not vg and self.sk_name.endswith(".L"): - vg = set_vg("Side.L") - if not vg and self.sk_name.endswith(".R"): - vg = set_vg("Side.R") - - sk_name: StringProperty( - name="Name", - description="Name to set for the new shape key", - default="Key", - update=update_sk_name, - ) - vg_name: StringProperty( - name="Vertex Group", - description="Vertex Group to assign as the masking group of this shape key", - default="", - ) + pose_key_name: StringProperty(name="Name", default="Pose Key") def invoke(self, context, event): - obj = context.object - if obj.data.shape_keys: - self.sk_name = f"Key {len(obj.data.shape_keys.key_blocks)}" - else: - self.sk_name = "Key" - - pose_key = get_active_pose_key(obj) - if pose_key.name: - self.sk_name = pose_key.name - return context.window_manager.invoke_props_dialog(self) def draw(self, context): - layout = self.layout - layout.prop(self, 'sk_name') - obj = context.object - layout.prop_search(self, 'vg_name', obj, "vertex_groups") + self.layout.prop(self, 'pose_key_name') + if not self.pose_key_name: + self.layout.alert = True + self.layout.label(text="Name cannot be empty.", icon='ERROR') def execute(self, context): + if not self.pose_key_name: + self.report({'ERROR'}, "Must specify a name.") + return {'CANCELLED'} + + my_list = self.get_list(context) + active_index = self.get_active_index(context) + + to_index = active_index + 1 + if len(my_list) == 0: + to_index = 0 + + psk = my_list.add() + psk.name = self.pose_key_name + my_list.move(len(my_list) - 1, to_index) + self.set_active_index(context, to_index) + + return {'FINISHED'} + + +class OBJECT_OT_pose_key_auto_init(Operator): + """Assign the current Action and scene frame number to this pose key""" + + bl_idname = "object.posekey_auto_init" + bl_label = "Initialize From Context" + bl_options = {'UNDO', 'REGISTER', 'INTERNAL'} + + @classmethod + def poll(cls, context): + obj = context.object + arm_ob = get_deforming_armature(obj) + if not arm_ob: + cls.poll_message_set("No deforming armature.") + return False + if not (arm_ob.animation_data and arm_ob.animation_data.action): + cls.poll_message_set("Armature has no Action assigned.") + return False obj = context.object - - # Ensure Basis shape key - if not obj.data.shape_keys: - basis = obj.shape_key_add() - basis.name = "Basis" - obj.data.update() - - # Add new shape key - new_sk = obj.shape_key_add() - new_sk.name = self.sk_name - new_sk.value = 1 - if self.vg_name: - new_sk.vertex_group = self.vg_name - pose_key = get_active_pose_key(obj) - target = pose_key.target_shapes[pose_key.active_target_shape_index] - target.name = new_sk.name + if ( + pose_key.action == arm_ob.animation_data.action + and pose_key.frame == context.scene.frame_current + ): + cls.poll_message_set("Action and frame number are already set.") + return False + return True - self.report({'INFO'}, f"Added shape key {new_sk.name}.") + def execute(self, context): + # Set action and frame number to the current ones. + obj = context.object + pose_key = get_active_pose_key(obj) + arm_ob = get_deforming_armature(obj) + pose_key.action = arm_ob.animation_data.action + pose_key.frame = context.scene.frame_current + self.report({'INFO'}, "Initialized Pose Key data.") + return {'FINISHED'} + + +class OBJECT_OT_pose_key_set_pose(Operator): + """Reset the rig, then set the above Action and frame number""" + + bl_idname = "object.posekey_set_pose" + bl_label = "Set Pose" + bl_options = {'UNDO', 'REGISTER', 'INTERNAL'} + + @classmethod + def poll(cls, context): + return poll_correct_pose_key_pose(cls, context, demand_pose=False) + + def execute(self, context): + set_pose_of_active_pose_key(context) return {'FINISHED'} @@ -209,212 +222,6 @@ class OperatorWithWarning: raise NotImplemented -def reset_rig(rig, *, reset_transforms=True, reset_props=True, pbones=[]): - if not pbones: - pbones = rig.pose.bones - for pb in pbones: - if reset_transforms: - pb.location = (0, 0, 0) - pb.rotation_euler = (0, 0, 0) - pb.rotation_quaternion = (1, 0, 0, 0) - pb.scale = (1, 1, 1) - - if not reset_props or len(pb.keys()) == 0: - continue - - rna_properties = [prop.identifier for prop in pb.bl_rna.properties if prop.is_runtime] - - # Reset custom property values to their default value - for key in pb.keys(): - if key.startswith("$"): - continue - if key in rna_properties: - continue # Addon defined property. - - property_settings = None - try: - property_settings = pb.id_properties_ui(key) - if not property_settings: - continue - property_settings = property_settings.as_dict() - if not 'default' in property_settings: - continue - except TypeError: - # Some properties don't support UI data, and so don't have a default value. (like addon PropertyGroups) - pass - - if not property_settings: - continue - - if type(pb[key]) not in (float, int, bool): - continue - pb[key] = property_settings['default'] - - -def set_pose_of_active_pose_key(context): - rigged_ob = context.object - pose_key = rigged_ob.data.pose_keys[rigged_ob.data.active_pose_key_index] - - arm_ob = get_deforming_armature(rigged_ob) - reset_rig(arm_ob) - if pose_key.action: - # Set Action and Frame to get the right pose - arm_ob.animation_data.action = pose_key.action - context.scene.frame_current = pose_key.frame - - -class OBJECT_OT_pose_key_set_pose(Operator): - """Reset the rig, then set the above Action and frame number""" - - bl_idname = "object.posekey_set_pose" - bl_label = "Set Pose" - bl_options = {'UNDO', 'REGISTER', 'INTERNAL'} - - @classmethod - def poll(cls, context): - return poll_correct_pose_key_pose(cls, context, demand_pose=False) - - def execute(self, context): - set_pose_of_active_pose_key(context) - return {'FINISHED'} - - -def get_active_pose_key(obj): - if obj.type != 'MESH': - return - if len(obj.data.pose_keys) == 0: - return - - return obj.data.pose_keys[obj.data.active_pose_key_index] - - -def poll_correct_pose_key_pose(operator, context, demand_pose=True): - """To make these operators foolproof, there are a lot of checks to make sure - that the user gets to see the effect of the operator. The "Set Pose" operator - can be used first to set the correct state and pass all the checks here. - """ - - obj = context.object - - if not obj: - operator.poll_message_set("There must be an active mesh object.") - return False - - pose_key = get_active_pose_key(obj) - if not pose_key: - operator.poll_message_set("A Pose Shape Key must be selected.") - return False - if not pose_key.name: - operator.poll_message_set("The Pose Shape Key must be named.") - - arm_ob = get_deforming_armature(obj) - - if not arm_ob: - operator.poll_message_set("This mesh object is not deformed by any Armature modifier.") - return False - - if not pose_key.action: - operator.poll_message_set("An Action must be associated with the Pose Shape Key.") - return False - - if demand_pose: - # Action must exist and match. - if not ( - arm_ob.animation_data - and arm_ob.animation_data.action - and arm_ob.animation_data.action == pose_key.action - ): - operator.poll_message_set( - "The armature must have the Pose Shape Key's action assigned. Use the Set Pose button." - ) - return False - - if pose_key.frame != context.scene.frame_current: - operator.poll_message_set( - "The Pose Shape Key's frame must be the same as the current scene frame. Use the Set Pose button." - ) - return False - - return True - - -class OBJECT_OT_pose_key_add(UILIST_OT_Entry_Add, Operator): - """Add Pose Shape Key""" - - bl_idname = "object.posekey_add" - bl_label = "Add Pose Shape Key" - bl_options = {'UNDO', 'REGISTER', 'INTERNAL'} - bl_property = "pose_key_name" # Focus the text input box - - list_context_path: StringProperty() - active_idx_context_path: StringProperty() - - pose_key_name: StringProperty(name="Name", default="Pose Key") - - def invoke(self, context, event): - return context.window_manager.invoke_props_dialog(self) - - def draw(self, context): - self.layout.prop(self, 'pose_key_name') - if not self.pose_key_name: - self.layout.alert = True - self.layout.label(text="Name cannot be empty.", icon='ERROR') - - def execute(self, context): - if not self.pose_key_name: - self.report({'ERROR'}, "Must specify a name.") - return {'CANCELLED'} - - my_list = self.get_list(context) - active_index = self.get_active_index(context) - - to_index = active_index + 1 - if len(my_list) == 0: - to_index = 0 - - psk = my_list.add() - psk.name = self.pose_key_name - my_list.move(len(my_list) - 1, to_index) - self.set_active_index(context, to_index) - - return {'FINISHED'} - - -class OBJECT_OT_pose_key_auto_init(Operator): - """Assign the current Action and scene frame number to this pose key""" - - bl_idname = "object.posekey_auto_init" - bl_label = "Initialize From Context" - bl_options = {'UNDO', 'REGISTER', 'INTERNAL'} - - @classmethod - def poll(cls, context): - obj = context.object - arm_ob = get_deforming_armature(obj) - if not arm_ob: - cls.poll_message_set("No deforming armature.") - return False - if not (arm_ob.animation_data and arm_ob.animation_data.action): - cls.poll_message_set("Armature has no Action assigned.") - return False - obj = context.object - pose_key = get_active_pose_key(obj) - if pose_key.action == arm_ob.animation_data.action and pose_key.frame == context.scene.frame_current: - cls.poll_message_set("Action and frame number are already set.") - return False - return True - - def execute(self, context): - # Set action and frame number to the current ones. - obj = context.object - pose_key = get_active_pose_key(obj) - arm_ob = get_deforming_armature(obj) - pose_key.action = arm_ob.animation_data.action - pose_key.frame = context.scene.frame_current - self.report({'INFO'}, "Initialized Pose Key data.") - return {'FINISHED'} - - class OBJECT_OT_pose_key_save(Operator, OperatorWithWarning, SaveAndRestoreState): """Save the deformed mesh vertex positions of the current pose into the Storage Object""" @@ -675,6 +482,84 @@ class OBJECT_OT_pose_key_push_all(Operator, OperatorWithWarning, SaveAndRestoreS return {'FINISHED'} +class OBJECT_OT_create_shape_key_for_pose(Operator): + """Create and assign a Shape Key""" + + bl_idname = "object.create_shape_key_for_pose" + bl_label = "Create Shape Key" + bl_options = {'UNDO', 'REGISTER', 'INTERNAL'} + bl_property = "sk_name" + + def update_sk_name(self, context): + def set_vg(vg_name): + obj = context.object + vg = obj.vertex_groups.get(vg_name) + if vg: + self.vg_name = vg.name + return vg + + obj = context.object + vg = set_vg(self.sk_name) + if not vg and self.sk_name.endswith(".L"): + vg = set_vg("Side.L") + if not vg and self.sk_name.endswith(".R"): + vg = set_vg("Side.R") + + sk_name: StringProperty( + name="Name", + description="Name to set for the new shape key", + default="Key", + update=update_sk_name, + ) + vg_name: StringProperty( + name="Vertex Group", + description="Vertex Group to assign as the masking group of this shape key", + default="", + ) + + def invoke(self, context, event): + obj = context.object + if obj.data.shape_keys: + self.sk_name = f"Key {len(obj.data.shape_keys.key_blocks)}" + else: + self.sk_name = "Key" + + pose_key = get_active_pose_key(obj) + if pose_key.name: + self.sk_name = pose_key.name + + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + layout = self.layout + layout.prop(self, 'sk_name') + obj = context.object + layout.prop_search(self, 'vg_name', obj, "vertex_groups") + + def execute(self, context): + obj = context.object + + # Ensure Basis shape key + if not obj.data.shape_keys: + basis = obj.shape_key_add() + basis.name = "Basis" + obj.data.update() + + # Add new shape key + new_sk = obj.shape_key_add() + new_sk.name = self.sk_name + new_sk.value = 1 + if self.vg_name: + new_sk.vertex_group = self.vg_name + + pose_key = get_active_pose_key(obj) + target = pose_key.target_shapes[pose_key.active_target_shape_index] + target.name = new_sk.name + + self.report({'INFO'}, f"Added shape key {new_sk.name}.") + return {'FINISHED'} + + class OBJECT_OT_pose_key_clamp_influence(Operator): """Clamp the influence of this pose key's shape keys to 1.0 for each vertex, by normalizing the vertex weight mask values of vertices where the total influence is greater than 1""" @@ -865,6 +750,125 @@ class OBJECT_OT_pose_key_copy_data(Operator): return {'FINISHED'} +def get_deforming_armature(mesh_ob: Object) -> Object | None: + for mod in mesh_ob.modifiers: + if mod.type == 'ARMATURE': + return mod.object + + +def reset_rig(rig, *, reset_transforms=True, reset_props=True, pbones=[]): + if not pbones: + pbones = rig.pose.bones + for pb in pbones: + if reset_transforms: + pb.location = (0, 0, 0) + pb.rotation_euler = (0, 0, 0) + pb.rotation_quaternion = (1, 0, 0, 0) + pb.scale = (1, 1, 1) + + if not reset_props or len(pb.keys()) == 0: + continue + + rna_properties = [prop.identifier for prop in pb.bl_rna.properties if prop.is_runtime] + + # Reset custom property values to their default value + for key in pb.keys(): + if key.startswith("$"): + continue + if key in rna_properties: + continue # Addon defined property. + + property_settings = None + try: + property_settings = pb.id_properties_ui(key) + if not property_settings: + continue + property_settings = property_settings.as_dict() + if not 'default' in property_settings: + continue + except TypeError: + # Some properties don't support UI data, and so don't have a default value. (like addon PropertyGroups) + pass + + if not property_settings: + continue + + if type(pb[key]) not in (float, int, bool): + continue + pb[key] = property_settings['default'] + + +def set_pose_of_active_pose_key(context): + rigged_ob = context.object + pose_key = rigged_ob.data.pose_keys[rigged_ob.data.active_pose_key_index] + + arm_ob = get_deforming_armature(rigged_ob) + reset_rig(arm_ob) + if pose_key.action: + # Set Action and Frame to get the right pose + arm_ob.animation_data.action = pose_key.action + context.scene.frame_current = pose_key.frame + + +def poll_correct_pose_key_pose(operator, context, demand_pose=True): + """To make these operators foolproof, there are a lot of checks to make sure + that the user gets to see the effect of the operator. The "Set Pose" operator + can be used first to set the correct state and pass all the checks here. + """ + + obj = context.object + + if not obj: + operator.poll_message_set("There must be an active mesh object.") + return False + + pose_key = get_active_pose_key(obj) + if not pose_key: + operator.poll_message_set("A Pose Shape Key must be selected.") + return False + if not pose_key.name: + operator.poll_message_set("The Pose Shape Key must be named.") + + arm_ob = get_deforming_armature(obj) + + if not arm_ob: + operator.poll_message_set("This mesh object is not deformed by any Armature modifier.") + return False + + if not pose_key.action: + operator.poll_message_set("An Action must be associated with the Pose Shape Key.") + return False + + if demand_pose: + # Action must exist and match. + if not ( + arm_ob.animation_data + and arm_ob.animation_data.action + and arm_ob.animation_data.action == pose_key.action + ): + operator.poll_message_set( + "The armature must have the Pose Shape Key's action assigned. Use the Set Pose button." + ) + return False + + if pose_key.frame != context.scene.frame_current: + operator.poll_message_set( + "The Pose Shape Key's frame must be the same as the current scene frame. Use the Set Pose button." + ) + return False + + return True + + +def get_active_pose_key(obj): + if obj.type != 'MESH': + return + if len(obj.data.pose_keys) == 0: + return + + return obj.data.pose_keys[obj.data.active_pose_key_index] + + registry = [ OBJECT_OT_pose_key_auto_init, OBJECT_OT_pose_key_add, diff --git a/scripts-blender/addons/pose_shape_keys/ui.py b/scripts-blender/addons/pose_shape_keys/ui.py index 705408c0..a9c369ae 100644 --- a/scripts-blender/addons/pose_shape_keys/ui.py +++ b/scripts-blender/addons/pose_shape_keys/ui.py @@ -1,73 +1,13 @@ import bpy -from bpy.types import Object, Panel, UIList, Menu +from bpy.types import Panel, UIList, Menu from bl_ui.properties_data_mesh import DATA_PT_shape_keys from bpy.props import EnumProperty from .ui_list import draw_ui_list -from .ops import get_deforming_armature +from .ops import get_deforming_armature, poll_correct_pose_key_pose from .prefs import get_addon_prefs -class CK_UL_pose_keys(UIList): - def draw_item(self, context, layout, data, item, _icon, _active_data, _active_propname): - pose_key = item - - if self.layout_type != 'DEFAULT': - # Other layout types not supported by this UIList. - return - - split = layout.row().split(factor=0.7, align=True) - - icon = 'SURFACE_NCIRCLE' if pose_key.storage_object else 'CURVE_NCIRCLE' - name_row = split.row() - if not pose_key.name: - name_row.alert = True - split = name_row.split() - name_row = split.row() - split.label(text="Unnamed!", icon='ERROR') - name_row.prop(pose_key, 'name', text="", emboss=False, icon=icon) - - -class CK_UL_target_keys(UIList): - def draw_item(self, context, layout, data, item, _icon, _active_data, _active_propname): - obj = context.object - pose_key_target = item - key_block = pose_key_target.key_block - - if self.layout_type != 'DEFAULT': - # Other layout types not supported by this UIList. - return - - split = layout.row().split(factor=0.7, align=True) - - name_row = split.row() - name_row.prop(pose_key_target, 'name', text="", emboss=False, icon='SHAPEKEY_DATA') - - value_row = split.row(align=True) - value_row.emboss = 'NONE_OR_STATUS' - if not key_block: - return - if ( - key_block.mute - or (obj.mode == 'EDIT' and not (obj.use_shape_key_edit_mode and obj.type == 'MESH')) - or (obj.show_only_shape_key and key_block != obj.active_shape_key) - ): - name_row.active = value_row.active = False - - value_row.prop(key_block, "value", text="") - - mute_row = split.row() - mute_row.alignment = 'RIGHT' - mute_row.prop(key_block, 'mute', emboss=False, text="") - - -def obj_has_armature_mod(obj: Object) -> bool: - for mod in obj.modifiers: - if mod.type == 'ARMATURE': - return True - return False - - class MESH_PT_pose_keys(Panel): bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' @@ -106,7 +46,7 @@ class MESH_PT_pose_keys(Panel): draw_ui_list( groups_col, context, - class_name='CK_UL_pose_keys', + class_name='POSEKEYS_UL_pose_keys', list_context_path='object.data.pose_keys', active_idx_context_path='object.data.active_pose_key_index', menu_class_name='MESH_MT_pose_key_utils', @@ -123,7 +63,7 @@ class MESH_PT_pose_keys(Panel): active_posekey = context.object.data.pose_keys[idx] action_split = layout.row().split(factor=0.4, align=True) - action_split.alignment='RIGHT' + action_split.alignment = 'RIGHT' action_split.label(text="Action") row = action_split.row(align=True) icon = 'FORWARD' @@ -142,12 +82,43 @@ class MESH_PT_pose_keys(Panel): row = layout.row(align=True) text = "Save Posed Mesh" if active_posekey.storage_object: - text="Overwrite Posed Mesh" + text = "Overwrite Posed Mesh" row.operator('object.posekey_save', text=text, icon="FILE_TICK") row.prop(active_posekey, 'storage_object', text="") row.operator('object.posekey_jump_to_storage', text="", icon='RESTRICT_SELECT_OFF') +class POSEKEYS_UL_pose_keys(UIList): + def draw_item(self, context, layout, data, item, _icon, _active_data, _active_propname): + pose_key = item + + if self.layout_type != 'DEFAULT': + # Other layout types not supported by this UIList. + return + + split = layout.row().split(factor=0.7, align=True) + + icon = 'SURFACE_NCIRCLE' if pose_key.storage_object else 'CURVE_NCIRCLE' + name_row = split.row() + if not pose_key.name: + name_row.alert = True + split = name_row.split() + name_row = split.row() + split.label(text="Unnamed!", icon='ERROR') + name_row.prop(pose_key, 'name', text="", emboss=False, icon=icon) + + +class MESH_MT_pose_key_utils(Menu): + bl_label = "Pose Key Utilities" + + def draw(self, context): + layout = self.layout + layout.operator('object.posekey_object_grid', icon='LIGHTPROBE_VOLUME') + layout.operator('object.posekey_push_all', icon='WORLD') + layout.operator('object.posekey_clamp_influence', icon='NORMALIZE_FCURVES') + layout.operator('object.posekey_copy_data', icon='PASTEDOWN') + + class MESH_PT_shape_key_subpanel(Panel): bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' @@ -158,13 +129,12 @@ class MESH_PT_shape_key_subpanel(Panel): @classmethod def poll(cls, context): - obj = context.object - return ( - obj.data.shape_key_ui_type == 'POSE_KEYS' - and len(obj.data.pose_keys) > 0 - and obj.data.pose_keys[obj.data.active_pose_key_index].storage_object - and obj_has_armature_mod(obj) - ) + try: + return poll_correct_pose_key_pose(cls, context) + except AttributeError: + # Happens any time that function tries to set a poll message, + # since panels don't have poll messages, lol. + return False def draw(self, context): obj = context.object @@ -182,7 +152,7 @@ class MESH_PT_shape_key_subpanel(Panel): draw_ui_list( layout, context, - class_name='CK_UL_target_keys', + class_name='POSEKEYS_UL_target_shape_keys', list_context_path=f'object.data.pose_keys[{idx}].target_shapes', active_idx_context_path=f'object.data.pose_keys[{idx}].active_target_shape_index', ) @@ -217,15 +187,37 @@ class MESH_PT_shape_key_subpanel(Panel): col.row().prop(sk, 'relative_key') -class MESH_MT_pose_key_utils(Menu): - bl_label = "Pose Key Utilities" +class POSEKEYS_UL_target_shape_keys(UIList): + def draw_item(self, context, layout, data, item, _icon, _active_data, _active_propname): + obj = context.object + pose_key_target = item + key_block = pose_key_target.key_block - def draw(self, context): - layout = self.layout - layout.operator('object.posekey_object_grid', icon='LIGHTPROBE_VOLUME') - layout.operator('object.posekey_push_all', icon='WORLD') - layout.operator('object.posekey_clamp_influence', icon='NORMALIZE_FCURVES') - layout.operator('object.posekey_copy_data', icon='PASTEDOWN') + if self.layout_type != 'DEFAULT': + # Other layout types not supported by this UIList. + return + + split = layout.row().split(factor=0.7, align=True) + + name_row = split.row() + name_row.prop(pose_key_target, 'name', text="", emboss=False, icon='SHAPEKEY_DATA') + + value_row = split.row(align=True) + value_row.emboss = 'NONE_OR_STATUS' + if not key_block: + return + if ( + key_block.mute + or (obj.mode == 'EDIT' and not (obj.use_shape_key_edit_mode and obj.type == 'MESH')) + or (obj.show_only_shape_key and key_block != obj.active_shape_key) + ): + name_row.active = value_row.active = False + + value_row.prop(key_block, "value", text="") + + mute_row = split.row() + mute_row.alignment = 'RIGHT' + mute_row.prop(key_block, 'mute', emboss=False, text="") @classmethod @@ -236,8 +228,8 @@ def shape_key_panel_new_poll(cls, context): registry = [ - CK_UL_pose_keys, - CK_UL_target_keys, + POSEKEYS_UL_pose_keys, + POSEKEYS_UL_target_shape_keys, MESH_PT_pose_keys, MESH_PT_shape_key_subpanel, MESH_MT_pose_key_utils, -- 2.30.2 From 485f405d8385bf0f38f16ff6a7734d20f66fa86e Mon Sep 17 00:00:00 2001 From: Demeter Dzadik Date: Tue, 2 Jul 2024 18:09:14 +0200 Subject: [PATCH 07/10] Smarter shape key add/remove operators - Adding both Pose Keys and Shape Keys now prompts to enter a name, rather than having to do it as a separate action. - Adding Shape Keys also prompts Vertex Groups. You can also choose to either create new or browse existing Shape Key and Vertex Group. - Removing a Shape Key Slot also removes the corresponding shape key and all of its drivers, unless Shift is held, which is mentioned in the tooltip. --- scripts-blender/addons/pose_shape_keys/ops.py | 286 +++++++++++++----- .../addons/pose_shape_keys/props.py | 4 + scripts-blender/addons/pose_shape_keys/ui.py | 13 +- 3 files changed, 215 insertions(+), 88 deletions(-) diff --git a/scripts-blender/addons/pose_shape_keys/ops.py b/scripts-blender/addons/pose_shape_keys/ops.py index e89bb0f7..83edb29e 100644 --- a/scripts-blender/addons/pose_shape_keys/ops.py +++ b/scripts-blender/addons/pose_shape_keys/ops.py @@ -1,12 +1,12 @@ import bpy from bpy.types import Object, Operator -from bpy.props import StringProperty +from bpy.props import StringProperty, BoolProperty from mathutils import Vector from math import sqrt from .symmetrize_shape_key import mirror_mesh from .prefs import get_addon_prefs -from .ui_list import UILIST_OT_Entry_Add +from .ui_list import UILIST_OT_Entry_Add, UILIST_OT_Entry_Remove # When saving or pushing shapes, disable any modifier NOT in this list. DEFORM_MODIFIERS = [ @@ -47,10 +47,13 @@ class OBJECT_OT_pose_key_add(UILIST_OT_Entry_Add, Operator): return context.window_manager.invoke_props_dialog(self) def draw(self, context): - self.layout.prop(self, 'pose_key_name') + layout = self.layout.column() + layout.use_property_split=True + + layout.prop(self, 'pose_key_name') if not self.pose_key_name: - self.layout.alert = True - self.layout.label(text="Name cannot be empty.", icon='ERROR') + layout.alert = True + layout.label(text="Name cannot be empty.", icon='ERROR') def execute(self, context): if not self.pose_key_name: @@ -482,84 +485,6 @@ class OBJECT_OT_pose_key_push_all(Operator, OperatorWithWarning, SaveAndRestoreS return {'FINISHED'} -class OBJECT_OT_create_shape_key_for_pose(Operator): - """Create and assign a Shape Key""" - - bl_idname = "object.create_shape_key_for_pose" - bl_label = "Create Shape Key" - bl_options = {'UNDO', 'REGISTER', 'INTERNAL'} - bl_property = "sk_name" - - def update_sk_name(self, context): - def set_vg(vg_name): - obj = context.object - vg = obj.vertex_groups.get(vg_name) - if vg: - self.vg_name = vg.name - return vg - - obj = context.object - vg = set_vg(self.sk_name) - if not vg and self.sk_name.endswith(".L"): - vg = set_vg("Side.L") - if not vg and self.sk_name.endswith(".R"): - vg = set_vg("Side.R") - - sk_name: StringProperty( - name="Name", - description="Name to set for the new shape key", - default="Key", - update=update_sk_name, - ) - vg_name: StringProperty( - name="Vertex Group", - description="Vertex Group to assign as the masking group of this shape key", - default="", - ) - - def invoke(self, context, event): - obj = context.object - if obj.data.shape_keys: - self.sk_name = f"Key {len(obj.data.shape_keys.key_blocks)}" - else: - self.sk_name = "Key" - - pose_key = get_active_pose_key(obj) - if pose_key.name: - self.sk_name = pose_key.name - - return context.window_manager.invoke_props_dialog(self) - - def draw(self, context): - layout = self.layout - layout.prop(self, 'sk_name') - obj = context.object - layout.prop_search(self, 'vg_name', obj, "vertex_groups") - - def execute(self, context): - obj = context.object - - # Ensure Basis shape key - if not obj.data.shape_keys: - basis = obj.shape_key_add() - basis.name = "Basis" - obj.data.update() - - # Add new shape key - new_sk = obj.shape_key_add() - new_sk.name = self.sk_name - new_sk.value = 1 - if self.vg_name: - new_sk.vertex_group = self.vg_name - - pose_key = get_active_pose_key(obj) - target = pose_key.target_shapes[pose_key.active_target_shape_index] - target.name = new_sk.name - - self.report({'INFO'}, f"Added shape key {new_sk.name}.") - return {'FINISHED'} - - class OBJECT_OT_pose_key_clamp_influence(Operator): """Clamp the influence of this pose key's shape keys to 1.0 for each vertex, by normalizing the vertex weight mask values of vertices where the total influence is greater than 1""" @@ -750,6 +675,198 @@ class OBJECT_OT_pose_key_copy_data(Operator): return {'FINISHED'} +class OBJECT_OT_pose_key_shape_add(UILIST_OT_Entry_Add, Operator): + """Add Target Shape Key""" + + bl_idname = "object.posekey_shape_add" + bl_label = "Add Target Shape Key" + bl_options = {'UNDO', 'REGISTER', 'INTERNAL'} + + list_context_path: StringProperty() + active_idx_context_path: StringProperty() + + def update_sk_name(self, context): + def set_vg(vg_name): + obj = context.object + vg = obj.vertex_groups.get(vg_name) + if vg: + self.vg_name = vg.name + return vg + + obj = context.object + vg = set_vg(self.sk_name) + if not vg and self.sk_name.endswith(".L"): + vg = set_vg("Side.L") + if not vg and self.sk_name.endswith(".R"): + vg = set_vg("Side.R") + + sk_name: StringProperty( + name="Name", + description="Name to set for the new shape key", + default="Key", + update=update_sk_name, + ) + create_sk: BoolProperty( + name="Create New Shape Key", + description="Create a new blank Shape Key to push this pose into", + default=True, + ) + vg_name: StringProperty( + name="Vertex Group", + description="Vertex Group to assign as the masking group of this shape key", + default="", + ) + + def update_create_vg(self, context): + if self.create_vg: + self.vg_name = self.sk_name + create_vg: BoolProperty( + name="Create New Vertex Group", + description="Create a new blank Vertex Group as a mask for this shape key. This means the shape key won't work until this mask is authored", + default=False, + update=update_create_vg + ) + + create_slot: BoolProperty( + name="Create New Slot", + description="Internal. Whether to assign the chosen (or created) shape key to the current slot, or to create a new one", + default=True + ) + + def invoke(self, context, event): + obj = context.object + if obj.data.shape_keys: + self.sk_name = f"Key {len(obj.data.shape_keys.key_blocks)}" + else: + self.sk_name = "Key" + + pose_key = get_active_pose_key(obj) + if pose_key.name: + self.sk_name = pose_key.name + if not self.create_slot and pose_key.active_target: + self.sk_name = pose_key.active_target.name + + return context.window_manager.invoke_props_dialog(self, width=350) + + def draw(self, context): + layout = self.layout.column() + layout.use_property_split=True + + obj = context.object + + row = layout.row(align=True) + if self.create_sk: + row.prop(self, 'sk_name', icon='SHAPEKEY_DATA') + else: + row.prop_search( + self, 'sk_name', obj.data.shape_keys, 'key_blocks', icon='SHAPEKEY_DATA' + ) + row.prop(self, 'create_sk', text="", icon='ADD') + + row = layout.row(align=True) + if self.create_vg: + if obj.vertex_groups.get(self.vg_name): + row.alert=True + layout.label(text="Cannot create that vertex group because it already exists!", icon='ERROR') + row.prop(self, 'vg_name', icon='GROUP_VERTEX') + else: + row.prop_search(self, 'vg_name', obj, "vertex_groups") + row.prop(self, 'create_vg', text="", icon='ADD') + + def execute(self, context): + obj = context.object + + if self.create_vg and obj.vertex_groups.get(self.vg_name): + self.report({'ERROR'}, f"Vertex group '{self.vg_name}' already exists!") + return {'CANCELLED'} + + # Ensure Basis shape key + if not obj.data.shape_keys: + basis = obj.shape_key_add() + basis.name = "Basis" + obj.data.update() + + if self.create_sk: + # Add new shape key + key_block = obj.shape_key_add() + key_block.name = self.sk_name + key_block.value = 1 + else: + key_block = obj.data.shape_keys.key_blocks.get(self.sk_name) + + if self.create_vg: + obj.vertex_groups.new(name=self.vg_name) + + if self.vg_name: + key_block.vertex_group = self.vg_name + + pose_key = get_active_pose_key(obj) + + if self.create_slot: + super().execute(context) + + target_slot = pose_key.active_target + target_slot.name = key_block.name + + self.report({'INFO'}, f"Added shape key {key_block.name}.") + return {'FINISHED'} + + +class OBJECT_OT_pose_key_shape_remove(UILIST_OT_Entry_Remove, OperatorWithWarning, Operator): + """Remove Target Shape Key. Hold Shift to only remove the slot and keep the actual shape key""" + + bl_idname = "object.posekey_shape_remove" + bl_label = "Remove Target Shape Key" + bl_options = {'UNDO', 'REGISTER', 'INTERNAL'} + + list_context_path: StringProperty() + active_idx_context_path: StringProperty() + + delete_sk: BoolProperty( + name="Delete Shape Key", + description="Delete the underlying Shape Key", + default=True, + ) + + def get_key_block(self, context): + obj = context.object + pose_key = get_active_pose_key(obj) + target_slot = pose_key.active_target + return target_slot.key_block + + def invoke(self, context, event): + if self.get_key_block(context): + # If this actually targets a shape key, prompt for removal. + self.delete_sk = not event.shift + if self.delete_sk: + return super().invoke(context, event) + return self.execute(context) + + def get_warning_text(self, context): + return "Delete this Shape Key?" + + @staticmethod + def delete_shapekey_with_drivers(obj, key_block): + shape_key = key_block.id_data + if shape_key.animation_data: + for fcurve in shape_key.animation_data.drivers: + if fcurve.data_path.startswith(f'key_blocks["{key_block.name}"]'): + shape_key.animation_data.drivers.remove(fcurve) + obj.shape_key_remove(key_block) + + def execute(self, context): + obj = context.object + + key_block = self.get_key_block(context) + if key_block and self.delete_sk: + self.delete_shapekey_with_drivers(obj, key_block) + + super().execute(context) + + self.report({'INFO'}, f"Removed shape key slot.") + return {'FINISHED'} + + def get_deforming_armature(mesh_ob: Object) -> Object | None: for mod in mesh_ob.modifiers: if mod.type == 'ARMATURE': @@ -876,9 +993,10 @@ registry = [ OBJECT_OT_pose_key_set_pose, OBJECT_OT_pose_key_push, OBJECT_OT_pose_key_push_all, - OBJECT_OT_create_shape_key_for_pose, OBJECT_OT_pose_key_clamp_influence, OBJECT_OT_pose_key_place_objects_in_grid, OBJECT_OT_pose_key_jump_to_storage, OBJECT_OT_pose_key_copy_data, + OBJECT_OT_pose_key_shape_add, + OBJECT_OT_pose_key_shape_remove, ] diff --git a/scripts-blender/addons/pose_shape_keys/props.py b/scripts-blender/addons/pose_shape_keys/props.py index ebb355d1..b668eb1b 100644 --- a/scripts-blender/addons/pose_shape_keys/props.py +++ b/scripts-blender/addons/pose_shape_keys/props.py @@ -74,6 +74,10 @@ class PoseShapeKey(PropertyGroup): active_target_shape_index: IntProperty(update=update_active_sk_index) + @property + def active_target(self): + return self.target_shapes[self.active_target_shape_index] + def update_name(self, context): if self.name == "": self.name = "Pose Key" diff --git a/scripts-blender/addons/pose_shape_keys/ui.py b/scripts-blender/addons/pose_shape_keys/ui.py index a9c369ae..bfef46d1 100644 --- a/scripts-blender/addons/pose_shape_keys/ui.py +++ b/scripts-blender/addons/pose_shape_keys/ui.py @@ -129,6 +129,9 @@ class MESH_PT_shape_key_subpanel(Panel): @classmethod def poll(cls, context): + obj = context.object + if not (obj and obj.data and obj.data.shape_key_ui_type=='POSE_KEYS'): + return False try: return poll_correct_pose_key_pose(cls, context) except AttributeError: @@ -155,19 +158,21 @@ class MESH_PT_shape_key_subpanel(Panel): class_name='POSEKEYS_UL_target_shape_keys', list_context_path=f'object.data.pose_keys[{idx}].target_shapes', active_idx_context_path=f'object.data.pose_keys[{idx}].active_target_shape_index', + add_op_name='object.posekey_shape_add', + remove_op_name='object.posekey_shape_remove', ) if len(active_posekey.target_shapes) == 0: return - active_target = active_posekey.target_shapes[active_posekey.active_target_shape_index] + active_target = active_posekey.active_target row = layout.row() if not mesh.shape_keys: - row.operator('object.create_shape_key_for_pose', icon='ADD') return row.prop_search(active_target, 'shape_key_name', mesh.shape_keys, 'key_blocks') - if not active_target.name: - row.operator('object.create_shape_key_for_pose', icon='ADD', text="") + if not active_target.key_block: + add_shape_op = row.operator('object.posekey_shape_add', icon='ADD', text="") + add_shape_op.create_slot=False sk = active_target.key_block if not sk: return -- 2.30.2 From f1b4c47749ac85ca79eeb30068da6311ca4d44b6 Mon Sep 17 00:00:00 2001 From: Demeter Dzadik Date: Tue, 2 Jul 2024 19:46:11 +0200 Subject: [PATCH 08/10] Implement automatic driver set-up operator This detects which bones are posed in the pose associated with the Pose Key, creates driver variables for all posed channels, and multiplies them together. Should work with scaling. Uses Swing and Y twist for rotations. Displays in a pretty and compact fashion in advance which transforms will be used for the driver. --- scripts-blender/addons/pose_shape_keys/ops.py | 134 ++++++++++++++++-- scripts-blender/addons/pose_shape_keys/ui.py | 3 +- 2 files changed, 128 insertions(+), 9 deletions(-) diff --git a/scripts-blender/addons/pose_shape_keys/ops.py b/scripts-blender/addons/pose_shape_keys/ops.py index 83edb29e..ec52ecf3 100644 --- a/scripts-blender/addons/pose_shape_keys/ops.py +++ b/scripts-blender/addons/pose_shape_keys/ops.py @@ -1,8 +1,9 @@ import bpy from bpy.types import Object, Operator from bpy.props import StringProperty, BoolProperty -from mathutils import Vector +from mathutils import Vector, Euler from math import sqrt +from collections import OrderedDict from .symmetrize_shape_key import mirror_mesh from .prefs import get_addon_prefs @@ -48,7 +49,7 @@ class OBJECT_OT_pose_key_add(UILIST_OT_Entry_Add, Operator): def draw(self, context): layout = self.layout.column() - layout.use_property_split=True + layout.use_property_split = True layout.prop(self, 'pose_key_name') if not self.pose_key_name: @@ -720,17 +721,18 @@ class OBJECT_OT_pose_key_shape_add(UILIST_OT_Entry_Add, Operator): def update_create_vg(self, context): if self.create_vg: self.vg_name = self.sk_name + create_vg: BoolProperty( name="Create New Vertex Group", description="Create a new blank Vertex Group as a mask for this shape key. This means the shape key won't work until this mask is authored", default=False, - update=update_create_vg + update=update_create_vg, ) create_slot: BoolProperty( name="Create New Slot", description="Internal. Whether to assign the chosen (or created) shape key to the current slot, or to create a new one", - default=True + default=True, ) def invoke(self, context, event): @@ -750,7 +752,7 @@ class OBJECT_OT_pose_key_shape_add(UILIST_OT_Entry_Add, Operator): def draw(self, context): layout = self.layout.column() - layout.use_property_split=True + layout.use_property_split = True obj = context.object @@ -766,8 +768,10 @@ class OBJECT_OT_pose_key_shape_add(UILIST_OT_Entry_Add, Operator): row = layout.row(align=True) if self.create_vg: if obj.vertex_groups.get(self.vg_name): - row.alert=True - layout.label(text="Cannot create that vertex group because it already exists!", icon='ERROR') + row.alert = True + layout.label( + text="Cannot create that vertex group because it already exists!", icon='ERROR' + ) row.prop(self, 'vg_name', icon='GROUP_VERTEX') else: row.prop_search(self, 'vg_name', obj, "vertex_groups") @@ -801,7 +805,7 @@ class OBJECT_OT_pose_key_shape_add(UILIST_OT_Entry_Add, Operator): key_block.vertex_group = self.vg_name pose_key = get_active_pose_key(obj) - + if self.create_slot: super().execute(context) @@ -867,6 +871,119 @@ class OBJECT_OT_pose_key_shape_remove(UILIST_OT_Entry_Remove, OperatorWithWarnin return {'FINISHED'} +class OBJECT_OT_pose_key_magic_driver(Operator): + """Automatically drive this shape key based on current pose""" + + bl_idname = "object.posekey_magic_driver" + bl_label = "Auto-initialize Driver" + bl_options = {'UNDO', 'REGISTER', 'INTERNAL'} + + key_name: StringProperty() + + @classmethod + def poll(cls, context): + return poll_correct_pose_key_pose(cls, context) + + @staticmethod + def get_posed_channels(context) -> OrderedDict[str, tuple[str, float]]: + obj = context.object + arm_ob = get_deforming_armature(obj) + + channels = OrderedDict() + + for pb in arm_ob.pose.bones: + bone_channels = OrderedDict({'loc' : [], 'rot': [], 'scale': []}) + + for axis in "xyz": + value = getattr(pb.location, axis) + if value != 0.0: + bone_channels['loc'].append((axis.upper(), value)) + channels[pb.name] = bone_channels + + if len(pb.rotation_mode) == 3: + # Euler rotation: Check each axis. + value = getattr(pb.rotation_euler, axis) + if value != 0.0: + bone_channels['rot'].append((axis.upper(), value)) + channels[pb.name] = bone_channels + else: + # Quat/etc: Add variables for all 3 axes. + euler_rot = pb.matrix_channel.to_euler() + if euler_rot != Euler((0, 0, 0)): + value = getattr(euler_rot, axis) + bone_channels['rot'].append((axis.upper(), value)) + channels[pb.name] = bone_channels + + value = getattr(pb.scale, axis) + if value != 1.0: + bone_channels['scale'].append((axis.upper(), value)) + channels[pb.name] = bone_channels + + return channels + + def invoke(self, context, event): + self.posed_channels = self.get_posed_channels(context) + return context.window_manager.invoke_props_dialog(self, width=300) + + def draw(self, context): + layout = self.layout + layout.label(text="Driver will be created based on these transforms:") + + obj = context.object + arm_ob = get_deforming_armature(obj) + + col = layout.column(align=True) + for bone_name, transforms in self.posed_channels.items(): + pb = arm_ob.pose.bones.get(bone_name) + bone_box = col.box() + bone_box.prop(pb, 'name', icon='BONE_DATA', text="", emboss=False) + for transform, trans_inf in transforms.items(): + axes = [inf[0] for inf, val in trans_inf] + if not axes: + continue + + if transform == 'rot': + icon = 'CON_ROTLIKE' + elif transform == 'scale': + icon = 'CON_SIZELIKE' + else: + icon = 'CON_LOCLIKE' + bone_box.row().label(text=", ".join(axes), icon=icon) + + def execute(self, context): + obj = context.object + arm_ob = get_deforming_armature(obj) + key_block = obj.data.shape_keys.key_blocks.get(self.key_name) + + key_block.driver_remove('value') + fc = key_block.driver_add('value') + drv = fc.driver + + expressions = [] + + for bone_name, transforms in self.posed_channels.items(): + for transform, trans_inf in transforms.items(): + for axis, value in trans_inf: + transf_type = transform.upper()+"_"+axis + var = drv.variables.new() + var.name = bone_name.replace(" ", "_") + "_" + transf_type.lower() + var.type = 'TRANSFORMS' + var.targets[0].id = arm_ob + var.targets[0].bone_target = bone_name + var.targets[0].transform_type = transf_type + var.targets[0].rotation_mode = 'SWING_TWIST_Y' + var.targets[0].transform_space = 'LOCAL_SPACE' + if transf_type.startswith("SCALE"): + expressions.append(f"((1-{var.name})/{value})") + else: + expressions.append(f"({var.name}/{value})") + + drv.expression = " * ".join(expressions) + + self.report({'INFO'}, "Created automatic driver.") + return {'FINISHED'} + + def get_deforming_armature(mesh_ob: Object) -> Object | None: for mod in mesh_ob.modifiers: if mod.type == 'ARMATURE': @@ -999,4 +1116,5 @@ registry = [ OBJECT_OT_pose_key_copy_data, OBJECT_OT_pose_key_shape_add, OBJECT_OT_pose_key_shape_remove, + OBJECT_OT_pose_key_magic_driver, ] diff --git a/scripts-blender/addons/pose_shape_keys/ui.py b/scripts-blender/addons/pose_shape_keys/ui.py index bfef46d1..71735cc8 100644 --- a/scripts-blender/addons/pose_shape_keys/ui.py +++ b/scripts-blender/addons/pose_shape_keys/ui.py @@ -133,7 +133,7 @@ class MESH_PT_shape_key_subpanel(Panel): if not (obj and obj.data and obj.data.shape_key_ui_type=='POSE_KEYS'): return False try: - return poll_correct_pose_key_pose(cls, context) + return poll_correct_pose_key_pose(cls, context, demand_pose=False) except AttributeError: # Happens any time that function tries to set a poll message, # since panels don't have poll messages, lol. @@ -218,6 +218,7 @@ class POSEKEYS_UL_target_shape_keys(UIList): ): name_row.active = value_row.active = False + value_row.operator('object.posekey_magic_driver', text="", icon='DECORATE_DRIVER').key_name = key_block.name value_row.prop(key_block, "value", text="") mute_row = split.row() -- 2.30.2 From 23ebcbc12eeb68a88524c3583b50f1adb8cca155 Mon Sep 17 00:00:00 2001 From: Demeter Dzadik Date: Tue, 2 Jul 2024 19:48:03 +0200 Subject: [PATCH 09/10] Update .toml --- scripts-blender/addons/pose_shape_keys/blender_manifest.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts-blender/addons/pose_shape_keys/blender_manifest.toml b/scripts-blender/addons/pose_shape_keys/blender_manifest.toml index 95847ebf..a555f94d 100644 --- a/scripts-blender/addons/pose_shape_keys/blender_manifest.toml +++ b/scripts-blender/addons/pose_shape_keys/blender_manifest.toml @@ -1,9 +1,9 @@ schema_version = "1.0.0" id = "pose_shape_keys" -version = "1.0.0" +version = "0.0.2" name = "Pose Shape Keys" -tagline = "Continue iterating on your weights and constraints without destroying your shape keys" +tagline = "Preserve your shape keys through weight changes, and much more" maintainer = "Demeter Dzadik " type = "add-on" website = "https://studio.blender.org/pipeline/addons/pose_shape_keys" -- 2.30.2 From 39cb3753099eb11a3165e9a94e0549abaff1ec17 Mon Sep 17 00:00:00 2001 From: Demeter Dzadik Date: Tue, 2 Jul 2024 20:11:59 +0200 Subject: [PATCH 10/10] Support for non-Euler rotations (by conversion) --- scripts-blender/addons/pose_shape_keys/ops.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/scripts-blender/addons/pose_shape_keys/ops.py b/scripts-blender/addons/pose_shape_keys/ops.py index ec52ecf3..7cd37b61 100644 --- a/scripts-blender/addons/pose_shape_keys/ops.py +++ b/scripts-blender/addons/pose_shape_keys/ops.py @@ -1,7 +1,7 @@ import bpy from bpy.types import Object, Operator from bpy.props import StringProperty, BoolProperty -from mathutils import Vector, Euler +from mathutils import Vector, Euler, Quaternion from math import sqrt from collections import OrderedDict @@ -907,12 +907,17 @@ class OBJECT_OT_pose_key_magic_driver(Operator): bone_channels['rot'].append((axis.upper(), value)) channels[pb.name] = bone_channels else: - # Quat/etc: Add variables for all 3 axes. - euler_rot = pb.matrix_channel.to_euler() + if pb.rotation_mode == 'QUATERNION': + euler_rot = pb.rotation_quaternion.to_euler() + elif pb.rotation_mode == 'AXIS_ANGLE': + quat = Quaternion(Vector(pb.rotation_axis_angle).yzw, pb.rotation_axis_angle[0]) + euler_rot = quat.to_euler() + if euler_rot != Euler((0, 0, 0)): value = getattr(euler_rot, axis) - bone_channels['rot'].append((axis.upper(), value)) - channels[pb.name] = bone_channels + if abs(value) > 0.00001: + bone_channels['rot'].append((axis.upper(), value)) + channels[pb.name] = bone_channels value = getattr(pb.scale, axis) if value != 1.0: -- 2.30.2