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 352 additions and 356 deletions
Showing only changes of commit 6ca9d82ebc - Show all commits

View File

@ -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,

View File

@ -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',
@ -148,6 +88,37 @@ class MESH_PT_pose_keys(Panel):
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,