Writing a basic deformer for Maya in python

Tutorial / 26 May 2020

This tutorial functions as a starting point and to give some basic pointers on how to approach writing a deformer for Maya, I've based this on the mnCollisionDeformer I recently wrote, the video will cover the basic idea but will leave room for you to implement your own algorithm.

You can find the deformer template to get started from here: https://github.com/mvn882/tutorials/blob/master/plugins/deformerTemplate.py 

You can start from the template this already has the basic functions and class for the deformer set up.
It starts with the imports, we want to import OpenMaya and OpenMayaMPX from the old python API, the newer API does not support MPxDeformerNode's yet. We also want to import Mel eval, this will be used for evaluating custom attribute editor templates.

import maya.OpenMaya as OpenMaya
import maya.OpenMayaMPx as OpenMayaMPx
from maya.mel import eval as mel_eval

Then we'll set a couple of global variables, these are pre-defined in Maya to access geometry data and will work from 2016 upwards.
Sidenote: the MPxDeformerNode inherits from the MPxGeopmetryFilter base class, hence that's what the variables are called.

kInput = OpenMayaMPx.cvar.MPxGeometryFilter_input
kInputGeom = OpenMayaMPx.cvar.MPxGeometryFilter_inputGeom
kOutputGeom = OpenMayaMPx.cvar.MPxGeometryFilter_outputGeom
kEnvelope = OpenMayaMPx.cvar.MPxGeometryFilter_envelope
kGroupId = OpenMayaMPx.cvar.MPxGeometryFilter_groupId

Now we'll define the actual class, this will inherit from the MPxDeformerNode as this is a simple deformer.

class deformer(OpenMayaMPx.MPxDeformerNode):
    def __init__(self):
        OpenMayaMPx.MPxDeformerNode.__init__(self)

You will have to set a type id and a type name to be used in the initializePlugin method.

type_id = OpenMaya.MTypeId(0x00001)  
type_name = "deformer"

The initalizePlugin method also needs a creator and an initalize method so we'll define these as well.
The creator should be a clas method as it does not use any instance data and will need to be callable without a class instance, this method will simply return a new instance of the class.

@classmethod
def creator(cls):
    return cls()

The initialize method is where we create all the attributes that we want the deformer to have as inputs and outputs, for a collision deformer you'll want to at least have a collider mesh as an input.

@classmethod
def initialize(cls):
    generic_attr_fn = OpenMaya.MFnGenericAttribute()
    cls.collider_attr = generic_attr_fn.create('collider', 'cl')
    generic_attr_fn.addDataAccept( OpenMaya.MFnData.kMesh )
    cls.addAttribute( cls.collider_attr )

This is also where you would define dependencies between in and outputs, so that when an input is changed the corresponding output will be recomputed.

cls.attributeAffects( cls.collider_attr, kOutputGeom )

Now you can add the initializePlugin and uninitializePlugin methods, outside of the class.
The initializePlugin is simply used to register the plugin in Maya, this is also where you set the author and version of the plugin and load any custom Attribute Editor templates.

def initializePlugin(plugin):
    plugin_fn = OpenMayaMPx.MFnPlugin(plugin, "Marieke van Neutigem", "0.0.1")
    plugin_fn.registerNode(
            deformer.type_name,
            deformer.type_id,
            deformer.creator,
            deformer.initialize,
            OpenMayaMPx.MPxNode.kDeformerNode
        )
    mel_eval( gui_template )

The uninitializePlugin method simply deregisters the plugin using the plugin id when the plugin is unloaded from maya.

def uninitializePlugin(plugin):
    plugin_fn = OpenMayaMPx.MFnPlugin(plugin)
    plugin_fn.deregisterNode(deformer.type_id)

Now you can start implementing the deform method, you could implement the compute method instead as you would on a basic deformer but really I recommend only doing that if the deformation is not a simple per vertex operation and requires you to use compute instead.

The deform method gives you access to the data_block, this can be used to retrieve the values of any of the attributes you added to your plugin. You also have a geometry_iterator, this can be used to iterate over all the vertices of the input geometry and modify them on the fly.
Then there's the local to world space matrix that you can use to transform the vertices from the input mesh to world space for any calculations that require that. And lastly, there is the geometry index which is needed to access to the input geometry from the global geometry filter variables.

def deform(self, data_block, geometry_iterator, local_to_world_matrix, geometry_index):

After you've implemented the deform method there's one last thing; you can add a custom attribute editor template. This is useful if you have added some attributes to your plugin but they don't show the way you would want them to. This is a simple Mel script that needs to be evaluated on loading the plugin, the proc has to be named AE[Name of your deformer]Template.

gui_template = """global proc AEdeformerTemplate( string $nodeName )
    {
        editorTemplate -beginScrollLayout;
            editorTemplate -beginLayout "Deformer Attributes" -collapse 0;
                // Add your own attributes here in the way you want them to be displayed.
            editorTemplate -endLayout;
            AEdependNodeTemplate $nodeName;
            editorTemplate -addExtraControls;
        editorTemplate -endScrollLayout;
    }"""

I hope that get's you a bit of a starting point to start writing your own deformers!

You can find the full source code for the mnCollisionDeformer here for reference: https://github.com/mvanneutigem/tutorials/blob/master/plugins/mnCollisionDeformer.py