Creating my custom Rivet Locator- Pymel Tutorial
What's the aim? What are we creating?
We'll be creating a series of nodes that once all working together, will make a certain object follow the surface normals of a mesh section even when deformed, follow rotation and position in world space.
This tool is very similar if not the same as either creating a folicle on the surface or a pointOnPoly Constraint, however I wanted to tackle this my own way, and the pointOnPoly Constraint isn't my biggest fan...
I have a much more advanced version of this script, but I'll just go through the basic version to help you get started with Pymel.
Unfortunately, unless you have Maya 2014 which comes pre shipped with the decomposeMatrix node, you may have to add in this node to your bundle, or load it from the plugin manager.
First of all, what do we have to do?
We will have to trace a section of components, draw a surface from these components, decompose the matrix and connect it to our Rivet Locator
Let's start by importing our PyMel Module:
Now that we've done that, we can start writing our Rivet class!
Everything we're about to write, will be wrapped in a class, why are we using a class? I class has all the standard features of Object Oriented Programming, I class can be modified after creation, it's much nicer to read, and well, they're awesome.
So let's build our class and it's core functions:
class RivetSimple(): def __init__(self, *args): ## Perform various tasks on load of the script def setAttributes(self, *args) ## Define our attributes def createNodes(self, *args): ## Create our nodes for use for this tool def createConnections(self, *args): ## Create our connections to and from our nodes
The above is pretty simple, all we're doing is instantiating our main class, and functions that belong to it.
Okay, we're done here! Let's start with something simple, creating the nodes that we'll need throughout this little project.
We'll need 7 nodes to get this baby functioning the way we want, here's what we'll need and why:
curveFromMeshEdge - This node defines a NURBS curve that is derived from a mesh edge that's selected, we will need two of these nodes.
pointOnSurfaceInfo - This node will be used to compute information from the surface created when we loft our new curve edges, we'll be using this information to plug into a four by four matrix node.
fourByFourMatrix - This node can output a 4x4 matrix based on it's input, we'll be decomposing this matrix information to connect to our decomposeMatrix node.
decomposeMatrix - This Node, as it's name suggests, will decompose the inputMatrix from our 4x4 node, and convert it to easily accessible information to connect to our object.
loft - This node will simply be used to loft a surface between our two curveFromMeshEdge nodes we created earlier.
Locator - This node will simply be used so that we have something to parent or constrain to for animation.
Group - We'll be using a group node to group our locator, and connect our rig to this group so that we have an offset control (locator).
Hopefully that helps make you understand what/why we're creating these nodes.
Let's add the above nodes into our class function 'createNodes()'. We'll use a dictionary for these nodes as It's easier to access information and reads nicer.
class RivetSimple(): def __init__(self, *args): ## Perform various tasks on load of the script def createNodes(self, *args): self.node = { 'meshEdgeNode1' : pm.createNode('curveFromMeshEdge'), 'meshEdgeNode2' : pm.createNode('curveFromMeshEdge'), 'ptOnSurfaceIn' : pm.createNode('pointOnSurfaceInfo'), 'matrixNode' : pm.createNode('fourByFourMatrix'), 'decomposeMatrix' : pm.createNode('decomposeMatrix'), 'loftNode' : pm.createNode('loft'), 'locator' : pm.createNode('locator') } def createConnections(self, *args): ## Create our connections to and from our nodes
Now that we have done this, let's make our connections! I'll add a comment in for each node connection.
class RivetSimple(): def __init__(self, *args): ## Perform various tasks on load of the script def createNodes(self, *args): self.node = { 'meshEdgeNode1' : pm.createNode('curveFromMeshEdge'), 'meshEdgeNode2' : pm.createNode('curveFromMeshEdge'), 'ptOnSurfaceIn' : pm.createNode('pointOnSurfaceInfo'), 'matrixNode' : pm.createNode('fourByFourMatrix'), 'decomposeMatrix' : pm.createNode('decomposeMatrix'), 'loftNode' : pm.createNode('loft'), 'locator' : pm.createNode('locator') } def createConnections(self, *args): ## Connect our main object's (where we selected our edges from) world mesh information ## To our inputMesg of our MeshEdgeNode, this matches info to our new curves. self.main['meshObject'].worldMesh.connect(self.node['meshEdgeNode1'].inputMesh) self.main['meshObject'].worldMesh.connect(self.node['meshEdgeNode2'].inputMesh) ## Connect both our meshEdgeNodes to our loftNode, this completes our loftNode self.node['meshEdgeNode1'].outputCurve.connect(self.node['loftNode'].inputCurve[0]) self.node['meshEdgeNode2'].outputCurve.connect(self.node['loftNode'].inputCurve[1]) ## Connect our loftNode Output Sruface information to our ptOnSurface nodes input ## This allows us to get the information from this surface and plug it into our 4x4 Matrix self.node['loftNode'].outputSurface.connect(self.node['ptOnSurfaceIn'].inputSurface) ## Connect the Normalized Normals (X,Y,Z) to the first row of the 4x4 Matrix Node self.node['ptOnSurfaceIn'].normalizedNormalX.connect(self.node['matrixNode'].in00) self.node['ptOnSurfaceIn'].normalizedNormalY.connect(self.node['matrixNode'].in01) self.node['ptOnSurfaceIn'].normalizedNormalZ.connect(self.node['matrixNode'].in02) ## Connect the Normalized TangentU (X,Y,Z) to the second row of the 4x4 Matrix Node self.node['ptOnSurfaceIn'].normalizedTangentUX.connect(self.node['matrixNode'].in10) self.node['ptOnSurfaceIn'].normalizedTangentUY.connect(self.node['matrixNode'].in11) self.node['ptOnSurfaceIn'].normalizedTangentUZ.connect(self.node['matrixNode'].in12) ## Connect the Normalized TangentV (X,Y,Z) to the third row of the 4x4 Matrix Node self.node['ptOnSurfaceIn'].normalizedTangentVX.connect(self.node['matrixNode'].in20) self.node['ptOnSurfaceIn'].normalizedTangentVY.connect(self.node['matrixNode'].in21) self.node['ptOnSurfaceIn'].normalizedTangentVZ.connect(self.node['matrixNode'].in22) ## Connect the surface positions (X,Y,Z) to the fourth row of the 4x4 Matrix Node self.node['ptOnSurfaceIn'].positionX.connect(self.node['matrixNode'].in30) self.node['ptOnSurfaceIn'].positionY.connect(self.node['matrixNode'].in31) self.node['ptOnSurfaceIn'].positionZ.connect(self.node['matrixNode'].in32) ## All of the above will be processed by the decomposed matrix node to output what we need. ## Connect the 4x4 Matrix output to our decompose matrix Input self.node['matrixNode'].output.connect(self.node['decomposeMatrix'].inputMatrix) ## Now that our decompose matrix node has our correct information, we can connect the -- ## rotate and the translate information to our offset locator group node we created earler, We're done! self.node['decomposeMatrix'].outputTranslate.connect(self.node['locator'].getParent().translate) self.node['decomposeMatrix'].outputRotate.connect(self.node['locator'].getParent().rotate)
Now that we've made all our vital connections for each node, we can run through the main section, where we start to define variables and set up the main settings for each node we created earlier.
Obviously we're performing everything above, based on a selection, so let's start with that, in our __init__ function, which is the main section of this plugin, we'll define and setup everything we need to.
To get the selection, we can use the ls command derived from maya.core.
selection = pm.ls(sl=1, flatten=1)
Flatten will just store our selection in it's own index, rather than storing things as merged arrays.
Now that we have our selection, we can define our three main variables, we'll need to get the object that the edges selected belong to, and also the edge indices for each edge selected. We can prefix our variable names with 'self.', this allows as to access these variables from each function from any scope in our class, this is very very handy.
So let's start by getting our meshObject. Inside our selection variable, we should have two edges, something similar to this when we return it.
# Result: [MeshEdge(u'pSphereShape1.e[287]'), MeshEdge(u'pSphereShape1.e[307]')] #
With this result, we can see our edge index's, as well as our object name's shape. So, let's dive in! We'll create another dictionary to access this information later on.
self.main = { 'meshObject' : selection[0].node().getParent() }
By running the function node on the unicode string of the selection, we can convert this to a readable pymel node. This makes it very easy for us to get the parent shape.
So we're accessing the first index of selection with selection[0] which would equal to:
# Result: MeshEdge(u'pSphereShape1.e[287]') #
We could go through and split apart the string, which is what I've seen in many cases that people do, this works just fine, however it's ugly, not easy to read and Pymel almost always has an alternative.
# Result: u'pSphereShape1' #
Awesome, now that we have our objects unicode shape, once wrapped in our PyNode, we get this:
# Result: nt.Mesh(u'pSphereShape1') #
Now we have our mesh object of the shape! So let's get it's parent using .getParent() which will output:
# Result: nt.Transform(u'pSphere1') #
That's it for our mesh object, I'll show you how I've seen most people get this information, and how I've found through the pymel docs, the below is using the split function coupled with a PyNode wrapper. It's ugly right?
self.meshObject = pm.PyNode(selection[0].split('.')[0]).getParent() self.edgeIndex1 = int(selection[0].split('[')[1].split(']')[0]) self.edgeIndex2 = int(selection[1].split('[')[1].split(']')[0])
Or, a simple, Pymel solution:
self.main = { 'meshObject' : selection[0].node().getParent(), 'edgeIndex1' : selection[0].indices()[0], 'edgeIndex2' : selection[1].indices()[0] }
After we do this, let's run our three other functions to setup the scene.
## Create all the nodes we'll need for this tool self.createNodes() ## Connect all our nodes together self.createConnections() ## Set all Attributes self.setAttributes()
Lets set up our attributes on each node that needs to be changed.
self.node['meshEdgeNode1'].isHistoricallyInteresting.set(1) self.node['meshEdgeNode2'].isHistoricallyInteresting.set(1) self.node['meshEdgeNode1'].edgeIndex[0].set(self.main['edgeIndex1']) self.node['meshEdgeNode2'].edgeIndex[0].set(self.main['edgeIndex2'])
Setting the isHistoricallyInteresting attribute to 1 will allow us to see the edgeIndex connected to this node in the channel box.
We also se the edgeIndex of each meshEdgeNode we created to our two edge Index we created above. This tells our meshEdge node where/what to gather information!
Now let's move onto our loft node.
self.node['loftNode'].reverseSurfaceNormals.set(1) self.node['loftNode'].inputCurve.set(size=2) self.node['loftNode'].uniform.set(True) self.node['loftNode'].sectionSpans.set(3) self.node['loftNode'].caching.set(True)
First we reverse our surfaceNormals, then we set the size of the inputCurveArray to 2, then we make sure we're set to uniform, make our loft have 3 spans, and turn caching on, caching will greatly speed up this constraint.
Now we can set up our pointOnSurfaceInfo attributes.
self.node['ptOnSurfaceIn'].turnOnPercentage.set(True) self.node['ptOnSurfaceIn'].parameterU.set(0.5) self.node['ptOnSurfaceIn'].parameterV.set(0.5) self.node['ptOnSurfaceIn'].caching.set(True)
First we make sure that we're set to percentageMode, this makes our U and V parameters think as a percentage rather than a unit value. We then set our U and V parameters to 0.5 (50%), and turn caching on here too.
Guess what, we're done! Let's link everything together!
import pymel.core as pm class SimpleRivet(): def __init__(self): selection = pm.ls(sl=1, fl=1) if (len(selection) == 2 and isinstance(selection[0], pm.MeshEdge) and isinstance(selection[1], pm.MeshEdge)): ## Now both our selections are edges, so let's continue! self.main = { 'meshObject' : selection[0].node().getParent(), 'edgeIndex1' : selection[0].indices()[0], 'edgeIndex2' : selection[1].indices()[0] } ## Create all the nodes we'll need for this tool self.createNodes() ## Connect all our nodes together self.createConnections() ## Set all Attributes self.setAttributes() else: ## The selection is not two edges, tell them no! pm.warning('Please make sure you select two edges only.') def setAttributes(self): ## Set attributes on our MeshEdgeNodes self.node['meshEdgeNode1'].isHistoricallyInteresting.set(1) self.node['meshEdgeNode2'].isHistoricallyInteresting.set(1) self.node['meshEdgeNode1'].edgeIndex[0].set(self.main['edgeIndex1']) self.node['meshEdgeNode2'].edgeIndex[0].set(self.main['edgeIndex2']) ## Setup our loft node, caching improved performance alot self.node['loftNode'].reverseSurfaceNormals.set(1) self.node['loftNode'].inputCurve.set(size=2) self.node['loftNode'].uniform.set(True) self.node['loftNode'].sectionSpans.set(3) self.node['loftNode'].caching.set(True) ## position the surfaceInfoNode in the absolute center position with cacheing self.node['ptOnSurfaceIn'].turnOnPercentage.set(True) self.node['ptOnSurfaceIn'].parameterU.set(0.5) self.node['ptOnSurfaceIn'].parameterV.set(0.5) self.node['ptOnSurfaceIn'].caching.set(True) def createNodes(self, *args): self.node = { 'meshEdgeNode1' : pm.createNode('curveFromMeshEdge'), 'meshEdgeNode2' : pm.createNode('curveFromMeshEdge'), 'ptOnSurfaceIn' : pm.createNode('pointOnSurfaceInfo'), 'matrixNode' : pm.createNode('fourByFourMatrix'), 'decomposeMatrix' : pm.createNode('decomposeMatrix'), 'loftNode' : pm.createNode('loft'), 'locator' : pm.createNode('locator') } def createConnections(self, *args): ## Connect our main object's (where we selected our edges from) world mesh information ## To our inputMesg of our MeshEdgeNode, this matches info to our new curves. self.main['meshObject'].worldMesh.connect(self.node['meshEdgeNode1'].inputMesh) self.main['meshObject'].worldMesh.connect(self.node['meshEdgeNode2'].inputMesh) ## Connect both our meshEdgeNodes to our loftNode, this completes our loftNode self.node['meshEdgeNode1'].outputCurve.connect(self.node['loftNode'].inputCurve[0]) self.node['meshEdgeNode2'].outputCurve.connect(self.node['loftNode'].inputCurve[1]) ## Connect our loftNode Output Sruface information to our ptOnSurface nodes input ## This allows us to get the information from this surface and plug it into our 4x4 Matrix self.node['loftNode'].outputSurface.connect(self.node['ptOnSurfaceIn'].inputSurface) ## Connect the Normalized Normals (X,Y,Z) to the first row of the 4x4 Matrix Node self.node['ptOnSurfaceIn'].normalizedNormalX.connect(self.node['matrixNode'].in00) self.node['ptOnSurfaceIn'].normalizedNormalY.connect(self.node['matrixNode'].in01) self.node['ptOnSurfaceIn'].normalizedNormalZ.connect(self.node['matrixNode'].in02) ## Connect the Normalized TangentU (X,Y,Z) to the second row of the 4x4 Matrix Node self.node['ptOnSurfaceIn'].normalizedTangentUX.connect(self.node['matrixNode'].in10) self.node['ptOnSurfaceIn'].normalizedTangentUY.connect(self.node['matrixNode'].in11) self.node['ptOnSurfaceIn'].normalizedTangentUZ.connect(self.node['matrixNode'].in12) ## Connect the Normalized TangentV (X,Y,Z) to the third row of the 4x4 Matrix Node self.node['ptOnSurfaceIn'].normalizedTangentVX.connect(self.node['matrixNode'].in20) self.node['ptOnSurfaceIn'].normalizedTangentVY.connect(self.node['matrixNode'].in21) self.node['ptOnSurfaceIn'].normalizedTangentVZ.connect(self.node['matrixNode'].in22) ## Connect the surface positions (X,Y,Z) to the fourth row of the 4x4 Matrix Node self.node['ptOnSurfaceIn'].positionX.connect(self.node['matrixNode'].in30) self.node['ptOnSurfaceIn'].positionY.connect(self.node['matrixNode'].in31) self.node['ptOnSurfaceIn'].positionZ.connect(self.node['matrixNode'].in32) ## All of the above will be processed by the decomposed matrix node to output what we need. ## Connect the 4x4 Matrix output to our decompose matrix Input self.node['matrixNode'].output.connect(self.node['decomposeMatrix'].inputMatrix) ## Now that our decompose matrix node has our correct information, we can connect the -- ## rotate and the translate information to our offset locator group node we created earler, We're done! self.node['decomposeMatrix'].outputTranslate.connect(self.node['locator'].getParent().translate) self.node['decomposeMatrix'].outputRotate.connect(self.node['locator'].getParent().rotate) SimpleRivet()
Notice at the end I've added RivetSimple(), this runs our class and performs the tasks necessary! Remember to select your two edges!
There we have it, we don't have any error checking, it's not smart enough to filter out other components, but you get the idea and you can expand on this from here. I'm just trying to teach you the basics of this tool!
The advanced version of this tool can be found here shortly, I'll add a download link to both the simple and the advanced version, please bug me if you think I haven't updated this section and I'll get back to you straight away!