[Maya C++ API] paint weights with MPxDeformerNode

In Maya you can create custom nodes to deform a mesh via the C++ API by inheriting from MPxDeformerNode. In this article I will describe how you can add and paint weight maps (associate each vertex to a float value) and access this vertex map inside your custom MPxDeformerNode. In other word how to enable attribute painting on a per vertex basis.

MPxDeformerNode

In the example below, the sphere is assigned to a custom deformer named ACustom_MPxDeformerNode and we paint the area we wish the deform. This painting defines a weight map that associates a float to each vertex, in the code we just linearly interpolate between the input vertex and output vertex of our deformer node:

Built-in weight map

By default, MPxDeformerNode provides the attribute:

.weightList[mesh_index].weights[vertex index]


So each mesh geometry mesh_index stores its own 'weight maps'. That's because MPxDeformerNode can accept multiple mesh in input, so we need to define a 'weight map' for each geometry. Otherwise said, an array of floats is defined for each input mesh. The size of these arrays is equal to the number of vertices of the corresponding mesh.


You can access the built in attribute .weightList[].weights[] of MPxDeformerNode through:

float MPxDeformerNode::weightValue(MDataBlock& dataBlock, int geometryIndex, int vertexIndex );

However, you need to explicitly call the MEL command makePaintable to enable painting in Maya UI:

makePaintable -attrType multiFloat -sm deformer "My_custom_MPxDeformerNode_name" weights

This call can be made after registering your custom MPxDeformerNode for instance. (some tutorial call makePaintable in the MPxDeformerNode::initialize() method but I don't recommend it as I noticed it triggered weird warnings in Maya's console). Also, be sure to use -attrType multiFloat for float attributes (which is the case for the built-in .weightList[mesh_index].weights attribute) and -attrType multiDouble for double attributes (which might be the case when you define additional weight attributes yourself).

/* Weight map
 * ==========
 *
 * MPxDeformerNode provide scalar weight maps through the attribute
 * `.weightList[index].weights`
 * (where 'index' is the index of the input geometry)
 * For instance this allows the user to partially apply the deformer.
*/


MStatus My_custom_MPxDeformerNode::initialize()
{
    try
    {
        // Although some do, I don't advice you call makePaintable here.
    }
    catch (std::exception& e)
    {
        maya_print_error( e );
        return MS::kFailure;
    }
    return MStatus::kSuccess;
}

// --------------------------------------

MStatus My_custom_MPxDeformerNode::deform(
        MDataBlock& block,
        MItGeometry& geom_it,
        const MMatrix& object_matrix,
        unsigned int mesh_index)// index of the geometry
{
    try
    {
        MStatus status;        

        while( !geom_it.isDone() )
        {
            int vidx = geom_it.index(&status);
            mayaCheck(status);            

            // Store vertex position in global space:
            MPoint point = geom_it.position() * object_matrix;
            _vertex_buffer[vidx] = point;
            
            // Access and store associated weight map value:
            float f = weightValue(block, mesh_index, vidx );
            _weight_buffer[vidx] = f;
            geom_it.next();
        }

        // Do something:
        process_vertices(_vertex_buffer, _weight_buffer);

        MMatrix object_inv_matrix = object_matrix.inverse();
        mayaCheck( geom_it.reset() );
        while( !geom_it.isDone() )
        {    
            int vidx = geom_it.index(&status);
            mayaCheck(status);   
        
            MPoint point = geom_it.position() * object_matrix;
            double t = double(envelope_val);
            MPoint new_point = _vertex_buffer[vidx] * t + point * (1.0-t);
            geom_it.setPosition(new_point * object_inv_matrix);
            geom_it.next();
        }

    }
    catch (std::exception& e)
    {
        maya_print_error( e );
        return MS::kFailure;
    }

    return MStatus::kSuccess;
}

/// @brief first function executed by Maya when loading the plugin
MStatus initializePlugin(MObject obj)
{
    MStatus result;

    try
    {
        MFnPlugin plugin(obj, "My corpo", "1.0.0", "Any");

        mayaCheck( plugin.registerNode(
            "YourNodeName",
            0x000020,
            &My_custom_MPxDeformerNode::creator,
            &My_custom_MPxDeformerNode::initialize,
            MPxNode::kDeformerNode
            )
        );


        // -------
        // By default MPxDeformerNode provide per vertex weights through the
        // built in attribute weight. The following makes those weights paintable        
        MString cmd = "makePaintable -attrType multiFloat -sm deformer ";
        cmd += "YourNodeName";
        cmd += " weights;";
        MGlobal::executeCommand(cmd, true);

    }
    catch (std::exception& e)
    {
        maya_print_error(e);
        return MS::kFailure;
    }
    return result;
}

