top of page

Maya | Python | Rigging Part 3 | Add control to IK

  • Writer: Max Liu
    Max Liu
  • Dec 16, 2024
  • 6 min read

  1. Create Reference bone and locator as the video shows.

  2. 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


bottom of page