[PYTHON] [Maya] Write custom nodes in Open Maya 2.0

It's easy to forget the workflow of implementing a custom node in Maya, so I'll write it as a reminder.

This time, I will present a node with simple input / output, a node that can handle arrays, and a node that can handle compound attributes, and finally use it to create a node that outputs the center positions of multiple points.

For custom nodes (dependency nodes, nodes with input / output), refer to the following official pages. Basics of Dependency Graph Plugin Dependency Graph Plugin

Elements required for scripts that define dependency nodes

At least in the script

  1. An empty function that tells you to use Maya Python API 2.0 If you define a function called maya_useNewAPI (), you can clearly indicate that you will use API 2.0 on the Maya side.

  2. Plugin entry point / exit point You need ʻinitializePlugin (mobject) and ʻuninitializePlugin (mobject) to be called when loading and terminating the plugin. Here we define ** node name **, ** node ID **, ** the following 4/5 functions **, ** node type **, ** node classification ** (see below).

  3. Create function that returns an instance of a node

  4. Function that initializes node attributes

  5. Node body class

Will be needed.

Node with simple I / O

The sample node that I actually wrote is as follows. It is a node that multiplies the Float value put in the input attribute by the sin function and outputs it from the output attribute. I will explain one by one.

# -*- coding: utf-8 -*-
import maya.api.OpenMaya as om
import maya.api.OpenMayaUI as omui
import math, sys

# Maya API 2.Functions required to use 0
def maya_useNewAPI():
    pass

#Actual class
class sampleNode(om.MPxNode):
    id = om.MTypeId(0x7f001) #Unique ID https://download.autodesk.com/us/maya/2011help/API/class_m_type_id.html
    input = om.MObject()
    output = om.MObject()

    #Method to return an instance
    @staticmethod
    def creator():
        return sampleNode()

    #Method called by Maya at initialization
    #Set attributes
    @staticmethod
    def initialize():
        #Attributes are defined using the create method of a subclass of the MFnAttribute class.
        nAttr = om.MFnNumericAttribute()
        sampleNode.input = nAttr.create(
            'input', 'i', om.MFnNumericData.kFloat, 0.0)
        nAttr.storable = True
        nAttr.writable = True

        nAttr = om.MFnNumericAttribute()
        sampleNode.output = nAttr.create('output', 'o', om.MFnNumericData.kFloat, 0.0)
        nAttr.storable = True
        nAttr.writable = True

        #After defining, execute addAttribute of MPxNode
        sampleNode.addAttribute(sampleNode.input)
        sampleNode.addAttribute(sampleNode.output)
        #Also, set the output to be recalculated when the input is changed.
        sampleNode.attributeAffects( sampleNode.input, sampleNode.output)
        
    #Constructor calls parent constructor
    def __init__(self):
        om.MPxNode.__init__(self)

    #A method called by Maya when the value of an attribute is calculated
    def compute(self, plug, dataBlock):
        if(plug == sampleNode.output):
            dataHandle = dataBlock.inputValue(sampleNode.input)
            inputFloat = dataHandle.asFloat()
            result = math.sin(inputFloat) * 10.0
            outputHandle = dataBlock.outputValue(sampleNode.output)
            outputHandle.setFloat(result)
            dataBlock.setClean(plug)
            
    # http://help.autodesk.com/view/MAYAUL/2016/ENU/
    # api1.0 means MStatus unless you explicitly tell us not to process the plug.kUnknownParameter is not returned
    # api2.At 0, there is no MStatus in the first place, so you can ignore it.

#A function called by Maya that registers a new node
def initializePlugin(obj):
    mplugin = om.MFnPlugin(obj)

    try:
        mplugin.registerNode('sampleNode', sampleNode.id, sampleNode.creator,
                             sampleNode.initialize, om.MPxNode.kDependNode)
    except:
        sys.stderr.write('Faled to register node: %s' % 'sampleNode')
        raise

