Skip to content

Instantly share code, notes, and snippets.

@chris-lesage
Last active April 14, 2024 20:43
Show Gist options
  • Save chris-lesage/0dd01f1af56c00668f867393bb68c4d7 to your computer and use it in GitHub Desktop.
Save chris-lesage/0dd01f1af56c00668f867393bb68c4d7 to your computer and use it in GitHub Desktop.
A script to pin an object to a NurbsSurface in Autodesk Maya
import pymel.core as pm
'''
Here are some examples of how to use the pin_to_surface.py script.
'''
# make a nurbsPlane
oNurbs = pm.nurbsPlane(n='nurbsPlane1')
# You can specify the nurbsSurface by string, PyNode transform or PyNode shape.
pin_to_surface('nurbsPlane1', uPos=0.5, vPos=0.5)
pin_to_surface(pm.PyNode('nurbsPlane1'), uPos=0.5, vPos=0.5)
# To place a range of "follicles", just loop over U or V or both.
# Make sure to multiply by the U or V range of your surface. UV does not always go 0 to 1.
paramLengthU = oNurbs.getShape().minMaxRangeU.get()
numberOfFollicles = 20
for i in range(numberOfFollicles):
uPos = (i/float(numberOfFollicles-1)) * paramLengthU[1]
pin_to_surface('nurbsPlane1', uPos=uPos, vPos=0.5)
# Make a locator and place it somewhere near your surface
oLoc = pm.spaceLocator(n='positionLocator')
# If you specify sourceObj in the function, it will place the follicle
# as close as possible to your sourceObj. It can be any transform.
# You can use this feature to place follicles near the joints of a rig, for example.
pin_to_surface(pm.PyNode('nurbsPlane1'), sourceObj=oLoc)
import pymel.core as pm
'''
Author: Chris Lesage, https://rigmarolestudio.com
A script snippet for pinning an object to a nurbs surface in Autodesk Maya.
This results in a surface pin, much like a follicle, but I've found
follicles to be less stable, sometimes flipping since Maya 2018 or so. Details in this thread:
https://tech-artists.org/t/flipping-follicles-in-ribbon-ik/11022/10?u=clesage
sourceObj is an optional parameter. If you pass a PyNode object, it will place the "follicle"
as close to the object as possible. Otherwise, you can specify U and V coordinates.
'''
def pin_to_surface(oNurbs, sourceObj=None, uPos=0.5, vPos=0.5):
"""
This function replaces what I used to use follicles for.
It pins an object to a surface's UV coordinates.
In rare circumstances follicles can flip and jitter. This seems to solve that.
1. oNurbs is the surface you want to pin to.
Pass a PyNode transform, NurbsSurface or valid string name.
2. sourceObj is an optional reference transform. If specified the UV coordinates
will be placed as close as possible. Otherwise, specify U and V coordinates.
Pass a PyNode transform, shape node or valid string name.
3. uPos and vPos can be specified, and default to 0.5
"""
#TODO: Can I support polygons?
# Parse whether it is a nurbsSurface shape or transform
if type(oNurbs) == str and pm.objExists(oNurbs):
oNurbs = pm.PyNode(oNurbs)
if type(oNurbs) == pm.nodetypes.Transform:
pass
elif type(oNurbs) == pm.nodetypes.NurbsSurface:
oNurbs = oNurbs.getTransform()
elif type(oNurbs) == list:
pm.warning('Specify a NurbsSurface, not a list.')
return False
else:
pm.warning('Invalid surface object specified.')
return False
pointOnSurface = pm.createNode('pointOnSurfaceInfo')
oNurbs.getShape().worldSpace.connect(pointOnSurface.inputSurface)
# follicles remap from 0-1, but closestPointOnSurface must take minMaxRangeV into account
paramLengthU = oNurbs.getShape().minMaxRangeU.get()
paramLengthV = oNurbs.getShape().minMaxRangeV.get()
if sourceObj:
# Place the follicle at the position of the sourceObj
# Otherwise use the UV coordinates passed in the function
if isinstance(sourceObj, str) and pm.objExists(sourceObj):
sourceObj = pm.PyNode(sourceObj)
if isinstance(sourceObj, pm.nodetypes.Transform):
pass
elif isinstance(sourceObj, pm.nodetypes.Shape):
sourceObj = sourceObj.getTransform()
elif type(sourceObj) == list:
pm.warning('sourceObj should be a transform, not a list.')
return False
else:
pm.warning('Invalid sourceObj specified.')
return False
oNode = pm.createNode('closestPointOnSurface', n='ZZZTEMP')
oNurbs.worldSpace.connect(oNode.inputSurface, force=True)
oNode.inPosition.set(sourceObj.getTranslation(space='world'))
uPos = oNode.parameterU.get()
vPos = oNode.parameterV.get()
pm.delete(oNode)
pName = '{}_foll#'.format(oNurbs.name())
result = pm.spaceLocator(n=pName).getShape()
result.addAttr('parameterU', at='double', keyable=True, dv=uPos)
result.addAttr('parameterV', at='double', keyable=True, dv=vPos)
# set min and max ranges for the follicle along the UV limits.
result.parameterU.setMin(paramLengthU[0])
result.parameterV.setMin(paramLengthV[0])
result.parameterU.setMax(paramLengthU[1])
result.parameterV.setMax(paramLengthV[1])
result.parameterU.connect(pointOnSurface.parameterU)
result.parameterV.connect(pointOnSurface.parameterV)
# Compose a 4x4 matrix
mtx = pm.createNode('fourByFourMatrix')
outMatrix = pm.createNode('decomposeMatrix')
mtx.output.connect(outMatrix.inputMatrix)
outMatrix.outputTranslate.connect(result.getTransform().translate)
outMatrix.outputRotate.connect(result.getTransform().rotate)
'''
Thanks to kiaran at https://forums.cgsociety.org/t/rotations-by-surface-normal/1228039/4
# Normalize these vectors
[tanu.x, tanu.y, tanu.z, 0]
[norm.x, norm.y, norm.z, 0]
[tanv.x, tanv.y, tanv.z, 0]
# World space position
[pos.x, pos.y, pos.z, 1]
'''
pointOnSurface.normalizedTangentUX.connect(mtx.in00)
pointOnSurface.normalizedTangentUY.connect(mtx.in01)
pointOnSurface.normalizedTangentUZ.connect(mtx.in02)
mtx.in03.set(0)
pointOnSurface.normalizedNormalX.connect(mtx.in10)
pointOnSurface.normalizedNormalY.connect(mtx.in11)
pointOnSurface.normalizedNormalZ.connect(mtx.in12)
mtx.in13.set(0)
pointOnSurface.normalizedTangentVX.connect(mtx.in20)
pointOnSurface.normalizedTangentVY.connect(mtx.in21)
pointOnSurface.normalizedTangentVZ.connect(mtx.in22)
mtx.in23.set(0)
pointOnSurface.positionX.connect(mtx.in30)
pointOnSurface.positionY.connect(mtx.in31)
pointOnSurface.positionZ.connect(mtx.in32)
mtx.in33.set(1)
return result
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment