Rigging with Python in Maya
Character rigging is one of the most time-consuming tasks in a 3D pipeline. A single biped rig can take days to build manually - and then you need to do it again for the next character. Python scripting lets you automate rig construction, enforce consistency across characters, and rebuild rigs in seconds when specs change.
Why Script Your Rigs
Manual rigging is fragile. Every hand-placed constraint, every manually typed attribute name, every clicked checkbox is a potential point of failure. Scripted rigs give you:
- Reproducibility - Run the script, get the same rig every time. No forgotten steps, no human error.
- Speed - A rig that takes 8 hours to build manually can be generated in under 30 seconds with a script.
- Iteration - When the animation lead wants changes to the spine setup, you modify one function and regenerate all character rigs.
- Consistency - Every character in the project follows the same naming conventions, control shapes, and hierarchy structure.
- Version control - Your rig definition lives in Python files that can be tracked with Git, diffed, and reviewed.
The examples in this guide use maya.cmds for clarity and broad compatibility. In production, you might prefer PyMEL for cleaner syntax or the Python API for performance-critical operations.
Joint Chain Creation
Joints are the foundation of every rig. Let's build a function that creates joint chains from a list of positions - this is the building block for arms, legs, spines, fingers, and tails.
import maya.cmds as cmds
def create_joint_chain(positions, names, orient_axis='xyz', up_axis='yup',
radius=1.0, parent=None):
"""
Create a joint chain from a list of world-space positions.
Args:
positions: List of (x, y, z) tuples for each joint.
names: List of names for each joint (must match length of positions).
orient_axis: Primary axis for joint orient (e.g., 'xyz', 'xzy').
up_axis: Secondary axis up direction ('xup', 'yup', 'zup').
radius: Display radius for joints.
parent: Optional parent node to place the chain under.
Returns:
List of joint names in chain order.
"""
if len(positions) != len(names):
cmds.warning('positions and names must be the same length')
return []
cmds.select(clear=True)
joints = []
for pos, name in zip(positions, names):
jnt = cmds.joint(position=pos, name=name, radius=radius)
joints.append(jnt)
# Orient joints so the primary axis points to the child
cmds.joint(joints[0], edit=True, orientJoint=orient_axis,
secondaryAxisOrient=up_axis, children=True, zeroScaleOrient=True)
# Zero out orientation on the end joint (no child to aim at)
cmds.joint(joints[-1], edit=True, orientation=(0, 0, 0))
# Parent to specified node
if parent and cmds.objExists(parent):
cmds.parent(joints[0], parent)
return joints
# Create a 5-joint spine chain
spine_positions = [
(0, 100, 0), # spine base (hip height)
(0, 110, -1), # spine_01
(0, 120, -1.5), # spine_02
(0, 130, -1), # spine_03
(0, 140, 0), # chest
]
spine_names = ['spine_base_JNT', 'spine_01_JNT', 'spine_02_JNT',
'spine_03_JNT', 'chest_JNT']
spine_joints = create_joint_chain(spine_positions, spine_names, radius=2.0)
import maya.cmds as cmds
def create_limb_joints(side='L', limb='arm', positions=None, radius=1.5):
"""
Create a standard 3-joint limb chain (shoulder/hip, elbow/knee, wrist/ankle).
Args:
side: 'L' or 'R'.
limb: 'arm' or 'leg'.
positions: Optional list of 3 positions. Uses defaults if None.
radius: Joint display radius.
Returns:
Dict with 'joints' list and 'root'/'mid'/'end' keys.
"""
if limb == 'arm':
default_positions = [
(15 if side == 'L' else -15, 140, 0), # shoulder
(40 if side == 'L' else -40, 140, -5), # elbow
(65 if side == 'L' else -65, 140, 0), # wrist
]
part_names = ['shoulder', 'elbow', 'wrist']
elif limb == 'leg':
default_positions = [
(10 if side == 'L' else -10, 95, 0), # hip
(10 if side == 'L' else -10, 50, 2), # knee
(10 if side == 'L' else -10, 5, 0), # ankle
]
part_names = ['hip', 'knee', 'ankle']
else:
cmds.warning(f'Unknown limb type: {limb}')
return None
positions = positions or default_positions
names = [f'{side}_{part}_JNT' for part in part_names]
joints = create_joint_chain(positions, names, radius=radius)
return {
'joints': joints,
'root': joints[0],
'mid': joints[1],
'end': joints[2],
}
# Create left and right arms
l_arm = create_limb_joints(side='L', limb='arm')
r_arm = create_limb_joints(side='R', limb='arm')
Always call cmds.select(clear=True) before creating joints. Maya automatically parents new joints under the currently selected joint, which can silently create incorrect hierarchies if something is selected.
IK/FK Setup
Most production rigs use an IK/FK blending system. The animator can seamlessly switch between inverse kinematics (goal-driven) and forward kinematics (rotation-driven) control. Here's how to set one up programmatically.
import maya.cmds as cmds
def create_ikfk_setup(bind_joints, side='L', limb='arm'):
"""
Create a full IK/FK system for a 3-joint limb.
Args:
bind_joints: Dict with 'root', 'mid', 'end' joint names (from create_limb_joints).
side: 'L' or 'R'.
limb: 'arm' or 'leg'.
Returns:
Dict containing IK joints, FK joints, IK handle, and switch attribute info.
"""
prefix = f'{side}_{limb}'
root_jnt = bind_joints['root']
mid_jnt = bind_joints['mid']
end_jnt = bind_joints['end']
# --- Duplicate chains for IK and FK ---
def duplicate_chain(suffix):
"""Duplicate bind chain and rename with a new suffix."""
chain = cmds.duplicate(root_jnt, renameChildren=True)
mapping = {}
for orig, dup in zip([root_jnt, mid_jnt, end_jnt], chain[:3]):
new_name = orig.replace('_JNT', f'_{suffix}_JNT')
cmds.rename(dup, new_name)
mapping[orig] = new_name
return [mapping[j] for j in [root_jnt, mid_jnt, end_jnt]]
ik_chain = duplicate_chain('IK')
fk_chain = duplicate_chain('FK')
# --- Create IK handle on the IK chain ---
ik_handle, ik_effector = cmds.ikHandle(
name=f'{prefix}_IKH',
startJoint=ik_chain[0],
endEffector=ik_chain[2],
solver='ikRPsolver'
)
# Create pole vector locator
mid_pos = cmds.xform(mid_jnt, query=True, translation=True, worldSpace=True)
pole_offset = 30 if limb == 'arm' else 30
pole_pos = [mid_pos[0], mid_pos[1], mid_pos[2] - pole_offset]
pole_loc = cmds.spaceLocator(name=f'{prefix}_poleVector_LOC')[0]
cmds.xform(pole_loc, translation=pole_pos, worldSpace=True)
cmds.poleVectorConstraint(pole_loc, ik_handle)
# --- Create IK/FK switch attribute ---
switch_ctrl = f'{prefix}_settings_CTRL'
if not cmds.objExists(switch_ctrl):
switch_ctrl = cmds.spaceLocator(name=switch_ctrl)[0]
if not cmds.attributeQuery('ikFkSwitch', node=switch_ctrl, exists=True):
cmds.addAttr(switch_ctrl, longName='ikFkSwitch', attributeType='float',
minValue=0, maxValue=1, defaultValue=0, keyable=True)
# --- Blend IK/FK into bind chain ---
for bind_jnt, ik_jnt, fk_jnt in zip(
[root_jnt, mid_jnt, end_jnt], ik_chain, fk_chain
):
# Create blend nodes for rotation
blend = cmds.createNode('blendColors', name=f'{bind_jnt}_ikfk_blend')
cmds.connectAttr(f'{ik_jnt}.rotate', f'{blend}.color1')
cmds.connectAttr(f'{fk_jnt}.rotate', f'{blend}.color2')
cmds.connectAttr(f'{switch_ctrl}.ikFkSwitch', f'{blend}.blender')
cmds.connectAttr(f'{blend}.output', f'{bind_jnt}.rotate', force=True)
# --- Control visibility by IK/FK mode ---
reverse_node = cmds.createNode('reverse', name=f'{prefix}_ikfk_reverse')
cmds.connectAttr(f'{switch_ctrl}.ikFkSwitch', f'{reverse_node}.inputX')
# IK visible when switch = 1, FK visible when switch = 0
cmds.connectAttr(f'{switch_ctrl}.ikFkSwitch', f'{ik_handle}.visibility')
cmds.connectAttr(f'{switch_ctrl}.ikFkSwitch', f'{pole_loc}.visibility')
cmds.connectAttr(f'{reverse_node}.outputX', f'{fk_chain[0]}.visibility')
print(f'IK/FK setup complete for {prefix}')
print(f' Switch attr: {switch_ctrl}.ikFkSwitch (0=FK, 1=IK)')
return {
'ik_chain': ik_chain,
'fk_chain': fk_chain,
'ik_handle': ik_handle,
'pole_vector': pole_loc,
'switch_ctrl': switch_ctrl,
}
When duplicating joint chains, Maya appends numbers to the duplicated names. The duplicate_chain helper renames them immediately, but be careful with complex hierarchies - always verify your chain order after duplication.
Control Curve Creation and Coloring
Control curves are the animator's interface to the rig. Good controls are easy to select, clearly indicate their purpose, and follow a consistent color scheme (typically blue for left, red for right, yellow for center).
import maya.cmds as cmds
# Color index mapping for override colors
SIDE_COLORS = {
'L': 6, # Blue
'R': 13, # Red
'C': 17, # Yellow
}
CONTROL_SHAPES = {
'circle': lambda name, radius: cmds.circle(
name=name, normal=(1, 0, 0), radius=radius, constructionHistory=False
)[0],
'square': lambda name, radius: cmds.curve(
name=name, degree=1,
point=[(-radius, 0, -radius), (-radius, 0, radius),
(radius, 0, radius), (radius, 0, -radius),
(-radius, 0, -radius)]
),
'cube': lambda name, radius: cmds.curve(
name=name, degree=1,
point=[(-radius, radius, -radius), (-radius, radius, radius),
(radius, radius, radius), (radius, radius, -radius),
(-radius, radius, -radius), (-radius, -radius, -radius),
(-radius, -radius, radius), (-radius, radius, radius),
(-radius, -radius, radius), (radius, -radius, radius),
(radius, radius, radius), (radius, -radius, radius),
(radius, -radius, -radius), (radius, radius, -radius),
(radius, -radius, -radius), (-radius, -radius, -radius)]
),
'diamond': lambda name, radius: cmds.curve(
name=name, degree=1,
point=[(0, radius, 0), (radius, 0, 0), (0, 0, radius),
(-radius, 0, 0), (0, 0, -radius), (radius, 0, 0),
(0, -radius, 0), (-radius, 0, 0), (0, radius, 0),
(0, 0, radius), (0, -radius, 0), (0, 0, -radius),
(0, radius, 0)]
),
'arrow': lambda name, radius: cmds.curve(
name=name, degree=1,
point=[(0, 0, -radius * 2), (radius, 0, 0), (radius * 0.4, 0, 0),
(radius * 0.4, 0, radius * 2), (-radius * 0.4, 0, radius * 2),
(-radius * 0.4, 0, 0), (-radius, 0, 0), (0, 0, -radius * 2)]
),
}
def create_control(name, shape='circle', radius=1.0, color=None, side=None):
"""
Create a NURBS control curve with the specified shape and color.
Args:
name: Control name (should end with _CTRL).
shape: Shape type from CONTROL_SHAPES dict.
radius: Scale of the control shape.
color: Override color index (0-31). Overrides side color.
side: 'L', 'R', or 'C' - sets color automatically.
Returns:
Name of the created control transform.
"""
if shape not in CONTROL_SHAPES:
cmds.warning(f'Unknown shape "{shape}". Using circle.')
shape = 'circle'
ctrl = CONTROL_SHAPES[shape](name, radius)
# Apply color
color_index = color or SIDE_COLORS.get(side, 17)
ctrl_shape = cmds.listRelatives(ctrl, shapes=True)[0]
cmds.setAttr(f'{ctrl_shape}.overrideEnabled', 1)
cmds.setAttr(f'{ctrl_shape}.overrideColor', color_index)
return ctrl
def create_control_at(name, target, shape='circle', radius=1.0, side=None,
freeze=True):
"""
Create a control at the position/orientation of a target node.
Args:
name: Control name.
target: Node to match position/orientation to.
shape: Control shape type.
radius: Control size.
side: 'L', 'R', or 'C' for coloring.
freeze: If True, freeze transforms after positioning.
Returns:
Tuple of (control, offset_group).
"""
ctrl = create_control(name, shape=shape, radius=radius, side=side)
# Create offset group (zero group) to hold the control
offset_grp = cmds.group(ctrl, name=name.replace('_CTRL', '_offset_GRP'))
# Match transform to target
cmds.matchTransform(offset_grp, target, position=True, rotation=True)
if freeze:
cmds.makeIdentity(ctrl, apply=True, translate=True, rotate=True, scale=True)
return ctrl, offset_grp
# Example: Create arm controls
l_shoulder_ctrl, l_shoulder_grp = create_control_at(
'L_shoulder_CTRL', 'L_shoulder_JNT', shape='circle', radius=5, side='L')
l_elbow_ctrl, l_elbow_grp = create_control_at(
'L_elbow_CTRL', 'L_elbow_JNT', shape='circle', radius=4, side='L')
l_wrist_ctrl, l_wrist_grp = create_control_at(
'L_wrist_CTRL', 'L_wrist_JNT', shape='cube', radius=3, side='L')
The "offset group" (also called a "zero group") is essential. It stores the world-space position of the control so the control itself can have zeroed-out transforms. When an animator resets the control to (0,0,0), it returns to its default pose - not the world origin.
Constraint-Based Setups
Constraints are how controls drive joints. The right constraint type depends on what the control needs to do - drive rotation only, follow a position, or blend between multiple targets.
import maya.cmds as cmds
def constrain_fk_chain(controls, joints, maintain_offset=True):
"""
Apply orient constraints from FK controls to their corresponding joints.
Args:
controls: List of FK control names.
joints: List of joint names (same order as controls).
maintain_offset: Whether to maintain offset on constraints.
Returns:
List of constraint names.
"""
constraints = []
for ctrl, jnt in zip(controls, joints):
constraint = cmds.orientConstraint(
ctrl, jnt, maintainOffset=maintain_offset,
name=f'{jnt}_orientConstraint'
)[0]
constraints.append(constraint)
return constraints
def create_space_switch(driven_node, spaces, switch_attr_node=None,
switch_attr_name='space', constraint_type='parent'):
"""
Create a space switching system for a control.
Args:
driven_node: The node to be space-switched (usually the offset group).
spaces: Dict of {space_name: driver_node}, e.g., {'world': 'world_CTRL', 'hip': 'C_hip_CTRL'}.
switch_attr_node: Node to hold the enum attribute. Defaults to driven_node.
switch_attr_name: Name of the enum attribute.
constraint_type: 'parent', 'orient', or 'point'.
Returns:
Dict with constraint name and attribute info.
"""
switch_node = switch_attr_node or driven_node
space_names = list(spaces.keys())
drivers = list(spaces.values())
# Create the enum attribute
enum_str = ':'.join(space_names)
if not cmds.attributeQuery(switch_attr_name, node=switch_node, exists=True):
cmds.addAttr(switch_node, longName=switch_attr_name,
attributeType='enum', enumName=enum_str, keyable=True)
# Create the constraint with all drivers
constraint_fn = {
'parent': cmds.parentConstraint,
'orient': cmds.orientConstraint,
'point': cmds.pointConstraint,
}[constraint_type]
constraint = constraint_fn(
*drivers, driven_node,
maintainOffset=True,
name=f'{driven_node}_spaceSwitch'
)[0]
# Get weight aliases from the constraint
weight_attrs = constraint_fn(constraint, query=True, weightAliasList=True)
# Create condition nodes to drive weights from the enum
for i, (space_name, weight_attr) in enumerate(zip(space_names, weight_attrs)):
condition = cmds.createNode('condition', name=f'{driven_node}_{space_name}_condition')
cmds.setAttr(f'{condition}.secondTerm', i)
cmds.setAttr(f'{condition}.colorIfTrueR', 1)
cmds.setAttr(f'{condition}.colorIfFalseR', 0)
cmds.setAttr(f'{condition}.operation', 0) # Equal
cmds.connectAttr(f'{switch_node}.{switch_attr_name}', f'{condition}.firstTerm')
cmds.connectAttr(f'{condition}.outColorR', f'{constraint}.{weight_attr}')
print(f'Space switch created on {driven_node}: {space_names}')
return {'constraint': constraint, 'attr': f'{switch_node}.{switch_attr_name}'}
# Example: FK arm constraints
# constrain_fk_chain(
# ['L_shoulder_CTRL', 'L_elbow_CTRL', 'L_wrist_CTRL'],
# ['L_shoulder_FK_JNT', 'L_elbow_FK_JNT', 'L_wrist_FK_JNT']
# )
# Example: Space switch on the wrist
# create_space_switch(
# 'L_wrist_offset_GRP',
# spaces={'world': 'world_CTRL', 'chest': 'C_chest_CTRL', 'hip': 'C_hip_CTRL'},
# switch_attr_node='L_wrist_CTRL'
# )
Space switching is one of the most requested features by animators. It lets them change what coordinate space a control operates in - for example, switching a hand from following the body to staying fixed in world space for a wall-push animation.
Building a Simple Auto-Rigger
Let's combine everything into a function that builds a basic biped upper body rig - spine, arms, and head - in a single call.
import maya.cmds as cmds
def build_biped_rig():
"""
Build a simple biped rig with spine, arms, neck, and head.
Creates joints, controls, and constraints from scratch.
"""
print('--- Building Biped Rig ---')
# ==========================================
# 1. Create rig hierarchy groups
# ==========================================
rig_grp = cmds.group(empty=True, name='rig_GRP')
jnt_grp = cmds.group(empty=True, name='skeleton_GRP', parent=rig_grp)
ctrl_grp = cmds.group(empty=True, name='controls_GRP', parent=rig_grp)
geo_grp = cmds.group(empty=True, name='geometry_GRP', parent=rig_grp)
# Lock geo group transforms to prevent accidental movement
for attr in ['tx', 'ty', 'tz', 'rx', 'ry', 'rz', 'sx', 'sy', 'sz']:
cmds.setAttr(f'{geo_grp}.{attr}', lock=True)
# ==========================================
# 2. Build spine joints
# ==========================================
spine_positions = [
(0, 100, 0), (0, 108, -1), (0, 116, -1.5),
(0, 124, -1), (0, 132, 0),
]
spine_names = ['C_spine_base_JNT', 'C_spine_01_JNT', 'C_spine_02_JNT',
'C_spine_03_JNT', 'C_chest_JNT']
spine_jnts = create_joint_chain(spine_positions, spine_names,
radius=2.0, parent=jnt_grp)
print(f' Spine: {len(spine_jnts)} joints')
# ==========================================
# 3. Build neck and head
# ==========================================
neck_positions = [(0, 140, 0), (0, 148, 1), (0, 156, 0)]
neck_names = ['C_neck_base_JNT', 'C_neck_01_JNT', 'C_head_JNT']
neck_jnts = create_joint_chain(neck_positions, neck_names, radius=1.5)
cmds.parent(neck_jnts[0], spine_jnts[-1])
print(f' Neck/Head: {len(neck_jnts)} joints')
# ==========================================
# 4. Build arms
# ==========================================
arm_data = {}
for side in ['L', 'R']:
mirror = 1 if side == 'L' else -1
arm_positions = [
(15 * mirror, 135, 0), # shoulder
(40 * mirror, 135, -5), # elbow
(65 * mirror, 135, 0), # wrist
]
arm_names = [f'{side}_shoulder_JNT', f'{side}_elbow_JNT', f'{side}_wrist_JNT']
arm_jnts = create_joint_chain(arm_positions, arm_names, radius=1.5)
cmds.parent(arm_jnts[0], spine_jnts[-1])
arm_data[side] = {
'joints': arm_jnts,
'root': arm_jnts[0],
'mid': arm_jnts[1],
'end': arm_jnts[2],
}
print(f' {side} Arm: {len(arm_jnts)} joints')
# ==========================================
# 5. Create controls
# ==========================================
# Global control
global_ctrl = create_control('C_global_CTRL', shape='arrow', radius=12, side='C')
cmds.parent(global_ctrl, ctrl_grp)
cmds.parentConstraint(global_ctrl, jnt_grp, maintainOffset=True)
# COG control
cog_ctrl, cog_grp = create_control_at(
'C_cog_CTRL', spine_jnts[0], shape='circle', radius=10, side='C')
cmds.parent(cog_grp, global_ctrl)
# Spine FK controls
spine_ctrls = []
spine_ctrl_grps = []
for i, jnt in enumerate(spine_jnts):
ctrl, grp = create_control_at(
f'C_spine_{i:02d}_CTRL', jnt, shape='circle',
radius=8 - i * 0.5, side='C')
spine_ctrls.append(ctrl)
spine_ctrl_grps.append(grp)
# Parent spine controls in hierarchy
cmds.parent(spine_ctrl_grps[0], cog_ctrl)
for i in range(1, len(spine_ctrl_grps)):
cmds.parent(spine_ctrl_grps[i], spine_ctrls[i - 1])
# Constrain spine joints to controls
for ctrl, jnt in zip(spine_ctrls, spine_jnts):
cmds.orientConstraint(ctrl, jnt, maintainOffset=True)
cmds.pointConstraint(spine_ctrls[0], spine_jnts[0], maintainOffset=True)
# Head control
head_ctrl, head_grp = create_control_at(
'C_head_CTRL', neck_jnts[-1], shape='cube', radius=5, side='C')
cmds.parent(head_grp, spine_ctrls[-1])
cmds.orientConstraint(head_ctrl, neck_jnts[-1], maintainOffset=True)
# Arm FK controls
for side in ['L', 'R']:
limb = arm_data[side]
arm_ctrls = []
arm_ctrl_grps = []
for jnt_name in limb['joints']:
part = jnt_name.split('_')[1] # shoulder, elbow, wrist
ctrl, grp = create_control_at(
f'{side}_{part}_CTRL', jnt_name, shape='circle',
radius=4, side=side)
arm_ctrls.append(ctrl)
arm_ctrl_grps.append(grp)
# Parent arm control hierarchy
cmds.parent(arm_ctrl_grps[0], spine_ctrls[-1])
for i in range(1, len(arm_ctrl_grps)):
cmds.parent(arm_ctrl_grps[i], arm_ctrls[i - 1])
# Constrain arm joints
for ctrl, jnt in zip(arm_ctrls, limb['joints']):
cmds.orientConstraint(ctrl, jnt, maintainOffset=True)
# ==========================================
# 6. Lock and hide unused attributes
# ==========================================
all_ctrls = cmds.ls('*_CTRL')
for ctrl in all_ctrls:
for attr in ['sx', 'sy', 'sz', 'v']:
cmds.setAttr(f'{ctrl}.{attr}', lock=True, keyable=False, channelBox=False)
print('--- Biped Rig Complete ---')
print(f' Rig group: {rig_grp}')
print(f' Total joints: {len(cmds.ls(type="joint"))}')
print(f' Total controls: {len(all_ctrls)}')
cmds.select(clear=True)
return rig_grp
# Run the auto-rigger
# build_biped_rig()
This auto-rigger uses the create_joint_chain, create_control, and create_control_at functions defined earlier on this page. In a real production setup, these would live in a shared rig_utils module that all your rig scripts import from.
Organizing Rig Hierarchies
A well-organized rig is easier to debug, animate, and maintain. Here's how to structure your rig hierarchy programmatically and enforce naming conventions.
import maya.cmds as cmds
RIG_HIERARCHY = {
'char_GRP': {
'skeleton_GRP': {},
'controls_GRP': {
'global_CTRL_GRP': {},
'body_CTRL_GRP': {},
'face_CTRL_GRP': {},
},
'geometry_GRP': {
'render_GEO_GRP': {},
'proxy_GEO_GRP': {},
},
'rig_systems_GRP': {
'ik_systems_GRP': {},
'constraints_GRP': {},
'utility_nodes_GRP': {},
},
'DO_NOT_TOUCH_GRP': {},
}
}
def create_hierarchy(structure, parent=None):
"""
Recursively create a group hierarchy from a nested dict.
Args:
structure: Dict where keys are group names, values are child dicts.
parent: Parent node name. None for world level.
Returns:
Dict mapping group names to their full DAG paths.
"""
created = {}
for group_name, children in structure.items():
if cmds.objExists(group_name):
grp = group_name
else:
grp = cmds.group(empty=True, name=group_name)
if parent:
cmds.parent(grp, parent)
created[group_name] = grp
if children:
child_groups = create_hierarchy(children, parent=grp)
created.update(child_groups)
return created
def validate_rig_naming():
"""Check all rig nodes follow the naming convention: side_name_type."""
valid_suffixes = ['GRP', 'CTRL', 'JNT', 'GEO', 'LOC', 'IKH', 'CRV', 'OFFSET']
valid_sides = ['L', 'R', 'C']
issues = []
for node in cmds.ls(transforms=True):
# Skip default cameras and UI elements
if node in ['persp', 'top', 'front', 'side'] or '|' in node:
continue
parts = node.split('_')
if len(parts) < 2:
issues.append(f' Missing suffix: {node}')
continue
suffix = parts[-1]
if suffix not in valid_suffixes:
issues.append(f' Invalid suffix "{suffix}": {node}')
if issues:
print(f'Naming issues found ({len(issues)}):')
for issue in issues[:20]: # limit output
print(issue)
else:
print('All naming conventions pass.')
return issues
# Usage:
# groups = create_hierarchy(RIG_HIERARCHY)
# validate_rig_naming()
Always run your auto-rigger on a clean scene or with a delete-and-rebuild workflow. If you run it on top of an existing rig, you'll get duplicate nodes and broken connections. A common pattern is to have a delete_rig() function that removes everything under the top-level rig group before rebuilding.
This guide covers the fundamentals of scripted rigging. To take it further, explore adding stretch/squash systems, ribbon spines, twist extractors, and facial rigging. For the underlying Python techniques, see our PyMEL guide and Python API tutorial.