Once again a little off the usual track for this blog, but I’ll be back to the Nuke tools soon (I had an unfortunate incident with my laptop where a lot of my dev work got lost, but should be back on track soon). In the meantime, here’s a little proof of concept plugin I’ve thrown together for Maya – A noise deformer! Why Maya doesn’t have one in the first place is anyone’s guess, but this simple little script will apply a basic perlin noise to a mesh with a locator to manipulate it’s effect. The script is fairly basic, as trying to calculate noise on a heavy mesh in python would be too computationally expensive, but with the theory in place it should be relatively easy to recreate it in C++.
Perlin is a fairly standard noise, and there are a lot of good libraries out there already to save us having to re-write it. The noise module in fact is the perfect example, so we’ll use that for our plugin.
Python plugins for Maya
Let’s do a quick run through of how to write a plugin for maya. There are a few functions that will need to be defined in the script for it to work:
All maya plugins require the following two functions:
def initializePlugin(mobject): mplugin = OpenMayaMPx.MFnPlugin(mobject) try: # mplugin.register... Depending on the type of plugin, eg, registerNode, registerCommand etc... except: sys.stderr.write('Failed to register') raise def uninitializePlugin(mobject): mplugin = OpenMayaMPx.MFnPlugin(mobject) try: # mplugin.deregister... Depending on the type of plugin, eg, deregisterNode, deregisterCommand etc... except: sys.stderr.write('Failed to deregister') raise
Any maya plugin that creates a node requires the following:
def nodeCreator(): # API v1.0 return OpenMayaMPx.asMPxPtr(CUSTOM_NODE_CLASS()) # API v2.0 # return CUSTOM_NODE_CLASS() def nodeInitializer(): # More on this later... pass
Additionally, maya’s python API has two versions, 1.0 and 2.0, which are not compatible with each other. By default, Maya assumes the plugins are written with 1.0, but if you’re using 2.0 you have to define the following function in the script:
def maya_useNewAPI(): pass
The functions are fairly self explanatory:
- initializePlugin registers the plugin with maya so that it can used after it is loaded via the plugin manager
- uninitializePlugin deregisters the plugin.
- nodeCreator returns an instance of the class we define
- nodeInitializer sets up our node’s attributes
Maya runs using a nodal system known as the Dependency Graph. A part of that graph is the DAG (Directed Acyclic Graph) which is where all the nodes you’re used to dealing with live (like transforms and shapes). However, anything and everything can be a node in the Dependency Graph, and they are connected to each other by “plugs”. Each node has input and output plugs, which connect nodes together, but also have dependencies between themselves; for example, an output plug might know that it needs two input plugs to calculate it’s value. Maya can then tell what needs to be calculated using what’s called “Dirty Propagation”, where if a value is changed for one of the plugs, any plugs that are dependent on it’s value are marked as “dirty”, and the effect recurses down the chain marking all affected plugs as dirty. It is then able to recalculate each of these in turn to update the scene. This calculation is done by each node’s compute function, which should set the new values for the affected plugs and mark them as clean so they don’t get stuck recalculating.
So, when we write a plugin, we need to setup our input and output plugs, and create our method to set and clean values for any required plugs. What method we need to override depends on what api class you’re inheriting from. The two most common would be MPxNode and MPxCommand, for writing, you guessed it, nodes and commands. Each of these has various subclasses for specific types you want to override, but in our case we’ll be looking at the MPxDeformerNode, which means we’ll be overriding it’s key method, deform. A word of warning for this node, it is not yet supported by Maya’s 2.0 api, which makes things just a little more awkward to work with.
Applying our noise
Ok, first things first, let’s define our class:
class NoiseDeformerNode(OpenMayaMPx.MPxDeformerNode): locateMatrix = OpenMaya.MObject() def __init__(self): OpenMayaMPx.MPxDeformerNode.__init__(self) self.noise = SimplexNoise()
You’ll notice we defined a class member locateMatrix, which is an MObject (MObject is the absolute base class for all our maya objects, it has very little functionality in itself but can be passed to compatible “function sets” to use their methods). We define this here so that we can later set it up as an attribute inside the nodeInitializer function we mentioned earlier.
We also want to have a few static variables, but there was some naming changes between versions, so rather than do these checks in the class, we’ll set them outside of it. Before our class definition we’ll want the following:
import maya.cmds as cmds kApiVersion = cmds.about(apiVersion=True) if kApiVersion < 201600: kInput = OpenMayaMPx.cvar.MPxDeformerNode_input kInputGeom = OpenMayaMPx.cvar.MPxDeformerNode_inputGeom kOutputGeom = OpenMayaMPx.cvar.MPxDeformerNode_outputGeom kEnvelope = OpenMayaMPx.cvar.MPxDeformerNode_envelope else: kInput = OpenMayaMPx.cvar.MPxGeometryFilter_input kInputGeom = OpenMayaMPx.cvar.MPxGeometryFilter_inputGeom kOutputGeom = OpenMayaMPx.cvar.MPxGeometryFilter_outputGeom kEnvelope = OpenMayaMPx.cvar.MPxGeometryFilter_envelope
Now for the heavy lifting, the deform method – don’t be alarmed if this doesn’t make sense at first glance, we’ll go through it in a second.
def deform(self, data_block, geometry_iterator, local_to_world_matrix, geometry_index): """ Deform each vertex using the geometry iterator. """ envelope_value = data_block.inputValue(kEnvelope).asFloat() matrix = data_block.inputValue(NoiseDeformerNode.locateMatrix) matrix_value = matrix.asMatrix() matrix_inverse = matrix_value.inverse() # Obtain the list of normals for each vertex in the mesh. input_geometry_object = self.get_deformer_input_geometry(data_block, geometry_index) normals = OpenMaya.MFloatVectorArray() mesh_fn = OpenMaya.MFnMesh(input_geometry_object) mesh_fn.getVertexNormals(True, normals, OpenMaya.MSpace.kTransform) # Iterate over the vertices to move them. while not geometry_iterator.isDone(): point = geometry_iterator.position() pre_noise_pt = point * matrix_inverse vertexIndex = geometry_iterator.index() normal = OpenMaya.MVector(normals[vertexIndex]) # Cast the MFloatVector into a simple vector. noise = self.noise.noise3(*pre_noise_pt) new_point = point + normal * noise * envelope_value geometry_iterator.setPosition(new_point) geometry_iterator.next()
… which makes a call to another function we have to write…
def get_deformer_input_geometry(self, data_block, geometry_index): input_handle = data_block.outputArrayValue(kInput) input_handle.jumpToElement(geometry_index) input_geometry_object = input_handle.outputValue().child(kInputGeom).asMesh() return input_geometry_object
This looks like a lot, but it’s actually quite simple. First, let’s look at the arguments that get passed into deform.
- data_block – Seeing as we don’t necessarily need to calculate all our output plugs (only the dirty ones), maya builds a data block which contains “handles” for the input plugs that are required. We can then pull the values from the handles for what we’ll need.
- geometry_iterator – The key to deform, this is an iterator which steps over all the vertices of the mesh.
- local_to_world_matrix – Self explanatory really, the matrix that transforms the current object back to world space.
- geometry_index – The index of the geometry within the data_block.
The first thing we’ll do then is collect the values for the variables we need. We do this by getting the desired handle from the data block using inputValue and requesting the specific attribute. We can then cast it out to the desired format, eg, asFloat. We get the envelope value (A standard in all deformers, effectively just a strength multiplier), as well as our custom locateMatrix this way. You may be wondering where this is getting a value assigned, as we haven’t set anything up, but we’ll get to that soon. For now, know that we’re using a transform matrix to position the noise, but we’ll need to invert it to match what the user would expect in their viewport.
Getting the input normals is a little more involved, we have to walk up the input plugs to get the mesh object, which is just an MObject by default, so we’ll pass it to MFnMesh to use it’s function set to request the normals. Here we can see the difference between 1.0 and 2.0 api, we have to pass in the array as an argument which will be populated with the normals instead of having them returned from the function.
Now to apply the noise!
Unlike normal python iterators that we can use a for loop on, the geometry_iterator will require us to check whether there is anything left by using isDone, and calling next at the end of each loop. We can use index and position to get their respective values, which we can use to look up the corresponding normal. Make sure to cast the normal to a regular MVector as the position is a regular MPoint which can’t be added to the normals default MFloatVector. Seeing as we want the transform to move the noise, we multiply our point by our transform before passing it to the noise method and getting our noise value. Now we have all the pieces to get our output point. Multiply the noise and envelope (strength) by the normal and add it to our point, then set the new value. Obviously, this is only one way to do it, we could use a noise and it’s derivative or take multiple samples to build an offset vector for a new point which would then allow for more deformative noise, but as this is a proof of concept, we won’t go that far. Feel free to make the changes though!
There is a useful feature of the maya deformer node that allows us to add “accessory nodes” which are created along with the deformer and (should) be cleaned up with it whenever it’s deleted. There are two methods we can override for this:
def accessoryAttribute(self): return self.locateMatrix def accessoryNodeSetup(self, cmd): # Build a handle to control the noise transformation matrix obj_loc = cmd.createNode('locator') cmd.renameNode(obj_loc, 'noiseDeformHandle') fn_loc = OpenMaya.MFnDependencyNode(obj_loc) attr_matrix = fn_loc.attribute('matrix') result = cmd.connect(obj_loc, attr_matrix, self.thisMObject(), self.locateMatrix) return result
- accessoryAttribute defines which attributes will come from our accessory node
- accessoryNodeSetup handles the construction of the node.
The argument passed into accessoryNodeSetup is a MDGModifier, which is a clever little class that can queue up node creation rather than do it immediately. This means it’s able to correctly insert constructions into the undo queue so that maya can correctly track the undo state. Suffice to say, we should use it for creating our locator and connecting it’s output matrix to our locateMatrix.
Initializing the Node
The class is complete, but we need to ensure our plugins custom attributes will be correctly set up.
def nodeInitializer(): """ Defines the input and output attributes as static variables in our plug-in class. """ matrix_attr = OpenMaya.MFnMatrixAttribute() NoiseDeformerNode.locateMatrix = matrix_attr.create("locateMatrix", "lm") matrix_attr.setStorable(False) matrix_attr.setConnectable(True) matrix_attr.setReadable(True) NoiseDeformerNode.addAttribute(NoiseDeformerNode.locateMatrix) NoiseDeformerNode.attributeAffects(NoiseDeformerNode.locateMatrix, kOutputGeom)
We create the correct attribute type, and set our class’s static variable to the newly created attribute which we give both a long and short name, much like any other attribute in maya. We can also set a number of key properties, such as whether it can be connected to other plugs, whether it’s editable, keyable, visible in the channel box etc… it’s worth checking out the different options available for these.
We add the attribute to the class, and most importantly we tell maya what output plugs it should affect whenever it’s change. This is for the “Dirty Propagation” I mentioned before to work. Now it knows that when locateMatrix‘s value changes it needs to update the output geometry, which is pretty cool.
And last but not least, inside our initializePlugin method, we register the node with:
mplugin.registerNode(kPluginNodeName, kPluginNodeId, nodeCreator, nodeInitializer, OpenMayaMPx.MPxNode.kDeformerNode)
The registerNode command requires five arguments:
- The name of our node. This is the type that we’ll use to create it and what it will show up as.
- A unique ID for the plugin. Maya reserves ids in the range: 0x80000 – 0xfffff, and while you can use any value below that internally, it may clash with other plugins. For this reason, if you start making plugins that you intend to be distributed you should contact Autodesk to request one or more unique IDs.
- The function that will create and return an instance of the node.
- The function that will initialize the node.
- The type of the node.
And that’s it! You’ve finished your first plugin. To see it in action, add the plugin to one of your plugin paths (By default, your maya/<version>/plug-ins folder works, though the folder may not exist already), launch maya, select an object and run the following:
import maya.cmds as cmds cmds.deformer(name='whatever_you_want', type='kPluginNodeName')
…where kPluginNodeName is the value we used to register the node above. Grab the locator handle and move / scale / rotate it to manipulate the noise. Just a word of caution, as this is python it will start to slow down pretty quickly as you increase the mesh resolution. I’ve found about 10k vertices is just bordering on the limit of responsiveness, though your machine may differ. Enjoy!