Maya C++ API: write a custom Linear Blending Skinning node

To write your custom LBS (Linear Blending Skinning) skin cluster deformer you need to extend the interface MPxSkinCluster. It provides all the attributes to write a LBS node deformer. Maya documentation gives some sample code to reproduce LBS unfortunately this code is partially wrong so I had to re-write it. One problem was it would not support sparse skin weights (they mixed up physical and logical indices when looking up skin weights and joint matrices). Anyhow this it the better example:

MyCustomSkinCluster.hpp

#pragma once

#include <maya/MTypeId.h>
#include <maya/MPxSkinCluster.h>

class MyCustomSkinCluster : public MPxSkinCluster {
public:
    static const MTypeId _s_id;
    static void* creator();
    static MStatus initialize();

    // Deformation function    
    MStatus deform(MDataBlock& block,
        MItGeometry& iter,
        const MMatrix& mat,
        unsigned int multi_index) override;
};

MyCustomSkinCluster.cpp

#include "MyCustomSkinCluster.h"

#include <maya/MFnMatrixData.h>
#include <maya/MItGeometry.h>
#include <maya/MMatrix.h>
#include <maya/MPoint.h>

const MTypeId MyCustomSkinCluster::_s_id(0x00A0B7C8);

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

MStatus MyCustomSkinCluster::initialize(){ return MStatus::kSuccess; }
void* MyCustomSkinCluster::creator(){ return new MyCustomSkinCluster(); }

MStatus MyCustomSkinCluster::deform(MDataBlock& block,
                                    MItGeometry& iter,
                                    const MMatrix& worldMat,
                                    unsigned int /*multiIndex*/)
{

    // get the influence transforms
    MArrayDataHandle transformsHandle = block.inputArrayValue(matrix);
    int nb_joints = transformsHandle.elementCount();
    if (nb_joints == 0) {
        return MS::kSuccess;
    }

    MArrayDataHandle bindHandle = block.inputArrayValue(bindPreMatrix);

    // LBS
    MArrayDataHandle weightListHandle = block.inputArrayValue( weightList );
    if ( weightListHandle.elementCount() == 0 ) {
        // no weights - nothing to do
        return MS::kSuccess;
    }

    const MMatrix worldMatInverse = worldMat.inverse();
    for ( iter.reset(); !iter.isDone(); iter.next())
    {
        MPoint pt = iter.position() * worldMat;
        MPoint skinned;
        // get the weights for this point
        MArrayDataHandle weightsHandle = weightListHandle.inputValue().child( weights );
        int nb_weights = weightsHandle.elementCount();
        for (int i = 0; i < nb_weights; i++)
        {
            weightsHandle.jumpToArrayElement(i);
            double w = weightsHandle.inputValue().asDouble();

            // logical index represent the actuall joint index
            int joint_idx = weightsHandle.elementIndex();

            transformsHandle.jumpToElement( joint_idx ); // Jump to logical index
            MMatrix mat = MFnMatrixData(transformsHandle.inputValue().data()).matrix();

            bindHandle.jumpToElement( joint_idx ); // Jump to logical index
            MMatrix preBindMatrix = MFnMatrixData( bindHandle.inputValue().data() ).matrix();
            mat = preBindMatrix * mat;

            skinned += ( pt * mat ) * w;
        }

        // Set the final position.
        iter.setPosition( skinned * worldMatInverse );
        // advance the weight list handle
        weightListHandle.next();
    }

    return MS::kSuccess;
}

main.cpp

MStatus initializePlugin( MObject obj )
{
    MStatus result;
    MFnPlugin plugin( obj, PLUGIN_COMPANY, "3.0", "Any");
    result = plugin.registerNode(
        "basicSkinCluster" ,
        MyCustomSkinCluster::id ,
        &MyCustomSkinCluster::creator ,
        &MyCustomSkinCluster::initialize ,
        MPxNode::kSkinCluster
        );
    return result;
}
MStatus uninitializePlugin( MObject obj )
{
    MStatus result;
    MFnPlugin plugin( obj );
    result = plugin.deregisterNode( basicSkinCluster::id );
    return result;
}

Convert SkinCluster to our Custom MPxSkinCluster

Below are the MEL procedures you would use to convert some existing rig to your MPxSkinCluster, this will transfer the connections and necessary values of a SkinCluster's attribute to our custom node.

// Functions such as get_selected_meshes(), find_skin_cluster_from_mesh() are 
// utilities I define below

proc connectJointCluster( string $jointName,
                          int $jointIndex,
                          string $srcSkinCluster,
                          string $dstSkinCluster )
{
    int $i = $jointIndex;
    print("joint: "+$i+" : "+$jointName+"\n" );
    if ( !objExists( $jointName+".lockInfluenceWeights" ) )
    {
        connectAttr ($jointName+".liw") ($dstSkinCluster + ".lockWeights["+$i+"]");
    }

    connectAttr ($jointName+".worldMatrix[0]") ($dstSkinCluster + ".matrix["+$i+"]");
    connectAttr ($jointName+".objectColorRGB") ($dstSkinCluster + ".influenceColor["+$i+"]");

    float $m[] = `getAttr ($jointName+".wim")`;
    // todo prefer using this instead:
    //float $m[] = `getAttr ($srcSkinCluster + ".bindPreMatrix["+$i+"]")`;


    setAttr ($dstSkinCluster + ".bindPreMatrix["+$i+"]") -type "matrix" $m[0] $m[1] $m[2] $m[3] $m[4] $m[5] $m[6] $m[7] $m[8] $m[9] $m[10] $m[11] $m[12] $m[13] $m[14] $m[15];
}

