Deformers

Deformers are a subset of dependency graph nodes that perform deformation algorithms on input geometry and outputs the deformed geometry to the output attribute of the deformer.  When you think of an algorithm to move points or cvs around on mesh or surface, you usually want to create a deformer.  Example deformers that I have implemented in the past include wrap, jiggle, displacement, blend shape, and skin sliding deformers.  To create a deformer node, we create a class that inherits from the proxy class, MPxDeformerNode.

Since deformers are a subset of dependency nodes, they can be created with the createNode command just like all other nodes, however, deformers have a lot of special functionality that depends on additional connections made to the deformer node.  These connections get made for you when you use the deformer command.  This functionality includes membership editing and reordering of deformation history.

A deformer has the same structure as a normal dependency node except we implement a deform function instead of a compute function.  To show some of the process involved with making deformers, we will create a simple blend shape node that blends one mesh to another mesh.

As inputs to our deformer, we will need a target mesh and a blend weight value:

MStatus BlendNode::initialize() {
  MFnTypedAttribute tAttr;
  MFnNumericAttribute nAttr;
     
  aBlendMesh = tAttr.create("blendMesh", "blendMesh", MFnData::kMesh);
  addAttribute(aBlendMesh);
  attributeAffects(aBlendMesh, outputGeom);
  
  aBlendWeight = nAttr.create("blendWeight", "bw", MFnNumericData::kFloat);
  nAttr.setKeyable(true);
  addAttribute(aBlendWeight);
  attributeAffects(aBlendWeight, outputGeom);
 
  // Make the deformer weights paintable
  MGlobal::executeCommand( "makePaintable -attrType multiFloat -sm deformer blendNode weights;" );
 
  return MS::kSuccess;
}

Above is our deformers initialize function where we specify all the attributes for our deformer.  To create a mesh attribute, we use MFnTypedAttribute.  A typed attribute is used to create most attributes of non-simple types like meshes, surfaces, curves, arrays, etc.  After the mesh attribute is added, we set it to affect the output geometry attribute, outputGeom.  All deformers have an outputGeom attribute which is part of the MPxDeformerNode class.  Once the mesh attribute is finished, we create the blend weight attribute which is identical to the float attributes we added in the dependency node example.

The last code of interest in the initialize function is the makePaintable call.  makePaintable is a MEL (and Python) command used to make a particular attribute paintable.  All deformers come with a per-vertex weight attribute so we are simple activating the paintability of that attribute.   With our deformer initialized, we can move on to the deform function.

MStatus BlendNode::deform(MDataBlock& data, MItGeometry& itGeo,
                          const MMatrix &localToWorldMatrix,
                          unsigned int mIndex) {
  MStatus status;
 
  // Get the envelope and blend weight
  float env = data.inputValue(envelope).asFloat();
  float blendWeight = data.inputValue(aBlendWeight).asFloat();
  blendWeight *= env;
 
  // Get the blend mesh
  MObject oBlendMesh = data.inputValue(aBlendMesh).asMesh();
  if (oBlendMesh.isNull()) {
    // No blend mesh attached so exit node.
    return MS::kSuccess;
  }
 
  // Get the blend points
  MFnMesh fnBlendMesh(oBlendMesh, &status);
  CHECK_MSTATUS_AND_RETURN_IT(status);
  MPointArray blendPoints;
  fnBlendMesh.getPoints(blendPoints);
 
  MPoint pt;
  float w = 0.0f;
  for (; !itGeo.isDone(); itGeo.next()) {
    // Get the input point
    pt = itGeo.position();
    // Get the painted weight value
    w = weightValue(data, mIndex, itGeo.index());
    // Perform the deformation
    pt = pt + (blendPoints[itGeo.index()] - pt) * blendWeight * w;
    // Set the new output point
    itGeo.setPosition(pt);
  }
 
  return MS::kSuccess;
}

The deform function is where all the deformation implementation code occurs.  The workflow is similar to normal dependency nodes where we:

  1. Get our inputs from the data block
  2. Perform the deformation
  3. Store the deformed geometry back into the data block.

Our deform function begins with getting the envelope and blend weight input values from the data block.  The envelope is a built-in deformer attribute that you can use as a magnitude multiplier for your deformation.  After we grab the numeric attribute values, we get the blend target mesh.  We need to check if the data is valid because if there is no target mesh connected to the deformer, the MObject will be null and we cannot perform the deformation.  Once we have a valid MObject representing our target mesh, we need to extract its point positions.  We do this by attaching an MFnMesh function set to the MObject.  MFnMesh is the function set used to query, edit, and create polygonal meshes.  Now that we have all the inputs, we can begin the deformation algorithm.

The deform function comes with an MItGeometry parameter that is used to iterate over the components of the input mesh.  If you want to take advantage of deformer set membership, you must use this iterator as it only iterates over the vertices or cvs included in the deformer membership.  You can query the current vertex id with MItGeometry::index().

As we iterate over each vertex, we grab the painted weight value for that vertex.  MPxDeformerNode has a built-in convenience function, weightValue, which we can use to query each painted weight value.  The multiIndex attribute passed into this function is the index of the input geometry.  Some deformers can affect multiple meshes at the same time, each having their own painted weight value map.  This index specifies which index to use.  Most of the time, this is 0.  It is for this reason that all paintable attributes need a parent compound array attribute, which we will learn about in later sections.

After we have the weight value, we can perform the actual deformation, which is on one line.  The blend shape deformation is a simple weighted vector delta addition to the current vertex.  With the new point position calculated, we put it back into the geometry iterator.  Notice we do not have to do any setClean calls.  Unless we add any custom outputs to a deformer, MPxDeformerNode automatically handles that for us when we use the deform function.

You’ll notice that out of all that code, the actual deformation algorithm is just one line.  This is the common case when making plug-ins in the Maya API.  Most of the code is usually all the node setup, event handling, and clean up.  Coming up with the algorithm is almost the easy part.

And here is the full code listing for the blendNode deformer.  Notice when we register the node with Maya, we also specify that it is a deformer node.

// BlendNode.h

#ifndef BLENDNODE_H
#define BLENDNODE_H
 
#include <maya/MDataBlock.h>
#include <maya/MDataHandle.h>
#include <maya/MGlobal.h>
#include <maya/MItGeometry.h>
#include <maya/MMatrix.h>
#include <maya/MPointArray.h>
#include <maya/MStatus.h>
 
#include <maya/MFnMesh.h>
#include <maya/MFnNumericAttribute.h>
#include <maya/MFnTypedAttribute.h>
 
#include <maya/MPxDeformerNode.h>
 
class BlendNode : public MPxDeformerNode {
 public:
  BlendNode() {};
  virtual MStatus deform(MDataBlock& data, MItGeometry& itGeo,
                         const MMatrix &localToWorldMatrix, unsigned int mIndex);
  static void* creator();
  static MStatus initialize();
 
  static MTypeId id;
  static MObject aBlendMesh;
  static MObject aBlendWeight;
};
#endif
// BlendNode.cpp

#include "include/BlendNode.h"
#include <maya/MFnPlugin.h>
 
MTypeId BlendNode::id(0x00000002);
MObject BlendNode::aBlendMesh;
MObject BlendNode::aBlendWeight;
 
void* BlendNode::creator() { return new BlendNode; }
 
MStatus BlendNode::deform(MDataBlock& data, MItGeometry& itGeo,
                          const MMatrix &localToWorldMatrix, unsigned int mIndex) {
  MStatus status;
 
  // Get the envelope and blend weight
  float env = data.inputValue(envelope).asFloat();
  float blendWeight = data.inputValue(aBlendWeight).asFloat();
  blendWeight *= env;
 
  // Get the blend mesh
  MObject oBlendMesh = data.inputValue(aBlendMesh).asMesh();
  if (oBlendMesh.isNull()) {
    // No blend mesh attached so exit node.
    return MS::kSuccess;
  }
 
  // Get the blend points
  MFnMesh fnBlendMesh(oBlendMesh, &status);
  CHECK_MSTATUS_AND_RETURN_IT(status);
  MPointArray blendPoints;
  fnBlendMesh.getPoints(blendPoints);
 
  MPoint pt;
  float w = 0.0f;
  for (; !itGeo.isDone(); itGeo.next()) {
    // Get the input point
    pt = itGeo.position();
    // Get the painted weight value
    w = weightValue(data, mIndex, itGeo.index());
    // Perform the deformation
    pt = pt + (blendPoints[itGeo.index()] - pt) * blendWeight * w;
    // Set the new output point
    itGeo.setPosition(pt);
  }
 
  return MS::kSuccess;
}
 
MStatus BlendNode::initialize() {
  MFnTypedAttribute tAttr;
  MFnNumericAttribute nAttr;
   
  aBlendMesh = tAttr.create("blendMesh", "blendMesh", MFnData::kMesh);
  addAttribute(aBlendMesh);
  attributeAffects(aBlendMesh, outputGeom);
  
  aBlendWeight = nAttr.create("blendWeight", "bw", MFnNumericData::kFloat);
  nAttr.setKeyable(true);
  addAttribute(aBlendWeight);
  attributeAffects(aBlendWeight, outputGeom);
 
  // Make the deformer weights paintable
  MGlobal::executeCommand("makePaintable -attrType multiFloat -sm deformer blendNode weights;");
 
  return MS::kSuccess;
}
 
