top of page

Maya | Python | Rigging Part 1 | Create control that blend between FK and IK

  • Writer: Max Liu
    Max Liu
  • Dec 11, 2024
  • 5 min read

Updated: Dec 16, 2024

  1. Create reference sketon shows in the video.

  2. Run the code.

  3. Change FK bones.

  4. Select Plus sign control.

  5. Ctrl and Drag the IK FK attribute.

  6. See the result.




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_vectors = '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_vectors:
        cmds.error('Must provide a pole vector guide.')

    #pa = 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 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=True, sel=None):
    """
    A custom alignment function that:
    - Takes a list `sel` where sel[0] is the control and sel[1] is the target object (joint).
    - Creates an offset group above the control.
    - Aligns the offset group to the target object so the control matches the joint's transform.
    - Returns the name of the offset group.

    Args:
        snap_align (bool): If True, the control is snapped and aligned to the target.
        sel (list): [control, target] The objects to align. The control is sel[0], the target is sel[1].

    Returns:
        str: The name of the offset group node created.
    """
    if sel is None or len(sel) < 2:
        cmds.error("align_lras requires a control and a target object.")

    ctrl = sel[0]
    target = sel[1]

    if not cmds.objExists(ctrl) or not cmds.objExists(target):
        cmds.error("One of the given objects does not exist in the scene.")

    # Create an offset group node
    # A common naming convention is to append "_OFF" or "_grp"
    offset_grp = cmds.group(empty=True, name=ctrl + "_OFF")

    if snap_align:
        # Get the world matrix of the target
        target_matrix = cmds.xform(target, q=True, ws=True, m=True)
        
        # Apply the target’s transform to the offset group
        cmds.xform(offset_grp, ws=True, m=target_matrix)

        # Now parent the control under this offset group so it inherits the same transform
        # Before parenting, ensure the control has no existing parent relationship that might cause issues
        current_parent = cmds.listRelatives(ctrl, parent=True)
        if current_parent:
            cmds.parent(ctrl, world=True)

        cmds.parent(ctrl, offset_grp)

        # After parented, the control now matches the target's position and orientation.
        # If desired, you could also freeze transforms here. However, since this is a rigging setup,
        # it's often not needed as the offset group preserves the control's zeroed out state.

    return offset_grp

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


limb()

Comments


bottom of page