Paint inside Maya

To switch to paint mode in maya, select the mesh create the deformer and call the mel procedure

ArtPaintAttrTool();

Or right click on the mesh and select the available weight map:

Adding custom weight maps to MPxDeformerNode

In addition to the built-in attribute .weightList[mesh_index].weights[vertex index] you can define your own weight maps. To this end you will need to define an array of MFnCompoundAttribute (one for each input geometry) which child will be an MFnNumericAttribute holding the per vertex float values. In the example below we define the custom attributes:

.perGeometry[mesh_index].smoothMap[vertex index]

#include <maya/MPxDeformerNode.h>

class WeightMapHolder : public MPxDeformerNode {
public:   

    static MObject _s_smoothMap;
    static MObject _s_perGeometry;

    static const MTypeId _s_id;
    static const MString _s_name;

    static void* creator();
    static MStatus initialize();

    // Manually call this to allocate the weight maps after the creation
    // of this node (you may do this once in 
    // WeightMapHolder::deform() as well)
    void allocate_weights(int nb_vertices);

         
    MStatus deform(MDataBlock& block,
        MItGeometry& iter,
        const MMatrix& mat,
        unsigned int multiIndex) override;



};

#include <maya/MFnNumericAttribute.h>
#include <maya/MFnCompoundAttribute.h>
#include <maya/MGlobal.h>
#include <maya/MItGeometry.h>
#include <maya/MFnDependencyNode.h>

#include "toolbox_maya/utils/maya_error.hpp"

const MTypeId WeightMapHolder::_s_id(0x0000000a);
const MString WeightMapHolder::_s_name("WeightMapHolder");

MObject WeightMapHolder::_s_smoothMap;
MObject WeightMapHolder::_s_perGeometry;

// -----------------------------------------------------------------------------

void* WeightMapHolder::creator()
{
    return new WeightMapHolder();
}

// -----------------------------------------------------------------------------

MStatus WeightMapHolder::initialize()
{
    try{

        MStatus status;
        MFnNumericAttribute nAttr;
        _s_smoothMap = nAttr.create("smoothMap", "smoothMap", MFnNumericData::kFloat, 1.0, &status);
        mayaCheck( status );
        nAttr.setMin(0.0);
        nAttr.setMax(1.0);
        nAttr.setArray(true);        

        MFnCompoundAttribute cAttr;
        _s_perGeometry = cAttr.create("perGeometry", "perGeometry", &status);
        cAttr.setArray(true);
        cAttr.addChild(_s_smoothMap);        
        addAttribute(_s_perGeometry);

        mayaCheck( attributeAffects(_s_smoothMap, outputGeom) );
        mayaCheck( attributeAffects(_s_perGeometry, outputGeom) );

        // To avoid triggering suspicious warnings I prefer not to call makePaintable here
	// instead I do it after the node is registered. (see at the end of the code snippet)
    }
    catch (std::exception& e)
    {
        maya_print_error(e);
        return MS::kFailure;
    }

    return MStatus::kSuccess;
}

// -----------------------------------------------------------------------------

unsigned num_elements(MArrayDataHandle& handle)
{
    MStatus status;
    unsigned num_elements = handle.elementCount(&status);
    mayaCheck( status );
    return num_elements;
}

// -----------------------------------------------------------------------------

float get_float_at(MArrayDataHandle array_handle, int physical_index)
{
    MStatus status;
    mayaCheck( array_handle.jumpToArrayElement(physical_index) );
    MDataHandle item_handle = array_handle.inputValue(&status);
    mayaCheck(status);
    return item_handle.asFloat();
}

// -----------------------------------------------------------------------------

MDataHandle get_handle_at(MArrayDataHandle array_handle, int physical_index)
{
    MStatus status;
    mayaCheck( array_handle.jumpToArrayElement(physical_index) );
    MDataHandle item_handle = array_handle.inputValue(&status);
    mayaCheck(status);
    return item_handle;
}

// -----------------------------------------------------------------------------

MPlug get_plug(const MObject& node, const MObject& attribute)
{
    MStatus status;
    MFnDependencyNode dg_fn ( node );
    MPlug plug = dg_fn.findPlug ( attribute, true, &status );
    mayaCheck(status);
    return plug;
}