MStatus initializePlugin(MObject obj) {
  MStatus status;
  MFnPlugin plugin(obj, "Chad Vernon", "1.0", "Any");
 
  // Specify we are making a deformer node
  status = plugin.registerNode("blendNode", BlendNode::id, BlendNode::creator,
                               BlendNode::initialize, MPxNode::kDeformerNode);
  CHECK_MSTATUS_AND_RETURN_IT(status);
 
  return status;
}
 
MStatus uninitializePlugin(MObject obj) {
  MStatus     status;
  MFnPlugin plugin(obj);
 
  status = plugin.deregisterNode(BlendNode::id);
  CHECK_MSTATUS_AND_RETURN_IT(status);
 
  return status;
}

And here is the corresponding Python implementation:

import maya.OpenMayaMPx as OpenMayaMPx
import maya.OpenMaya as OpenMaya
import maya.cmds as cmds
 
class BlendNode(OpenMayaMPx.MPxDeformerNode):
    kPluginNodeId = OpenMaya.MTypeId(0x00000002)
     
    aBlendMesh = OpenMaya.MObject()
    aBlendWeight = OpenMaya.MObject()
     
    def __init__(self):
        OpenMayaMPx.MPxDeformerNode.__init__(self)
 
    def deform(self, data, itGeo, localToWorldMatrix, mIndex):
        envelope = OpenMayaMPx.cvar.MPxDeformerNode_envelope
        env = data.inputValue(envelope).asFloat()
        blendWeight = data.inputValue(BlendNode.aBlendWeight).asFloat()
        blendWeight *= env
 
        oBlendMesh = data.inputValue(BlendNode.aBlendMesh).asMesh()
        if oBlendMesh.isNull():
            return OpenMaya.MStatus.kSuccess
 
        fnBlendMesh = OpenMaya.MFnMesh(oBlendMesh)
        blendPoints = OpenMaya.MPointArray()
        fnBlendMesh.getPoints(blendPoints)
 
        while not itGeo.isDone():
            pt = itGeo.position()
            w = self.weightValue(data, mIndex, itGeo.index())
            pt = pt + (blendPoints[itGeo.index()] - pt) * blendWeight * w
            itGeo.setPosition(pt)
            itGeo.next()
 
        return OpenMaya.MStatus.kSuccess
 
def creator():
    return OpenMayaMPx.asMPxPtr(BlendNode())
 
def initialize():
    tAttr = OpenMaya.MFnTypedAttribute()
    nAttr = OpenMaya.MFnNumericAttribute()
     
    BlendNode.aBlendMesh = tAttr.create('blendMesh', 'bm', OpenMaya.MFnData.kMesh)
    BlendNode.addAttribute( BlendNode.aBlendMesh )
     
    outputGeom = OpenMayaMPx.cvar.MPxDeformerNode_outputGeom
    BlendNode.attributeAffects(BlendNode.aBlendMesh, outputGeom)
 
    BlendNode.aBlendWeight = nAttr.create('blendWeight', 'bw', OpenMaya.MFnNumericData.kFloat)
    nAttr.setKeyable(True)
    BlendNode.addAttribute(BlendNode.aBlendWeight)
    BlendNode.attributeAffects(BlendNode.aBlendWeight, outputGeom)
 
    # Make deformer weights paintable
    cmds.makePaintable('blendNode', 'weights', attrType='multiFloat', shapeMode='deformer')
 
def initializePlugin(obj):
    plugin = OpenMayaMPx.MFnPlugin(obj, 'Chad Vernon', '1.0', 'Any')
    try:
        plugin.registerNode('blendNode', BlendNode.kPluginNodeId, creator, initialize, OpenMayaMPx.MPxNode.kDeformerNode)
    except:
        raise RuntimeError('Failed to register node')
 
def uninitializePlugin(obj):
    plugin = OpenMayaMPx.MFnPlugin(obj)
    try:
        plugin.deregisterNode(BlendNode.kPluginNodeId)
    except:
        raise RuntimeError('Failed to deregister node')

The only difference besides syntax here is the access of the built-in static variables, outputGeom and envelope, of the proxy class, MPxDeformerNode.  We cannot just use self.outputGeom or self.envelope; instead, we can use similar code:

outputGeom = OpenMayaMPx.cvar.MPxDeformerNode_outputGeom
envelope = OpenMayaMPx.cvar.MPxDeformerNode_envelope

You can read more about scripted plug-in workflow in the Maya Python API section of the documentation.

comments powered by Disqus