Add Closest Surface Constraint

This one is quite Interesting I think. During the Anomalia rigging workshop in Litomyšl (Czech Republic) the instructor Josh Carey from ReelFX has constantly used follicle constraints for all kinds of rigging topics. In Softimage language a follicle constraint is just a simple surface constraint. But that was not what I found interesting. I liked the way Josh was creating the constraints. He had a small script that searched the closest location from locator, read the u and v coordinates of the location and appled a surface constraint with exact this coordinates. With this technique it is quite easy, to get the controllers exact to the point where I want them. In the case of surface constraints, it is not always as easy as you think.

To visualize that, I have rebuild a simple ICE tree that shows exact what my piece of code does.

add closest surface constraint 01

As shown in this picture, I need just four nodes to get the information I want. First: The surface mesh. Second: The global position of the null. Third: A get closest location node. Fourth: a get data node where I select the 2D vector with the UV coordinates.

Ok, that’s clear. Lets port that to python.

from win32com.client import constants as c
xsi = Application
log = Application.LogMessage
collSel = xsi.Selection

def addClosestSurfaceConstraint(inMesh, inObj, inDoParent):
	if inMesh.type == "surfmsh":
		selSurface = inMesh.ActivePrimitive.Geometry
		selObj = inObj
		selObjParent = inObj.Parent.Name
		log(selObjParent)
		CubePositionArray = selObj.Kinematics.Global.Transform.GetTranslationValues2()
		CubeOnSpherePointLocators = selSurface.GetClosestLocations(CubePositionArray)
		PtLocSubSurf = selSurface.GetSubSurfaceIndexArray(CubeOnSpherePointLocators,)
		PtLocUVs = selSurface.GetNormalizedUVArray(CubeOnSpherePointLocators,)
		#Debugging
		#log("Point locator is on subsurface " + str(PtLocSubSurf[0]) + " at {U = " + str(PtLocUVs[0]) + " , V = " + str(PtLocUVs[1]) + "}.")
		constrNull = xsi.GetPrim("Null", inObj.Name + "_SFC", "", "")
		if selObjParent != "Scene_Root":		
			xsi.ParentObj(selObjParent, constrNull.FullName)
		xsi.ApplyCns("Surface", constrNull.FullName, inMesh.FullName, "")
		xsi.SetValue(constrNull.FullName + ".kine.surfcns.posu", PtLocUVs[0], "")
		xsi.SetValue(constrNull.FullName + ".kine.surfcns.posv", PtLocUVs[1], "")
		xsi.SetValue(constrNull.FullName + ".kine.surfcns.tangent", True, "")
		xsi.SetValue(constrNull.FullName + ".kine.surfcns.upvct_active", True, "")
		if inDoParent == True:
			xsi.ParentObj("B:" + str(constrNull.FullName), inObj.FullName)
	else:
		log("debug: 1.Select a Surface Mesh, 2. Select any Object Type")

	return True

addClosestSurfaceConstraint(collSel(0),collSel(1),True)

When running this I get this result

closest_surfaceconstraint_02add closest surface constraint 01

As you could see, it has set the surface constraint to the position that I had calculated in my ICE prototype previously. there is little difference in the values, but that’s the fourth spot after the point. For me the tricky part in this script was, to find the function GetNormalizedUVArray, because all other functions give back not normalized values(0-1) that are needed for the surface constraint.

Usage: The fuction takes three arguments.
1. inMesh – Surface the Null will be constraint to.
2. inObj – Object from witch global position the closest Point on Surface will be calculated.
3. inDoParent – parent boolean – If true, the Input Object is parented to the new Null. Otherwise, this will be ignored.