Automate Maya Viewport Rig Poses: A Python Script for Batch Captures
06 Aug 26 (1mo ago)
Testing a complex Maya rig—especially facial setups with dozens of interconnected controllers—can be incredibly tedious. To ensure a rig doesn't break when multiple controllers are pushed to their limits simultaneously, riggers must test hundreds of permutations. Doing this manually takes hours.
By leveraging Maya Python (maya.cmds) and Python's built-in itertools module, you can automate this process. Below is a complete script that iterates through defined X and Y ranges for any number of controls, sets the attributes, hides the viewport curves, and saves a clean screenshot of every single combination.
The Automation Script
This script consists of two main functions: one for handling the viewport capture (using playblast), and the core matrix function that calculates the permutations and applies them to the rig.
import maya.cmds as cmds
import os
import itertools
def take_viewport_screenshot(output_path, width=1920, height=1080, hide_ui=True):
"""Takes a single screenshot of the active viewport."""
directory = os.path.dirname(output_path)
if directory and not os.path.exists(directory):
os.makedirs(directory)
current_frame = cmds.currentTime(query=True)
captured_file = cmds.playblast(
frame=[current_frame],
framePadding=0,
format='image',
compression='png',
filename=output_path,
forceOverwrite=True,
widthHeight=[width, height],
showOrnaments=not hide_ui,
viewer=False,
percent=100,
clearCache=True
)
return captured_file
def automate_control_matrix(control_configs, base_dir, prefix=""):
"""
Generates permutations using custom X and Y ranges defined per-control.
"""
# 1. Verify all controls exist
for cfg in control_configs:
if not cmds.objExists(cfg["name"]):
cmds.warning(f"Control '{cfg['name']}' does not exist! Aborting.")
return
# 2. Generate permutation matrix based on unique per-control values
all_control_states = []
for cfg in control_configs:
states = list(itertools.product(cfg["vals_x"], cfg["vals_y"]))
all_control_states.append(states)
all_combinations = list(itertools.product(*all_control_states))
# 3. Hide curves in viewports for clean captures
model_panels = cmds.getPanel(type="modelPanel") or []
original_curve_states = {}
for panel in model_panels:
original_curve_states[panel] = cmds.modelEditor(panel, query=True, nurbsCurves=True)
cmds.modelEditor(panel, edit=True, nurbsCurves=False)
try:
total_shots = len(all_combinations)
print(f"Starting capture: {total_shots} total permutations...")
for index, combo in enumerate(all_combinations, start=1):
filename_parts = []
for i, (x_val, y_val) in enumerate(combo):
ctrl_info = control_configs[i]
c_name = ctrl_info["name"]
ax = ctrl_info["attr_x"]
ay = ctrl_info["attr_y"]
# Apply the values to the rig
cmds.setAttr(f"{c_name}.{ax}", x_val)
cmds.setAttr(f"{c_name}.{ay}", y_val)
filename_parts.append(f"{c_name}_X{x_val}_Y{y_val}")
cmds.refresh()
combo_str = "__".join(filename_parts)
file_prefix = f"{prefix}_" if prefix else ""
file_name = f"{file_prefix}{combo_str}"
full_path = os.path.join(base_dir, file_name).replace("\\", "/")
take_viewport_screenshot(full_path)
print(f"[{index}/{total_shots}] Captured: {file_name}")
print(f"Successfully generated all {total_shots} screenshots!")
finally:
# 4. Restore curve visibility
for panel in model_panels:
if panel in original_curve_states:
cmds.modelEditor(panel, edit=True, nurbsCurves=original_curve_states[panel])
How to Configure and Run the Script
To use the script, you need to define your target controls and their desired coordinate arrays. Here is how to format your configuration dictionary at the bottom of the script:
# --- Example Usage ---
save_directory = "C:/temp/rig_poses"
# Define controls with their own independent value arrays
my_controls = [
{
"name": "ctrlMouthCorner_R",
"attr_x": "translateX", "vals_x": [-1.0, 0.0, 1.0],
"attr_y": "translateY", "vals_y": [-1.0, 0.0, 1.0]
},
{
"name": "ctrlMouthCorner_L",
"attr_x": "translateX", "vals_x": [-1.0, 0.0, 1.0],
"attr_y": "translateY", "vals_y": [-1.0, 0.0, 1.0]
},
{
"name": "ctrlMouth_M",
"attr_x": "translateX", "vals_x": [-1.0, 0.0, 1.0],
"attr_y": "translateY", "vals_y": [0.0, -1.0] # Custom constrained range
}
]
automate_control_matrix(
control_configs=my_controls,
base_dir=save_directory,
prefix="v01"
)
Not necessarily a fully accompanying script but this is a sample logic on how to read those images in similar named controls in C4D.
import c4d
prev_tex = ""
# Mirrors the controls and ranges
CONTROL_CONFIGS = [
{
"name": "ctrlMouthCorner_R",
"vals_x": [-1.0, 0.0, 1.0],
"vals_y": [-1.0, 0.0, 1.0]
},
{
"name": "ctrlMouthCorner_L",
"vals_x": [-1.0, 0.0, 1.0],
"vals_y": [-1.0, 0.0, 1.0]
},
{
"name": "ctrlMouth_M",
"vals_x": [-1.0, 0.0, 1.0],
"vals_y": [0.0, -1.0]
}
]
def snap_to_closest(val, valid_values):
closest = min(valid_values, key=lambda x: abs(x - val))
if closest == 0.0: closest = 0.0
return "{:.1f}".format(closest)
def main():
global prev_tex
debug_logs = []
filename_parts = []
# 1. Evaluate all controls and gather logs
for cfg in CONTROL_CONFIGS:
ctrl_name = cfg["name"]
obj = doc.SearchObject(ctrl_name)
if not obj:
if prev_tex != ctrl_name + "_missing":
print(">>> ERROR: Cannot find object '{}' in Object Manager.".format(ctrl_name))
prev_tex = ctrl_name + "_missing"
return
pos = obj.GetRelPos()
x_str = snap_to_closest(pos.x, cfg["vals_x"])
y_str = snap_to_closest(pos.y, cfg["vals_y"])
# Store the debug string for this specific control
debug_logs.append(" - {}: Raw(X:{:.3f}, Y:{:.3f}) -> Snapped(X:{}, Y:{})".format(
ctrl_name, pos.x, pos.y, x_str, y_str))
filename_parts.append("{}_X{}_Y{}".format(ctrl_name, x_str, y_str))
# 2. Construct the target filename
# Added .0.png to perfectly match Maya's playblast frame numbering
tex_name = "__".join(filename_parts) + ".0.png"
# 3. Print the diagnostic block ONLY if the state has changed
if prev_tex != tex_name:
print("\n--- NEW TEXTURE EVALUATION ---")
for log in debug_logs:
print(log)
print(" TARGET TEXTURE: " + tex_name)
# 4. Attempt to assign to material
mat = doc.SearchMaterial("face_mat")
if not mat:
print(" ERROR: Material 'face_mat' not found!")
else:
shader = mat[c4d.MATERIAL_LUMINANCE_SHADER]
if shader and shader.CheckType(c4d.Xbitmap):
shader[c4d.BITMAPSHADER_FILENAME] = tex_name
mat.Message(c4d.MSG_UPDATE)
mat.Update(True, True)
print(" SUCCESS: Texture string injected into shader.")
else:
print(" ERROR: Could not find Bitmap Shader in the Luminance Channel.")
print("------------------------------")
# Update state
prev_tex = tex_name