Search code examples
pythonmayamaya-api

Maya MPxNode multiple outputs


I'm trying to create an MPxNode with multiple outputs, but I can only get one to work properly. The other output doesn't set properly after connecting the node and during undos.

Is it possible to set both outputs at the same time in compute like how I'm trying to? It does work if I change the first line in compute to if plug != self.output1 and plug != self.output2, but that means it would calculate twice which is a waste of memory. And you could imagine how bad this would be if there were even more outputs.

I managed to minimize the code to this simple example. I'm scripting it in Python on Maya 2018:

import maya.OpenMayaMPx as OpenMayaMPx
import maya.OpenMaya as OpenMaya


class MyAwesomeNode(OpenMayaMPx.MPxNode):

    # Define node properties.
    kname = "myAwesomeNode"
    kplugin_id = OpenMaya.MTypeId(0x90000005)

    # Define node attributes.
    in_val = OpenMaya.MObject()
    output1 = OpenMaya.MObject()
    output2 = OpenMaya.MObject()

    def __init__(self):
        OpenMayaMPx.MPxNode.__init__(self)

    def compute(self, plug, data):
        # Only operate on output1 attribute.
        if plug != self.output1:
            return OpenMaya.kUnknownParameter

        # Get input value.
        val = data.inputValue(MyAwesomeNode.in_val).asFloat()

        # Set output 2.
        # This fails when setting up the node and during undos.
        out_plug_2 = data.outputValue(self.output2)
        if val > 0:
            out_plug_2.setFloat(1)
        else:
            out_plug_2.setFloat(0)
        out_plug_2.setClean()

        # Set output 1.
        # This works as expected.
        out_plug_1 = data.outputValue(self.output1)
        out_plug_1.setFloat(val)
        out_plug_1.setClean()

        data.setClean(plug)

        return True


def creator():
    return OpenMayaMPx.asMPxPtr(MyAwesomeNode())


def initialize():
    nattr = OpenMaya.MFnNumericAttribute()

    MyAwesomeNode.output2 = nattr.create("output2", "output2", OpenMaya.MFnNumericData.kFloat)
    nattr.setWritable(False)
    nattr.setStorable(False)
    MyAwesomeNode.addAttribute(MyAwesomeNode.output2)

    MyAwesomeNode.output1 = nattr.create("output1", "output1", OpenMaya.MFnNumericData.kFloat)
    nattr.setWritable(False)
    nattr.setStorable(False)
    MyAwesomeNode.addAttribute(MyAwesomeNode.output1)

    MyAwesomeNode.in_val = nattr.create("input", "input", OpenMaya.MFnNumericData.kFloat, 1)
    nattr.setKeyable(True)
    MyAwesomeNode.addAttribute(MyAwesomeNode.in_val)
    MyAwesomeNode.attributeAffects(MyAwesomeNode.in_val, MyAwesomeNode.output2)
    MyAwesomeNode.attributeAffects(MyAwesomeNode.in_val, MyAwesomeNode.output1)


def initializePlugin(obj):
    plugin = OpenMayaMPx.MFnPlugin(obj, "Me", "1.0", "Any")
    try:
        plugin.registerNode(MyAwesomeNode.kname, MyAwesomeNode.kplugin_id, creator, initialize)
    except:
        raise RuntimeError, "Failed to register node: '{}'".format(MyAwesomeNode.kname)


def uninitializePlugin(obj):
    plugin = OpenMayaMPx.MFnPlugin(obj)
    try:
        plugin.deregisterNode(MyAwesomeNode.kplugin_id)
    except:
        raise RuntimeError, "Failed to register node: '{}'".format(MyAwesomeNode.kname)


# Example usage of node
if __name__ == "__main__":
    import maya.cmds as cmds

    cmds.createNode("transform", name="result")
    cmds.setAttr("result.displayLocalAxis", True)

    cmds.createNode("myAwesomeNode", name="myAwesomeNode")
    cmds.connectAttr("myAwesomeNode.output1", "result.translateX")

    # This output fails.
    cmds.polyCube(name="cube")
    cmds.setAttr("cube.translate", 0, 3, 0)
    cmds.connectAttr("myAwesomeNode.output2", "cube.scaleX")
    cmds.connectAttr("myAwesomeNode.output2", "cube.scaleY")
    cmds.connectAttr("myAwesomeNode.output2", "cube.scaleZ")

