{ "cells": [ { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ " # Creating Material Graphs\n", "\n", "The following topics will be covered in this book:\n", "1. Creating a node graph container.\n", "2. Creating container input and output interfaces.\n", "3. Creating nodes in a graph.\n", "4. Connecting nodes in a graph.\n", "5. Creating a material and connecting the graph to the material.\n", "\n", "At the end of this book, a simple shader graph will have been created. \n", "\n", "The utilities used in this tutorial are available in the `mtlxutils` file: mtlxutls/mxnodegraph.py for reuse." ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Setup\n", "\n", "The following pre-requisite setup steps need to performed first:\n", "* Load MaterialX\n", "* Creating a working document\n", "* Loading in the standard library definitions\n", "* Setting up a predicate to filter definitions on write." ] }, { "cell_type": "code", "execution_count": 2384, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "MaterialX version 1.39.0. Loaded 743 standard library definitions\n" ] } ], "source": [ "import MaterialX as mx\n", "\n", "# Version check\n", "from mtlxutils.mxbase import *\n", "haveVersion1387 = haveVersion(1, 38, 7) \n", "if not haveVersion1387:\n", " print(\"** Warning: Minimum version is 1.38.7 for tutorials. Have version: \", mx.__version__)\n", "\n", "stdlib = mx.createDocument()\n", "searchPath = mx.getDefaultDataSearchPath()\n", "libraryFolders = mx.getDefaultDataLibraryFolders()\n", "try:\n", " libFiles = mx.loadLibraries(libraryFolders, searchPath, stdlib)\n", " print('MaterialX version %s. Loaded %d standard library definitions' % (mx.__version__, len(stdlib.getNodeDefs())))\n", "except mx.Exception as err:\n", " print('Failed to load standard library definitions: \"', err, '\"')\n", "\n", "doc = mx.createDocument()\n", "doc.importLibrary(stdlib)\n", "\n", "# Write predicate\n", "def skipLibraryElement(elem):\n", " return not elem.hasSourceUri()\n", "\n", "def validateDocument(doc):\n", " valid, errors = doc.validate()\n", " if not valid:\n", " print('> Document is not valid')\n", " print('> ' + errors)\n", " else:\n", " print('> Document is valid')" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "# Creating a Node Graph\n", "\n", "## Create `` Container\n", "The first step to creating a useful node graph is to create the parent container (`NodeGraph`).\n", "The interface `addNodeGraph()` can be used to do so. \n", "\n", "As with documents, all children must be uniquely named. Name generation of child names uses the\n", "`createValidChildName()` interface which can be used for documents, nodes, and node graphs. \n" ] }, { "cell_type": "code", "execution_count": 2385, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Created nodegraph: \n", "\n" ] } ], "source": [ "def addNodeGraph(parent, name):\n", " \"\"\"\n", " Add named nodegraph under parent\n", " \"\"\"\n", " # Create a uniquely named node graph container under the parent document\n", " childName = parent.createValidChildName(name)\n", " \n", " # Create the node graph\n", " nodegraph = parent.addChildOfCategory('nodegraph', childName)\n", " return nodegraph\n", "\n", "nodeGraph = addNodeGraph(doc,\"test_nodegraph\")\n", "if nodeGraph:\n", " print('Created nodegraph:', mx.prettyPrint(nodeGraph)) " ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Creating Output Interfaces\n", "\n", "A node graph container without any outputs (`Output`) isn't of much use as no data flow can occur.\n", "Thus, at a minimum a `NodeGraph`s should create at least one child output. \n", "This can be done using the `addOutput()` interface on a `NodeGraph`. \n", "\n", "The same considerations should be given for creating an output for nodes. Namely:\n", "* a unique name\n", "* a proper type \n", "should be used. \n", "\n", "In this case we want to create a graph which outputs a `surfaceshader`." ] }, { "cell_type": "code", "execution_count": 2386, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", " \n", "\n" ] } ], "source": [ "def addNodeGraphOutput(parent, type, name='out'):\n", " \"\"\"\n", " Create an output with a unique name and proper type\n", " \"\"\"\n", " if not parent.isA(mx.NodeGraph):\n", " return None\n", " \n", " newOutput = None\n", " childName = parent.createValidChildName(name)\n", " newOutput = parent.addOutput(childName, type)\n", " return newOutput\n", "\n", "type = 'surfaceshader'\n", "graphOutput = addNodeGraphOutput(nodeGraph, type)\n", "\n", "# Print the graph\n", "text = mx.prettyPrint(nodeGraph)\n", "print(text + '')" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Note that we are using `getNamePath()` to check parent / child relationships. \n", "\n", "The path string (`test_nodegraph/out`) indicates that the new output has been correctly added as a child under the node graph container `test_nodegraph`. (where `/` is the parent/child path separator) " ] }, { "cell_type": "code", "execution_count": 2387, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Path to output is: \"test_nodegraph/out\"\n" ] } ], "source": [ "# Examine the path to the output\n", "print('Path to output is: \"%s\"' % graphOutput.getNamePath())" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Creating Graph Nodes\n", "\n", "Nodes can now be created to add logic to the graph.\n", "\n", "The basics book demonstrates how to create nodes as direct children of a `Document`.\n", "The same interfaces are reused here, with the key difference being that the\n", "they are created with respect to a `NodeGraph` instead of the `Document`.\n", "\n", "That is, we call `NodeGraph.addNodeInstance()` instead of `Document.addNodeInstance()` to add\n", "a node under a graph instead of a document.\n", "\n", "A utility called `createNode()` is added for reuse. " ] }, { "cell_type": "code", "execution_count": 2388, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "- Create shader node with path: test_nodegraph/test_shader\n", "- Graph contents:\n", "\n", "\n", " \n", " \n", "\n" ] } ], "source": [ "def createNode(definitionName, parent, name):\n", " \"Utility to create a node under a given parent using a definition name and desired instance name\"\n", " nodeName = parent.createValidChildName(name)\n", " nodedef = doc.getNodeDef(definitionName)\n", " if nodedef:\n", " newNode = parent.addNodeInstance(nodedef, nodeName)\n", " if newNode:\n", " return newNode\n", " else:\n", " print('Cannot find definition:', definitionName)\n", " return None\n", "\n", "shaderNode = createNode('ND_standard_surface_surfaceshader', nodeGraph, 'test_shader')\n", "if shaderNode:\n", " print('- Create shader node with path:', shaderNode.getNamePath())\n", "\n", "# Print contents of graph\n", "print('- Graph contents:\\n')\n", "text = mx.prettyPrint(nodeGraph)\n", "print(text + '')" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Connecting Nodes To Output Interfaces\n", "\n", "To allow output data from the shader node to be accessible the shader node's **output** is connected to the \n", "graph containers **output**.\n", "\n", "A utility called `connectOutputToOutput()` is used to hide the syntactic differences between connecting to an upstream node graph as\n", "opposed to a node, and to check for \"type compatibility\", where \"compatible\" means both ports are of the exact same type. \n", "\n", "Note that only upstream nodes, and graphs can to a downstream output. Inputs cannot be directly connected to an output. A `dot` node\n", "should be used as a pass-through in this case.\n", "\n", "> Unfortunately, adding explicit outputs to nodes is not recommended, otherwise these can be pre-populated on a node to avoid the constant search on the definition if it is not found on the node. Basically a `addOutputFromNodeDef()` utility could be called before\n", "making any connections." ] }, { "cell_type": "code", "execution_count": 2389, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Connected output \"test_nodegraph/out\" to upstream output: test_nodegraph/test_shader.out\n", "\n", "\n", " \n", " \n", "\n" ] } ], "source": [ "def connectOutputToOutput(outputPort, upstream, upstreamOutputName):\n", " \"Utility to connect a downstream output to an upstream node / node output\"\n", " \"If the types differ then no connection is made\"\n", " if not upstream:\n", " return False\n", " \n", " # Cannot directly connect an input to an output\n", " if upstream.isA(mx.Input):\n", " return False\n", "\n", " upstreamType = upstream.getType()\n", "\n", " # Check for an explicit upstream output on the upstream node\n", " # or upstream node's definition\n", " if upstreamOutputName:\n", " upStreamPort = upstream.getActiveOutput(upstreamOutputName)\n", " if not upStreamPort:\n", " upstreamNodeDef = upstream.getNodeDef()\n", " if upstreamNodeDef:\n", " upStreamPort = upstreamNodeDef.getActiveOutput(upstreamOutputName)\n", " else:\n", " return False\n", " if upStreamPort:\n", " upstreamType = upStreamPort.getType()\n", " \n", " outputPortType = outputPort.getType() \n", " if upstreamType != outputPortType:\n", " return False\n", " \n", " upstreamName = upstream.getName()\n", " attributeName = 'nodename'\n", " if upstream.isA(mx.NodeGraph):\n", " attributeName = 'nodegraph'\n", " outputPort.setAttribute(attributeName, upstreamName)\n", " \n", " # If an explicit output is specified on the upstream node/graph then\n", " # set it.\n", " if upstreamOutputName and upstream.getType() == 'multioutput':\n", " outputPort.setOutputString(upstreamOutputName) \n", " \n", " return True\n", "\n", "# Make the connection\n", "shaderNodeOutput = \"out\"\n", "if connectOutputToOutput(graphOutput, shaderNode, shaderNodeOutput):\n", " print('Connected output \"%s\" to upstream output: %s.%s' % (graphOutput.getNamePath(), shaderNode.getNamePath(), shaderNodeOutput))\n", "else:\n", " print('Failed to connected output \"%s\" to upstream output: %s.%s' % (graphOutput.getNamePath(), shaderNode.getNamePath(), shaderNodeOutput))\n", "\n", "\n", "# Check the graph\n", "text = mx.prettyPrint(nodeGraph)\n", "print(\"\")\n", "print(text + '')" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ " ## Making Connections Between Nodes\n", " \n", " Connections are formed from a downstream `input` to an upstream `output`. For this a wrapper function is\n", " used to hide some of the syntactic peculiarities.\n", "\n", "Setting a connection can be cumbersome for the same reason that setting a value can be cumbersome\n", "in that a node instance when created has no inputs instantiated. So a check\n", "must be made to see if it exists and if its not added. Then if input and outputs types match\n", "then the input can make the connection.\n", "\n", "Additionally it is considered \"invalid\" to have both a `value` and a connection on an input, so\n", "if a value has been set it must be removed. Conversely when a connection is removed a value must be\n", "re-assigned. \n", "\n", "As with value setting, the interface `addInputFromNodeDef()` is used to add individual inputs\n", "if they do not exist. A utility called `createNode()` is added for convenience.\n", "\n", "Having a `connectNodeToNode()` interface would be a useful to have in the core API to avoid having\n", "to rewrite this logic.\n", "\n", "> Note that is currently considered undesirable to have explicit outputs defined on nodes which\n", "also adds undue complexity. " ] }, { "cell_type": "code", "execution_count": 2390, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Connected \"test_nodegraph/test_image\" to \"test_nodegraph/test_shader\" in node graph \"test_nodegraph\"\n", "\n", "\n", "\n", " \n", " \n", " \n", " \n", "\n" ] } ], "source": [ "def connectNodeToNode(inputNode, inputName, outputNode, outputName):\n", " \"Connect an input on one node to an output on another node. Existence and type checking are performed.\"\n", " \"Returns input port with connection set if succesful. Otherwise None is returned.\"\n", "\n", " if not inputNode or not outputNode:\n", " return None\n", "\n", "\n", " # Check for the type.\n", " outputType = outputNode.getType() \n", " \n", " # If there is more than one output then we need to find the output type \n", " # from the output with the name we are interested in.\n", " outputPortFound = None\n", " outputPorts = outputNode.getOutputs()\n", " if outputPorts:\n", " # Look for an output with a given name, or the first if not found \n", " if not outputName:\n", " outputPortFound = outputPorts[0]\n", " else:\n", " outputPortFound = outputNode.getOutput(outputName)\n", "\n", " # If the output port is not found on the node instance then\n", " # look for it the corresponding definition\n", " if not outputPortFound:\n", " outputNodedef = outputNode.getNodeDef()\n", " if outputNodedef:\n", " outputPorts = outputNodedef.getOutputs()\n", " \n", " if outputPorts:\n", " # Look for an output with a given name, or the first if not found \n", " if not outputName:\n", " outputPortFound = outputPorts[0]\n", " else:\n", " outputPortFound = outputNodedef.getOutput(outputName)\n", "\n", " if outputPortFound:\n", " outputType = outputPortFound.getType()\n", " else:\n", " print('No output port found matching: ', outputName) \n", "\n", " # Add an input to the downstream node if it does not exist\n", " inputPort = inputNode.addInputFromNodeDef(inputName)\n", " \n", " if inputPort.getType() != outputType:\n", " print('Input type (%s) and output type (%s) do not match: ' % (inputPort.getType(), outputType))\n", " return None\n", "\n", " if inputPort:\n", " # Remove any value, and set a \"connection\" but setting the node name\n", " inputPort.removeAttribute('value')\n", " attributeName = 'nodename' if outputNode.isA(mx.Node) else 'nodegraph'\n", " inputPort.setAttribute(attributeName, outputNode.getName())\n", " if outputNode.getType() == 'multioutput' and outputName:\n", " inputPort.setOutputString(outputName)\n", " return inputPort\n", " \n", "# Create a unique child name under the node graph container\n", "imageNode = createNode(\"ND_image_color3\", nodeGraph, \"test_image\")\n", "if imageNode and shaderNode:\n", " inputConnnected = connectNodeToNode(shaderNode, \"base_color\", imageNode, \"\")\n", " if inputConnnected:\n", " print('Connected \"%s\" to \"%s\" in node graph \"%s\"' % (imageNode.getNamePath(), shaderNode.getNamePath(), \n", " nodeGraph.getNamePath()))\n", " \n", "# Check the graph\n", "text = mx.prettyPrint(nodeGraph)\n", "print('\\n')\n", "print(text + '')" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Adding Input Interfaces\n", "\n", "Just as child outputs can be added to a `NodeGraph`, child inputs (`Input`) can also be added.\n", "Adding inputs can be thought of as exposing the internal inputs as \"public\" interfaces.\n", "\n", "The interface `addInputInterface()` can be used to add one or more inputs. These inputs can then be connected to inputs on node children within the node graph container.\n", "\n", "> Note that `NodeGraph.addInterfaceName()` can **only** be used for a graph which represents an implementation of a definition ('functional nodegraph'). An error condition will always be thrown\n", "otherwise. It would be useful if this interface handled non-functional nodegraphs as well.) " ] }, { "cell_type": "code", "execution_count": 2391, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Added input interfaces: \"input_file\" and \"color_scale\"\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", "\n" ] } ], "source": [ "def addInputInterface(name, typeString, parent):\n", " \"Add a type input interface. Will always create a new interface\"\n", "\n", " validType = False\n", " typedefs = parent.getDocument().getTypeDefs()\n", " for t in typedefs:\n", " if typeString in t.getName():\n", " validType = True\n", " break\n", "\n", " if validType:\n", " validName = parent.createValidChildName(name)\n", " parent.addInput(validName, typeString)\n", " \n", "# Add interfaces\n", "addInputInterface('input_file', 'filename', nodeGraph)\n", "addInputInterface('color_scale', 'float', nodeGraph)\n", "\n", "# Check the graph\n", "text = mx.prettyPrint(nodeGraph)\n", "print('Added input interfaces: \"input_file\" and \"color_scale\"\\n')\n", "print(text + '')" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "The connection for interfaces is slightly different in that instead of an `Output` an `Input` is being connected to a downstream `Input`.\n", "\n", "We will again write a utility to hide some of the syntactic peculiarities." ] }, { "cell_type": "code", "execution_count": 2392, "metadata": {}, "outputs": [], "source": [ "def connectInterface(nodegraph, interfaceName, internalInput):\n", " \"Add an interface input to a nodegraph if it does not already exist.\" \n", " \"Connect the interface to the internal input. Returns interface input\"\n", "\n", " if not nodegraph or not interfaceName or not internalInput:\n", " return None\n", "\n", " interfaceInput = nodegraph.getInput(interfaceName)\n", "\n", " # Create a new interface with the desired type\n", " if not interfaceInput:\n", " interfaceName = nodeGraph.createValidChildName(interfaceName) \n", " interfaceInput = nodegraph.addInput(interfaceName, internalInput.getType())\n", "\n", " # Copy attributes from internal input to interface. \n", " # Remove undesired attributes.\n", " interfaceInput.copyContentFrom(internalInput)\n", " interfaceInput.removeAttribute('sourceUri')\n", " interfaceInput.removeAttribute('interfacename')\n", "\n", " # Logic transfer any value from the internal input to the interface.\n", " # If none is found then use the the default value as defined by the definition.\n", " internalInputType = internalInput.getType()\n", " if internalInput.getValue():\n", " internaInputValue = internalInput.getValue() \n", " if internaInputValue:\n", " interfaceInput.setValue(internaInputValue, internalInputType)\n", " else:\n", " internalNode = internalInput.getParent() \n", " internalNodeDef = internalNode.getNodeDef() if internalNode else None\n", " internalNodeDefInput = internalNodeDef.getInput(interfaceName) if internalNodeDef else None\n", " internaInputValue = internalNodeDefInput.getValue() if internalNodeDefInput else None\n", " if internaInputValue:\n", " interfaceInput.setValue(internaInputValue, internalInputType)\n", "\n", " # Remove \"value\" from internal input as it's value is via a connection\n", " internalInput.removeAttribute('value')\n", "\n", " # \"Connect\" the internal node's input to the interface. Remove any\n", " # specified value\n", " internalInput.setInterfaceName(interfaceName)\n", "\n", " return interfaceInput\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "First example exposes the 'file' input as an 'input_file' interface to the graph." ] }, { "cell_type": "code", "execution_count": 2393, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n" ] } ], "source": [ "# Add a 'file' input to the child node \n", "imageFileInput = imageNode.addInputFromNodeDef('file')\n", "imageFileInputType = imageFileInput.getType()\n", "imageFileInput.setValue(\"checker.png\", imageFileInputType)\n", "# Connect it to interface intput on \"input_file\" \n", "connectInterface(nodeGraph, \"input_file\", imageNode.getInput('file'))\n", "\n", "print(mx.prettyPrint(nodeGraph) + '')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Second example to expose \"base\" as a \"color_scale\" input on the graph." ] }, { "cell_type": "code", "execution_count": 2394, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n" ] } ], "source": [ "# Second example: Publish 'base' as an interface. \"Transfer\"\n", "# the default value from 'base' on the shader node to the interfce input. \n", "baseInput = shaderNode.addInputFromNodeDef('base')\n", "connectInterface(nodeGraph, \"color_scale\", baseInput)\n", "\n", "print(mx.prettyPrint(nodeGraph) + '')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Third example to add the 'color_scale' input with the non-default value from 'base_color'" ] }, { "cell_type": "code", "execution_count": 2395, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n" ] } ], "source": [ "# Set a non-default value to be added to the published interface\n", "baseInput.setValue(0.2, baseInput.getType())\n", "connectInterface(nodeGraph, \"color_scale\", baseInput)\n", "\n", "print(mx.prettyPrint(nodeGraph) + '')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As a final step, we check that the document is valid and then write out the entire document to a file." ] }, { "cell_type": "code", "execution_count": 2396, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Wrote document to file: data/sample_nodegraph.mtlx\n", "\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", "\n" ] } ], "source": [ "# Check the entire document\n", "isValid = doc.validate()\n", "if not isValid:\n", " print('Document is not valid')\n", "else:\n", " writeOptions = mx.XmlWriteOptions()\n", " writeOptions.writeXIncludeEnable = False\n", " writeOptions.elementPredicate = skipLibraryElement\n", "\n", " # Save document\n", " mx.writeToXmlFile(doc, 'data/sample_nodegraph.mtlx', writeOptions)\n", "\n", " print('Wrote document to file: data/sample_nodegraph.mtlx\\n')\n", " documentContents = mx.writeToXmlString(doc, writeOptions)\n", " print(documentContents)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Connecting Node to a NodeGraph\n", "\n", "Now that we have a graph with appropriate interfaces we can create a \"material\" by connecting it to a downstream material node (`material`)." ] }, { "cell_type": "code", "execution_count": 2397, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Create material node: my_material\n", "\n", "\n", " \n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n" ] } ], "source": [ "# Create material node \n", "materialNode = createNode('ND_surfacematerial', doc, 'my_material')\n", "if materialNode:\n", " print('Create material node: %s\\n' % materialNode.getName())\n", "\n", "# Connect the material node to the output of the graph\n", "connectNodeToNode(materialNode, 'surfaceshader', nodeGraph, 'out')\n", "\n", "# Check results\n", "print(mx.prettyPrint(materialNode) + '')\n", "print(mx.prettyPrint(nodeGraph) + '')" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Material Graph Result\n", "\n", "The resulting document is shown in XML, diagram and rendered form. (The render is performed using the `MaterialXView` utility)\n", "\n", "\n", "" ] }, { "cell_type": "code", "execution_count": 2398, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "> Document is valid\n", "Wrote document to file: data/sample_nodegraph.mtlx\n", "\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", "\n" ] } ], "source": [ "# Check the entire document\n", "validateDocument(doc)\n", "writeOptions = mx.XmlWriteOptions()\n", "writeOptions.writeXIncludeEnable = False\n", "writeOptions.elementPredicate = skipLibraryElement\n", "\n", "# Save document\n", "mx.writeToXmlFile(doc, 'data/sample_nodegraph.mtlx', writeOptions)\n", "\n", "print('Wrote document to file: data/sample_nodegraph.mtlx\\n')\n", "documentContents = mx.writeToXmlString(doc, writeOptions)\n", "print(documentContents)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Renaming Nodes \n", "\n", "If you just rename a node without renaming references to it, then the references will be broken.\n", "Currently the interface for setting node names is \"unsafe\" in that it does not check for references to the node.\n", "\n", "Below a utility is added to rename a node and update all references to it. It uses the existing interface getDownStreamPorts() to find all references to a node\n", "and updates them." ] }, { "cell_type": "code", "execution_count": 2399, "metadata": {}, "outputs": [], "source": [ "def renameNode(node, newName : str, updateReferences : bool = True):\n", "\n", " if not node or not newName:\n", " return\n", " if not (node.isA(mx.Node) or node.isA(mx.NodeGraph)):\n", " print('A non-node or non-nodegraph was passed to renameNode()')\n", " return \n", " if node.getName() == newName:\n", " return\n", "\n", " parent = node.getParent()\n", " if not parent:\n", " return\n", "\n", " newName = parent.createValidChildName(newName)\n", "\n", " if updateReferences:\n", " downStreamPorts = node.getDownstreamPorts()\n", " if downStreamPorts:\n", " for port in downStreamPorts:\n", " #if (port.getNodeName() == node.getName()): This is assumed from getDownstreamPorts()\n", " oldName = port.getNodeName()\n", " if (port.getAttribute('nodename')):\n", " port.setNodeName(newName)\n", " print(' > Update downstream port: \"' + port.getNamePath() + '\" from:\"' + oldName + '\" to \"' + port.getAttribute('nodename') + '\"')\n", " elif (port.getAttribute('nodegraph')):\n", " port.setAttribute('nodegraph', newName)\n", " print(' > Update downstream port: \"' + port.getNamePath() + '\" from:\"' + oldName + '\" to \"' + port.getAttribute('nodegraph') + '\"')\n", " elif (port.getAttribute('interfacename')):\n", " port.setAttribute('interfacename', newName)\n", " print(' > Update downstream port: \"' + port.getNamePath() + '\" from:\"' + oldName + '\" to \"' + port.getAttribute('interfacename') + '\"')\n", "\n", " node.setName(newName)\n" ] }, { "cell_type": "code", "execution_count": 2400, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "> Result with renaming to same name:\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n" ] } ], "source": [ "# Test renaming to the same name. This will be a no-op\n", "shaderNode = nodeGraph.getNode('test_shader')\n", "renameNode(shaderNode, 'test_shader') \n", "print('> Result with renaming to same name:\\n')\n", "print(mx.prettyPrint(nodeGraph) + '')\n" ] }, { "cell_type": "code", "execution_count": 2401, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "> Rename with new names, but without updating references:\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n" ] } ], "source": [ "\n", "print('> Rename with new names, but without updating references:')\n", "# Then rename to a new name\n", "renameNode(shaderNode, 'new_shader', False) \n", "# Also rename the image node\n", "imageNode = nodeGraph.getNode('test_image')\n", "renameNode(imageNode, 'new_image', False)\n", "\n", "print(mx.prettyPrint(nodeGraph) + '')\n" ] }, { "cell_type": "code", "execution_count": 2402, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "> Document is not valid\n", "> Invalid port connection: \n", "Invalid port connection: \n", "\n" ] } ], "source": [ "validateDocument(doc)\n", "\n", "# Restore old names\n", "shaderNode.setName('test_shader')\n", "imageNode.setName('test_image')\n" ] }, { "cell_type": "code", "execution_count": 2403, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "> Rename with new names, but with references updated:\n", " > Update downstream port: \"test_nodegraph/out\" from:\"test_shader\" to \"new_shader\"\n", " > Update downstream port: \"test_nodegraph/new_shader/base_color\" from:\"test_image\" to \"new_image\"\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", "> Document is valid\n" ] } ], "source": [ "\n", "print('> Rename with new names, but with references updated:')\n", "renameNode(shaderNode, 'new_shader', True) \n", "renameNode(imageNode, 'new_image', True)\n", "print(mx.prettyPrint(nodeGraph) + '')\n", "\n", "# Check the entire document\n", "validateDocument(doc);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " ## Finding Input Interfaces\n", " \n", " Sometimes it can be useful to find what inputs nodegraph are connected downstream to a given interface input.\n", " The `findInputsUsingInterface()` utility demonstrates how to do this.\n" ] }, { "cell_type": "code", "execution_count": 2404, "metadata": {}, "outputs": [], "source": [ "def findDownStreamElementsUsingInterface(nodegraph, interfaceName):\n", "\n", " connectedInputs = [] \n", " connectedOutputs = []\n", " interfaceInput = nodegraph.getInput(interfaceName)\n", " if not interfaceInput:\n", " return\n", " \n", " # Find all downstream connections for this interface\n", " \n", " for child in nodegraph.getChildren():\n", " if child == interfaceInput:\n", " continue\n", "\n", " # Check inputs on nodes\n", " if child.isA(mx.Node):\n", " for input in child.getInputs():\n", " childInterfaceName = input.getAttribute('interfacename')\n", " if childInterfaceName == interfaceName:\n", " connectedInputs.append(input.getNamePath())\n", "\n", " # Check outputs on a nodegraph. Note this is not fully supported for code generation\n", " # as of 1.38.10 but instead a 'dot' node is recommended to be used between an\n", " # input interface and an output interface. This would be found in the node check above.\n", " elif child.isA(mx.Output):\n", " childInterfaceName = child.getAttribute('interfacename')\n", " if childInterfaceName == interfaceName:\n", " connectedOutputs.append(child.getNamePath())\n", "\n", " return connectedInputs, connectedOutputs\n" ] }, { "cell_type": "code", "execution_count": 2405, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Connected inputs: ['test_nodegraph/new_image/file']\n", "Connected outputs: []\n" ] } ], "source": [ "connectedInputs, connectedOutputs = findDownStreamElementsUsingInterface(nodeGraph, \"input_file\")\n", "print('Connected inputs:', connectedInputs)\n", "print('Connected outputs:', connectedOutputs)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ " ## Disconnecting and Removing Input Interfaces\n", " \n", " These interfaces can be \"unpublished\" by removing them from the graph and breaking any connections to\n", " downstream nodes or outputs. It is may be desirable to leave the interface input for later usage as well.\n", "\n", "The `unconnectInterface()` utility demonstrates how to do this with the option to remove the interface input as well. \n", "To attempt to keep the behaviour the same the interface's value is copied to the input. " ] }, { "cell_type": "code", "execution_count": 2406, "metadata": {}, "outputs": [], "source": [ "\n", "def unconnectInterface(nodegraph, interfaceName, removeInterface=True):\n", " \n", " interfaceInput = nodegraph.getInput(interfaceName)\n", " if not interfaceInput:\n", " return\n", " \n", " # Find all downstream connections for this interface\n", " \n", " for child in nodegraph.getChildren():\n", " if child == interfaceInput:\n", " continue\n", "\n", " # Remove connection on node inputs and copy interface value\n", " # to the input value so behaviour does not change\n", " if child.isA(mx.Node):\n", " for input in child.getInputs():\n", " childInterfaceName = input.getAttribute('interfacename')\n", " if childInterfaceName == interfaceName:\n", " input.setValueString(interfaceInput.getValueString())\n", " input.removeAttribute('interfacename')\n", "\n", " # Remove connection on the output. Value are not copied over.\n", " elif child.isA(mx.Output):\n", " childInterfaceName = child.getAttribute('interfacename')\n", " if childInterfaceName == interfaceName:\n", " input.removeAttribute('interfacename')\n", "\n", " if removeInterface:\n", " nodegraph.removeChild(interfaceName)\n" ] }, { "cell_type": "code", "execution_count": 2407, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n" ] } ], "source": [ "# Disconnect and remove the interface\n", "unconnectInterface(nodeGraph, \"input_file\", True)\n", "\n", "# Check the graph\n", "print(mx.prettyPrint(nodeGraph) + '') " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Connecting an upstream node to a graph\n", "\n", "As a final connection example we look at connecting an upstream node to a graph. This is done by connecting the node's output to the graph's input.\n", "\n", "1. As noted previsouly, an `input` cannot have both a connection and a value. Thus if the graph input has a value it will be removed before the connection is made. \n", "2. The utility function has some additional logic to transfer the value to an input on the upstream node if desired. \n", "3. If the upstream node has multiple outputs, the correct output be specified by setting the `output` attribute on the graph's input. \n", "\n", "> This would be a good general practice to always specify the output but the validation logic currently considers this to be an error." ] }, { "cell_type": "code", "execution_count": 2408, "metadata": {}, "outputs": [], "source": [ "def connectToGraphINput(node, outputName, nodegraph, inputName, transferNodeInput):\n", " \"Connect an output on a node to an input on a nodegraph\"\n", " \"Returns the input port on the nodegraph if successful, otherwise None\"\n", "\n", " if not node or not nodegraph:\n", " print('No node or nodegraph specified')\n", " return None\n", "\n", " nodedef = node.getNodeDef()\n", " if not nodedef:\n", " print('Cannot find node definition for node:', node.getName())\n", " return None\n", "\n", " outputPort = nodedef.getOutput(outputName)\n", " if not outputPort:\n", " print('Cannot find output port:', outputName, 'for the node:', node.getName())\n", " return None\n", "\n", " inputPort = nodegraph.getInput(inputName)\n", " if not inputPort:\n", " print('Cannot find input port:', inputName, 'for the nodegraph:', nodegraph.getName())\n", " return None\n", "\n", " if outputPort.getType() != inputPort.getType():\n", " print('Output type:', outputPort.getType(), 'does not match input type:', inputPort.getType())\n", " return None\n", "\n", " # Transfer the value from the graph input to a specified upstream input\n", " if transferNodeInput: \n", " if inputPort.getValue():\n", " newInput = node.addInputFromNodeDef(transferNodeInput)\n", " if newInput and (newInput.getType() == inputPort.getType()):\n", " newInput.setValueString(inputPort.getValueString())\n", "\n", " # Remove any value, and set a \"connection\" but setting the node name \n", " inputPort.removeAttribute('value')\n", " inputPort.setAttribute('nodename', node.getName())\n", " if node.getType() == 'multioutput':\n", " inputPort.setOutputString(outputName)\n", "\n", " return inputPort" ] }, { "cell_type": "code", "execution_count": 2409, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Create color node: constant_float\n", "\n" ] } ], "source": [ "# Create an upstream constant float node\n", "colorNode = createNode('ND_constant_float', doc, 'constant_float')\n", "if colorNode:\n", " print('Create color node: %s\\n' % colorNode.getName())\n", " # Connect the color node to the graph input\n", " result = connectToGraphINput(colorNode, 'out', nodeGraph, 'color_scale', 'value')\n", " if not result:\n", " print('Failed to connect color node to graph input\\n')\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The resulting document looks like this:\n", "" ] }, { "cell_type": "code", "execution_count": 2410, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", "\n" ] } ], "source": [ "\n", "# Check the graph\n", "documentContents = mx.writeToXmlString(doc, writeOptions)\n", "print(documentContents)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Renaming (Revisited)\n", "\n", "Now that we have a fully formed graph, we can perform some final renaming on the top level constant and nodegraph." ] }, { "cell_type": "code", "execution_count": 2411, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " > Update downstream port: \"test_nodegraph/color_scale\" from:\"constant_float\" to \"new_constant_float\"\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", "\n", "> Document is valid\n" ] } ], "source": [ "renameNode(doc.getNode('constant_float'), 'new_constant_float', True);\n", "documentContents = mx.writeToXmlString(doc, writeOptions)\n", "print(documentContents)\n", "validateDocument(doc)" ] }, { "cell_type": "code", "execution_count": 2412, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " > Update downstream port: \"my_material/surfaceshader\" from:\"\" to \"new_nodegraph\"\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", "\n", "> Document is valid\n" ] } ], "source": [ "nodegraph = doc.getNodeGraph('test_nodegraph')\n", "renameNode(nodegraph, 'new_nodegraph', True)\n", "documentContents = mx.writeToXmlString(doc, writeOptions)\n", "print(documentContents)\n", "validateDocument(doc)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Renaming Ports\n", "\n", "At the current time there is no API support for quickly finding the downstream ports from a given port.\n", "The renameNode() utility has a firewall check to avoid trying to call getDownstreamPorts() on a port." ] }, { "cell_type": "code", "execution_count": 2413, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "A non-node or non-nodegraph was passed to renameNode()\n", "> No change in color scale name: color_scale\n" ] } ], "source": [ "# This does not work\n", "colorScale = nodegraph.getInput('color_scale')\n", "if colorScale:\n", " renameNode(colorScale, 'new_color_scale', True)\n", " print('> No change in color scale name:', colorScale.getName())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " The workaround is to use logic like findDownStreamElementsUsingInterface() to find the downstream ports and rename them." ] }, { "cell_type": "code", "execution_count": 2414, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "> Rename:new_nodegraph/new_shader/base interfacename from color_scale to new_color_scale\n", " \n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", "\n", "> Document is valid\n" ] } ], "source": [ "# Use traversal to find and rename\n", "newScaleName = 'new_color_scale'\n", "downstreamInputs, downstreamOutputs = findDownStreamElementsUsingInterface(nodegraph, colorScale.getName())\n", "# Combine inputs and outputs\n", "downstreamInputs.extend(downstreamOutputs)\n", "if downstreamInputs:\n", " for item in downstreamInputs:\n", " downStreamElement = doc.getDescendant(item)\n", " if (downStreamElement):\n", " print('> Rename:' + downStreamElement.getNamePath() + ' interfacename from ' + colorScale.getName() + ' to ' + newScaleName)\n", " downStreamElement.setAttribute('interfacename', newScaleName)\n", " colorScale.setName(newScaleName)\n", "\n", "print(' ')\n", "documentContents = mx.writeToXmlString(doc, writeOptions)\n", "print(documentContents)\n", "validateDocument(doc)" ] } ], "metadata": { "kernelspec": { "display_name": ".venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.10" }, "orig_nbformat": 4, "vscode": { "interpreter": { "hash": "b729d0e20edc59430665dadd095c679f1a6a6ae416a8655f956120c3270c9bf6" } } }, "nbformat": 4, "nbformat_minor": 2 }