Skip to content

Instantly share code, notes, and snippets.

@benmorgantd
Last active September 18, 2023 20:14
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save benmorgantd/72a06a83007c937846effb27725a4fc8 to your computer and use it in GitHub Desktop.
Save benmorgantd/72a06a83007c937846effb27725a4fc8 to your computer and use it in GitHub Desktop.
Nurbs Ribbon: create a ribbon in Maya with j drive joints and k bind joints. Useful for rigging lips, eyebrows, eyelids, forearms...
# bm_nurbsRibbon
import maya.cmds as cmds
# sets up a ribbon with j drive joints and k bind joints
class NurbsRibbon(object):
def __init__(self, numFollicles=6, numDrivers=3, name="nurbsRibbon"):
if numDrivers <= 1:
return
# Create our nurbs plane which will be the basis for our ribbon. We're using a nurbs plane and not a
# polyPlane because it has cleaner UV space and gives better normal results for the follicles.
self.ribbon = cmds.nurbsPlane(w=6, ax=(0, 1, 0), ch=0, u=6, v=1, lr=.1666666667, n=name + "_Ribbon")
self.ribbonShape = cmds.pickWalk(self.ribbon, d="down")
self.ribbon = [self.ribbon[0], self.ribbonShape[0]]
cmds.makeIdentity(self.ribbon, s=1)
cmds.delete(self.ribbon, constructionHistory=1)
cmds.select(self.ribbon[0])
# Now we manually setup a "hair" system by making follicle nodes and making the correct attachments.
self.ribbonFollicles = []
i = 0
while i < numFollicles:
follicle = cmds.createNode("follicle")
# a weird thing we have to do when making follicle nodes..
follicle = cmds.pickWalk(follicle, d="up")[0]
follicle = cmds.rename(follicle, name + "_follicle#")
follicleShape = cmds.pickWalk(follicle, d="down")[0]
self.ribbonFollicles.append([follicle, follicleShape])
cmds.connectAttr(self.ribbon[1] + ".local", follicleShape + ".inputSurface")
cmds.connectAttr(self.ribbon[1] + ".worldMatrix[0]", follicleShape + ".inputWorldMatrix")
cmds.connectAttr(follicleShape + ".outRotate", follicle + ".rotate")
cmds.connectAttr(follicleShape + ".outTranslate", follicle + ".translate")
# U value is between 0 and 1. Here we calculate its value based on the number of follicles the user wants
U = (1 / float(2 * numFollicles)) + (2 * (1 / float(2 * numFollicles)) * float(i))
cmds.setAttr(follicleShape + ".parameterU", U)
cmds.setAttr(follicleShape + ".parameterV", .5)
i += 1
# make the bind joints
self.bindJnts = []
i = 0
while i < numFollicles:
cmds.select(cl=1)
# same deal so that the Bind joints are in the same location as the follicles they're going to follow
U = (1 / float(2 * numFollicles)) + (2 * (1 / float(2 * numFollicles)) * float(i))
xLoc = -3 - 2 * (-3 * float(U))
self.bindJnts.append(cmds.joint(p=[xLoc, 0, 0], n=name + "_" + "follicleJnt" + str(i + 1) + "_BIND"))
cmds.parent(self.bindJnts[i], self.ribbonFollicles[i][0])
i += 1
# make the drive joints
self.driveJnts = []
i = 0
while i < numDrivers:
cmds.select(cl=1)
self.driveJnts.append(cmds.joint(p=(0, 0, 0), radius=1.5, n=name + "_DriveJnt#"))
i += 1
# orient our joints and give them some SDK groups
i = 0
k = 6.0 / float(numDrivers - 1)
self.driverOrientGrps = []
self.driverSDK1Grps = []
self.driverSDK2Grps = []
for jnt in self.driveJnts:
xLoc = -3 + i * k
sdk2 = cmds.group(jnt, n=jnt + "_SDK2")
sdk1 = cmds.group(sdk2, n=jnt + "_SDK1")
orient = cmds.group(sdk1, n=jnt + "_ORIENT")
self.driverSDK1Grps.append(sdk1)
self.driverSDK2Grps.append(sdk2)
cmds.xform(orient, t=(xLoc, 0, 0))
self.driverOrientGrps.append(orient)
i += 1
cmds.select(cl=1)
for obj in self.driveJnts: cmds.select(obj, add=1)
cmds.select(self.ribbon, add=1)
# bind the drive joints to the ribbon
ribbonCluster = cmds.skinCluster(dropoffRate=1.55, n=name + "_" + "ribbonCluster#")[0]
cmds.select(cl=1)
# edit our weights so the end drive joints stick to the end of the self.ribbon 1:1
cmds.skinPercent(ribbonCluster, self.ribbon[0] + ".cv[8][0:3]", transformValue=(self.driveJnts[-1], 1))
cmds.skinPercent(ribbonCluster, self.ribbon[0] + ".cv[0][0:3]", transformValue=(self.driveJnts[0], 1))
# organize our ribbon
self.driveGrp = cmds.group(self.driverOrientGrps, n=name + "_" + "ribbonJnts_DRIVE")
cmds.select(cl=1)
for obj in self.ribbonFollicles: cmds.select(obj[0], add=1)
self.follicleGrp = cmds.group(n=name + "_" + "ribbon_FOLLICLES")
self.allGrp = cmds.group(self.driveGrp, self.follicleGrp, self.ribbon, n=name + "_ALL")
# our driveJnts group will be used to orient the ribbon as a whole
i = 0
while i < numFollicles:
cmds.scaleConstraint(self.driveJnts, self.ribbonFollicles[i][0], mo=1)
i += 1
# make a set for our BIND joints. This is really only to benefit the rigger, as the animator has no use for them
# Therefore, be sure to delete all these sets once you're ready to send the rig out.
cmds.select(cl=1)
for obj in self.bindJnts: cmds.select(obj, add=1)
cmds.sets(n=name + "_BIND_SET")
cmds.select(cl=1)
# finally, make our ribbon invisible when rendered
cmds.setAttr(self.ribbon[1] + ".primaryVisibility", 0)
cmds.setAttr(self.ribbon[1] + ".castsShadows", 0)
cmds.setAttr(self.ribbon[1] + ".receiveShadows", 0)
cmds.setAttr(self.ribbon[1] + ".visibleInReflections", 0)
cmds.setAttr(self.ribbon[1] + ".visibleInRefractions", 0)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment