Last active
September 18, 2023 20:14
-
-
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...
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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