I have written a small addon that takes a mesh with its shapekeys and adds a custom property per shapekey to a selected bone of an armature and connects the two with a driver, so that in the end, all shapekeys on the mesh can be controlled through the armature.
The addon works and all elements are created, the trouble is that for the drivers in order to work, I need to manually refresh the dependencies.
Is there an error in how I add them or do I have to add some kind of update function to make this work?
I am posting the code and a sample file with an armature object and a cube with two shape keys on it to test things.
The normal mode of operation is to select the shapekey mesh, then the armature object and, in Pose Mode, selecting the target bone and then running the operator from the N-Panel > Item > ShapeKeys to Driven Properties.
The .py file can be installed like an addon or run from the text editor.
### Create Custom Properties on an Armature that drive all Shape Keys on its Child Mesh ###
Select the Mesh with the Shape Keys on it, then select the Armature that is to hold
the Custom Properties with which you are going to drive the ShapeKeys and run
bl_info = {
"name": "ShapeKeys to Driven Armature Properties",
"author": "",
"version": (1, 0),
"blender": (2, 90, 0),
"location": "View3D > Property Panel > Item Tab",
"description": "Adds Shapekeys from a Mesh Object as Driven Custom Properties to an Armature",
"warning": "",
"doc_url": "https://github.com/theoldben/ShapekeysToCustomProperties",
"category": "Rigging",
}
import bpy
Error in case Selection does not meet requirements
def error_selection(self, context):
self.report({'WARNING'}, 'Incorrect Object Selection: Please Select a Mesh Object followed by an Armature Object')
return {'FINISHED'}
def main(context):
pass
class OBJECT_OT_ShapeKeysToDrivenProps(bpy.types.Operator):
"""ShapeKeys to Driven Armature Properties"""
bl_idname = "object.shapekeystodrivenprops"
bl_label = "ShapeKeys to Driven Armature Properties"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = 'Item'
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return context.active_object is not None
def execute(self, context):
main(context)
# Variable Definition
context = bpy.context
obj = bpy.context.object
active_obj = bpy.context.active_object
selected_obj = bpy.context.selected_objects
other_obj = [obj for obj in bpy.context.selected_objects if obj.type == 'MESH']
active_bone = bpy.context.active_pose_bone
try:
active_bone.name
active_bone_name = active_bone.name
key_number = len(other_obj[0].data.shape_keys.key_blocks)
# Check for Object type: Active Object: Armature, Selected Object: Mesh
if len(selected_obj) == 2 and active_obj.type == 'ARMATURE' and other_obj[0].type == 'MESH':
# If other objects are in selection: return Error
for things in selected_obj:
# Iterate over ShapeKey List, Store Names, Add Properties to Active Object,
for i in range(1, key_number):
# Store ShapeKey Name
name_shapekey = other_obj[0].data.shape_keys.key_blocks[i].name
#Strip current Shapekey Name of all alpha-numerical characters
alphanumeric_filter = filter(str.isalnum, name_shapekey)
alphanumeric_string = "".join(alphanumeric_filter)
clean_name_shapekey = alphanumeric_string
# Access RNA_UI Dictionary of PoseBone, create if it does not exist
if "_RNA_UI" not in active_bone.keys():
active_bone['_RNA_UI'] = {}
act_bone_rna_ui = active_bone.get('_RNA_UI')
# If there is already a property with the current shapekey's name, skip
if name_shapekey in act_bone_rna_ui:
continue
# Add Property with ShapeKey's name to Active PoseBone and set Value to 0
active_bone[name_shapekey] = 0.0
# active_bone['_RNA_UI'].update({name_shapekey : 0.0})
# Add Custom Properties to RNA_UI dictionary, fill values
active_bone['_RNA_UI'].update({name_shapekey :
{
"default": 0.0,
"min":0.0,
"max":1.0,
"soft_min":0.0,
"soft_max":1.0,
"description":"Shape Key Influence",
}
}
)
act_bone_rna_ui[name_shapekey]["min"] = 0.0
act_bone_rna_ui[name_shapekey]["max"] = 1.0
# Add the Drivers to the ShapeKeys Value Field, so ttheir influence can be controlled via Custom Properties
driver = other_obj[0].data.shape_keys.key_blocks[i].driver_add("value").driver
# Create new input Variable for driver
driver_var = driver.variables.new()
driver_var.name = clean_name_shapekey
# Set number of Variables in the driver stack, since there is only one, that is 0
target = driver_var.targets[0]
# Set the Target object for the Driver's Variable Input, i.e. the Armature Object, where the input sliders are
target.id = active_obj
# Set the drivers Data Path, i.e. the RNA Path to the Property it uses as input
target.data_path = "pose.bones"+"["+"\""+active_bone.name+"\""+"]"+"[" +"\""+ name_shapekey + "\"" +"]"
# Set the Expression Field of the Driver, in this case with the sanitised variable name
driver.expression = driver_var.name
# Jiggle the driver to force update
driver.expression += " "
driver.expression = driver.expression[:-1]
# Set ShapeKey Startup Value
other_obj[0][name_shapekey] = 0.0
# property attributes.for UI
# redraw Properties panel
for window in bpy.context.window_manager.windows:
screen = window.screen
for area in screen.areas:
if area.type == 'PROPERTIES':
area.tag_redraw()
break
for area in screen.areas:
if area.type == 'UI':
area.tag_redraw()
break
return {'FINISHED'}
else:
error_selection(self, context)
except:
error_selection(self, context)
return {'FINISHED'}
class PANEL_PT_ShapeKeysToDrivenProps(bpy.types.Panel):
"""ShapeKeys to Driven Armature Properties"""
# bl_idname = "panel.shapekeystodrivenprops"
bl_label = "ShapeKeys to Driven Armature Properties"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = 'Item'
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
self.layout.operator("object.shapekeystodrivenprops", text="ShapeKeysToDrivenProps")
def register():
bpy.utils.register_class(OBJECT_OT_ShapeKeysToDrivenProps)
bpy.utils.register_class(PANEL_PT_ShapeKeysToDrivenProps)
def unregister():
bpy.utils.unregister_class(PANEL_PT_ShapeKeysToDrivenProps)
bpy.utils.unregister_class(OBJECT_OT_ShapeKeysToDrivenProps)
if name == "main":
register()
