Dynamic Chain Tool

Hey guys,

As the title points out, I’m in the process of building a tool that creates a dynamic joint chain. Right now the tool works fine more or less but things really break down once you create a second chain.

The biggest problem at the moment is the following:

When I try and create a second chain (or even the first one) my ikHandle fails to connect to the Output Curve shape node. I did try and think of some sort of workaround in the code (line 153) to fix the issue…

ex. mc.connectAttr(outCurve[0] + “.worldSpace”, dynIKSol[0] + “.inCurve”)

…or at least warn the user what is happening (lines 164,165). I know there has to be a better approach.

Hopefully this is enough info to get you guys going in the right direction. I have provided comments throughout the code to help keep things simple. If anything is unclear, please let me know. I appreciate any help or suggestions.
-B

# nChain
# This script creates a dynamic joint chain that can be used for ojects such as rope,
# hair strands, or any 'hanging' object that interacts with gravity.

# Import Maya commands
import maya.cmds as mc

# Import Maya's MEL intepreter
import maya.mel as mel

# If window already exists, it will be replaced by new window
nChainUIName = "nChain_UI"
if mc.window("nChain_UI", exists=True):
    mc.deleteUI("nChain_UI", window=True)
    
# nChain Window
def nChain_UI():
    nChainWin = mc.window(nChainUIName, title="nChain UI", sizeable=True)
    mc.columnLayout(adj=True)
    mc.text(label="This tool creates a dynamic joint chain at world origin. Press PLAY to view results.", wordWrap=True, al="center")
    mc.separator(height=10, style="in")
    mc.text(label="Joint Creation Properties", wordWrap=True, al="center")
    mc.separator(height=10, style="in")
    mc.text(label="Please select Point Lock method, then specify nChain values.", wordWrap=True, al="center")
    mc.radioButtonGrp("point_lock", numberOfRadioButtons=3, label="Lock Chain to Point(s):    ", labelArray3=["Base", "Tip", "Both Ends"], select=1)
    mc.intSliderGrp("number_of_joints", field=True, label="Number of Joints", min=2, max=25, fieldMaxValue=100, value=1)
    mc.intSliderGrp("units_between_joints", field=True, label="Units Between Joints", min=1, max=10, fieldMaxValue=1000, value=1)
    mc.floatSliderGrp("joint_radius", field=True, label="Joint Radius", fieldMinValue=0.1, min=1, max=10, fieldMaxValue=1000, value=1)
    mc.separator(height=10, style="in")
    mc.text(label="Wind Properties for Nucleus", wordWrap=True, al="center")
    mc.separator(height=10, style="in")
    mc.text(label="NOTE: The attributes below can be readjusted within the Nucleus Node under the 'Gravity and Wind' Tab.", wordWrap=True, al="center")
    mc.floatSliderGrp("air_density", field=True, label="Air Density", min=0, max=10, fieldMaxValue=100000, value=1)
    mc.floatSliderGrp("wind_speed", field=True, label="Wind Speed", min=0, max=50, fieldMaxValue=100000, value=0)
    mc.floatFieldGrp("wind_direction", label="Wind Direction", numberOfFields=3, value1=1, value2=0, value3=0 )
    mc.floatSliderGrp("wind_noise", field=True, label="Wind Noise", min=0, max=1, fieldMaxValue=100000, value=0)
    mc.separator(height=25, style="in")
    mc.button(label="Create nChain", width=10, height= 40, command="nChain.createNChain()")
    mc.showWindow(nChainWin)

