Ben Traje
← Back to maya

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