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 128 additions and 9 deletions
Showing only changes of commit f1b4c47749 - Show all commits

View File

@ -1,8 +1,9 @@
import bpy import bpy
from bpy.types import Object, Operator from bpy.types import Object, Operator
from bpy.props import StringProperty, BoolProperty from bpy.props import StringProperty, BoolProperty
from mathutils import Vector from mathutils import Vector, Euler
from math import sqrt from math import sqrt
from collections import OrderedDict
from .symmetrize_shape_key import mirror_mesh from .symmetrize_shape_key import mirror_mesh
from .prefs import get_addon_prefs 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): def draw(self, context):
layout = self.layout.column() layout = self.layout.column()
layout.use_property_split=True layout.use_property_split = True
layout.prop(self, 'pose_key_name') layout.prop(self, 'pose_key_name')
if not 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): def update_create_vg(self, context):
if self.create_vg: if self.create_vg:
self.vg_name = self.sk_name self.vg_name = self.sk_name
create_vg: BoolProperty( create_vg: BoolProperty(
name="Create New Vertex Group", 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", 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, default=False,
update=update_create_vg update=update_create_vg,
) )
create_slot: BoolProperty( create_slot: BoolProperty(
name="Create New Slot", 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", 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): 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): def draw(self, context):
layout = self.layout.column() layout = self.layout.column()
layout.use_property_split=True layout.use_property_split = True
obj = context.object obj = context.object
@ -766,8 +768,10 @@ class OBJECT_OT_pose_key_shape_add(UILIST_OT_Entry_Add, Operator):
row = layout.row(align=True) row = layout.row(align=True)
if self.create_vg: if self.create_vg:
if obj.vertex_groups.get(self.vg_name): if obj.vertex_groups.get(self.vg_name):
row.alert=True row.alert = True
layout.label(text="Cannot create that vertex group because it already exists!", icon='ERROR') layout.label(
text="Cannot create that vertex group because it already exists!", icon='ERROR'
)
row.prop(self, 'vg_name', icon='GROUP_VERTEX') row.prop(self, 'vg_name', icon='GROUP_VERTEX')
else: else:
row.prop_search(self, 'vg_name', obj, "vertex_groups") row.prop_search(self, 'vg_name', obj, "vertex_groups")
@ -867,6 +871,119 @@ class OBJECT_OT_pose_key_shape_remove(UILIST_OT_Entry_Remove, OperatorWithWarnin
return {'FINISHED'} 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: def get_deforming_armature(mesh_ob: Object) -> Object | None:
for mod in mesh_ob.modifiers: for mod in mesh_ob.modifiers:
if mod.type == 'ARMATURE': if mod.type == 'ARMATURE':
@ -999,4 +1116,5 @@ registry = [
OBJECT_OT_pose_key_copy_data, OBJECT_OT_pose_key_copy_data,
OBJECT_OT_pose_key_shape_add, OBJECT_OT_pose_key_shape_add,
OBJECT_OT_pose_key_shape_remove, OBJECT_OT_pose_key_shape_remove,
OBJECT_OT_pose_key_magic_driver,
] ]

View File

@ -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'): if not (obj and obj.data and obj.data.shape_key_ui_type=='POSE_KEYS'):
return False return False
try: try:
return poll_correct_pose_key_pose(cls, context) return poll_correct_pose_key_pose(cls, context, demand_pose=False)
except AttributeError: except AttributeError:
# Happens any time that function tries to set a poll message, # Happens any time that function tries to set a poll message,
# since panels don't have poll messages, lol. # 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 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="") value_row.prop(key_block, "value", text="")
mute_row = split.row() mute_row = split.row()