# Note: This tool will create a Hair System (including Nucleus Node), an ikHandle, and an Air Field
# nChain Creation
def createNChain():
    # Create base joint and determine input values
    mc.select(clear=True)
    baseJoint = mc.joint(p=(0, 0, 0), name="base_nJoint")
    
    # Number of joints and space between each joint
    jntNumber = mc.intSliderGrp("number_of_joints", query=True, value=True)
    spaceJoints = mc.intSliderGrp("units_between_joints", query=True, value=True)
    for jNum in range(jntNumber-1):
        chainJoint = mc.joint(p=(0, spaceJoints *-1, 0), name="nJoint_#", relative=True)

    # Radius for each joint
    jntRadius = mc.floatSliderGrp("joint_radius", query=True, value=True)
    mc.select(baseJoint, replace=True) # selecting base joint only...
    radiusSel= mc.setAttr(".radius", jntRadius) # setting radius.
    for everyJoint in range(jNum+1): # selecting the rest of the joints in the chain...
        mc.pickWalk(direction="down")
        mc.setAttr(".radius", jntRadius) # setting radius value.
        
    # Select base joint (top of hierarcy) and group    
    mc.select(baseJoint, replace=True)
    masterGroup = mc.group(name="nJoint_grp_#")
    pivTop = mc.xform(query=True, boundingBox=True) # moving pivot for group to base joint...
    mc.xform(piv=(0, pivTop[4], 0), absolute=True) #...moved.
    mc.select(clear=True) # at this point, joint creation is done.
    
    # Create curve...
    dynCurve = mc.curve(degree=3, point=(0, 0, 0))
    mc.curve(dynCurve, append=True, degree=3, point=(0, spaceJoints *-1, 0))
    for everyJoint in range(jNum): # point will be created for every joint in chain.
        lastPoint = mc.xform(query=True, boundingBox=True)
        mc.curve(dynCurve, append=True, degree=3, point=(0, lastPoint[1] + spaceJoints *-1, 0))
    
    # Parent Curve to 'masterGroup'
    mc.select(masterGroup, add=True)
    mc.parent()
    
    # Make Curve Dynamic using MEL Interpreter and place new objects in group
    mel.eval('makeCurvesDynamic 2 { "0", "0", "1", "1", "0"}')
    
    # Let's group a few extra nodes that were just created and put them in 'masterGroup'
    hairSys = mc.ls(sl=True)
    mc.select(masterGroup, add=True)
    mc.parent() # hair system parented to group...
    mc.select(clear=True)
    checkConnect = mc.listConnections(hairSys, connections=True) # listing connections to access Nucleus node later on...
    
    # Now we will group the last object parented to world and toss that into 'masterGroup'
    mc.select(hairSys[0], replace=True)
    newSel = mc.pickWalk(direction="up")
    mc.select(newSel[0] + "OutputCurves", masterGroup)
    mc.parent() # output curves group parented to 'masterGroup'...
    mc.select(clear=True)
    
    # Before moving on, we will determine our Point Lock method
    mc.select(hairSys, replace=True)
    mc.pickWalk(direction="up")
    mc.pickWalk(direction="left")
    mc.pickWalk(direction="down") # Follicle shape selection
    shapeFollicle = mc.ls(sl=True)
    
    # Point Lock method for radioButton)
    pointLockOp = mc.radioButtonGrp("point_lock", query=True, select=True)
    if pointLockOp == 1:
        mc.setAttr(shapeFollicle[0] + ".pointLock", 1)
    elif pointLockOp == 2:
        mc.setAttr(shapeFollicle[0] + ".pointLock", 2)
    elif pointLockOp == 3:
        mc.setAttr(shapeFollicle[0] + ".pointLock", 3)
    else:
        print ""
    
    # Time to add the IK Handle to dynamic drive the joint chain
    # We have to select the Rotate Pivot's for base and end joints, otherwise the IK Handle won't function properly
    mc.select("base_nJoint.rotatePivot", replace=True)
    chainJointRP = mc.select (chainJoint + ".rotatePivot", add=True)
    dynIKSol = mc.ikHandle(sol="ikSplineSolver", createCurve=False, twistType="easeInOut", parentCurve=False, rootTwistMode=True)
    mc.select(dynCurve, add=True)
    mc.select(clear=True)
    
    # Input values for Wind Properties... starting with Air Density.
    airDen = mc.floatSliderGrp("air_density", query=True, value=True)
    mc.select(checkConnect[3], replace=True)
    nucleusNode = mc.ls(sl=True)
    mc.setAttr(nucleusNode[0] + ".airDensity", airDen)
    # Now Wind Speed...
    windSpeed = mc.floatSliderGrp("wind_speed", query=True, value=True)
    mc.setAttr(nucleusNode[0] + ".windSpeed", windSpeed)
    # Wind Direction...
    windDir = mc.floatFieldGrp("wind_direction", query=True, value1=True)
    mc.setAttr(nucleusNode[0] + ".windDirectionX", windDir) # input value for X
    windDir = mc.floatFieldGrp("wind_direction", query=True, value2=True)
    mc.setAttr(nucleusNode[0] + ".windDirectionY", windDir) # input value for Y
    windDir = mc.floatFieldGrp("wind_direction", query=True, value3=True)
    mc.setAttr(nucleusNode[0] + ".windDirectionZ", windDir) # input value for Z
    # Lastly, Wind Noise
    windNoise = mc.floatSliderGrp("wind_noise", query=True, value=True)
    mc.setAttr(nucleusNode[0] + ".windNoise", windNoise)
    
    # Repatch disconnected node!!
    mc.select(clear=True)
    mc.select(baseJoint, replace=True) # selcting 'base_nJoint'
    mc.pickWalk(direction="right")
    mc.pickWalk(direction="right")
    mc.pickWalk(direction="right")
    mc.pickWalk(direction="down") # output curve selected
    outCurve = mc.pickWalk(direction="down") # output curve shape selected
    # If you are creating more than one nChain: 
    # Make sure your output curve SHAPE node is connected to the ikHandle ".inCurve".
    # Without this connection, nothing works.
    mc.connectAttr(outCurve[0] + ".worldSpace", dynIKSol[0] + ".inCurve") 
    
    # To fininsh up...
    mc.select(baseJoint, replace=True) # selcting 'base_nJoint'
    mc.pickWalk(direction="right")
    mc.pickWalk(direction="right")
    mc.pickWalk(direction="right")
    mc.pickWalk(direction="down") # output curve selected
    shutVisibility = mc.ls(sl=True)
    mc.setAttr(shutVisibility[0] + ".visibility", 0) # shutting off visibility for output curve
    mc.select(clear=True)
    
    # For more than one nChain, we will make sure connections are made here.
    if mc.objExists("nJoint_grp_2"):
       mc.error("If you are creating more than one nChain: Make sure your new output curve SHAPE node is connected to the ikHandle '.inCurve'. Without this connection, nothing works.")
    else:
        print "nChain created. Press Play to see results."