#Functions called by Maya when exiting the plug-in
def uninitializePlugin(mobject):
    mplugin = om.MFnPlugin(mobject)
    try:
        mplugin.deregisterNode(sampleNode.id)
    except:
        sys.stderr.write('Faled to uninitialize node: %s' % 'sampleNode')
        raise

Definition of classes for nodes

First from the class definition

class sampleNode(om.MPxNode):
    id = om.MTypeId(0x7f001) #Unique ID https://download.autodesk.com/us/maya/2011help/API/class_m_type_id.html
    input = om.MObject()
    output = om.MObject()

Decide the id of the plug-in. For more information, see Autodesk Site, but usually you can use any value from 0x00000 to 0x7ffff. I will. And I prepared the input / output attributes as the class field ʻintput ʻoutput.

Instance generation function

    #Method to return an instance
    @staticmethod
    def creator():
        return sampleNode()

A function that instantiates a node. You can write it outside the class, but this time I wrote it as a static method of the class.

Function to initialize the attribute

#Method called by Maya at initialization
    #Set attributes
    @staticmethod
    def initialize():
        #Attributes are defined using the create method of a subclass of the MFnAttribute class.
        nAttr = om.MFnNumericAttribute()
        sampleNode.input = nAttr.create(
            'input', 'i', om.MFnNumericData.kFloat, 0.0)
        nAttr.storable = True
        nAttr.writable = True

        nAttr = om.MFnNumericAttribute()
        sampleNode.output = nAttr.create('output', 'o', om.MFnNumericData.kFloat, 0.0)
        nAttr.storable = True
        nAttr.writable = True

        #After defining, execute addAttribute of MPxNode
        sampleNode.addAttribute(sampleNode.input)
        sampleNode.addAttribute(sampleNode.output)
        #Also, set the output to be recalculated when the input is changed.
        sampleNode.attributeAffects( sampleNode.input, sampleNode.output)

This is the part corresponding to "4. Function that initializes node attributes" in the previous list. You can write it outside the class, but it's cluttered, so I wrote it as a static method. Attributes are defined using the appropriate subclasses of the MFnAttribute class. This time it's a Float value, so I'm using MFnNumericAttribute. Three-dimensional float values (coordinates, etc.) and bool values are also this MFnNumericAttribute. You can find the angle, distance, and time from MFnUnitAttribute, the matrix from MFnMatrixAttribute, and the others from the reference below. MFnAttribute Class Reference OpenMaya.MFnAttribute Class Reference

Specify the attribute name, abbreviation, type, and initial value with nAttr.create. Whether nAttr.storable writes the value of the attribute to the save file. There are other properties such as writable and readable, so set them appropriately.

sampleNode.addAttribute (sampleNode.input) Adds the created attribute to the node. sampleNode.attributeAffects (sampleNode.input, sampleNode.output) When the value of the input attribute changes, the output attribute will be updated.

Calculation body method

    #A method called by Maya when the value of an attribute is calculated
    def compute(self, plug, dataBlock):
        if(plug == sampleNode.output):
            dataHandle = dataBlock.inputValue(sampleNode.input)
            inputFloat = dataHandle.asFloat()
            result = math.sin(inputFloat) * 10.0
            outputHandle = dataBlock.outputValue(sampleNode.output)
            outputHandle.setFloat(result)
            dataBlock.setClean(plug)
            

