Pose Shape Keys: Major Update #321

Merged
Demeter Dzadik merged 10 commits from pose-shape-keys-extension into main 2024-07-03 14:50:58 +02:00
2 changed files with 104 additions and 99 deletions
Showing only changes of commit a0f32b4d0d - Show all commits

View File

@ -29,7 +29,6 @@ DEFORM_MODIFIERS = [
GOOD_MODIFIERS = ['ARMATURE'] GOOD_MODIFIERS = ['ARMATURE']
def get_deforming_armature(mesh_ob: Object) -> Object: def get_deforming_armature(mesh_ob: Object) -> Object:
for mod in mesh_ob.modifiers: for mod in mesh_ob.modifiers:
if mod.type == 'ARMATURE': if mod.type == 'ARMATURE':
@ -78,7 +77,7 @@ class OBJECT_OT_create_shape_key_for_pose(Operator):
else: else:
self.sk_name = "Key" 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: if pose_key.name:
self.sk_name = 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: if self.vg_name:
new_sk.vertex_group = 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 = pose_key.target_shapes[pose_key.active_target_shape_index]
target.name = new_sk.name 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): 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_idname = "object.posekey_set_pose"
bl_label = "Set Pose" bl_label = "Set Pose"
@ -273,7 +272,7 @@ class OBJECT_OT_pose_key_set_pose(Operator):
@classmethod @classmethod
def poll(cls, context): 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): def execute(self, context):
set_pose_of_active_pose_key(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] 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 obj = context.object
if not obj: if not obj:
@ -309,26 +313,14 @@ def poll_has_psk_and_deformed(operator, context):
operator.poll_message_set("This mesh object is not deformed by any Armature modifier.") operator.poll_message_set("This mesh object is not deformed by any Armature modifier.")
return False return False
return obj, pose_key, arm_ob if not pose_key.action:
operator.poll_message_set("An Action must be associated with the Pose Shape Key.")
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 return False
obj, pose_key, arm_ob = obj_psk_arm if demand_pose:
# Action must exist and match. # Action must exist and match.
if not ( if not (
pose_key.action arm_ob.animation_data
and arm_ob.animation_data
and arm_ob.animation_data.action and arm_ob.animation_data.action
and arm_ob.animation_data.action == pose_key.action and arm_ob.animation_data.action == pose_key.action
): ):
@ -352,6 +344,7 @@ class OBJECT_OT_pose_key_add(UILIST_OT_Entry_Add, Operator):
bl_idname = "object.posekey_add" bl_idname = "object.posekey_add"
bl_label = "Add Pose Shape Key" bl_label = "Add Pose Shape Key"
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'} bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
bl_property = "pose_key_name" # Focus the text input box
list_context_path: StringProperty() list_context_path: StringProperty()
active_idx_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'} 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): 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_idname = "object.posekey_save"
bl_label = "Overwrite Storage Object" bl_label = "Overwrite Storage Object"
@ -396,36 +424,18 @@ class OBJECT_OT_pose_key_save(Operator, OperatorWithWarning, SaveAndRestoreState
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
obj_psk_arm = poll_has_psk_and_deformed(cls, context) return poll_correct_pose_key_pose(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
def invoke(self, context, event): def invoke(self, context, event):
obj = context.object 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: if pose_key.storage_object:
return super().invoke(context, event) return super().invoke(context, event)
return self.execute(context) return self.execute(context)
def get_warning_text(self, context): def get_warning_text(self, context):
obj = context.object 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}"?' return f'Overwrite storage object "{pose_key.storage_object.name}"?'
def execute(self, context): def execute(self, context):
@ -450,13 +460,6 @@ class OBJECT_OT_pose_key_save(Operator, OperatorWithWarning, SaveAndRestoreState
pose_key.storage_object = storage_ob pose_key.storage_object = storage_ob
storage_ob.location = rigged_ob.location storage_ob.location = rigged_ob.location
storage_ob.location.x -= rigged_ob.dimensions.x * 1.1 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: else:
old_mesh = storage_ob.data old_mesh = storage_ob.data
storage_ob.data = storage_ob_mesh 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): def get_warning_text(self, context):
obj = context.object 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] target_shape_names = [target.name for target in pose_key.target_shapes if target]
return ( return (
"This will overwrite the following Shape Keys: \n " "This will overwrite the following Shape Keys: \n "
@ -775,7 +778,7 @@ class OBJECT_OT_pose_key_place_objects_in_grid(Operator):
return {'FINISHED'} 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""" """Place the storage object next to this object and select it"""
bl_idname = "object.posekey_jump_to_storage" bl_idname = "object.posekey_jump_to_storage"
@ -785,7 +788,7 @@ class OBJECT_OT_pose_key_jump_to_shape(Operator):
@staticmethod @staticmethod
def get_storage_object(context): def get_storage_object(context):
obj = context.object 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 return pose_key.storage_object
@classmethod @classmethod
@ -863,6 +866,7 @@ class OBJECT_OT_pose_key_copy_data(Operator):
registry = [ registry = [
OBJECT_OT_pose_key_auto_init,
OBJECT_OT_pose_key_add, OBJECT_OT_pose_key_add,
OBJECT_OT_pose_key_save, OBJECT_OT_pose_key_save,
OBJECT_OT_pose_key_set_pose, OBJECT_OT_pose_key_set_pose,
@ -871,6 +875,6 @@ registry = [
OBJECT_OT_create_shape_key_for_pose, OBJECT_OT_create_shape_key_for_pose,
OBJECT_OT_pose_key_clamp_influence, OBJECT_OT_pose_key_clamp_influence,
OBJECT_OT_pose_key_place_objects_in_grid, 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, OBJECT_OT_pose_key_copy_data,
] ]

View File

@ -1,9 +1,10 @@
import bpy import bpy
from bpy.types import Object, Panel, UIList, Menu from bpy.types import Object, Panel, UIList, Menu
from bl_ui.properties_data_mesh import DATA_PT_shape_keys from bl_ui.properties_data_mesh import DATA_PT_shape_keys
from .ui_list import draw_ui_list
from bpy.props import EnumProperty from bpy.props import EnumProperty
from .ui_list import draw_ui_list
from .ops import get_deforming_armature
from .prefs import get_addon_prefs from .prefs import get_addon_prefs
@ -72,7 +73,7 @@ class MESH_PT_pose_keys(Panel):
bl_region_type = 'WINDOW' bl_region_type = 'WINDOW'
bl_context = 'data' bl_context = 'data'
bl_options = {'DEFAULT_CLOSED'} bl_options = {'DEFAULT_CLOSED'}
bl_label = "Shape/Pose Keys" bl_label = "Pose Shape Keys"
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@ -81,16 +82,17 @@ class MESH_PT_pose_keys(Panel):
def draw(self, context): def draw(self, context):
obj = context.object obj = context.object
mesh = obj.data 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': if mesh.shape_key_ui_type == 'DEFAULT':
return DATA_PT_shape_keys.draw(self, context) 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.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 return
if mesh.shape_keys and not mesh.shape_keys.use_relative: 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 idx = context.object.data.active_pose_key_index
active_posekey = context.object.data.pose_keys[idx] active_posekey = context.object.data.pose_keys[idx]
col = layout.column(align=True) action_split = layout.row().split(factor=0.4, align=True)
col.prop(active_posekey, 'action') action_split.alignment='RIGHT'
action_split.label(text="Action")
row = action_split.row(align=True)
icon = 'FORWARD'
if active_posekey.action: if active_posekey.action:
col.prop(active_posekey, 'frame') icon = 'FILE_REFRESH'
row.operator('object.posekey_auto_init', text="", icon=icon)
if active_posekey.storage_object: row.prop(active_posekey, 'action', text="")
row = layout.row() layout.prop(active_posekey, 'frame')
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
layout.separator() layout.separator()
col = layout.column(align=True)
col.operator('object.posekey_set_pose', text="Set Pose", icon="ARMATURE_DATA")
col.separator()
row = col.row() layout.operator('object.posekey_set_pose', text="Set Pose", icon="ARMATURE_DATA")
row.operator('object.posekey_save', text="Overwrite Storage Object", icon="FILE_TICK")
row.operator('object.posekey_push', text="Overwrite Shape Keys", icon="IMPORT") 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): 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 idx = context.object.data.active_pose_key_index
active_posekey = context.object.data.pose_keys[idx] active_posekey = context.object.data.pose_keys[idx]
layout.operator('object.posekey_push', text="Overwrite Shape Keys", icon="IMPORT")
draw_ui_list( draw_ui_list(
layout, layout,
context, context,
@ -250,7 +251,7 @@ def register():
('DEFAULT', 'Shape Keys', "Show a flat list of shape keys"), ('DEFAULT', 'Shape Keys', "Show a flat list of shape keys"),
( (
'POSE_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", "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",
), ),
], ],