# End Script. Created by Bryan Godoy.

I forgot to mention that the code was written for Maya (Python). Here’s a few images that might help.

Interface:

Object:

Connections (without this connection, nothing works)

Does the failure produce an error?

Also I noticed that you rely a lot on the selection and scene hierarchy to find the nodes that you need to work with. I suggest that you do it using the search commands instead such as listRelatives, listConnections, listHistory and so on. Traversing the hierarchy in a “hard coded” way greatly increases the risk of errors and gives you far less flexibility. For instance, if you ever want to change the curve location in the hierarchy, all the pick walking will be screwed. Always remember that most commands can be given a specific object / list of objects to work with as an input.

I can give you examples later.

[QUOTE=gpz;26199]Does the failure produce an error?

Also I noticed that you rely a lot on the selection and scene hierarchy to find the nodes that you need to work with. I suggest that you do it using the search commands instead such as listRelatives, listConnections, listHistory and so on. Traversing the hierarchy in a “hard coded” way greatly increases the risk of errors and gives you far less flexibility. For instance, if you ever want to change the curve location in the hierarchy, all the pick walking will be screwed. Always remember that most commands can be given a specific object / list of objects to work with as an input.

I can give you examples later.[/QUOTE]

Thanks for the input gpz. Fortunately the script doesn’t produce an error. At the moment I’m able to get one fully functioning chain right out of the gate.

Essentially the biggest problem is that a very vital connection doesn’t get made for a second or third chain and I have to manually reconnect that connection (shown in the last two images above).

I think you hit the nail on the head though. Because I’m relying so heavily on an extremely specific hierarchy, things are prone to error once additional chains come into play. This script was actually for a school assignment and I was required to use the pickWalk command—that’s why things are set up this way. Moving forward I’m going to try to rely much less on scene hierarchy. I now see how it can lead to problems. You actually point out something I never thought about before so I appreciate you mentioning the things you did.

Thanks again for the feedback,
-B

The nature of the assignment seems a bit unusual to me. Nonetheless, your decision to make it a dynamic joint chain builder is a pretty nice challenge!

I did a few changes to the code and managed to get it to work “properly.” I put properly between quotes since the spline IK curve does not seem to be animated and if I had to expect anything to move on playback because of the dynamics, well, it didn’t XD.

Here are the changes I made, why and some more stuff that I think might help. By the way, I am only writing down some snippets from your code. Since most of the stuff worked no need to copy it all. Take note that I launched the whole thing directly from the script editor.

  1. When I first launched your code ( I had to change the command string of the “Create nChain” button since I guess you launch the “createNChain” function from a seperate module ) I got the following warnings:
// Warning: file: C:/Program Files/Autodesk/Maya2014/scripts/others/makeCurvesDynamic.mel line 351: rebuildCurve1 (Rebuild Curve): invalid input curve. // 
// Warning: file: C:/Program Files/Autodesk/Maya2014/scripts/others/makeCurvesDynamic.mel line 351: You have an empty result since history is on. You may want to undo the last command. // 
// Warning: file: C:/Program Files/Autodesk/Maya2014/scripts/others/createHairCurveNode.mel line 174: rebuildCurve1 (Rebuild Curve): invalid input curve. // 

