Pose Shape Keys: Major Update #321
@ -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
|
||||||
@ -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):
|
||||||
@ -767,7 +769,9 @@ class OBJECT_OT_pose_key_shape_add(UILIST_OT_Entry_Add, Operator):
|
|||||||
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,
|
||||||
]
|
]
|
||||||
|
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user