Object Soft Select Maya

Hi all!

Today’s script is a really cool one because it can be used to really add appeal to rigs. I’m calling it “object soft select” because what it does is it takes a group of objects and a controller and makes those objects be influenced by the controller object with an influence falloff based on the distance the object is from the controller. Whew! That’s a mouthfull.

Now, for a few days I was trying to write my own falloff formula. Many a Starbucks drinks were spent on it, but alas, nothing. In the back of my mind I had the idea of using the remapValue node (because that’s essentially what I’m trying to do) so I gave it a shot.

Turns out, that remapValue node really did the trick. My only wish is that I could have it all in just a single node (like a custom remapValue node) that will allow me to control the falloff curve shape. Some day when I’m better at writing plugins I’ll give that a shot!

I learned a couple of interesting things from this, primarily:

  1. The xform command has a nifty “pivots” (piv)  flag that allows you to query and change the rotate and scale axes at the same time.
  2. You can sort a “2 dimensional” python array by using lambda. Now, I’m not totally certain at what exactly it’s doing, but it works… I’ll do more research and perhaps make another post later.
    1. So say I have the array a = [[“a”,5],[“b”,3],[“c”,2]]
    2. I can sort it with the python function: a = sorted(distances, key=lambda x: x[1])
  3. Querying the world space location of the rotate pivot on a transform node can give you the right result if querying the object’s ws is wrong.

Here’s the script!

import maya.cmds as cmds
import math

# bm_softSelect
# Makes "soft select" for xform nodes. One object can influence a group of objects and their influence is based
# on their initial distance from the object. Useful for "fleshy" areas.

# we'll need our distance formula
def calcDistance(objA, objB):
    # query the object's rotate and scale pivot instead of its translate value for more accuracy
    pointA = cmds.xform(objA, q=1, piv=1, ws=1)
    pointB = cmds.xform(objB, q=1, piv=1, ws=1)
    # 3d distance formula is the square root of (x2-x1)**2 + (y2-y1)**2 + (z2-z1)**2
    distance = math.sqrt(((pointA[0] - pointB[0]) ** 2)
                         + ((pointA[1] - pointB[1]) ** 2)
                         + ((pointA[2] - pointB[2]) ** 2))
    return distance

def objSoftSelect(influenced=None, controller=None, maxInf=.5, minInf=.01):
    # quit if no objects are passed to influence
    if not influenced:
        return
    if not controller:
        return

    # create max and min Inf attributes on the controller so that the user can change them
    cmds.addAttr(controller,at="enum",en="Soft_Select",ln="_")
    cmds.setAttr(controller + "._",k=0,cb=1,l=1)
    cmds.addAttr(controller, at="float", ln="Min_Inf", dv=minInf, min=0, max=1, k=1)
    cmds.addAttr(controller, at="float", ln="Max_Inf", dv=maxInf, min=0, max=1, k=1)
    cmds.addAttr(controller, at="float", ln="Envelope", dv=1, min=0, max=1, k=1)

    # an array of the the distance each object is from our controller object. Will affect its influence amt
    distances = []
    for obj in influenced:
        # [obj,distance]
        distances.append([obj, calcDistance(controller, obj)])
    # sorts our array by the second value, distance
    distances = sorted(distances, key=lambda x: x[1])

    # because it's sorted, we know the shortest distance is at the start of the array and the longest is at the end
    minDist = distances[0][1]
    maxDist = distances[-1][1]

    # this node normalizes the transformations of the controller by subtracting its original location.
    # note: works only if there are no SDK groups above the controller that are already affecting its xform
    normXform = cmds.createNode("plusMinusAverage", n="normalizeXform#")
    cmds.setAttr(normXform + ".op", 2)
    cmds.connectAttr(controller + ".translate", normXform + ".input3D[0]")

    # if the object's transforms have been frozen, we don't want the input3D[1] values to also be 0
    objXform = cmds.xform(controller, q=1, t=1, os=1)
    if objXform[0] == 0 and objXform[1] == 0 and objXform[2] == 0:
        cmds.setAttr(normXform + ".input3D[1].input3Dx", 0)
        cmds.setAttr(normXform + ".input3D[1].input3Dy", 0)
        cmds.setAttr(normXform + ".input3D[1].input3Dz", 0)
    else:
        cmds.setAttr(normXform + ".input3D[1].input3Dx", cmds.xform(controller, piv=1, q=1, ws=1)[0])
        cmds.setAttr(normXform + ".input3D[1].input3Dy", cmds.xform(controller, piv=1, q=1, ws=1)[1])
        cmds.setAttr(normXform + ".input3D[1].input3Dz", cmds.xform(controller, piv=1, q=1, ws=1)[2])

    # the object(s) are the same distance from the controller. This also will work if we only have one obj selected
    if maxDist == minDist:
        for obj in distances:
            connectXform(controller, obj[0], normXform, obj[1], maxDist, minDist, maxInf, minInf)
        return

    for obj in distances:
        if obj[1] == minDist:
            connectXform(controller, obj[0], normXform, obj[1], maxDist, minDist)
        elif obj[1] == maxDist:
            connectXform(controller, obj[0], normXform, obj[1], maxDist, minDist)
        else:
            connectXform(controller, obj[0], normXform, obj[1], maxDist, minDist)

def connectXform(controller, obj, normXform, objDist, maxDist, minDist):
    # first group our control so that we can move it without affecting its xforms
    grp = cmds.group(obj, n=obj + "_SDK#")
    # create a remap node that will take the object's distance and give it an influence value
    remap = cmds.createNode("remapValue", n=obj + "_Remap#")
    cmds.setAttr(remap + ".inputValue", objDist)
    cmds.setAttr(remap + ".inputMin", minDist)
    cmds.setAttr(remap + ".inputMax", maxDist)
    cmds.connectAttr(controller + ".Min_Inf", remap + ".outputMin")
    cmds.connectAttr(controller + ".Max_Inf", remap + ".outputMax")
    cmds.setAttr(remap + ".color[1].color_Position", 0)
    cmds.setAttr(remap + ".color[0].color_Position", 1)

    # this node takes the normalized transformations of our controller and multiplies it by the value given by our
    # remap value node
    mult = cmds.createNode("multiplyDivide", n=obj + "_" + controller + "_influenceAmt#")
    cmds.connectAttr(normXform + ".output3D", mult + ".input1")
    # here is where we'll have to have the influence determined by the distance from the controller obj
    cmds.connectAttr(remap + ".outColor", mult + ".input2")

    # create a blendColors node to control the overall envelope of the operation
    blend = cmds.createNode("blendColors", n=obj + "_envelope#")
    cmds.connectAttr(mult + ".output", blend + ".color1")
    cmds.setAttr(blend + ".color2", 0, 0, 0, type="double3")
    # connect our controller object's Envelope attr to the blender amt
    cmds.connectAttr(controller + ".Envelope", blend + ".blender")
    cmds.connectAttr(blend + ".output", grp + ".translate")

"""
import bm_softSelect
reload(bm_softSelect)

sel = cmds.ls(sl=1)
bm_softSelect.objSoftSelect(influenced=sel[0:-1],controller=sel[-1],minInf=.25,maxInf=.5)
"""

Advertisements
Image

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s