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
- This old Maya plugin implements Dual Quaternion Skinning and Linear Blending Skinning with MPxNode
No comments