Solution

  • I have a solution which is working as expected. All outputs still must go through compute() but only one output will do the actual heavy calculations.

    When going through compute, it checks all output plugs if any are clean. If all are dirty, then we need to re-calculate, otherwise if we do find one clean plug we can just use the cached values we saved earlier.

    Here's an example:

    import maya.OpenMayaMPx as OpenMayaMPx
    import maya.OpenMaya as OpenMaya
    
    
    class MyAwesomeNode(OpenMayaMPx.MPxNode):
    
        # Define node properties.
        kname = "myAwesomeNode"
        kplugin_id = OpenMaya.MTypeId(0x90000005)
    
        # Define node attributes.
        in_val = OpenMaya.MObject()
        output1 = OpenMaya.MObject()
        output2 = OpenMaya.MObject()
    
        def __init__(self):
            OpenMayaMPx.MPxNode.__init__(self)
    
            # Store value here.
            self.cached_value = 0
    
        def compute(self, plug, data):
            # Include all outputs here.
            if plug != self.output1 and plug != self.output2:
                return OpenMaya.kUnknownParameter
    
            # Get plugs.
            val = data.inputValue(MyAwesomeNode.in_val).asFloat()
            out_plug_1 = data.outputValue(self.output1)
            out_plug_2 = data.outputValue(self.output2)
    
            dep_node = OpenMaya.MFnDependencyNode(self.thisMObject())
    
            # Determine if this output needs to recalculate or simply use cached values.
            use_cache_values = False
    
            for name in ["output1", "output2"]:
                mplug = dep_node.findPlug(name)
                if data.isClean(mplug):
                    # If we find a clean plug then just use cached values.
                    use_cache_values = True
                    break
    
            if use_cache_values:
                # Use cached value.
                value = self.cached_value
            else:
                # Calculate value.
                # We potentially can make big computations here.
                self.cached_value = val
                value = val
    
            # Set output 1.
            if plug == self.output1:
                out_plug_1.setFloat(value)
                out_plug_1.setClean()
    
            # Set output 2.
            if plug == self.output2:
                if value > 0:
                    out_plug_2.setFloat(1)
                else:
                    out_plug_2.setFloat(0)
                out_plug_2.setClean()
    
            data.setClean(plug)
    
            return True
    
    
    def creator():
        return OpenMayaMPx.asMPxPtr(MyAwesomeNode())
    
    
    def initialize():
        nattr = OpenMaya.MFnNumericAttribute()
    
        MyAwesomeNode.output2 = nattr.create("output2", "output2", OpenMaya.MFnNumericData.kFloat)
        nattr.setWritable(False)
        nattr.setStorable(False)
        MyAwesomeNode.addAttribute(MyAwesomeNode.output2)
    
        MyAwesomeNode.output1 = nattr.create("output1", "output1", OpenMaya.MFnNumericData.kFloat)
        nattr.setWritable(False)
        nattr.setStorable(False)
        MyAwesomeNode.addAttribute(MyAwesomeNode.output1)
    
        MyAwesomeNode.in_val = nattr.create("input", "input", OpenMaya.MFnNumericData.kFloat, -1)
        nattr.setKeyable(True)
        MyAwesomeNode.addAttribute(MyAwesomeNode.in_val)
    
        # Include both outputs.
        MyAwesomeNode.attributeAffects(MyAwesomeNode.in_val, MyAwesomeNode.output1)
        MyAwesomeNode.attributeAffects(MyAwesomeNode.in_val, MyAwesomeNode.output2)
    
    
    def initializePlugin(obj):
        plugin = OpenMayaMPx.MFnPlugin(obj, "Me", "1.0", "Any")
        try:
            plugin.registerNode(MyAwesomeNode.kname, MyAwesomeNode.kplugin_id, creator, initialize)
        except:
            raise RuntimeError, "Failed to register node: '{}'".format(MyAwesomeNode.kname)
    
    
    def uninitializePlugin(obj):
        plugin = OpenMayaMPx.MFnPlugin(obj)
        try:
            plugin.deregisterNode(MyAwesomeNode.kplugin_id)
        except:
            raise RuntimeError, "Failed to register node: '{}'".format(MyAwesomeNode.kname)
    
    
    # Example usage of node
    if __name__ == "__main__":
        import maya.cmds as cmds
    
        cmds.createNode("transform", name="result")
        cmds.setAttr("result.displayLocalAxis", True)
    
        cmds.createNode("myAwesomeNode", name="myAwesomeNode")
        cmds.connectAttr("myAwesomeNode.output1", "result.translateX")
    
        # This output fails.
        cmds.polyCube(name="cube")
        cmds.setAttr("cube.translate", 0, 3, 0)
        cmds.connectAttr("myAwesomeNode.output2", "cube.scaleX")
        cmds.connectAttr("myAwesomeNode.output2", "cube.scaleY")
        cmds.connectAttr("myAwesomeNode.output2", "cube.scaleZ")
    

    The outputs seem to be reacting ok when re-opening the file, when importing it to a new scene, and referencing it. I just need to transfer the same idea to c++ then it'll be golden.