// -----------------------------------------------------------------------------

template<typename T>
void set_as(MPlug& plug, float v ){
    static_assert( std::is_same<T, float>::value, "Not float" );
    mayaCheck( plug.setFloat(v) );
}

// -----------------------------------------------------------------------------

MPlug insert_float_at(MPlug& plug_array,
                        unsigned logical_index,
                        float elt)
{
    //mayaAssert( is_array(plug_array) );

    MStatus status;
    MPlug plug = plug_array.elementByLogicalIndex(logical_index, &status);
    mayaCheck(status);
    mayaCheck( plug.setFloat(elt) );
    return plug;
}

// -----------------------------------------------------------------------------

MPlug insert_element_at(MPlug& plug_array, unsigned logical_index)
{
    //mayaAssert( is_array(plug_array) );

    MStatus status;
    MPlug plug = plug_array.elementByLogicalIndex(logical_index, &status);
    mayaCheck(status);
    return plug;
}

// -----------------------------------------------------------------------------

MPlug get_child(const MPlug& plug, MObject attribute)
{
    MStatus status;
    MPlug child = plug.child(attribute, &status);
    mayaCheck(status);
    return child;
}

// -----------------------------------------------------------------------------

void WeightMapHolder::allocate_weights(int nb_vertices)
{
        MPlug per_geom = get_plug(thisMObject(), WeightMapHolder::_s_perGeometry);
        MPlug per_geom_elt = insert_element_at(per_geom, 0);
        MPlug smooth_map = get_child(per_geom_elt, WeightMapHolder::_s_smoothMap);

        // pre-allocate memory
        smooth_map.setNumElements( nb_vertices );

        for( unsigned i = 0; i < nb_vertices; ++i){
            insert_float_at( smooth_map, i, 1.0f);
        }
}

// -----------------------------------------------------------------------------

MStatus WeightMapHolder::deform(MDataBlock& block,
                                  MItGeometry& iter,
                                  const MMatrix& mat,
                                  unsigned int multiIndex)
{
    try{

        MStatus status;
        MArrayDataHandle hGeo = block.inputArrayValue(_s_perGeometry, &status);
        mayaCheck(status);

        if( multiIndex < num_elements(hGeo) )
        {
            MDataHandle hPerGeometry = get_handle_at( hGeo, multiIndex );
            MArrayDataHandle hLocalWeights = hPerGeometry.child(_s_smoothMap);

            MGlobal::displayInfo(MString("nb elements: ") + num_elements(hLocalWeights));


            // slow, you may want to cache the value if you use this:
            //const int nb_verts = iter.exactCount();

            // Alternatively:
            const int nb_verts = num_elements(hLocalWeights);	    
            for (unsigned i = 0; i < nb_verts; ++i)
            {
                float weight = get_float_at( hLocalWeights, i );
                //...
            }

            // Or simply use the iterator:
            int vert_idx = 0;
            for (iter.reset(); !iter.isDone(); iter.next(), vert_idx++) {
                float weight = get_float_at( hLocalWeights, vert_idx );
                //...
            }
            
        }


    }
    catch (std::exception& e)
    {
        maya_print_error(e);
        return MS::kFailure;
    }

    return MStatus::kSuccess;
}

/// @brief first function executed by Maya when loading the plugin
MStatus initializePlugin(MObject obj)
{
    MStatus result;

    try
    {
        MFnPlugin plugin(obj, "My corpo", "1.0.0", "Any");

        mayaCheck( plugin.registerNode(
            WeightMapHolder::_s_name,
            WeightMapHolder::_s_id,
            &WeightMapHolder::creator,
            &WeightMapHolder::initialize,
            MPxNode::kDeformerNode
            )
        );


        // -------
        // Be carefull to use multiDouble or multiFloat appropriatly
        // (i.e. according's to the attribute actual type.)
        mayaCheck( MGlobal::executeCommand(MString("makePaintable -attrType multiFloat -sm deformer ") + WeightMapHolder::_s_name + " smoothMap") );
    }
    catch (std::exception& e)
    {
        maya_print_error(e);
        return MS::kFailure;
    }
    return result;
}

As usual makePaintable Mel command must be called on 'smoothMap' to make it paintable. Once the MPxDeformerNode::initialize() is called and attributes created, you should allocate and initialize .perGeometry[].smoothMap[] according to the input meshes. Finally in the MPxDeformerNode::deform() method, you can access your custom attributes like you would do with any other attribute using dataBlock.