global proc ConvertToCustomSkinCluster()
{
    string $mesh_list[] = get_selected_meshes();
    if( size( $mesh_list ) < 1 ){
        return;
    }

    // Last selected mesh comes first in the list
    string $mesh = $mesh_list[0]; 
    string $cluster = find_skin_cluster_from_mesh( get_transform($mesh) );

    if( $cluster == "" ){
        return;
    }

    // Get joints influencing the mesh:
    int $joint_indices[] = `getAttr -multiIndices ($cluster+".matrix")`;
    string $joint_names[] = `listConnections ($cluster+".matrix")`;

    // Create custom skin cluster:
    string $deformers[] = `deformer -type "basicSkinCluster"`;
    string $customSkinCluster = $deformers[0];

    // Link joints and copy bind pose matrices from the source skin cluster:
    for( $i = 0; $i < size($joint_indices); ++$i) {
        int $j_idx = $joint_indices[$i];
        string $j_name = $joint_names[$i];
        connectJointCluster( $j_name, $j_idx , $cluster, $customSkinCluster );
    }

    // connect skin weights to force copy:
    connectAttr ($cluster+".weightList") ($customSkinCluster+".weightList");

    delete $cluster;
}

ConvertToCustomSkinCluster();

global proc string[] filter_by_type( string $type, string $objects[] )
{
    string $new_list[];
    for ($node in $objects) {
        if( $node != ""){
            if( nodeType( $node ) == $type ){
                $new_list[size($new_list)] = $node;
            }
        }
    }
    return $new_list;
}


/// @true if '$elt' is present in '$array'
global proc int exists_s(string $array[], string $elt) { 
    return stringArrayContains($elt, $array); 
}

global proc push_s(string $array[], string $elt){ 
    $array[ size($array) ] = $elt; 
}

/// Add '$elt' only if it doesn't exist in '$array'
global proc push_unique_s(string $array[], string $elt){ 
    if( !exists_s( $array, $elt ) ){ 
        push_s($array, $elt); 
    } 
}

global proc string[] get_shapes( string $xform[] )
{
    string $shapes[];
    for ($node in $xform) {
        $shapes[size($shapes)] = get_shape($node);
    }
    return $shapes;
}

global proc string get_shape( string $xform )
{
    string $shape;
    if ( "transform" == `nodeType $xform` ) {
        string $parents[] = `listRelatives -fullPath -shapes $xform`;
        if( size($parents) > 0) {
            $shape = $parents[0];
        }
    } else {
        // Assume it's already a shape;
        $shape = $xform;
    }

    return $shape;
}

global proc string get_transform(string $shape)
{
    string $transfo;
    if ( "transform" != `nodeType $shape` )
    {
        string $parents[] = `listRelatives -fullPath -parent $shape`;
        if( size($parents) > 0)
            $transfo = $parents[0];
    }else{
        // If given node is already a transform, just pass on through
        $transfo = $shape;
    }
    return $transfo;
}

// @return the list of selected meshes (first element is the active selection
// if any)
global proc string[] get_selected_meshes()
{
    // -objectsOnly because with component selection (vertex, face, etc.) it
    // will return the list of selected components, however, in some cases the
    // shape and tranform are both returned.
    string $sel[] = `ls -selection -objectsOnly -long`;
    // When converting to shape we might have duplicates
    $sel = get_shapes($sel);
    // remove those duplicates:
    string $list[];
    for( $i = 0; $i < size($sel); $i++ ){
        // Notice we reverse the order to garantee the active selection
        // comes first in the list
        push_unique_s($list, $sel[size($sel)-1-$i]);
    }

    return filter_by_type("mesh", $list);
}


///@return empty string if cannot find any skin cluster.
global proc string find_skin_cluster_from_mesh(string $mesh_name)
{
    // Try to walk through the dependency graph to find the first
    // skin cluster(s) starting from the $mesh_name node.
    string $to_visit[] = {get_shape($mesh_name)};// < our stack of node left to visit.
    int $size = 1; // < our stack pointer.
    string $visited[] = {};
    while($size > 0)
    {
        $size = $size-1;
        string $obj = $to_visit[$size];
        push_s($visited, $obj);
        string $types[] = `nodeType -inherited $obj`;

        if( exists_s($types, "skinCluster") )
        {
            return $obj;
        }
        else
        {
            string $list_objs[] = `listConnections -source true -destination false -plugs false $obj`;
            for($obj in $list_objs)
            {
                if( !exists_s( $visited, $obj )  ){
                    $to_visit[$size] = $obj;
                    $size = $size + 1;
                }
            }
        }
    }

    return "";
}

Reference

No comments

(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: