Nurbs Curves + translateUVN

When doing a joint-based rig it's quite common to have a setup where a small number of controls drive the cv's of a nurb curve which has a joint on it for each span. Here is an example from one of Josh Sobel's rigs.

Each joint is attached to a curve and each cv of the curve is driven by a control. There's often extra stuff and the exact setup varies but that is the basic core concept.

A lot of times riggers choose to use a nurbs curve like this because it ensures that they get clean smooth shapes that would be hard to achieve by painting the weights for one joint per control. But if you could get the same results with fewer joints that'd be great right?

I've already shown off how animCurves can be used to split weights and control the falloff between controls very easily, and we can use that same approach here. The influence each CV has on each point on the curve comes from the basis-function. I'm not going to go into any math related to these functions but what's important to notice is that each curve in the basis function looks like what the curve would look like if you moved a single CV.

Now these functions are for bezier curves (so one span of a nurbs curve) but the basis functions for full nurbs curves can be really complicated so we're gonna do something a little hacky :3

Here I have a curve roughly the shape of the neutral brow in the example from Josh Sobel, and I'm going to run a script to generate an animCurve in the shape of the basis function for each CV. Here's the code (super janky proof-of-concept stuff).

import maya.api.OpenMaya as om2
def generate_animCurve_from_nurbs(splitter,inputCurve):
    #flatten the nurbs
    sel = om2.MSelectionList()
    oCurve = sel.getDependNode(0)
    dSplitter = sel.getDagPath(1)
    #create the flattened curve
    mFnCurve = om2.MFnNurbsCurve(oCurve)
    curve_pnts = mFnCurve.cvPositions()
    splitterMatrix = dSplitter.inclusiveMatrixInverse()
    curve_pnts = [om2.MPoint((pnt*splitterMatrix)[0],0,0) for pnt in curve_pnts]
    mFnNewCurve = om2.MFnNurbsCurve()
    newCurve = mFnNewCurve.create(

    #for each cv
    for i in range(len(curve_pnts)):    
        newAnimCurve = cmds.createNode('animCurveUU')
        iterations = 100.0
        for j in range(int(iterations)+1):            
            #get the param along the curve
            sampleX = j*2.0/(iterations) -1.0
            samplePnt = om2.MPoint(sampleX,0,0)
            samplePnt, param = mFnNewCurve.closestPoint(samplePnt)
            #move it one unit
            curve_pnts[i] = curve_pnts[i] + om2.MVector(0,1,0)
            #get the height
            samplePnt = mFnNewCurve.getPointAtParam(param)
            #move it back one unit
            curve_pnts[i] = curve_pnts[i] + om2.MVector(0,-1,0)
            #create a key on an animCurve for that point
            cmds.setKeyframe(v = samplePnt[1], f = sampleX)

and here are the nice clean animCurves it produces.

What I would do at this point is create an animCurve with the minimum number of keys and fit it to these curves and then just use that. In the future I may actually put in the work to create that simple curve automatically from the curve but this is fine for now.

What's really powerful about this is that now we get the benefits of nurbs curves (uper clean interpolation) and we can use the weightSplitter with a custom deformer like the translateUVN to get all the benefits of that approach too. A Hannah Montana situation.

One thing you might notice in the code is that I am not stepping along the parameter, I'm stepping along the X-axis. If the CVs are adjusted left and right the graph will look slightly different,

I'll be putting some additional thought into this, because the results are better than what you get when manually adjusting animCurves, so if I can make it more user-friendly that'd be great.

That's it just sharing a little trick.

Featured Posts
Recent Posts