Sometimes we want to use a node as input for our custom nodes, and would like our custom node to be updated whenever our input is changed. Usually, we will create a message attribute with MFnMessageAttribute on our custom node and register callbacks to deal with it. The message attributes will never participate in dirty propagation, it is only for making connections.
For example, sometimes we would like to connect a camera to our custom node and update it with its projection matrix. Unfortunately, projection matrix is only available with API. Thus, we can't connect it directly to our node. To make the projection matrix an input for our node, we can use the camera node as input. I am going to create a node with accepts a message attribute from a cameraShape node and output the projection matrix of the camera.
First, I'll need to create a message attribute, a matrix attribute and a dummy attribute for dirtying our node:
// Create message attribute and matrix attribute here.
MStatus affects::initialize()
{
MFnMatrixAttribute matAttr;
MFnMessageAttribute msgAttr;
MFnNumericAttribute nAttr;
// Use for projection matrix
projectionMatrix = matAttr.create(PROJECTION_MATRIX_LN, PROJECTION_MATRIX_SN, MFnMatrixAttribute::kFloat);
// Message attribute
inputCamera = msgAttr.create(INPUT_CAMERA_LN, INPUT_CAMERA_SN);
// For attributeAffects
dirtyDummy = nAttr.create(DIRTY_DUMMY_LN, DIRTY_DUMMY_SN, MFnNumericData::kBoolean);
addAttribute(projectionMatrix);
addAttribute(inputCamera);
addAttribute(dirtyDummy);
attributeAffects(dirtyDummy, projectionMatrix);
return( MS::kSuccess );
}
The projection matrix is a MFloatMatrix, and it is created with MFnMatrixAttribute. Because we are going to update our node during the callbacks, we'll need to create an attribute and make it affect the output matrix. I created a method called setProjectionMatrixDirty for it:
void affects::setProjectionMatrixDirty()
{
auto dirtyPlug = MPlug(thisMObject(), dirtyDummy);
bool lastValue;
dirtyPlug.getValue(lastValue);
dirtyPlug.setValue(!lastValue);
}
When a plug is connected or disconnected, MFnNode::connectionMade or MFnNode::connectionBroken will be called. We could take advantage of this behavior, and register our callbacks in it. For the cameraShape node itself, we can use MNodeMessage::addNodeDirtyPlugCallback to track it, but it won't work for its transform. Luckily, Maya provides another callback MDagMessage::addWorldMatrixModifiedCallback to track its world matrix. The node itself is provided as custom data for the callbacks, so we have access to both the camera node and our node during the callback. We also need to define those callbacks as friend of our node, unless you make them directly members (cameraNode, projection matrix value, etc...) public.
MPxNode::connectionMade is coded like below
// Create an attribute change callback on the camera when connection is made
MStatus affects::connectionMade( const MPlug& plug, const MPlug& otherPlug, bool asSrc )
{
if(plug == inputCamera)
{
MObject otherNode = otherPlug.node();
// If is from camera.message
// It must be connected from cameraShape, otherwise won't work.
if(otherNode.hasFn(MFn::kCamera))
{
MGlobal::displayInfo("Connection made.");
isCameraSet = true;
cameraNode = otherNode;
MFnCamera fnCam(cameraNode);
projectionMatrixValue = fnCam.projectionMatrix();
setProjectionMatrixDirty();
cameraCallbackId = MNodeMessage::addNodeDirtyPlugCallback(cameraNode, cameraNodePlugDirty, this);
// Add world transform matrix
MSelectionList sel;
sel.add(fnCam.fullPathName());
MDagPath dagPath;
sel.getDagPath(0,dagPath);
worldMatrixCallbackId = MDagMessage::addWorldMatrixModifiedCallback(dagPath,cameraWorldMatrixChangeCallback,this);
return MStatus::kSuccess;
}
}
return MPxNode::connectionMade(plug, otherPlug, asSrc);
}
Then, here is MPxNode::connectionBroken. We need to remove callbacks when the plug is disconnected
// Remove callbacks when connection is broken.
MStatus affects::connectionBroken( const MPlug& plug, const MPlug& otherPlug, bool asSrc )
{
if(plug == inputCamera)
{
MObject otherNode = otherPlug.node();
// If is from cameraShape.message
if(otherNode.hasFn(MFn::kCamera))
{
MGlobal::displayInfo("Connection breaks.");
if(isCameraSet)
{
isCameraSet = false;
cameraNode = otherNode;
MFnCamera fnCam(cameraNode);
projectionMatrixValue = fnCam.projectionMatrix();
setProjectionMatrixDirty();
MNodeMessage::removeCallback(cameraCallbackId);
MDagMessage::removeCallback(worldMatrixCallbackId);
}
return MStatus::kSuccess;
}
}
return MPxNode::connectionBroken(plug, otherPlug, asSrc);
}
Now, we have to write our callbacks. We are going to update the projection matrix attribute in the compute method to have a better performance, and there is one issue we should try to avoid here: calling MFnCamera::projectionMatrix in the compute method. During DG evaluation, it is better not to access the plugs outside the data block. So, getting the projection matrix from the camera shape during the compute method will cause a problem. But there is no such limitation in the callback, so we can get the value and cache it during the callback, then update the projection matrix attribute with the cached value in the callback. The callback is written like below:
// Node plug dirty change callback
void cameraNodePlugDirty(MObject &node, MPlug &plug, void *clientData)
{
affects* affectNode = (affects*)clientData;
if(affectNode->isCameraSet)
{
MGlobal::displayInfo("Updating projection matrix after camera shape changes.");
MFnCamera fnCam(affectNode->cameraNode);
//Cache the projection matrix value here. It will prevent potential DG evaluation issues.
affectNode->projectionMatrixValue = fnCam.projectionMatrix();
affectNode->setProjectionMatrixDirty();
}
}
//World matrix change callback
void cameraWorldMatrixChangeCallback(MObject &transformNode, MDagMessage::MatrixModifiedFlags &modified, void *userData)
{
affects* affectNode = (affects*)userData;
if(affectNode->isCameraSet)
{
MGlobal::displayInfo("Updating projection matrix after transform changes.");
MFnCamera fnCam(affectNode->cameraNode);
//Cache the projection matrix value here. It will prevent potential DG evaluation issues.
affectNode->projectionMatrixValue = fnCam.projectionMatrix();
affectNode->setProjectionMatrixDirty();
}
}
And the last part is to provide the compute method:
MStatus affects::compute( const MPlug& plug, MDataBlock& data )
{
if(plug == projectionMatrix)
{
MGlobal::displayInfo("Updating projection matrix in compute");
auto handle = data.outputValue(projectionMatrix);
handle.setMFloatMatrix(projectionMatrixValue);
handle.setClean();
}
return MStatus::kSuccess;
}
The full sample is available on the GitHub, you can get it here.
Comments
You can follow this conversation by subscribing to the comment feed for this post.