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.

#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
#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.

14 thoughts on “Deformers”

nikos June 8, 2011 at 2:30 am

hello, are you using the UV parameter space of the spheres for the deformer shown here http://www.youtube.com/watch?v=XGYdT4p3S58&feature=channel_video_title
or is it a blend shape like explained in the notes above?

Cheers

Chad June 8, 2011 at 8:14 am

No, it’s not using the UVs and it’s not like a blendshape. It’s mostly matrix calculations.

Richard August 11, 2011 at 3:05 pm

Thanks Chad! Thanks to your detailed explanation, now my custom python deformer is now paintable! 🙂
Still very slow though… 🙁

MWilliams October 13, 2011 at 9:47 am

Just wanted to thank you for the great resources! Have been looking for something like this all over the place.

Kirk August 9, 2013 at 10:54 am

Do you know how to setup a deformer with multiple paintable influences, much like a skinCluster?

Chad August 9, 2013 at 10:07 pm

You can use callbacks with an index attribute to do it. I go over it in my Maya API videos: http://www.cgcircuit.com/lessondetailcomplete.php?val=592

Kirk August 29, 2013 at 1:26 pm

Thanks for your help, Chad! Much appreciated.

felixlechA February 10, 2014 at 5:01 am

Hi,
You said in this article :
“All deformers come with a per-vertex weight attribute so we are simple activating the paintability of that attribute. “.
Lattice are concern by this (makePaintable) ? I want to paint this fallof lattice influence and the only way I found to do this, was to buffering the deformation on an other mesh and reapply with a blend shape who is paintable. It works but not really clean.

Thanks

Chad February 20, 2014 at 9:50 pm

All deformers come with the map available but the implementation of the deformer must take advantage of it. The lattice, unfortunately, does not use its weight map.

illunara November 10, 2014 at 11:41 pm

Hi Chad
Thank for great tutorial
Just about the paint weight value, i did exactly like you said in video, but can’t get the node paintable.
I’m using Maya2014, wrote using C++,
thank

Meg December 9, 2014 at 8:26 pm

Has the Python version of the blend node been tested in Maya 2015? No matter how I fiddle with it, the output geometry is just equal to the input geometry. I added a print statement to the deform function and it seems like that never gets called. Here’s how I have it hooked up, and there are no errors in the script log: http://i.imgur.com/sV4EOFL.png Has something changed in 2015 or did I just hook this up wrong somehow?

Meg December 9, 2014 at 9:00 pm

Nevermind, I figured it out with the help of these lovely folks:

https://groups.google.com/forum/#!msg/maya_he3d/nSTt3avXzXI/OdnQdrZZd6cJ

Screenshot of what the fixed node graph looks like: http://i.imgur.com/y6CuSka.png

In case anyone else gets as lost as me, to hook up the deformer correctly:
– Create the input mesh (in my case tallCube)
– Select the input mesh and run this mel command: deformer -type ‘blendNode’;
– Create the blend mesh (longCube) and hook its Out Mesh into the blend node’s Blend Mesh attribute.
– Open up the blend node and fiddle with the blend weight and envelope values to see the deformation.

After I got everything hooked up I did notice there are some errors being thrown inside the deform function, not sure how that’s affecting the blending behavior (if at all) but it seems like OpenMaya.MStatus isn’t part of the Python API anymore in 2015.

Thank you so much for the tutorial, it was very helpful!

seema December 23, 2014 at 12:25 am

This plugin works for 2 objects with exact same topology. Do you have snippets/explaination on how this can be implemented across different topology like in your videos. I am guessing an Offset matrix using closestPoints has to calculated during initialization and then used back in the deform function? Any tips of that would be great!

Dave January 27, 2017 at 8:55 am

Your comment is awaiting moderation.

Hi Chad,
I’m trying to compile the deformer in Visual Studio 2015 for maya 2016 but I’m getting a linker error:
LNK2001 símbolo externo “__declspec(dllimport) public: virtual class MStatus __cdecl MPxNode::dependsOn(class MPlug const &,class MPlug const &,bool &)const ” (__imp_?dependsOn@MPxNode@@UEBA?AVMStatus@@AEBVMPlug@@0AEA_N@Z)
The bizarre thing is that in maya 2015 works perfectly.
Do you know if there is someting different in 2016?
Thanks

Leave A Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

<code> For inline code.
[sourcecode] For multiline code.