[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
- Keep MPxSkinCLuster but instead of trying to add weight maps to it, define an additional MPxDeformerNode that contains your custom weight maps.
Keep
MPxDeformerNode::deform()
empty and apply this dummy deformer to your mesh, connect the weight map attributes to your MPxSkinCLuster and use them to customize the deformation insideMPxSkinCLuster::deform()
, you will be able to paint the weight map as usual. - Hijack the 'blendWeights' attribute, originally here to interpolate between LBS and DQS, I don't recommend.
- Define a MPxDeformerNode with all the necessary attributes to reproduce a skin cluster by hand, this is quite heavy duty
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!
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
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 - 14/08/2022 -- 00:54Vincent
——————
Rodolphe:
Thanks for the kind words :) My pleasure!
I just updated the tutorial to be a tad more complete.