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