Maya | Python | Rigging Part 3 | Add control to IK
- Max Liu
- Dec 16, 2024
- 6 min read
Create Reference bone and locator as the video shows.
Run the code.
from ssl import DefaultVerifyPaths
import maya.cmds as cmds
import math
# Joint guides and aliases for a 3-joint limb (e.g. shoulder, elbow, wrist)
def limb(side='l', part='arm',
joint_list = ['l_shoulder_gde_JNT', 'l_elbow_gde_JNT', 'l_wrist_gde_JNT'],
alias_list = ['shoulder', 'elbow', 'wrist'],
pole_vector = 'l_shoulder_pv_gde_LOC', remove_guides = False, add_stretch = False, color_dict = False, primary_axis = 'X', up_axis = 'Y'):
if len(joint_list) != 3:
cmds.error('Must provide three guides to build three joint limb.')
if len(alias_list) != 3:
cmds.error('Must provide three aliases, one for each joint.')
if not pole_vector:
cmds.error('Must provide a pole vector guide.')
primaryAxis = define_axis(primary_axis)
base_name = side + '_' + part
# Create different chain types: IK, FK, and Bind
ik_chain = create_chain(side, joint_list, alias_list, 'IK')
fk_chain = create_chain(side, joint_list, alias_list, 'FK')
bind_chain = create_chain(side, joint_list, alias_list, 'bind')
# optimizae conrtol size by using a fraction of the start to end length.
r = distance_between(fk_chain[0], fk_chain[-1]) / float(5)
########################################
# CREATE THE FK CONTROL
########################################
# create FK controls and connect to fk joint chain
fk_ctrls = []
for i, alias in enumerate(alias_list):
# create fk controls
ctrl = cmds.circle(radius=r, normal=primaryAxis, degree=3, name='{}_{}_FK_CTRL'.format(side, alias))[0]
# align control to joint
# Note: It's generally better to align before parenting to avoid double transforms.
align_lras(snap_align=True, sel=[ctrl, fk_chain[i]])
if i != 0:
# If not the first control, parent it under the previous control
cmds.parent(ctrl, par)
# define parent control for the next iteration
par = ctrl
if i == 0:
# Since there's no offset group now, this can just refer to the first FK ctrl
fk_top_grp = ctrl
# connect control to joint
cmds.pointConstraint(ctrl, fk_chain[i], mo=False)
cmds.connectAttr(ctrl + ".rotate", fk_chain[i] + ".rotate")
fk_ctrls.append(ctrl)
########################################
# CREATE THE WORLD IK CONTROL
########################################
## Create a main/world IK control circle. This is often a higher-level control for the IK system.
world_ctrl = cmds.circle(radius=r * 1.2, normal=primaryAxis, degree=1, sections=4, constructionHistory=False, name=base_name + '_IK_CTRL')[0]
cmds.setAttr(world_ctrl + '.rotate' + primary_axis[-1], 45)
a_to_b(is_trans=True, is_rot=False, sel=[world_ctrl, ik_chain[-1]], freeze=True)
local_ctrl = cmds.circle(radius=r, normal=primaryAxis, degree=1, sections=4, constructionHistory=False, name=base_name + '_local_IK_CTRL')[0]
cmds.setAttr(local_ctrl + '.rotate' + primary_axis[-1], 45)
cmds.makeIdentity(local_ctrl, apply=True, rotate=True)
local_off = align_lras(snap_align=True, sel=[local_ctrl, ik_chain[-1]])
cmds.parent(local_off, world_ctrl)
loc_points = [
[0.0, 0.0, 0.0], [0.0, -1.0, 0.0],
[0.0, 0.0, 0.0], [1.0, 0.0, 0.0],
[0.0, 0.0, 0.0], [-1.0, 0.0, 0.0],
[0.0, 0.0, 0.0], [0.0, 0.0, 1.0],
[0.0, 0.0, 0.0], [0.0, 0.0,-1.0]]
pv_ctrl = curve_control(loc_points, name=base_name + '_PV_CTRL')
cmds.setAttr(pv_ctrl + '.scale', r * 0.25, r * 0.25, r * 0.25)
a_to_b(is_trans=True, is_rot=False, sel=[pv_ctrl, pole_vector], freeze=True)
base_ctrl = cmds.circle(
radius=r * 1.2,
normal=primaryAxis,
degree=1,
sections=4,
constructionHistory=False,
name='{}_{}_IK_CTRL'.format(side, alias_list[0])
)[0]
# Rotate the base control for consistency
cmds.setAttr(base_ctrl + '.rotate' + primary_axis[-1], 45)
# Align the base_ctrl translation to the start joint of the IK chain and freeze
a_to_b(is_trans=True, is_rot=False, sel=[base_ctrl, ik_chain[0]], freeze=True)
# Parent constrain the first joint of the IK chain to the base_ctrl to maintain its position
cmds.parentConstraint(base_ctrl, ik_chain[0], maintainOffset=True)
########################################
# CREATE THE IK HANDLE
########################################
# Create the actual IK handle, the first item return from the list is the ikh(IK handle).
ikh = cmds.ikHandle(
name=base_name + '_IKH',
startJoint=ik_chain[0],
endEffector=ik_chain[-1],
sticky='sticky',
solver='ikRPsolver',
setupForRPsolver=True
)[0]
# Parent constrain the IK handle to the local_ctrl (so moving the local_ctrl moves the IK)
cmds.parentConstraint(local_ctrl, ikh, maintainOffset=True)
# Add a pole vector constraint so pv_ctrl controls the knee/elbow direction of the IK chain
cmds.poleVectorConstraint(pv_ctrl, ikh)
########################################
# CREATE THE IF FK Blend Control
########################################
#Create puls shape.
# Example points for a settings control curve (plus shape)
plus_points = [
[-0.333, 0.333, 0.0],
[-0.333, 1.0, 0.0],
[ 0.333, 1.0, 0.0],
[ 0.333, 0.333, 0.0],
[ 1.0, 0.333, 0.0],
[ 1.0, -0.333, 0.0],
[ 0.333, -0.333, 0.0],
[ 0.333, -1.0, 0.0],
[-0.333, -1.0, 0.0],
[-0.333, -0.333, 0.0],
[-1.0, -0.333, 0.0],
[-1.0, 0.333, 0.0],
[-0.333, 0.333, 0.0] # Last point repeats the first to close the shape
]
settings_ctrl = curve_control(point_list = plus_points, name = base_name + '_settings_CTRL')
settings_off = align_lras(snap_align=True, sel = [settings_ctrl, bind_chain[-1]])
cmds.setAttr(settings_ctrl + '.scale', r * 0.25, r * 0.25, r * 0.25)
#offset the plus sign control to translateY direction.
if up_axis[0] == '-':
cmds.setAttr(settings_ctrl + '.translate' + up_axis[-1], r * -1.5)
else:
cmds.setAttr(settings_ctrl + '.translate' + up_axis[-1], r * 1.5)
#makeIdentity() Freeze/resets the object’s transforms so that the current position, rotation, and scale become its new "zeroed" state.
#After this, the control’s channels will read as zeroed out (0 for translate/rotate, 1 for scale), providing a clean starting point for animation.
cmds.makeIdentity(settings_ctrl, apply = True, translate = True, rotate = True, scale = True, normal = False)
#A parent constraint is applied so that if the limb (bind joint) moves, the settings control stays in a consistent relationship to it. maintainOffset=True means the control stays where it is, just following the joint’s movements, rather than snapping directly onto the joint.
cmds.parentConstraint(bind_chain[-1], settings_ctrl, maintainOffset = True)
#This adds a custom attribute fkIk to the settings control, allowing the user to blend between FK (value of 1) and IK (value of 0) modes. Ranging from 0 to 1 makes for a simple slider the animator can adjust.
cmds.addAttr(settings_ctrl, longName='fkIk', attributeType='double', minValue=0, maxValue=1,
defaultValue=1, keyable=True)
blend_chains(base_name, ik_chain, fk_chain, bind_chain)
# implement blend function utilizing blendcolors node.
def blend_chains(base_name, ik_chain, fk_chain, bind_chain):
for ik, fk, bind in zip(ik_chain, fk_chain, bind_chain):
for attr in ['translate', 'rotate', 'scale']:
bcn = cmds.createNode('blendColors', name = bind.replace('bind_JNT', attr + '_BCN'))
#connect ik.attr and bcm.color1
cmds.connectAttr(ik + '.' + attr, bcn + '.color1')
cmds.connectAttr(fk + '.' + attr, bcn + '.color2')
cmds.connectAttr(base_name + '_settings_CTRL.fkIk', bcn + '.blender')
cmds.connectAttr(bcn + '.output', bind + '.' + attr)
def create_chain(side, joint_list, alias_list, suffix):
chain = []
jnt = None
for j, a in zip(joint_list, alias_list):
# For the first joint, there is no parent
if j == joint_list[0]:
par = None
else:
par = jnt
# Create a joint with a naming convention side_alias_suffix
jnt = cmds.joint(par, n='{}_{}_{}.JNT'.format(side, a, suffix))
# Matching the transform from the guide joint to the created joint, and freezing transforms
match_and_freeze_transform(j, jnt)
chain.append(jnt)
return chain
def define_axis(axis):
#t checks the last character of the axis string. If it's 'X', it sets vector_axis to (1,0,0). For 'Y', (0,1,0), and for 'Z', (0,0,1).
if axis[-1] == 'X':
vector_axis = (1, 0, 0)
elif axis[-1] == 'Y':
vector_axis = (0, 1, 0)
elif axis[-1] == 'Z':
vector_axis = (0, 0, 1)
else:
cmds.error('Must provide either X, Y, or Z for the axis.')
#It then checks if there is a '-' character anywhere in the axis string. If so, it multiplies each component of the vector by -1. For example, if axis = '-X', we start with (1,0,0) and flip it to (-1,0,0)
if '-' in axis:
vector_axis = tuple(va * -1 for va in vector_axis)
return vector_axis
def match_and_freeze_transform(source, target):
"""
Matches the world-space transform (translation, rotation, scale) of 'target'
to that of 'source' and then freezes transforms on the 'target'.
"""
# Query the source transform matrix
source_matrix = cmds.xform(source, query=True, worldSpace=True, matrix=True)
# Apply the source's matrix to the target
cmds.xform(target, worldSpace=True, matrix=source_matrix)
# Freeze transformations on the target joint:
# This applies the current transformations (translation, rotation, scale)
# as the object's default and resets the transforms to zero-out in local space.
cmds.makeIdentity(target, apply=True, translate=True, rotate=True, scale=True, normal=False)
def align_lras(snap_align=False, sel=[]):
# `sel` should be [object_to_align, target_object]
if len(sel) < 2:
cmds.error("align_lras requires two objects in 'sel': [object_to_align, target].")
obj, target = sel
if snap_align:
# Create a temporary constraint to snap obj to target, then delete it
tmp_const = cmds.parentConstraint(target, obj, mo=False)
cmds.delete(tmp_const)
# Freeze transforms after alignment
cmds.makeIdentity(obj, apply=True, t=1, r=1, s=1, n=0)
return obj
def distance_between(node_a, node_b):
# Query the world space positions of the rotate pivot of the nodes
point_a = cmds.xform(node_a, query=True, worldSpace=True, rotatePivot=True)
point_b = cmds.xform(node_b, query=True, worldSpace=True, rotatePivot=True)
# Calculate the Euclidean distance between the two points
dist = math.sqrt(sum([pow((b - a), 2) for b, a in zip(point_b, point_a)]))
return dist
def curve_control(point_list, name, degree = 1):
crv = cmds.curve(degree = degree, editPoint = point_list, name = name)
shp = cmds.listRelatives(crv, shapes = True)[0]
cmds.rename(shp, crv + 'Shape')
return crv
def a_to_b(is_trans=True, is_rot=True, sel=[], freeze=False):
if len(sel) < 2:
cmds.error("a_to_b requires two objects in 'sel': [object_to_align, reference_object].")
obj, ref = sel
# Determine which channels to skip
skipTranslate = []
skipRotate = []
if not is_trans:
skipTranslate = ['x','y','z']
if not is_rot:
skipRotate = ['x','y','z']
# Use a parent constraint to align obj to ref
temp_constraint = cmds.parentConstraint(
ref, obj, mo=False,
skipTranslate=skipTranslate,
skipRotate=skipRotate
)
cmds.delete(temp_constraint)
# Freeze transforms if requested
if freeze:
cmds.makeIdentity(obj, apply=True, t=True, r=True, s=True, n=0)
return obj
limb()
Comments