EDIT: This issue only occurs when building with less than 4 joints ( default: 2 )
POST: The reason for that is that you are trying to build a cubic ( degree 3 ) curve with not enough control points to define it ( you have 2 control points but you need at least 4 .)
Changing the curve to linear ( degree 1 ) got rid of it. I noticed that after running the tool once, “curve2” ended up holding no data , explaining why the connection failed. My wild guess is that when you build a cubic curve, Maya draws it linear expecting you to add more points but declares it as cubic. When copied, the curve, not holding the proper data to make it cubic, failed.

    # Create curve...
    dynCurve = mc.curve(degree=1, point=(0, 0, 0))
    mc.curve(dynCurve, append=True, degree=1, point=(0, spaceJoints *-1, 0))
    for everyJoint in range(jNum): # point will be created for every joint in chain.
        lastPoint = mc.xform(query=True, boundingBox=True)
        mc.curve(dynCurve, append=True, degree=1, point=(0, lastPoint[1] + spaceJoints *-1, 0))
  1. Now that the whole thing worked without any warnings, I ran it a another time to build the second setup and got the following error:
# Error: More than two joints or effectors were selected.
# Traceback (most recent call last):
#   File "<maya console>", line 1, in <module>
#   File "<maya console>", line 119, in createNChain
# RuntimeError: More than two joints or effectors were selected. # 

The reason behind that is that since you select a joint with a hard coded short name, and since that short name is also used in the original hierarchy, you end up selecting both. This leads the ikHandle command to receive the wrong number of inputs. The first thing to do is to make sure the name is unique like you did with the master group. ( By the way, I have no idea how you managed to get the whole thing to work a second time without getting that error… You might want to make sure that you have no suppress options activated in the script editor’s History menu. )

    # Create base joint and determine input values
    mc.select(clear=True)
    baseJoint = mc.joint(p=(0, 0, 0), name="base_nJoint_#")

Then you can send it to the ikHandle without any worry. By the way, there is no reason to select the rotatePivot attribute since the ikHandle command expects an object, so I took the liberty to take that off. The joints’ preferred angle affects the ikSolver among other things but the command expects a node nonetheless. Also, since you stored the joints’ name in variables in the first place, there is no reason to hard code it. Just use the variables. There is also, no need to select the curve and then immediately deselect it since.

    # Time to add the IK Handle to dynamic drive the joint chain
    # We have to select the Rotate Pivot's for base and end joints, otherwise the IK Handle won't function properly
    mc.select(baseJoint, replace=True)
    chainJointRP = mc.select (chainJoint, add=True)
    dynIKSol = mc.ikHandle(sol="ikSplineSolver", createCurve=False, twistType="easeInOut", parentCurve=False, rootTwistMode=True)
    mc.select(dynCurve, add=True)
    mc.select(clear=True)

A more proper way to assign the joints to the IK though would be to specify the startJoint flag and the endEffector flag of the ikHandle command directly:

    # Time to add the IK Handle to dynamic drive the joint chain
    # We have to select the Rotate Pivot's for base and end joints, otherwise the IK Handle won't function properly
    dynIKSol = mc.ikHandle(sol="ikSplineSolver", createCurve=False, startJoint=baseJoint, endEffector=chainJoint, twistType="easeInOut", parentCurve=False, rootTwistMode=True)
  1. Sky is the limit. I am pretty sure you already know how to improve that code. If the stuff I included here doesn’t solve it ( it worked on my side, ) feel free to ask more questions.

All the best,
G

Wow, thanks a lot gpz. I appreciate you taking the time to really digging into the code! You provided such great feedback.

Since I just now finished reading your post, I haven’t implemented your suggestions yet. As far as I can tell though, all of your input is 100% on the money. I can see where I went wrong now, especially when it comes to the way I built the curve (degree cubic as opposed to linear) and when it comes to some of the naming conventions and flags.

As soon as I get things implemented, I’ll let you know what I find. In the meantime I just wanted to say thanks for all the excellent suggestions :slight_smile:
-B

Just to be sure since I edited afterwards. ( I tested on default settings and couldn’t load your images to see your chain :s )

Point 1 only applies when you build with less than 4 joints. I suggest you make a check and build linear if less than 4 joints were specified, otherwise go cubic. Point 2 was the real issue since I managed to build countless chains after that :slight_smile:

Happy it helped.

G