Maya | Python | Rigging Part 5 | Add stretch to FK
- Max Liu
- Dec 18, 2024
- 13 min read
add_fk_stretch function:
def add_fk_stretch(fk_ctrls, fk_chain, primary_axis):
for i, ctrl in enumerate(fk_ctrls):
if not ctrl == fk_ctrls[-1]:
cmds.addAttr(ctrl, attributeType='double', min=0.001, defaultValue=1, keyable=True, longName='stretch')
mdl = cmds.createNode('multDoubleLinear', name=ctrl.replace('CTRL', 'MDL'))
loc = cmds.spaceLocator(name=fk_chain[i+1].replace('JNT', 'OFF_LOC'))[0]
cmds.parent(loc, fk_chain[i])
a_to_b(is_trans=True, is_rot=True, sel=[loc, fk_chain[i+1]], freeze=True)
offset_val = cmds.getAttr(loc + '.translate' + primary_axis)
cmds.setAttr(mdl + '.input1', offset_val)
cmds.connectAttr(ctrl + '.stretch', mdl + '.input2')
cmds.connectAttr(mdl + '.output', loc + '.translate' + primary_axis)
# Connect the stretch attribute to the scale of the current joint
cmds.connectAttr(ctrl + '.stretch', fk_chain[i] + '.scale' + primary_axis)
# Create a point constraint to move the next joint along with the current joint's end
point_const = cmds.pointConstraint(loc, fk_chain[i+1], mo=False)
# Ensure the next control follows the movement
if cmds.objExists(fk_ctrls[i + 1] + '.offsetParentMatrix'):
cmds.connectAttr(loc + '.matrix', fk_ctrls[i + 1] + '.offsetParentMatrix')
else:
dcm = cmds.createNode('decomposeMatrix', name=loc + '_DCM')
cmds.connectAttr(loc + '.matrix', dcm + '.inputMatrix')
for attr in ['translate', 'rotate', 'scale']:
cmds.connectAttr(dcm + '.output' + attr.title(), fk_ctrls[i + 1] + '_OFF_GRP.' + attr)
# Ensure the next control moves along with the end of the current joint
cmds.pointConstraint(loc, fk_ctrls[i + 1] + '_OFF_GRP', mo=False)
Add a "stretch" capability to a chain of FK joints and their corresponding FK controls, allowing the joints to lengthen (scale) along a specified axis and their subsequent controls to follow that lengthening.
Main Steps:
Add a stretch attribute to each FK control (except the last one):For each control in fk_ctrls (except the last), create a "stretch" attribute. This attribute will be used to scale the joint and its subsequent positioning nodes.
Create and position a locator to measure offset:A locator is created and placed at the next joint’s position. Using a_to_b, the locator is aligned to the next joint. The distance along the primary axis from the current joint to the next joint is stored as an offset value.
Set up stretch math with multDoubleLinear node:A multDoubleLinear (MDL) node is created to multiply the "stretch" attribute by the original offset distance. Connecting stretch to MDL.input2 and the locator’s original offset to MDL.input1 produces a dynamic offset. This output controls the locator’s translation along the primary axis, effectively moving it further away as stretch increases.
Scaling the joint along the primary axis:The "stretch" attribute from the control is also connected to the joint’s scale on the specified axis. As "stretch" increases, the joint lengthens accordingly.
Maintaining control hierarchy and positions:
A point constraint ensures the next joint follows the newly positioned locator, so the limb actually stretches in the scene.
If the next control supports offsetParentMatrix, the locator’s world matrix is connected directly to that control’s offsetParentMatrix, ensuring the next control moves with the stretched joint. Otherwise, a decomposeMatrix node breaks down the locator’s matrix into translation, rotation, and scale, which are then connected to the next control’s offset group.
Another point constraint ensures the next control’s offset group follows the locator, maintaining proper hierarchical behavior as the limb stretches.
In essence, this script sets up a chain of driven attributes and constraints so that increasing the "stretch" attribute on a control causes its corresponding joint to lengthen and properly positions subsequent joints and controls to follow that stretching motion, keeping the limb’s FK system coherent and functional.
Full 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 = True, color_dict = False, primary_axis = 'X', up_axis = 'Y'):
"""
Creates a 3-joint limb rig with FK and IK controls.
:param side: str, Side of the limb (e.g., 'l' for left, 'r' for right)
:param part: str, Part of the limb (e.g., 'arm', 'leg')
:param joint_list: list, List of joint guide names
:param alias_list: list, List of aliases for the joints
:param pole_vector: str, Name of the pole vector guide
:param remove_guides: bool, Whether to remove guides after creating the rig
:param add_stretch: bool, Whether to add stretch functionality to the IK chain
:param color_dict: bool, Dictionary for color coding controls (not used in this function)
:param primary_axis: str, Primary axis for control orientation (e.g., 'X', 'Y', 'Z')
:param up_axis: str, Up axis for control orientation (e.g., 'Y', '-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]
# Create the offset group for the control
off_grp = cmds.group(empty=True, name='{}_{}_FK_CTRL_OFF_GRP'.format(side, alias))
cmds.parent(ctrl, off_grp)
# align control to joint
# Note: It's generally better to align before parenting to avoid double transforms.
align_lras(snap_align=True, sel=[off_grp, fk_chain[i]])
if i != 0:
# If not the first control, parent it under the previous control
cmds.parent(off_grp, 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 = off_grp
# 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)
if add_stretch:
ik_stretch = add_ik_stretch(side, part, ik_chain, base_ctrl, local_ctrl, world_ctrl, primary_axis)
add_fk_stretch(fk_ctrls, fk_chain, primary_axis)
# implement blend function utilizing blendcolors node.
def blend_chains(base_name, ik_chain, fk_chain, bind_chain):
"""
Blends between IK and FK chains using blendColors nodes.
:param base_name: str, Base name for the rig components
:param ik_chain: list, List of IK joint names
:param fk_chain: list, List of FK joint names
:param bind_chain: list, List of bind joint names
"""
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)
# Create a curve control with the given points and name with arguments
def create_chain(side, joint_list, alias_list, suffix):
"""
Creates a joint chain based on provided guides and aliases.
:param side: str, Side of the limb (e.g., 'l' for left, 'r' for right)
:param joint_list: list, List of joint guide names
:param alias_list: list, List of aliases for the joints
:param suffix: str, Suffix for the joint names (e.g., 'IK', 'FK', 'bind')
"""
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):
"""
Defines the primary axis vector based on the given axis string.
:param axis: str, Axis string (e.g., 'X', '-Y')
:return: tuple, Axis vector
"""
#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 of the target to the source and freezes transforms.
:param source: str, Name of the source transform
:param target: str, Name of the target transform
"""
# 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=[]):
"""
Aligns the local rotation axes of the first object to the second object.
:param snap_align: bool, Whether to snap align the objects
:param sel: list, List of objects to align [object_to_align, target_object]
:return: str, Name of the aligned object
"""
# `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):
"""
Calculates the Euclidean distance between two nodes.
:param node_a: str, Name of the first node
:param node_b: str, Name of the second node
:return: float, Distance between the nodes
"""
# 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):
"""
Creates a curve control with the given points and name.
:param point_list: list, List of points for the curve
:param name: str, Name of the curve control
:param degree: int, Degree of the curve
:return: str, Name of the created curve
"""
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):
"""
Aligns the first object to the second object using a parent constraint.
:param is_trans: bool, Whether to align translation
:param is_rot: bool, Whether to align rotation
:param sel: list, List of objects to align [object_to_align, reference_object]
:param freeze: bool, Whether to freeze transforms after alignment
:return: str, Name of the aligned object
"""
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
def add_ik_stretch(side, part, ik_chain, base_ctrl, local_ctrl, world_ctrl, primary_axis):
"""
Adds stretch functionality to the IK chain.
:param side: str, Side of the limb (e.g., 'l' for left, 'r' for right)
:param part: str, Part of the limb (e.g., 'arm', 'leg')
:param ik_chain: list, List of IK joint names
:param base_ctrl: str, Name of the base control
:param local_ctrl: str, Name of the local control
:param world_ctrl: str, Name of the world control
:param primary_axis: str, Primary axis for scaling (e.g., 'X', 'Y', 'Z')
:return: dict, Dictionary of useful nodes and values
"""
# Constructing a base name for all created nodes using the side and part strings
base_name = side + '_' + part
# ------------------------------------------------------------------------
# Create nodes required for measuring and controlling the stretch behavior
# ------------------------------------------------------------------------
# Create a distanceBetween node to measure the distance between two points (endpoints)
# This node calculates the distance between two input points.
limb_dist = cmds.createNode('distanceBetween', name=base_name + '_DST')
# Create a condition node to compare distances and decide if stretching should occur
# This node will check if the current distance is greater than the original total length.
limb_cnd = cmds.createNode('condition', name=base_name + '_CND')
# Create spaceLocators which serve as reference points to measure the limb length
# The start locator and end locator represent the initial and final positions to measure from.
start_loc = cmds.spaceLocator(name=base_name + '_start_LOC')[0]
end_loc = cmds.spaceLocator(name=base_name + '_end_LOC')[0]
# Create a multiplyDivide node to compute the scaling ratio (distance / original_length)
# This will give us a multiplier to adjust the limb length.
stretch_mdn = cmds.createNode('multiplyDivide', name=base_name + '_stretch_MDN')
# ------------------------------------------------------------------------
# Calculate the original limb length
# ------------------------------------------------------------------------
# Assuming you have a custom function `distance_between` that returns the distance
# between two given joints or transforms in the IK chain.
# Measure distance between the first and second joint of the IK chain
length_a = distance_between(ik_chain[0], ik_chain[1])
# Measure distance between the second and third joint of the IK chain
length_b = distance_between(ik_chain[1], ik_chain[2])
# The total original length of the limb (sum of the two segments)
total_length = length_a + length_b
# ------------------------------------------------------------------------
# Set up constraints for measuring the limb's dynamic length
# ------------------------------------------------------------------------
# The start_loc is constrained to the base controller so it moves with it.
cmds.pointConstraint(base_ctrl, start_loc, maintainOffset=False)
# The end_loc is constrained to the local controller at the other end of the limb.
cmds.pointConstraint(local_ctrl, end_loc, maintainOffset=False)
# Connect the start and end locator worldMatrix attributes to the distanceBetween node
# so the distance node always knows where these locators are in world space.
cmds.connectAttr(start_loc + '.worldMatrix[0]', limb_dist + '.inMatrix1')
cmds.connectAttr(end_loc + '.worldMatrix[0]', limb_dist + '.inMatrix2')
# ------------------------------------------------------------------------
# Calculate the length ratio (current_length / original_length)
# ------------------------------------------------------------------------
# Connect the measured distance from limb_dist to the multiplyDivide node input1X
cmds.connectAttr(limb_dist + '.distance', stretch_mdn + '.input1X')
# Set the original total length as input2X
cmds.setAttr(stretch_mdn + '.input2X', total_length)
# Set the operation of multiplyDivide node to "Divide" (2)
# Now stretch_mdn.outputX = current_length / original_length
cmds.setAttr(stretch_mdn + '.operation', 2)
# ------------------------------------------------------------------------
# Use the condition node to decide whether to stretch
# ------------------------------------------------------------------------
# Connect the current distance as the firstTerm of the condition
cmds.connectAttr(limb_dist + '.distance', limb_cnd + '.firstTerm')
# If the current distance is greater than the original, use the ratio as output
cmds.connectAttr(stretch_mdn + '.outputX', limb_cnd + '.colorIfTrueR')
# Set the secondTerm to the original total length to compare with current distance
cmds.setAttr(limb_cnd + '.secondTerm', total_length)
# Set the condition operation to "Greater Than" (3)
# If current distance > total_length => stretch occurs
cmds.setAttr(limb_cnd + '.operation', 3)
# ------------------------------------------------------------------------
# Add attributes to the world_ctrl to turn stretch on/off and adjust upper/lower segments
# ------------------------------------------------------------------------
# Add a 'stretch' attribute to enable or disable stretching (0 to 1)
cmds.addAttr(world_ctrl, attributeType='double', min=0, max=1, defaultValue=1, keyable=True, longName='stretch')
# Dynamically name the up and lo attributes based on the 'part' name provided
up_name = 'up' + part.title() # e.g. if part = 'arm', up_name = 'upArm'
lo_name = 'lo' + part.title() # e.g. if part = 'arm', lo_name = 'loArm'
# Add upper segment scaling attribute
cmds.addAttr(world_ctrl, attributeType='double', defaultValue=1, keyable=True, longName=up_name)
# Add lower segment scaling attribute
cmds.addAttr(world_ctrl, attributeType='double', defaultValue=1, keyable=True, longName=lo_name)
# Create a blendTwoAttr node to blend between no stretch and full stretch
# The blendTwoAttr allows us to smoothly transition the scaling factor.
stretch_bta = cmds.createNode('blendTwoAttr', name=base_name + '_stretch_BTA')
cmds.setAttr(stretch_bta + '.input[0]', 1)
cmds.connectAttr(limb_cnd + '.outColorR', stretch_bta + '.input[1]')
# Connect the world_ctrl.stretch attribute to the blender of stretch_bta
cmds.connectAttr(world_ctrl + '.stretch', stretch_bta + '.attributesBlender')
# Create plusMinusAverage nodes for upper and lower segments to handle the scaling math
up_pma = cmds.createNode('plusMinusAverage', name=up_name + '_PMA')
lo_pma = cmds.createNode('plusMinusAverage', name=lo_name + '_PMA')
# Connect the upper/lower attributes from the world_ctrl to the respective plusMinusAverage nodes
cmds.connectAttr(world_ctrl + '.' + up_name, up_pma + '.input1D[0]')
cmds.connectAttr(world_ctrl + '.' + lo_name, lo_pma + '.input1D[0]')
# Connect the stretch blend output to these nodes so they consider the stretching factor
cmds.connectAttr(stretch_bta + '.output', up_pma + '.input1D[1]')
cmds.connectAttr(stretch_bta + '.output', lo_pma + '.input1D[1]')
# Set negative inputs to these nodes to ensure correct scaling math (subtract 1 to get desired ratio)
cmds.setAttr(up_pma + '.input1D[2]', -1)
cmds.setAttr(lo_pma + '.input1D[2]', -1)
# Finally, connect the output of the plusMinusAverage nodes to the actual joints in the IK chain
# This will scale them along the specified primary axis, effectively stretching or compressing the limb.
cmds.connectAttr(up_pma + '.output1D', ik_chain[0] + '.scale' + primary_axis[-1])
cmds.connectAttr(lo_pma + '.output1D', ik_chain[1] + '.scale' + primary_axis[-1])
# ------------------------------------------------------------------------
# Return a dictionary of useful nodes and values
# This can be used by other functions for further adjustments or cleanups
# ------------------------------------------------------------------------
return_dict = {
'measure_locs': [start_loc, end_loc],
'length_total': total_length,
'mdn': stretch_mdn,
'cnd': limb_cnd
}
return return_dict
def add_fk_stretch(fk_ctrls, fk_chain, primary_axis):
for i, ctrl in enumerate(fk_ctrls):
if not ctrl == fk_ctrls[-1]:
cmds.addAttr(ctrl, attributeType='double', min=0.001, defaultValue=1, keyable=True, longName='stretch')
mdl = cmds.createNode('multDoubleLinear', name=ctrl.replace('CTRL', 'MDL'))
loc = cmds.spaceLocator(name=fk_chain[i+1].replace('JNT', 'OFF_LOC'))[0]
cmds.parent(loc, fk_chain[i])
a_to_b(is_trans=True, is_rot=True, sel=[loc, fk_chain[i+1]], freeze=True)
offset_val = cmds.getAttr(loc + '.translate' + primary_axis)
cmds.setAttr(mdl + '.input1', offset_val)
cmds.connectAttr(ctrl + '.stretch', mdl + '.input2')
cmds.connectAttr(mdl + '.output', loc + '.translate' + primary_axis)
# Connect the stretch attribute to the scale of the current joint
cmds.connectAttr(ctrl + '.stretch', fk_chain[i] + '.scale' + primary_axis)
# Create a point constraint to move the next joint along with the current joint's end
point_const = cmds.pointConstraint(loc, fk_chain[i+1], mo=False)
# Ensure the next control follows the movement
if cmds.objExists(fk_ctrls[i + 1] + '.offsetParentMatrix'):
cmds.connectAttr(loc + '.matrix', fk_ctrls[i + 1] + '.offsetParentMatrix')
else:
dcm = cmds.createNode('decomposeMatrix', name=loc + '_DCM')
cmds.connectAttr(loc + '.matrix', dcm + '.inputMatrix')
for attr in ['translate', 'rotate', 'scale']:
cmds.connectAttr(dcm + '.output' + attr.title(), fk_ctrls[i + 1] + '_OFF_GRP.' + attr)
# Ensure the next control moves along with the end of the current joint
cmds.pointConstraint(loc, fk_ctrls[i + 1] + '_OFF_GRP', mo=False)
limb()
Comentarios