How to Generate Maya Blendshapes from Joint Rotations Using Python
15 Sep 26 (1mo ago)
If you do character rigging in Autodesk Maya, you know that extracting facial or mechanical blendshapes based on joint poses is a tedious process. Doing it manually means constantly rotating joints, duplicating meshes, zeroing things out, extracting correctives, and manually setting up driven keys.
The Python script below automates this entire workflow. You just define your joint rotations and driver connections in a simple configuration block, and the script handles the rest.
DISCLAIMER: Only works for rotation. Like creating blendshape from an eyeshape.
What This Script Does
- Automates Target Creation: It rotates a target joint (like an eye or jaw joint) to specific angles, duplicates the deformed geometry, and stores it as a base blendshape.
- Generates In-betweens: It automatically calculates and creates 50% in-between targets so you don't lose volume during extreme rotations.
- Calculates Corrective Deltas: When two shapes conflict (e.g., rotating up and right at the same time), it extracts the mathematical delta and creates a corrective shape.
- Wires the Rig: It automatically connects all the generated shapes and correctives to your UI controller using Set Driven Keys and Math nodes (Multiply/Divide, Remap Value).
The Python Script
To use this, open the Maya Script Editor and update the Configuration variables at the top to match your scene's naming conventions (your specific joint, base geometry, and driver controller).
'''
A simple Maya script to generate blendshapes from joint rotations,
including in-betweens and corrective deltas.
'''
import maya.cmds as cmds
# --- Configuration ---
EYE_JNT = "eye_jnt"
GEO_SPHERE = "geo_sphere"
NEUTRAL_geo = "neutral_geo"
EYE_CON = "eye_driver_con"
BSHAPE_GEO_NAME = "bshape_geo"
ACTION_CONFIG = [
{
"name": "ry_pos", "axis": "ry", "angle": 30,
"driver": f"{EYE_CON}.tx", "points": [(-10, 1), (0, 0)]
},
{
"name": "ry_neg", "axis": "ry", "angle": -30,
"driver": f"{EYE_CON}.tx", "points": [(10, 1), (0, 0)]
},
{
"name": "rx_neg", "axis": "rx", "angle": -35,
"driver": f"{EYE_CON}.ty", "points": [(-10, 1), (0, 0)]
},
{
"name": "rx_pos", "axis": "rx", "angle": 35,
"driver": f"{EYE_CON}.ty", "points": [(10, 1), (0, 0)]
},
]
CORRECTIVE_CONFIG = [
{"name": "ry_pos_rx_pos", "targets": ["ry_pos", "rx_pos"]},
{"name": "ry_pos_rx_neg", "targets": ["ry_pos", "rx_neg"]},
{"name": "ry_neg_rx_pos", "targets": ["ry_neg", "rx_pos"]},
{"name": "ry_neg_rx_neg", "targets": ["ry_neg", "rx_neg"]},
]
# --- Script ---
def get_name_suffix(angle):
'''Generates a name suffix based on the angle.'''
if isinstance(angle, (int, float)):
if angle >= 0:
return f"pos{angle}"
else:
return f"neg{abs(angle)}"
return str(angle)
def create_delta_shape(base_geo, posed_geo, sculpt_geo, delta_name=None):
"""
Extract corrective delta.
"""
if not delta_name:
delta_name = base_geo + "_corrective"
delta_geo = cmds.duplicate(base_geo, name=delta_name)[0]
vtx_count = cmds.polyEvaluate(base_geo, v=True)
for i in range(vtx_count):
base_pos = cmds.pointPosition(f"{base_geo}.vtx[{i}]", world=False)
posed_pos = cmds.pointPosition(f"{posed_geo}.vtx[{i}]", world=False)
sculpt_pos = cmds.pointPosition(f"{sculpt_geo}.vtx[{i}]", world=False)
# delta = sculpt - posed
delta = [sculpt_pos[j] - posed_pos[j] for j in range(3)]
# apply delta to base
new_pos = [base_pos[j] + delta[j] for j in range(3)]
cmds.xform(f"{delta_geo}.vtx[{i}]", os=True, t=new_pos)
return delta_geo
def run():
'''Main function to run the script.'''
cmds.setAttr(f"{EYE_JNT}.rx", 0); cmds.setAttr(f"{EYE_JNT}.ry", 0); cmds.setAttr(f"{EYE_JNT}.rz", 0)
# --- Object Checks ---
for obj in [EYE_JNT, GEO_SPHERE, EYE_CON]:
if not cmds.objExists(obj):
raise ValueError(f"Error: Object '{obj}' not found in the scene.")
bshape_geo = cmds.duplicate(GEO_SPHERE, name=BSHAPE_GEO_NAME)[0]
# --- Create Shapes ---
bshape_list = []
inbetween_list = []
shape_name_map = {}
for i, config in enumerate(ACTION_CONFIG):
axis = config["axis"]
angle = config["angle"]
name_suffix = get_name_suffix(angle)
shape_name = f"{GEO_SPHERE}_{axis}_{name_suffix}"
config["shape_name"] = shape_name
shape_name_map[config["name"]] = shape_name
cmds.setAttr(f"{EYE_JNT}.rx", 0); cmds.setAttr(f"{EYE_JNT}.ry", 0); cmds.setAttr(f"{EYE_JNT}.rz", 0)
cmds.setAttr(f"{EYE_JNT}.{axis}", angle)
duplicated_geo = cmds.duplicate(GEO_SPHERE, name=shape_name)[0]
bshape_list.append(duplicated_geo)
inbetween_angle = angle * 0.5
inbetween_name_suffix = get_name_suffix(inbetween_angle) + "_inbetween"
inbetween_shape_name = f"{GEO_SPHERE}_{axis}_{inbetween_name_suffix}"
cmds.setAttr(f"{EYE_JNT}.{axis}", inbetween_angle)
inbetween_geo = cmds.duplicate(GEO_SPHERE, name=inbetween_shape_name)[0]
inbetween_list.append({"name": inbetween_geo, "index": i})
cmds.setAttr(f"{EYE_JNT}.rx", 0); cmds.setAttr(f"{EYE_JNT}.ry", 0); cmds.setAttr(f"{EYE_JNT}.rz", 0)
bshape_node = cmds.blendShape(bshape_list, bshape_geo, name=f"{GEO_SPHERE}_bshape")[0]
for item in inbetween_list:
cmds.blendShape(bshape_node, edit=True, inBetween=True, target=(bshape_geo, item["index"], item["name"], 0.5))
for config in ACTION_CONFIG:
driver = config["driver"]
shape_name = config["shape_name"]
for driver_value, driven_value in config["points"]:
cmds.setDrivenKeyframe(f"{bshape_node}.{shape_name}", currentDriver=driver, driverValue=driver_value, value=driven_value, inTangentType='linear', outTangentType='linear')
print("Successfully created and connected blendshapes with in-betweens.")
# --- Add Corrective Targets ---
print("Creating and connecting corrective shapes...")
for correct_config in CORRECTIVE_CONFIG:
# --- Create Goal Shape ---
cmds.setAttr(f"{EYE_JNT}.rx", 0); cmds.setAttr(f"{EYE_JNT}.ry", 0); cmds.setAttr(f"{EYE_JNT}.rz", 0)
for target_name in correct_config["targets"]:
action = next(ac for ac in ACTION_CONFIG if ac["name"] == target_name)
cmds.setAttr(f"{EYE_JNT}.{action['axis']}", action['angle'])
goal_shape = cmds.duplicate(GEO_SPHERE, name=f"goal_{correct_config['name']}")[0]
# --- Create Broken Shape ---
for shape in bshape_list:
cmds.setAttr(f"{bshape_node}.{shape}", 0)
for target_name in correct_config["targets"]:
shape_name = shape_name_map[target_name]
cmds.setAttr(f"{bshape_node}.{shape_name}", 1)
broken_shape = cmds.duplicate(bshape_geo, name=f"broken_{correct_config['name']}")[0]
# --- Create Delta and Add to Blendshape ---
delta_shape = create_delta_shape(NEUTRAL_geo, broken_shape, goal_shape, f"delta_{correct_config['name']}")
target_index = len(cmds.listAttr(f"{bshape_node}.w", m=True))
cmds.blendShape(bshape_node, edit=True, target=(bshape_geo, target_index, delta_shape, 1.0))
# --- Connect Delta Drivers ---
multiply_node = cmds.createNode('multiplyDivide', name=f"multiply_{correct_config['name']}")
drivers = [next(ac for ac in ACTION_CONFIG if ac["name"] == t) for t in correct_config["targets"]]
remap1 = cmds.createNode('remapValue', name=f"remap_{drivers[0]['name']}")
cmds.setAttr(f"{remap1}.inputMin", min(p[0] for p in drivers[0]['points']))
cmds.setAttr(f"{remap1}.inputMax", max(p[0] for p in drivers[0]['points']))
cmds.connectAttr(drivers[0]['driver'], f"{remap1}.inputValue")
cmds.connectAttr(f"{remap1}.outValue", f"{multiply_node}.input1X")
remap2 = cmds.createNode('remapValue', name=f"remap_{drivers[1]['name']}")
cmds.setAttr(f"{remap2}.inputMin", min(p[0] for p in drivers[1]['points']))
cmds.setAttr(f"{remap2}.inputMax", max(p[0] for p in drivers[1]['points']))
cmds.connectAttr(drivers[1]['driver'], f"{remap2}.inputValue")
cmds.connectAttr(f"{remap2}.outValue", f"{multiply_node}.input2X")
cmds.connectAttr(f"{multiply_node}.outputX", f"{bshape_node}.{delta_shape}")
cmds.setAttr(f"{EYE_JNT}.rx", 0); cmds.setAttr(f"{EYE_JNT}.ry", 0); cmds.setAttr(f"{EYE_JNT}.rz", 0)
print("Corrective shapes created and connected.")
# --- Run Script ---
if __name__ == "__main__":
run()