The compute method is the method that is called when the calculation is done. This node is calculating Sin. The value is passed in the form of a plug. For more information on plugins, see Autodesk Help (https://help.autodesk.com/view/MAYAUL/2016/JPN/?guid=__files_Dependency_graph_plugins_Attributes_and_plugs_htm). It's long because it takes a handle from the DataBlock to get the input and assign it to the output, but it's actually just computing the Sin function.

Entry point

#A function called by Maya that registers a new node
def initializePlugin(obj):
    mplugin = om.MFnPlugin(obj)

    try:
        mplugin.registerNode('sampleNode', sampleNode.id, sampleNode.creator,
                             sampleNode.initialize, om.MPxNode.kDependNode)
    except:
        sys.stderr.write('Faled to register node: %s' % 'sampleNode')
        raise

An entry point outside the class. Specify the node name and ID, the instance method defined in the class, and the node type.

Run

Load the script you created from Maya's Plug-in Manager. Create a node with cmds.createNode ('sampleNode') or cmds.shadingNode ('sampleNode', asUtility = True) on the Maya command line or script editor. If you created it with the latter, your node will be displayed in the tab called "Utilities" in the hypershade window. 無題.png

Array attributes

There are two methods, one is to connect the array data to the plug as one attribute, and the other is to use the ** array plug ** in which the plug itself is an array.

Array plug

Change the definition of the attribute in the ʻinitializemethod earlier as follows.nAttr.array = True is ok, but by setting nAttr.indexMatters = Falseto False, you can use-nextAvailable with the connectAttr` command. On the contrary, if it is True, it seems that the index to be inserted must be specified.

    @staticmethod
    def initialize():
        nAttr = om.MFnNumericAttribute()
        sampleArrayNode.input = nAttr.create(
            'input', 'i', om.MFnNumericData.kFloat, 0.0)
        nAttr.storable = True
        nAttr.writable = True
        nAttr.readable = True
        nAttr.array = True  #add to
        nAttr.indexMatters = False  #add to

Next, the compute method that is responsible for the actual calculation. This time, the process is to get the total value of the input array.

    def compute(self, plug, dataBlock):
        arrayDataHandle = dataBlock.inputArrayValue(
            sampleArrayNode.input
        )
        sum = 0
        while not arrayDataHandle.isDone():
            handle = arrayDataHandle.inputValue()
            v = handle.asFloat()
            sum += v
            arrayDataHandle.next()

        outhandle = dataBlock.outputValue( sampleArrayNode.output )
        outhandle.setFloat(sum)
        dataBlock.setClean(plug)

When using an array plug, get the MArrayDataHandle once with ʻinputArrayValue instead of ʻinputValue. Since this is an iterator, use next () or jumpToLogicalElement () to advance the iterator and ʻarrayDataHandle.inputValue ()` to get the value of the array element. After that, it is converted to a numerical value and calculated in the same way as a normal plug.

01.jpg ↑ The constant 1 + 2 + 3 + 4 = 10 was calculated correctly.

Composite attributes

A composite attribute is a collection of multiple attributes. Complex dynamic attributes In this example, "coordinates and weights" are used as compound attributes, and they are used as array plugs.

It looks like the image below on the node editor. 02.png The implementation of the class part is as follows. The entry point etc. are the same as the previous code (omitted).

class sampleArrayNode(om.MPxNode):
    #Unique ID https://download.autodesk.com/us/maya/2011help/API/class_m_type_id.html
    id = om.MTypeId(0x7f011)
    input = om.MObject()
    output = om.MObject()
    #Child attributes
    position = om.MObject()
    weight = om.MObject()

    #Method to return an instance
    @staticmethod
    def creator():
        return sampleArrayNode()

    #Method called by Maya at initialization
    #Set attributes
    @staticmethod
    def initialize():
        #Child attributes
        #Coordinate
        nAttr = om.MFnNumericAttribute()
        sampleArrayNode.position = nAttr.create(
            'position', 'pos', om.MFnNumericData.k3Float, 0
        )
        nAttr.readable = True
        #weight
        nAttr = om.MFnNumericAttribute()
        sampleArrayNode.weight = nAttr.create(
            'weight', 'w', om.MFnNumericData.kFloat, 1
        )
        nAttr.readable = True
        nAttr.setMax(1)  # Min,Max can also be specified
        nAttr.setMin(0)

        #Composite attributes
        nAttr = om.MFnCompoundAttribute()
        sampleArrayNode.input = nAttr.create(
            'input', 'i')
        nAttr.readable = True
        nAttr.array = True
        nAttr.indexMatters = False
        nAttr.addChild(sampleArrayNode.position)
        nAttr.addChild(sampleArrayNode.weight)

        #The output is coordinates this time (3D Float)
        nAttr = om.MFnNumericAttribute()
        sampleArrayNode.output = nAttr.create(
            'output', 'o', om.MFnNumericData.k3Float)
        nAttr.storable = True
        nAttr.writable = True
        nAttr.readable = True

        #After defining, execute addAttribute of MPxNode
        sampleArrayNode.addAttribute(sampleArrayNode.input)
        sampleArrayNode.addAttribute(sampleArrayNode.output)
        #Also, set the output to be recalculated when the input is changed.
        sampleArrayNode.attributeAffects(
            sampleArrayNode.input, sampleArrayNode.output)

    #Constructor calls parent constructor
    def __init__(self):
        om.MPxNode.__init__(self)

    #A method called by Maya when the value of an attribute is calculated
    def compute(self, plug, dataBlock):
        arrayDataHandle = dataBlock.inputArrayValue(
            sampleArrayNode.input
        )
        sumX = 0
        sumY = 0
        sumZ = 0
        num = len(arrayDataHandle)
        while not arrayDataHandle.isDone():
            #Data handle for composite attributes
            dataHandle = arrayDataHandle.inputValue()
            # .You can get child attributes with child
            childHandle = dataHandle.child(
                sampleArrayNode.position
            )
            pos = childHandle.asFloat3()
            childHandle = dataHandle.child(
                sampleArrayNode.weight
            )
            w = childHandle.asFloat()
            sumX += pos[0] * w
            sumY += pos[1] * w
            sumZ += pos[2] * w
            arrayDataHandle.next()

        outhandle = dataBlock.outputValue(sampleArrayNode.output)
        if(num != 0):
            outhandle.set3Float(sumX / num, sumY / num, sumZ / num)
        else:
            outhandle.set3Float(0, 0, 0)
        dataBlock.setClean(plug)

    # http://help.autodesk.com/view/MAYAUL/2016/ENU/
    # api1.0 means MStatus unless you explicitly tell us not to process the plug.kUnknownParameter is not returned
    # api2.At 0, there is no MStatus in the first place, so you can ignore it.

#A function called by Maya that registers a new node

What has changed is that the variables position and weight are prepared as class fields. Since these are used as child attributes of compound attributes, they are defined in the ʻinitialize method in the same way as normal attributes. The composite attribute that puts these together is ʻinput.

#Composite attributes
        nAttr = om.MFnCompoundAttribute()
        sampleArrayNode.input = nAttr.create(
            'input', 'i')
        nAttr.readable = True
        nAttr.array = True
        nAttr.indexMatters = False
        nAttr.addChild(sampleArrayNode.position) #← This is the point
        nAttr.addChild(sampleArrayNode.weight)

The difference from normal attributes is that the class of the attribute is MFnCompoundAttribute and the child attribute defined above in .addChild is added.

To use a composite attribute within a compute method

            #Data handle for composite attributes
            dataHandle = arrayDataHandle.inputValue()
            # .You can get child attributes with child
            childHandle = dataHandle.child(
                sampleArrayNode.position
            )
            pos = childHandle.asFloat3()
            childHandle = dataHandle.child(
                sampleArrayNode.weight
            )
            w = childHandle.asFloat()

Use the .child method from the composite attribute data handle to get and access the child attribute data handle.

Actual use

When you actually use the created node, it looks like this. 04.png Connect the positions of multiple objects to a node. Try connecting the output to the locator. 03.jpg The locator has moved to the center of the sphere, cone, and cube. This time, I added a weight value in addition to the coordinates as a compound attribute, so let's use it. 05.jpg If you lower the weight of the sphere ... 06.jpg The influence of the sphere has disappeared and the locator has moved to the center of the cone and cube.

Recommended Posts

[Maya] Write custom nodes in Open Maya 2.0
Maya | Get parent nodes in sequence
Write Pulumi in Go
Write DCGAN in Keras
Write decorator in class
mayapy --Python in Maya
Write Python in MySQL
Custom sort in Python3
How to write custom validations in the Django REST Framework
Write Pandoc filters in Python
Write beta distribution in Python
Write python in Rstudio (reticulate)
Write Spigot in VS Code
Write data in HDF format
List of nodes in diagrams
Write Spider tests in Scrapy