MPxSkinCluster

Be careful as in a MPxSkinCluster, contrary to MPxDeformerNode, the built-in attributes weightList is a completely different thing. In a MPxSkinCluster weightList defines skin weights:

.weightList[vertex_index].weights[joint index]

To correctly access these values refer to my notes on how to implement a custom skin cluster node with C++ API

Moreover the method:

float MPxSkinCluster::weightValue(MDataBlock& , int i, int j);

Remains a mystery. I could not figure out how your are supposed to use it. Outputting values for 'i' and 'j' indices return garbage and does not match any values such as the ones found inweightList.weights

Adding custom weight maps inside a MPxSkinCLuster

As far as I tested this is not possible, maybe it's a bug, I did the exact same thing as with MPxDeformerNode to add custom weight maps (and some variations), but when entering 'paint mode' in Maya's UI it won't paint anything.

Workarounds

Maya 2022 component tags

Maya 2022 introduced component tags: a mesh shape can now define a sub group of vertices that you can paint. In addition you can assign a specific component tag to any built-in deformer as well as custom MPxNodeDeformer. I have not investigated yet how you can efficiently access those sub groups and associated float values of the weight maps inside a MPxDeformer. I'll just leave some notes that may be the basis of a future article. Here is a video on how to use the component tag UI.

A mesh shape now define new attributes to access the "component tags":

getAttr "pSphereShape1Orig.componentTags[1].componentTagName";
// Result: test // 
getAttr "pSphereShape1Orig.componentTags[1].componentTagContents";
// Result: vtx[65:67] vtx[84:86] vtx[103:106] vtx[122:126] vtx[143:146] vtx[163:166] vtx[183:184] vtx[199] vtx[203] vtx[217:219] vtx[237:240] vtx[257:259] vtx[277:279] vtx[297] // 

Built-in and custom deformer are now equipped with new attributes to define which component tag should be used:

getAttr "cluster1.input[0].componentTagExpression";

In the case of a MPxDeformer, I imagine one could possibly read the values of its input mesh shape in order to extract the list of vsub ertices to be deformed. The weightList iteself is still stored on a per deformer basis in the .weightList[mesh_index].weights[vertex index] so the deformer would have to simply have to iterate only over the sub set of vertices defined by the active component tag. As long as the various component tags do not have overlaps it should be fine and the weight map can store each component tage associated sub weight map.

There are falloff functions etc that can be associated to a component tag of a deformer these should be also taken into account into into the deformer. How to get those functions and evaluate them I do not know yet. I think falloff functions are created as new DG nodes and the name of the node is stored in the "Deformer Attributes" section of the deformer...

two comments

Hi Rodolphe!

Thank you so much for writing these tutorials!

Something I have been biting myself over and over, you say: “In addition to the built-in attribute .weightList[mesh_index].weights[vertex index] you can define your own weight maps.”

I am currently incapable to add my own weight map… I would like to create one called “Stiffness” for example.

Could you please provide an example on how to create it and write data in?

Thank you so much for your help,
Vincent
——————
Rodolphe:
Thanks for the kind words :) My pleasure!
I just updated the tutorial to be a tad more complete.

Vincent - 14/08/2022 -- 00:54

Hi Rodolphe!

I recently started developing my own SkinCluster node for Maya, and stumbled on this page while I was looking for info on applying paintable maps on an MPxSkinCluster node.

Just like you, I found that it is seemingly impossible to do that the same way as you would do with an MPxDeformer. However, after investigating this issue, I realized that the MPxSkinCluster class doesn’t extend MPxDeformer (in fact, they both extend MPxGeometryFilter).
One important difference between the two seems to be that skin clusters are not really designed to work on multiple meshes.

As it turned out, it is possible to have a paintable map on an MPxSkinCluster if you get rid of the compound “perGeometry” attribute and all of the logic needed to handle multiple geometries. You would then have to execute “makePaintable -attrType multiFloat CustomSkinClusterName CustomAttributeName” (without the -sm flag), and that will do the trick.

I hope this will help someone!

Take care,
Jacopo

Jacopo - 12/05/2023 -- 21:28
(optional field, I won't disclose or spam but it's necessary to notify you if I respond to your comment)
All html tags except <b> and <i> will be removed from your comment. You can make links by just typing the url or mail-address.
Anti-spam question: