USD and MaterialX NodeGraphs¶

This notebook will look at some of the basic interop between MaterialX and USD focusing on nodegraphs and nodes. Material assignment will not be examined, nor is the intent to provide a tutorial about Usd, which can be found in other places such as from Pixar, NVIDIA, and Houdini

Topics covered include:

  1. Usd and MaterialX Package Setup
  2. Translating a Usd file with MaterialX materials to MaterialX format, focusing on traversal, pathing, and connection mapping.
  3. Translating a MaterialX into Usd format with the same focus.

Neither translators are intended to be a full importer or exporter but rather usable for learning purposes, noting that UsdMtlx import is not available as part of the core Usd Python package, and code such as UsdFilter for HDStorm is specifically targeted for a given render delegate and is also not exposed.

1. Usd Setup¶

To use Usd the "core" package can be installed as follows. Note that the latest test are done with the Usd and MaterialX versions shown in the execution logs.

In [1]:
#pip install usd-core

After installation various packages can be imported. Usd, UsdShade, Sdf, and Gf are the main packages used. The MaterialX package is also imported. | Note that the definition registry (Sdr) does not contain any MaterialX definitions.

In [2]:
from pxr import Usd
from pxr import UsdShade
from pxr import Sdf
from pxr import Gf
from pxr import UsdGeom
from pxr import Sdr

# For Markdown output display
from IPython.display import display_markdown

import MaterialX as mx

major, minor, build = Usd.GetVersion() 
print('Using Usd Version:', str(major) + "." + str(minor) + "." + str(build))
if Sdr.Registry():
    print('- Sdr nodes {', ', '.join(Sdr.Registry().GetNodeNames()), '}')
print('Using MaterialX Version:', mx.getVersionString())
Using Usd Version: 0.25.2
- Sdr nodes { PortalLight, GeometryLight, SphereLight, DomeLight_1, CylinderLight, RectLight, DistantLight, DomeLight, DiskLight, MeshLight, VolumeLight }
Using MaterialX Version: 1.39.2

As input, we load in an example Usd file that contains a shading network with a series of nodegraph connections and nodes which have MaterialX definitions. As there is no concept of a layer hierarchy in MaterialX, all layers in the imported Stage are pre-"flattened" using Stage.Flatten()

In [3]:
# Load in a sample file
stage_unflattend = Usd.Stage.Open('data/sphere_with_nodegraphs.usda') 

# Flatten layers
layer = stage_unflattend.Flatten()
stage = Usd.Stage.Open(layer)

# Print as String
stringResult = layer.ExportToString()
text = '<details><summary>Flattened Usd File</summary>\n\n' + '```usd\n' + stringResult + '```\n' + '</details>\n' 
display_markdown(text , raw=True)
Flattened Usd File
#usda 1.0
(
    doc = """Generated from Composed Stage of root layer d:\\Work\\materialx\\MaterialX_Learn\\pymaterialx\\data\\sphere_with_nodegraphs.usda
"""
    endTimeCode = 1
    framesPerSecond = 24
    metersPerUnit = 1
    startTimeCode = 1
    timeCodesPerSecond = 24
    upAxis = "Y"
)

def Xform "mySphere" (
    kind = "component"
)
{
    def Sphere "geo"
    {
        float3[] extent = [(-1, -1, -1), (1, 1, 1)]
        double radius = 1
        matrix4d xformOp:transform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) )
        uniform token[] xformOpOrder = ["xformOp:transform"]
    }

    def Scope "mtl"
    {
        def Material "collect1"
        {
            color3f inputs:base_color = (1, 0, 0) (
                displayName = "Base Color for Material Interface"
            )
            token outputs:mtlx:displacement.connect = </mySphere/mtl/collect1/my_materialx_subnet.outputs:displacement>
            token outputs:mtlx:surface.connect = </mySphere/mtl/collect1/my_materialx_subnet.outputs:surface>
            token outputs:surface.connect = </mySphere/mtl/collect1/usdpreview_subnet.outputs:surface>

            def NodeGraph "my_materialx_subnet"
            {
                color3f inputs:base_color.connect = </mySphere/mtl/collect1.inputs:base_color>
                token outputs:displacement.connect = </mySphere/mtl/collect1/my_materialx_subnet/mtlxdisplacement.outputs:out>
                token outputs:surface.connect = </mySphere/mtl/collect1/my_materialx_subnet/mtlxstandard_surface1.outputs:out>

                def Shader "mtlxstandard_surface1"
                {
                    uniform token info:id = "ND_standard_surface_surfaceshader"
                    float inputs:base = 1
                    color3f inputs:base_color.connect = </mySphere/mtl/collect1/my_materialx_subnet/image_readers.outputs:out>
                    float inputs:coat = 0
                    float inputs:coat_roughness = 0.1
                    float inputs:emission = 0
                    color3f inputs:emission_color = (1, 1, 1)
                    float inputs:metalness = 0
                    float inputs:specular = 1
                    color3f inputs:specular_color = (1, 1, 1)
                    float inputs:specular_IOR = 1.5
                    float inputs:specular_roughness = 0.2
                    float inputs:specular_roughness.connect = </mySphere/mtl/collect1/my_materialx_subnet/image_readers.outputs:out_2>
                    float inputs:transmission = 0
                    token outputs:out
                }

                def NodeGraph "image_readers"
                {
                    color3f inputs:_base_color.connect = </mySphere/mtl/collect1/my_materialx_subnet.inputs:base_color>
                    color3f outputs:out.connect = </mySphere/mtl/collect1/my_materialx_subnet/image_readers/mtlximage1.outputs:out>
                    float outputs:out_2.connect = </mySphere/mtl/collect1/my_materialx_subnet/image_readers/mtlximage2.outputs:out>

                    def Shader "mtlximage1"
                    {
                        uniform token info:id = "ND_image_color3"
                        color3f inputs:default.connect = </mySphere/mtl/collect1/my_materialx_subnet/image_readers.inputs:_base_color>
                        asset inputs:file = @file1.png@
                        color3f outputs:out
                    }

                    def Shader "mtlximage2"
                    {
                        uniform token info:id = "ND_image_float"
                        asset inputs:file = @file2.png@
                        float outputs:out
                    }
                }

                def Shader "mtlxdisplacement"
                {
                    uniform token info:id = "ND_displacement_float"
                    token outputs:out
                }
            }

            def NodeGraph "usdpreview_subnet"
            {
                color3f inputs:base_color.connect = </mySphere/mtl/collect1.inputs:base_color>
                token outputs:surface.connect = </mySphere/mtl/collect1/usdpreview_subnet/usdpreviewsurface1.outputs:surface>

                def Shader "usdpreviewsurface1"
                {
                    uniform token info:id = "UsdPreviewSurface"
                    color3f inputs:diffuseColor.connect = </mySphere/mtl/collect1/usdpreview_subnet.inputs:base_color>
                    token outputs:surface
                }
            }
        }
    }
}

2. Usd Traversal¶

As a starting point, a simple tree traversal logic is added. Note that this just traverses the entire stage and prints out the prims and their attributes.

In [4]:
# Start from the root
prim = stage.GetPrimAtPath('/')

# Utility to recursive traverse and print out children, and their attributes
def printChildren(indent, prim):
    children = prim.GetChildren()
    for child in children:
        if child:
            primtype = 'node'
            if child.IsA(UsdShade.Material):
                primtype = 'material'
            elif child.IsA(UsdShade.NodeGraph):
                primtype = 'graph'
            elif child.IsA(UsdShade.Shader):
                primtype = 'shader'
            print('%s - Name %s, Type %s Path %s' % (indent, child.GetName(), primtype, child.GetPrimPath())) 
            for attr in child.GetAttributes():
                if attr.Get():
                    print(indent, '  -', attr.GetName() + ' : ', attr.Get())
            printChildren(indent + '  ', child)    

# Print out tree
printChildren(' ', prim)
  - Name mySphere, Type node Path /mySphere
    - purpose :  default
    - visibility :  inherited
    - Name geo, Type node Path /mySphere/geo
      - extent :  [(-1, -1, -1), (1, 1, 1)]
      - orientation :  rightHanded
      - purpose :  default
      - radius :  1.0
      - visibility :  inherited
      - xformOp:transform :  ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) )
      - xformOpOrder :  [xformOp:transform]
    - Name mtl, Type node Path /mySphere/mtl
      - purpose :  default
      - visibility :  inherited
      - Name collect1, Type material Path /mySphere/mtl/collect1
        - inputs:base_color :  (1, 0, 0)
        - Name my_materialx_subnet, Type graph Path /mySphere/mtl/collect1/my_materialx_subnet
          - Name mtlxstandard_surface1, Type shader Path /mySphere/mtl/collect1/my_materialx_subnet/mtlxstandard_surface1
            - info:id :  ND_standard_surface_surfaceshader
            - info:implementationSource :  id
            - inputs:base :  1.0
            - inputs:coat_roughness :  0.10000000149011612
            - inputs:emission_color :  (1, 1, 1)
            - inputs:specular :  1.0
            - inputs:specular_color :  (1, 1, 1)
            - inputs:specular_IOR :  1.5
            - inputs:specular_roughness :  0.20000000298023224
          - Name image_readers, Type graph Path /mySphere/mtl/collect1/my_materialx_subnet/image_readers
            - Name mtlximage1, Type shader Path /mySphere/mtl/collect1/my_materialx_subnet/image_readers/mtlximage1
              - info:id :  ND_image_color3
              - info:implementationSource :  id
              - inputs:file :  @file1.png@
            - Name mtlximage2, Type shader Path /mySphere/mtl/collect1/my_materialx_subnet/image_readers/mtlximage2
              - info:id :  ND_image_float
              - info:implementationSource :  id
              - inputs:file :  @file2.png@
          - Name mtlxdisplacement, Type shader Path /mySphere/mtl/collect1/my_materialx_subnet/mtlxdisplacement
            - info:id :  ND_displacement_float
            - info:implementationSource :  id
        - Name usdpreview_subnet, Type graph Path /mySphere/mtl/collect1/usdpreview_subnet
          - Name usdpreviewsurface1, Type shader Path /mySphere/mtl/collect1/usdpreview_subnet/usdpreviewsurface1
            - info:id :  UsdPreviewSurface
            - info:implementationSource :  id

3. Examining Shader Graphs¶

This can be refined to only examine shading nodes and ports.

Two utility functions are added:

  • printValueElements() selectively examines inputs and outputs using GetInputs() and GetOutputs on a UsdShadeShader, or UsdShadeNodeGraph. Tokens are not considered in this example.

  • printShaderNodes() performs the traversal from a root prim visiting prims which would map to MaterialX -- namely "node graphs", "material" and "shader" nodes. Any geometry and tree nesting is ignored.

In [5]:
def printValueElements(shaderInterface, indent):
    """
    Print out the inputs and outputs 
    """
    for input in shaderInterface.GetInputs():
        if input:
            print(indent, '- Input:', input.GetBaseName())
    for output in shaderInterface.GetOutputs():
        if output:
            print(indent, '- Output:', output.GetBaseName())

def printShaderNodes(indent, prim):
    """
    Print out shader node information
    """
    # Use RTTI to find the desired types
    if prim.IsA(UsdShade.Material): 
        print(indent, '- Material %s, Path %s' % (prim.GetName(), prim.GetPrimPath()))
        material = UsdShade.Material(prim)
        printValueElements(material, indent + '  ')

    elif prim.IsA(UsdShade.NodeGraph):
        print(indent, '- Nodegraph: %s, Path %s' % (prim.GetName(), prim.GetPrimPath()))
        nodegraph = UsdShade.NodeGraph(prim)
        printValueElements(nodegraph, indent + '  ')


    elif prim.IsA(UsdShade.Shader): 
        shader = UsdShade.Shader(prim)
        print(indent, '- Shader %s, Path %s' % (prim.GetName(), prim.GetPrimPath()))
        print(indent, '  - Nodedef: ', (shader.GetIdAttr().Get()))
        printValueElements(shader, indent + '  ')

    # Visit children
    children = prim.GetChildren()
    if children:
        childIndent = indent+'  '
        for child in children:
            printShaderNodes(childIndent, child)

# Traverse and print output "shader" contents
prim = stage.GetPrimAtPath('/')
printShaderNodes(' ', prim)

materials = [x for x in stage.Traverse() if x.IsA(UsdShade.Material)]
        - Material collect1, Path /mySphere/mtl/collect1
          - Input: base_color
          - Output: mtlx:displacement
          - Output: mtlx:surface
          - Output: surface
          - Nodegraph: my_materialx_subnet, Path /mySphere/mtl/collect1/my_materialx_subnet
            - Input: base_color
            - Output: displacement
            - Output: surface
            - Shader mtlxstandard_surface1, Path /mySphere/mtl/collect1/my_materialx_subnet/mtlxstandard_surface1
              - Nodedef:  ND_standard_surface_surfaceshader
              - Input: base
              - Input: base_color
              - Input: coat
              - Input: coat_roughness
              - Input: emission
              - Input: emission_color
              - Input: metalness
              - Input: specular
              - Input: specular_color
              - Input: specular_IOR
              - Input: specular_roughness
              - Input: transmission
              - Output: out
            - Nodegraph: image_readers, Path /mySphere/mtl/collect1/my_materialx_subnet/image_readers
              - Input: _base_color
              - Output: out
              - Output: out_2
              - Shader mtlximage1, Path /mySphere/mtl/collect1/my_materialx_subnet/image_readers/mtlximage1
                - Nodedef:  ND_image_color3
                - Input: default
                - Input: file
                - Output: out
              - Shader mtlximage2, Path /mySphere/mtl/collect1/my_materialx_subnet/image_readers/mtlximage2
                - Nodedef:  ND_image_float
                - Input: file
                - Output: out
            - Shader mtlxdisplacement, Path /mySphere/mtl/collect1/my_materialx_subnet/mtlxdisplacement
              - Nodedef:  ND_displacement_float
              - Output: out
          - Nodegraph: usdpreview_subnet, Path /mySphere/mtl/collect1/usdpreview_subnet
            - Input: base_color
            - Output: surface
            - Shader usdpreviewsurface1, Path /mySphere/mtl/collect1/usdpreview_subnet/usdpreviewsurface1
              - Nodedef:  UsdPreviewSurface
              - Input: diffuseColor
              - Output: surface

4. Usd to MaterialX Translation¶

The previous example is modified to create MaterialX graphs, shaders and materials.

Note that the shading network contains nested nodegraphs. That is a nodegraph can contain another nodegraph. While this is part of the MaterialX specification, full support for this does not currently exist at time of writing. For the purposes of translation / interop, logic is included which can general enough to support any level of graph nesting.

As required a user can perform a "flattening" process by traversing through the node connections to remove nodegraph nesting as is done for UsdMtlx for conversion from Usd to MaterialX. This is not included as part of the logic for this example.

4.1 Setup¶

The first step is to add in a basic setup for MaterialX to create a working document and load in standard definitions.

In [6]:
# Perform basic setup
stdlib = mx.createDocument()
libFiles = []
searchPath = mx.getDefaultDataSearchPath()
libFiles = mx.loadLibraries(mx.getDefaultDataLibraryFolders(), searchPath, stdlib)
    
doc = mx.createDocument()
doc.importLibrary(stdlib)
print('Created working document. Loaded in %d definitions' % len(doc.getNodeDefs()))

# Write predicate
def skipLibraryElement(elem):
    return not elem.hasSourceUri()
Created working document. Loaded in 780 definitions

4.2 Translation Logic¶

Next, translation logic is broken up into a series of utilities which perform Usd to MaterialX mappings.

4.2.1 Type and Value Mapping¶

The first of these are utilities for value and type mapping:

  • The utility mapUsdTypeToMtlx() maps native Usd type strings to MaterialX native type strings.
  • The utility mapUsdValueToMtlx() is used to map a Usd Gf value to a MaterialX string value.

Note: Both mappings only handle a subset of all possible mappings.

In [7]:
def mapUsdTypeToMtlx(usdType):
    """ 
    Map a Usd type string to a MaterialX type string.
    Note this is not a complete mapping.
    """
    usdTypeString = str(usdType)
    mtlxType = 'color3'
    if 'color3' in usdTypeString:
        mtlxType ='color3'
    elif 'color4' in usdTypeString:
        mtlxType ='color4'
    elif 'float4' in usdTypeString:
        mtlxType ='vector4'
    elif 'vector3' in usdTypeString:
        mtlxType ='vector3'
    elif 'float2' in usdTypeString:
        mtlxType ='vector2'
    elif 'float' == usdType:
        mtlxType ='float'
    elif 'string' in usdTypeString:
        mtlxType = 'string'
    elif 'int' in usdTypeString:
        mtlxType = 'integer'
    elif 'bool' in usdTypeString:
        mtlxType = 'boolean'
    elif 'asset' in usdTypeString:
        mtlxType = 'filename'
    elif 'token' in usdTypeString:
        mtlxType = 'token'
    else: 
        mtlxType = usdTypeString
        print('--> Mapping of Usd type failed:', usdTypeString)
    return mtlxType

def mapUsdValueToMtlx(mtlxType, usdValue):
    """
    Map a Usd value to a MaterialX value.  
    Note this is not a complete mapping. Ideally, if this is a value on a node
    input / output, then the definition can be queried to get the default value.
    """
    mtlxValue = None
    if mtlxType == 'float':
        if not usdValue:
            mtlxValue = '0'
        else:
            mtlxValue = str(usdValue)
    elif mtlxType == 'integer':
        if not usdValue:
            mtlxValue = '0'
        else:
            mtlxValue = str(usdValue)
    elif mtlxType == 'boolean':                    
        if not usdValue:
            mtlxValue = 'false'
        else:
            mtlxValue = str(usdValue).lower() 
    elif mtlxType == 'string' or mtlxType == 'displacementshader' or mtlxType == 'surfaceshader':                    
        if not usdValue:
            mtlxValue = ''
        else:
            mtlxValue = usdValue
    elif mtlxType == 'filename':        
        if not usdValue:
            mtlxValue = ''
        else:
            mtlxValue = str(usdValue).removeprefix('@').removesuffix('@')
    elif mtlxType == 'vector2':
        if not usdValue:
            mtlxValue = '0, 0'
        else:
            mtlxValue = str(usdValue[0]) + ','  + str(usdValue[1])
    elif mtlxType == 'color3' or mtlxType == 'vector3':
        if not usdValue:
            mtlxValue = '0, 0, 0'
        else:
            mtlxValue = str(usdValue[0]) + ','  + str(usdValue[1]) + ','  + str(usdValue[2])
    elif mtlxType == 'color4' or mtlxType == 'vector4':
        if usdValue is None:
            mtlxValue = '0, 0, 0, 0'
        else:
            mtlxValue = str(usdValue[0]) + ','  + str(usdValue[1]) + ','  + str(usdValue[2]) + ','  + str(usdValue[3])

    if mtlxValue is None:
        print('--> Mapping of Usd Value %s failed for MaterialX type %s' % (usdValue, mtlxType))

    return mtlxValue

# Mapping from Sdf type to MaterialX type.
# It is possible to map using Sdf type. This function is unused in this example 
def mapUsdSdfTypeToMtlx(usdType):
    mtlxUsdMap = dict()
    mtlxUsdMap[Sdf.ValueTypeNames.Asset] = 'filename' 
    mtlxUsdMap[Sdf.ValueTypeNames.String] = 'string'
    mtlxUsdMap[Sdf.ValueTypeNames.Bool] = 'boolean' 
    mtlxUsdMap[Sdf.ValueTypeNames.Int] = 'integer' 
    mtlxUsdMap[Sdf.ValueTypeNames.Color3f] = 'color3' 
    mtlxUsdMap[Sdf.ValueTypeNames.Color4f] = 'color4' 
    mtlxUsdMap[Sdf.ValueTypeNames.Float] = 'float' 
    mtlxUsdMap[Sdf.ValueTypeNames.Float2] = 'vector2'     
    mtlxUsdMap[Sdf.ValueTypeNames.Float3] = 'vector3'     
    mtlxUsdMap[Sdf.ValueTypeNames.Vector3f] = 'vector3'     
    mtlxUsdMap[Sdf.ValueTypeNames.Float4] = 'vector4'   
    
    if usdType in mtlxUsdMap:
        return mtlxUsdMap[usdType]
    return 'string'

4.2.2 Multiple Output Detection¶

isMultiOutput is used to determine if the Usd prim (nodegraph, shader or material) has multiple outputs.

This detection is required as MaterialX connections has a specific syntax to specify the output (port) on an upstream element and this syntax is only added for upstream elements which have multiple outputs ('multioutput')

In [8]:
def isMultiOutput(prim):
    """ Test if the Usd prim has multiple outputs """
    outputCount = 0
    if prim.IsA(UsdShade.Material): 
        usdMaterial = UsdShade.Material(prim)
        outputCount = len(usdMaterial.GetOutputs())
    elif prim.IsA(UsdShade.NodeGraph):
        usdNodegraph = UsdShade.NodeGraph(prim)
        outputCount = len(usdNodegraph.GetOutputs())
    elif prim.IsA(UsdShade.Shader):     
        usdShader = UsdShade.Shader(prim)
        outputCount = len(usdShader.GetOutputs())

    return outputCount > 1

4.2.3 Value Element (Input / Output) Mapping¶

emitMtlxValueELements handles the mapping of Usd inputs and outputs to MaterialX inputs and outputs.

This includes:

  • Creating the input / output.
    • Unlike Usd which has explicit outputs, MaterialX never specifies outputs on nodes, only on nodegraphs.
    • This difference is handled when visiting Usd outputs.
  • Setting a value or (*)
  • Setting connection attributes.
    • GetConnectedSources() in Usd is roughly requivalent to getConnectedNode() in MaterialX. One interesting difference is that "valid" vs "invalid" sources can be returned.
    • Unlike Usd which has a single connect syntax and corresponding API for connection logic and behaviour, MaterialX can require multiple attributes to specified to when creating / modifying a connection.
    • This depends on:
      • if the upstream element is a nodegraph or node, or interface input then 1 of 3 different attributes are set;
      • if the upstream element has multiple outputs (multioutput type), an additional output attribute is required; and
      • if there is a specific channel extracted from the upstream port, an additional channel attribute is required. Logic for channels is not included as part of this example.
    • Ideally if MaterialX adopted a similar syntax to Usd then the mapping would be vastly simplified.

Notes

  1. Additional layer nesting in Usd not directly related to the shading network is not preserved in this example. This could be handled by additional nodegraph nesting or something like namepspace nesting could be used. The former is less lossy, as namespaces are flattened on import from MaterialX in UsdMtlx at the current time.

  2. It is assumed that the Usd string representation for a value can be mapped to a MaterialX one. For example, the string representation for a vector3 ((v1, v2, v3)) in Usd is valid syntax in MaterialX (v1, v2, v3).

  3. For a Usd port with a token type the type of the created MaterialX input / output is set based on the port's name if the Usd Port name is a 'surface' or 'displacement' shader. This logic is encapsulated in the utility function mapUsdTokenToStype() (There appears to be no way by just examining the Usd shading network to determine the type without this assumption at this time).

(*) MaterialX only allows either a connection or value to be specified on a port.

In [9]:
def mapUsdTokenToType(mtlxType, usdBaseName, mtlxPrefix=False):
    """
    Utility to test the base name for a semantic match to a surface or displacement shader
    If found return the appropriate MaterialX type. Othewise the type is simply `token`.
    Note: Only types specified with 'mtlx' are considered to be MaterialX shader, if mtlxPrefix is set to True
    """
    usdBaseNameSplit = mx.splitString(usdBaseName, ':')
    testName = usdBaseNameSplit[len(usdBaseNameSplit)-1]            
    if not mtlxPrefix or (mtlxPrefix and 'mtlx' in usdBaseNameSplit): 
        if 'displacement' == str(testName) or 'displacementshader' == str(testName):
            mtlxType = 'displacementshader'
        elif 'surface' == str(testName) or 'surfaceshader' == str(testName):
            mtlxType = 'surfaceshader'
        elif 'volume' == str(testName) or 'volumeshader' == str(testName):
            mtlxType = 'volumeshader'
    return mtlxType

def emitMtlxValueElements(shader, parent, emitInputs, emitOutputs):
    """
    Emit MaterialX value elements (currently only Inputs and Outputs)
    This is not a complete translation of all value element attributes.
    """
    if emitInputs:
        for input in shader.GetInputs():

            # Only output if there is a value or a connection
            if input:

                # Map Usd type to Mtlx type and create an input
                usdType = input.GetTypeName()
                mtlxType = mapUsdTypeToMtlx(usdType)
                usdBaseName = input.GetBaseName()
                mtlxType = mapUsdTokenToType(mtlxType, usdBaseName)
                usdBaseName = usdBaseName.replace(':', '_')    

                # Add a connection if encountered
                if input.HasConnectedSource():
                    newInput = parent.addInput(usdBaseName, mtlxType)

                    # Only consider "valid" inputs.
                    usdSources, invalidSources = input.GetConnectedSources() 
                    if usdSources and usdSources[0]:
                        # Check UsdShadeConnectionSourceInfo to extract
                        # out the upstream information
                        usdSource1 = usdSources
                        sourcePrim = usdSource1[0].source.GetPrim()
                        sourcePort = usdSource1[0].sourceName # e.g. out
                        sourceDirection = usdSource1[0].sourceType # e.g. Input / Output
                        sourceType = usdSource1[0].typeName # e.g. color3f

                        # Handle the complex MaterialX attribute syntax
                        # for specifying a connection.
                        # ---------------------------------------------
                        # Assume a node->input connection to start
                        mtlxConnectString = 'nodename'
                        mtlxConnectItem = sourcePrim.GetName()

                        # An input->input connection is denoted using
                        # "interfacename", but no "node", or "nodegraph"
                        if sourceDirection == UsdShade.AttributeType.Input:
                            mtlxConnectString = 'interfacename'
                            mtlxConnectItem = sourcePort

                            # Set the connection
                            newInput.setAttribute(mtlxConnectString, mtlxConnectItem)

                        else:
                            # A nodegraph->output connect uses "nodegraph" vs "node"                        
                            if sourcePrim.IsA(UsdShade.NodeGraph):
                                mtlxConnectString = 'nodegraph'

                            # Set the connection
                            newInput.setAttribute(mtlxConnectString, mtlxConnectItem)

                            # An output->intput connection is denoted using
                            # an additional `output` attribute` if the source is 
                            # does not have multiple outputs
                            if sourceDirection == UsdShade.AttributeType.Output:
                                if isMultiOutput(sourcePrim):
                                    newInput.setAttribute('output', sourcePort)                    
    
                # Set value if not connected.
                else:
                    usdVal = input.Get()
                    if usdVal is not None:
                        newInput = parent.addInput(usdBaseName, mtlxType)
                        if newInput:
                            mtlxVal = mapUsdValueToMtlx(mtlxType, usdVal)
                            if mtlxVal is not None:
                                newInput.setValueString(mtlxVal)
    
    # Emit outputs if specified. Unlike Usd, outputs are not explicitly defined
    # except for nodegraph. The branching toggle `emitOuputs` allows for outputs to be selectively emitted.
    if emitOutputs:
        for output in shader.GetOutputs():
            if output:

                usdType = output.GetTypeName()

                mtlxType = mapUsdTypeToMtlx(usdType)
                usdBaseName = output.GetBaseName()
                #usdFullName = output.GetFullName()
                
                newOutput = None
                # Note that MaterialX materials specify connections as input and NOT as outputs
                # as with Usd. Additionally only a subset of types. If there is more than one
                # input with the same type, only the first will be recorded.
                if parent.getType() == 'material':
                    mtlxType = mapUsdTokenToType(mtlxType, usdBaseName, True)
                    if mtlxType in ['surfaceshader', 'volumeshader', 'displacementshader']:
                        if parent.getInput(mtlxType):
                            print('Skip connecting > 1 shader of type %s on material %s' % (mtlxType, parent.getNamePath()))
                        else:
                            newOutput = parent.addInput(mtlxType, mtlxType)
                else:
                    mtlxType = mapUsdTokenToType(mtlxType, usdBaseName)
                    usdBaseName = usdBaseName.replace(':', '_')    
                    newOutput = parent.addOutput(usdBaseName, mtlxType)

                if newOutput and output.HasConnectedSource():
                    usdSources, invalidSources = output.GetConnectedSources() 
                    if usdSources and usdSources[0]:
                        # Check UsdShadeConnectionSourceInfo
                        usdSource1 = usdSources[0]
                        sourcePrim = usdSource1.source.GetPrim()
                        sourcePort = usdSource1.sourceName
                        sourceDirection = usdSource1.sourceType
                        sourceType = usdSource1.typeName

                        mtlxConnectString = 'nodename'
                        mtlxConnectItem = sourcePrim.GetName()

                        # An input->output connection should never occur
                        # and is ignored
                        #if sourceDirection == UsdShade.AttributeType.Input:

                        # Handle adding in node or nodegraph depending on source
                        # prim type.                                
                        if sourcePrim.IsA(UsdShade.NodeGraph):
                            mtlxConnectString = 'nodegraph'

                        newOutput.setAttribute(mtlxConnectString, mtlxConnectItem)

                        # Handle output->output connection
                        if sourceDirection == UsdShade.AttributeType.Output:
                            if isMultiOutput(sourcePrim):
                                newOutput.setAttribute('output', sourcePort)

4.2.4 Top Level Translation Logic¶

A final utility interface called emitMaterialX() wraps up the top level translation logic.

As in the previous example a tree traversal is performed.

The main addition is to create a MaterialX a shader, nodegraph or material when encountered and then adding in child portsusing the MaterialX utilities described.

For shader nodes, an additional check for an associated definition is performed. The MaterialX definition identifier is assumed to be available in the default id attribute using the UsdShadeShader interface GetIdAttr().

Notes:

  1. Usd materials are considered to be node graphs, while in MaterialX materials are nodes which connect to surface, volume or displacement shaders. During conversion anything else must be located at the same level as the material and not nested within the material graph as in Usd. Previously to version 1.38.6, MaterialX materials were closer in nature to Usd materials as they also embedded shader associations as part of the material and materials were not nodes.
  2. Usd separates out the functional API from the primitive and as such an interface needs to be instantiated given a UsdPrim. This differs from MaterialX which does not separate out the functional API, with all types deriving from a common Element class.
  3. Saved paths in Usd are absolute while paths in MaterialX are relative to the current parent scope. Usd has a root path specifier '/' while MaterialX does not.
  4. No specific logic is required to handle different definition versions as long as a different nodedef identifier is used for different versions. This should be the case within MaterialX and when MaterialX nodedefs are loaded into the Usd shader registriy (Sdr)

As MaterialX supports native definitions for Usd shader nodes these can also be handled. For example we assume if the node definition is UsdPreviewSurface that this maps directly to a MaterialX node.

In [10]:
def emitMaterialX(stage, indent, prim, parent):
    """
    Emit MaterialX for a given Usd Stage starting at a given root.
    Currently only nodegraphs, material and shader nodes are supported.
    """
    if prim:
        # Test if it's a material first as a material is a nodegraph
        # Ignore inputs as they have no meaning on a MaterialX material.
        if prim.IsA(UsdShade.Material): 
            doc = parent.getDocument()
            usdMaterial = UsdShade.Material(prim)
            mtlxName = parent.createValidChildName(prim.GetName())
            mtlxMaterial = parent.addMaterialNode(mtlxName)
            emitMtlxValueElements(usdMaterial, mtlxMaterial, False, True)

        elif prim.IsA(UsdShade.NodeGraph):
            doc = parent.getDocument()
            usdNodegraph = UsdShade.NodeGraph(prim)
            mtlxName = parent.createValidChildName(prim.GetName())
            mtlxNodeGraph = parent.addChildOfCategory('nodegraph', mtlxName)
            parent = mtlxNodeGraph
            emitMtlxValueElements(usdNodegraph, mtlxNodeGraph, True, True)

        elif prim.IsA(UsdShade.Shader): 
            usdShader = UsdShade.Shader(prim)
            mtlxNodeDefId = ''
            
            # Note: Only consider when the definition is specified in the identifier
            usdImplAttr = usdShader.GetImplementationSourceAttr()
            if usdImplAttr.Get() == 'id':
                mtlxNodeDefId = usdShader.GetIdAttr().Get()

            # Do a manual rename for built in UsdPreviewSurface
            # Could be done for other built-ins which have MaterialX
            # definitions.
            if mtlxNodeDefId == 'UsdPreviewSurface':
                mtlxNodeDefId = 'ND_UsdPreviewSurface_surfaceshader'

            # Look for an existing definition. If found add an instance and populate
            # it's inputs and outputs.
            doc = parent.getDocument()
            mtlxNodeDef = None
            if mtlxNodeDefId:
                mtlxNodeDef = doc.getNodeDef(mtlxNodeDefId)
            if mtlxNodeDef:
                mtlxShadername = parent.createValidChildName(prim.GetName())
                shaderNode = parent.addNodeInstance(mtlxNodeDef, mtlxShadername)                
                emitMtlxValueElements(usdShader, shaderNode, True, False)
            else:
                print('Skipping shader node %s: No MaterialX definition found.' % prim.GetName())  

        children = prim.GetChildren()
        for child in children:
            emitMaterialX(stage, indent+indent, child, parent)

def convertUsdToMtlx(stage, stdlib):

    doc = mx.createDocument()
    doc.importLibrary(stdlib)

    # Start at the root and emit child nodes 
    prim = stage.GetPrimAtPath('/')
    emitMaterialX(stage, ' ', prim, doc)

    return doc

stdlib = mx.createDocument()
libFiles = []
searchPath = mx.getDefaultDataSearchPath()
libFiles = mx.loadLibraries(mx.getDefaultDataLibraryFolders(), searchPath, stdlib)
print('Loaded in %d definitions' % len(stdlib.getNodeDefs()))
doc = convertUsdToMtlx(stage, stdlib)

# Write results to Markdown / file
writeOptions = mx.XmlWriteOptions()
writeOptions.writeXIncludeEnable = False
writeOptions.elementPredicate = skipLibraryElement
documentContents = mx.writeToXmlString(doc, writeOptions)

text = '<details open><summary>Resulting Generated MaterialX</summary>\n\n' + '```xml\n' + documentContents + '```\n' + '</details>\n' 
display_markdown(text , raw=True)

mx.writeToXmlFile(doc, 'data/test_usd_mtlx.mtlx', writeOptions)
Loaded in 780 definitions
Resulting Generated MaterialX
<?xml version="1.0"?>
<materialx version="1.39">
  <surfacematerial name="collect1" type="material">
    <input name="displacementshader" type="displacementshader" nodegraph="my_materialx_subnet" output="displacement" />
    <input name="surfaceshader" type="surfaceshader" nodegraph="my_materialx_subnet" output="surface" />
  </surfacematerial>
  <nodegraph name="my_materialx_subnet">
    <input name="base_color" type="color3" interfacename="base_color" />
    <output name="displacement" type="displacementshader" nodename="mtlxdisplacement" />
    <output name="surface" type="surfaceshader" nodename="mtlxstandard_surface1" />
    <standard_surface name="mtlxstandard_surface1" type="surfaceshader" nodedef="ND_standard_surface_surfaceshader">
      <input name="base" type="float" value="1.0" />
      <input name="base_color" type="color3" nodegraph="image_readers" output="out" />
      <input name="coat" type="float" value="0" />
      <input name="coat_roughness" type="float" value="0.10000000149011612" />
      <input name="emission" type="float" value="0" />
      <input name="emission_color" type="color3" value="1.0,1.0,1.0" />
      <input name="metalness" type="float" value="0" />
      <input name="specular" type="float" value="1.0" />
      <input name="specular_color" type="color3" value="1.0,1.0,1.0" />
      <input name="specular_IOR" type="float" value="1.5" />
      <input name="specular_roughness" type="float" nodegraph="image_readers" output="out_2" />
      <input name="transmission" type="float" value="0" />
    </standard_surface>
    <nodegraph name="image_readers">
      <input name="_base_color" type="color3" interfacename="base_color" />
      <output name="out" type="color3" nodename="mtlximage1" />
      <output name="out_2" type="float" nodename="mtlximage2" />
      <image name="mtlximage1" type="color3" nodedef="ND_image_color3">
        <input name="default" type="color3" interfacename="_base_color" />
        <input name="file" type="filename" value="file1.png" />
      </image>
      <image name="mtlximage2" type="float" nodedef="ND_image_float">
        <input name="file" type="filename" value="file2.png" />
      </image>
    </nodegraph>
    <displacement name="mtlxdisplacement" type="displacementshader" nodedef="ND_displacement_float" />
  </nodegraph>
  <nodegraph name="usdpreview_subnet">
    <input name="base_color" type="color3" interfacename="base_color" />
    <output name="surface" type="surfaceshader" nodename="usdpreviewsurface1" />
    <UsdPreviewSurface name="usdpreviewsurface1" type="surfaceshader" nodedef="ND_UsdPreviewSurface_surfaceshader">
      <input name="diffuseColor" type="color3" interfacename="base_color" />
    </UsdPreviewSurface>
  </nodegraph>
</materialx>

5. Updating MaterialX / Usd Inputs¶

There are different ways to approach handling an edit in Usd and then updating the corresponding MaterialX. This example only handles value changes by updating matching inputs via path lookups in Usd and MaterialX.

That is, the absolute Usd path is used to find the Usd input in the stage, and the corresponding MaterialX input in the working document.

  • The stage interface GetPrimAtPath() is used to lookup the node to edit in Usd, and
  • The document interface getDescendent() used for MaterialX.
  • The input on each node is then found using GetInput() and getInput() for Usd and MaterialX respectively.

Note that the Usd path differs from the MaterialX path as MaterialX does not accept a path that starts with '/' in it's path related interfaces.

This would be a good discrepancy to address, which could just be an implementation issue.

Monitoring and updating for graph connections is beyond the scope of this example, but it is useful to consider whether the target workflow involves just MaterialX data model updates or if code generation is involved as is the case for render delegates using MaterialX code generation.

In [11]:
# Note that the MaterialX path cannot start with '/' otherwise `getDescendent)` will fail to
# find the element. 
mtlxPath = 'my_materialx_subnet/mtlxstandard_surface1' 

# Add additional path nesting in the Usd stage including parenting of the shader
# graph under the material `collect1`` which does not exist in a MaterialX graph.
usdPath = '/mySphere/mtl/collect1/' + mtlxPath 

# Input to modify
inputName = 'coat_roughness'

# Update the input in Usd
currentValue = 999
prim = stage.GetPrimAtPath(usdPath)
if prim:
    stdsurf = UsdShade.Shader(prim)
    surfInput = stdsurf.GetInput(inputName)
    if surfInput:
        currentValue = surfInput.Get()
        surfInput.Set(0.9)

        print('Modified Usd from: %g to %g' % (currentValue, surfInput.Get()))

# Update the input in MaterialX
currentValue = 999
mtxlStdSurf = doc.getDescendant(mtlxPath)
if mtxlStdSurf:
    mtlxSurfInput = mtxlStdSurf.getInput(inputName)
    if mtlxSurfInput:
        currentValue = mtlxSurfInput.getValueString()
        mtlxSurfInput.setValue(0.9)

        print('Modified MaterialX from: %s to %s' % (currentValue, mtlxSurfInput.getValueString()))
Modified Usd from: 0.1 to 0.9
Modified MaterialX from: 0.10000000149011612 to 0.9

6. MaterialX to Usd Example¶

For completeness, we add in sample logic to convert from MaterialX to Usd. This is not meant to be a substitute for the UsdMtlx plugin. By default this module is not currently available as part of the core Python package for Usd so is not available unless a custom / local build is used.

To start, manual creation of Usd nodes based on the marble example demonstrates usage of some basic interfaces of shaders, materials, graphs, and ports.

Logic to consider includes creating the appropriate UsdShade type, setting a definition (nodedef) association, translating value and type constructs, and forming port connections.

Of note:

  1. Outputs are explicitly created on nodes as well as nodegraphs (unlike MaterialX)
  2. The explicit setting of the node definition name as the identifier to the SdrRegistry
  3. No Usd scopes are created as the MaterialX defines no scope.
In [12]:
def createSimpleMtlx(stage, materialScope):
    """
    Hard coded simple MaterialX to Usd example which produces 
    part of the marble example. 
    """
    if materialScope:
        UsdGeom.Scope.Define(stage, materialScope)
    
    materialPath = Sdf.Path(materialScope).AppendPath('Marble_3D')
    material = UsdShade.Material.Define(stage, materialPath)

    # Create a standard surface shader
    shaderPath = materialPath.AppendPath('SR_marble1')
    stdSurfShader = UsdShade.Shader.Define(stage, shaderPath)
    stdSurfShader.CreateIdAttr("ND_standard_surface_surfaceshader")
    stdSurfShader.CreateInput("base_color", Sdf.ValueTypeNames.Color3f).Set(Gf.Vec3f(0.8, 0.8, 0.8))
    stdSurfShader.CreateInput("specular_roughness", Sdf.ValueTypeNames.Float).Set(0.1)
    stdSurfShader.CreateInput("subsurface", Sdf.ValueTypeNames.Float).Set(0.4)
    stdSurfShader.CreateInput("subsurface_color", Sdf.ValueTypeNames.Float).Set(0.0)

    # Connect shader to material. Note that an output is explicitly created.
    nodeOutput = material.CreateOutput('mtlx:surface', Sdf.ValueTypeNames.Token)
    if nodeOutput:
        #nodeOutput.SetTypeName('mtlx:surface')
        nodeOutput.ConnectToSource(stdSurfShader.ConnectableAPI(), "surface")

    # Create upstream pattern graph
    patternGraphPath = materialPath.AppendPath('NG_marble1')
    patternGraph = UsdShade.NodeGraph.Define(stage, patternGraphPath)
    graphOutput = patternGraph.CreateOutput('out', Sdf.ValueTypeNames.Color3f)

    # Connect graph to shader input. Note that as with MaterialX, the existing value is not removed,
    base_color = stdSurfShader.GetInput('base_color')
    if base_color:
        base_color.ConnectToSource(patternGraph.ConnectableAPI(), "out")
        base_color.Set(Gf.Vec3f(1, 1, 1))

marbleStage = Usd.Stage.CreateInMemory()
mtlxScope = "/mtl"
createSimpleMtlx(marbleStage, mtlxScope)
stringResult = marbleStage.GetRootLayer().ExportToString()
display_markdown('```usd\n' + stringResult + '\n```\n', raw=True)
#usda 1.0

def Scope "mtl"
{
    def Material "Marble_3D"
    {
        token outputs:mtlx:surface.connect = </mtl/Marble_3D/SR_marble1.outputs:surface>

        def Shader "SR_marble1"
        {
            uniform token info:id = "ND_standard_surface_surfaceshader"
            color3f inputs:base_color = (1, 1, 1)
            color3f inputs:base_color.connect = </mtl/Marble_3D/NG_marble1.outputs:out>
            float inputs:specular_roughness = 0.1
            float inputs:subsurface = 0.4
            float inputs:subsurface_color = 0
            token outputs:surface
        }

        def NodeGraph "NG_marble1"
        {
            color3f outputs:out
        }
    }
}

6.1 MaterialX to Usd Utilities¶

For arbitrary MaterialX graphs, a series of utilities is provided to perform the translation.

This is again to show any notable differences in nomenclature, API, and mappings between Usd and MaterialX but in this case for the reverse mapping from MaterialX to Usd.

All logic creates the minimal amount of nesting to reflect how MaterialX does not support nesting via non-nested node graphs.

6.1.1 MaterialX to Usd : Type and Value Mapping¶

The mapMtxToUsdType() and mapMtxToUsdValue() utilities provide mappings for type and value respectively. The mapping is from MaterialX type name to an Usd Sdf type, and from a MaterialX Value to a Usd Gf value.

In [13]:
def mapMtxToUsdType(mtlxType):
    """
    Map a MaterialX type to an Usd Sdf type

    Parameters:
    -----------
    - mtxType : string
        MaterialX type 
    """
    mtlxUsdMap = dict()
    mtlxUsdMap['filename'] = Sdf.ValueTypeNames.Asset
    mtlxUsdMap['string'] = Sdf.ValueTypeNames.String
    mtlxUsdMap['boolean'] = Sdf.ValueTypeNames.Bool
    mtlxUsdMap['integer'] = Sdf.ValueTypeNames.Int
    mtlxUsdMap['float'] = Sdf.ValueTypeNames.Float
    mtlxUsdMap['color3'] = Sdf.ValueTypeNames.Color3f
    mtlxUsdMap['color4'] = Sdf.ValueTypeNames.Color4f
    mtlxUsdMap['vector2'] = Sdf.ValueTypeNames.Float2    
    mtlxUsdMap['vector3'] = Sdf.ValueTypeNames.Vector3f    
    mtlxUsdMap['vector4'] = Sdf.ValueTypeNames.Float4    
    mtlxUsdMap['surfaceshader'] = Sdf.ValueTypeNames.Token

    if mtlxType in mtlxUsdMap:
        return mtlxUsdMap[mtlxType]
    return Sdf.ValueTypeNames.Token

def mapMtxToUsdValue(mtlxType, mtlxValue):
    """
    Map a MaterialX value of a given type to a Usd value.
    Note: Not all types are included here.
    """
    usdValue = '__'
    if mtlxType == 'float':
        usdValue = mtlxValue
    elif mtlxType == 'integer':
        usdValue = mtlxValue
    elif mtlxType == 'boolean':                    
        usdValue = mtlxValue
    elif mtlxType == 'string':                    
        usdValue = mtlxValue
    elif mtlxType == 'filename':  
        usdValue = mtlxValue
    elif mtlxType == 'vector2':
        usdValue = Gf.Vec2f( mtlxValue[0], mtlxValue[1] )
    elif mtlxType == 'color3' or mtlxType == 'vector3':
        usdValue = Gf.Vec3f( mtlxValue[0], mtlxValue[1], mtlxValue[2] )
    elif mtlxType == 'color4' or mtlxType == 'vector4':
        usdValue = Gf.Vec4f( mtlxValue[0], mtlxValue[1], mtlxValue[2], mtlxValue[3] )

    return usdValue

6.1.2 MaterialX to Usd Connection Mapping¶

The logic to create connections is simpler going from MaterialX to Usd as all that is required is to assemble the appropriate absolute prim path.

Similar to the logic shown for the Nodegraph Traversal book, node and interface discovery needs to be performed by parsing the node, nodegraph, interface and output attributes.

Note that when looking for the node to connect to, the document root has an empty path string so logic must be added to insert the required Usd root string '/'. Again this would not be required if the root '/' specifier was supported in MaterialX, and the document path (from getNamePath() return '/' instead of an empty string)

Also note that for this example, geometric bindings including defaultgeomprop are not handled to create upstream input streams as they are in UsdMtlx.

In [14]:
def mapMtlxToUsdShaderNotation(name):
    '''
    Utility to map from a MaterialX shader notation to Usd.
    It would be easier if the same notation was used.
    '''
    if name == 'surfaceshader': 
        name = 'surface'
    elif name == 'displacementshader':
        name = 'displacement'
    elif name == 'volumshader':
        name = 'volume'
    return name

def emitUsdConnections(node, stage, rootPath):
    """ 
    Emit connections between MaterialX elements as Usd connections for 
    a given MaterialX node.

    Paramters:
    - node : 
        MaterialX node to examine
    - stage :
        Usd stage to write connection to
    """
    if not node:
        return
    
    materialPath = None
    if node.getType() == 'material':
        materialPath = node.getName()

    for valueElement in node.getActiveValueElements():
        isInput = valueElement.isA(mx.Input) 
        isOutput = valueElement.isA(mx.Output)
        if  isInput or isOutput:

            interfacename = ''

            # Find out what type of element is connected to upstream:
            # node, nodegraph, or interface input.
            mtlxConnection = valueElement.getAttribute('nodename')
            if not mtlxConnection:
                mtlxConnection = valueElement.getAttribute('nodegraph')
            if not isOutput:
                if not mtlxConnection:
                    mtlxConnection = valueElement.getAttribute('interfacename')
                    interfacename = mtlxConnection 

            connectionPath = ''
            if mtlxConnection:

                # Handle input connection by searching for the appropriate parent node.
                # - If it's an interface input we want the parent nodegraph. Otherwise
                # we want the node or nodegraph specified above.
                # - If the parent path is the root (getNamePath() is empty), then this is to 
                # nodes at the root document level. 
                if isInput:
                    parent = node.getParent()
                    if parent.getNamePath():
                        if interfacename:
                            connectionPath = rootPath + parent.getNamePath()
                        else:
                            connectionPath = rootPath + parent.getNamePath() + '/' + mtlxConnection
                    else:
                        # The connectio is to a prim at the root level so insert a '/' identifier
                        # as getNamePath() will return an empty string at the root Document level.
                        if interfacename:
                            connectionPath = rootPath
                        else:
                            connectionPath = rootPath + mtlxConnection

                # Handle output connection by looking for sibling elements
                else:
                    parent = node.getParent()                    
                    
                    # Connection is to sibling under the same nodegraph
                    if node.isA(mx.NodeGraph):
                        connectionPath = rootPath + node.getNamePath() + '/' + mtlxConnection
                    else:
                        # Connection is to a nodegraph parent of the current node 
                        if parent.getNamePath():
                            connectionPath = rootPath + parent.getNamePath() + '/' + mtlxConnection
                        # Connection is to the root document.
                        else:
                            connectionPath = rootPath + mtlxConnection

                # Find the source prim
                # Assumes that the source is either a nodegraph, a material or a shader
                connectionPath = connectionPath.removesuffix('/')
                sourcePrim = None
                sourcePort = 'out'
                source = stage.GetPrimAtPath(connectionPath)
                if not source:
                    if materialPath:
                        connectionPath = '/' + materialPath + connectionPath
                        source = stage.GetPrimAtPath(connectionPath)
                        if not source:
                            source = stage.GetPrimAtPath('/' + materialPath)
                if source:
                    if source.IsA(UsdShade.Material): 
                        sourcePrim = UsdShade.Material(source)
                    elif source.IsA(UsdShade.NodeGraph):
                        sourcePrim = UsdShade.NodeGraph(source)
                    elif source.IsA(UsdShade.Shader): 
                        sourcePrim = UsdShade.Shader(source)

                    # Special case handle interface input vs an output
                    if interfacename:
                        sourcePort =  interfacename
                    else:                          
                        sourcePort = valueElement.getAttribute('output')
                        if not sourcePort:
                            sourcePort = 'out'
                    if sourcePort:
                        mtlxConnection = mtlxConnection + '. Port:' + sourcePort

                else:
                    print('> Failed to find source at path:', connectionPath)

                # Find destination prim and port and make the appropriate connection.
                # Assumes that the destination is either a nodegraph, a material or a shader
                destInput = None
                if sourcePrim:
                    dest = stage.GetPrimAtPath(rootPath + node.getNamePath())
                    if not dest:
                        print('> Failed to find dest at path:', rootPath + node.getNamePath())
                    else:
                        destPort = None
                        portName = valueElement.getName()
                        destNode = None
                        if dest.IsA(UsdShade.Material): 
                            destNode = UsdShade.Material(dest)
                        elif dest.IsA(UsdShade.NodeGraph):
                            destNode = UsdShade.NodeGraph(dest)
                        elif dest.IsA(UsdShade.Shader): 
                            destNode = UsdShade.Shader(dest)
                        else:
                            print('> Encountered unsupport destinion type')

                        # Find downstream port (input or output)
                        if destNode:
                            if isInput:
                                # Map from MaterialX to Usd connection syntax
                                if dest.IsA(UsdShade.Material):
                                    portName = mapMtlxToUsdShaderNotation(portName)
                                    portName = 'mtlx:' + portName
                                    destPort = destNode.GetOutput(portName) 
                                else:
                                    destPort = destNode.GetInput(portName) 
                            else:
                                destPort = destNode.GetOutput(portName)                                

                        # Make connection to interface input, or node/nodegraph output
                        if destPort:
                            if interfacename:
                                interfaceInput = sourcePrim.GetInput(sourcePort) 
                                if interfaceInput:
                                    if not destPort.ConnectToSource(interfaceInput):
                                        print('> Failed to connect: ', source.GetPrimPath(), '-->', destPort.GetFullName())
                            else:
                                sourcePrimAPI = sourcePrim.ConnectableAPI()
                                if not destPort.ConnectToSource(sourcePrimAPI, sourcePort):
                                    print('> Failed to connect: ', source.GetPrimPath(), '-->', destPort.GetFullName())
                        else:
                            print('> Failed to find destination port:', portName)

6.1.3 MaterialX to Usd Value Element Mapping¶

Similar to how MaterialX ValueElements are created from Usd, the emitUsdValueElements() utility parses ValueElements to create Usd inputs and outputs.

In this example code, it is possible to create all the inputs based on the MaterialX definition if desired to provide a 'complete' interface for the Usd shader instance. For compactness, MaterialX does not create these additional inputs when instantiating a MaterialX node instance by default. It may be useful to do so for Usd instantiation to avoid any later dependency on the original MaterialX definition, especially if they are not registered in the Usd shader registry (Sdr).

Any inputs created from definitions will have default values which are overwritten by any values explicitly specified on the node instance.

getActiveValueElements() instead of getValueElements() is used when examining definitions and instances to ensure that inherited inputs or outputs are included.

In [15]:
def emitUsdValueElements(node, usdNode, emitAllValueElements):
    """
    Emit MaterialX value elements in Usd.

    Parameters
    ------------    
    node: 
        MaterialX node with value elements to scan
    usdNode:
        UsdShade node to create value elements on.
    emitAllValueElements: bool
        Emit value elements based on node definition, even if not specified on node instance.      
    """
    if not node:
        return    
    
    isMaterial = node.getType() == 'material'
 
    # Instantiate with all the nodedef inputs (if emitAllValueELements is True).
    # Note that outputs are always created.
    nodedef = node.getNodeDef()
    if nodedef and not isMaterial:
        for valueElement in nodedef.getActiveValueElements():
            if valueElement.isA(mx.Input):
                if emitAllValueElements:
                    mtlxType = valueElement.getType()
                    usdType = mapMtxToUsdType(mtlxType)

                    portName = valueElement.getName()
                    usdInput = usdNode.CreateInput(portName, usdType)

                    if len(valueElement.getValueString()) > 0:
                        mtlxValue = valueElement.getValue()
                        usdValue = mapMtxToUsdValue(mtlxType, mtlxValue)
                        if usdValue != '__':
                            usdInput.Set(usdValue)

            elif not isMaterial and valueElement.isA(mx.Output):
                usdOutput = usdNode.CreateOutput(valueElement.getName(), mapMtxToUsdType(valueElement.getType()))

            else:
                print('- Skip mapping of definition element: ', valueElement.getName(), '. Type: ', valueElement.getCategory())

    # From the given instance add inputs and outputs and set values.
    # This may override the default value specified on the definition.
    for valueElement in node.getActiveValueElements():
        if valueElement.isA(mx.Input):
            mtlxType = valueElement.getType()
            usdType = mapMtxToUsdType(mtlxType)
            portName = valueElement.getName()
            if isMaterial:
                # Map from Materials to Usd notation
                portName = mapMtlxToUsdShaderNotation(portName)    
                usdInput = usdNode.CreateOutput('mtlx:' + portName, usdType)
            else:            
                usdInput = usdNode.CreateInput(portName, usdType)

            # Set value. Note that we check the length of the value string
            # instead of getValue() as a 0 value will be skipped.
            if len(valueElement.getValueString()) > 0:
                mtlxValue = valueElement.getValue()
                usdValue = mapMtxToUsdValue(mtlxType, mtlxValue)
                if usdValue != '__':
                    usdInput.Set(usdValue)

        elif not isMaterial and valueElement.isA(mx.Output):
            usdOutput = usdNode.GetInput(valueElement.getName())
            if not usdOutput:
                usdOutput = usdNode.CreateOutput(valueElement.getName(), mapMtxToUsdType(valueElement.getType()))

        else:
            print('- Skip mapping of element: ', valueElement.getNamePath(), '. Type: ', valueElement.getCategory())

6.1.4 Emitting Usd Shading Graphs¶

To emit the Usd shading network a utility function called emitUsdShaderGraph() is added.

  • For each node, nodegraph, or material in the MaterialX document a corresponding node is created in Usd using:
    • UsdShade.Shader.Define(),
    • UsdShade.NodeGraph.Define(), and
    • UsdShade.Material.Define() respectively.
  • All MaterialX node instances are checked for a corresponding MaterialX definition (nodedef), and if found will set the identifier as the shader id for the Usd shader node.
  • Connections are then made between Usd nodes based on the connections found on MaterialX nodes.
In [16]:
def moveChild(newParent, child):
    newChild = newParent.addChildOfCategory(child.getCategory(), child.getName())
    print(newChild.getNamePath())
    newChild.copyContentFrom(child)
    oldParent = child.getParent()
    oldParent.removeChild(child.getName())

def emitUsdShaderGraph(doc, stage, mxnodes, emitAllValueElements):
    """
    Emit Usd shader graph to a given stage from a list of MaterialX nodes.

    Parameters
    ------------    
    doc: 
        MaterialX source document
    stage:
        Usd target stage
    mxnodes:
        MaterialX shader nodes.
    emitAllValueElements: bool
        Emit value elements based on node definition, even if not specified on node instance.      
    """
    materialPath = None

    for v in mxnodes:
        elem = doc.getDescendant(v)
        if elem.getType() == 'material':    
            materialPath = elem.getName()
            break
            
    # Emit Usd nodes
    for v in mxnodes:
        elem = doc.getDescendant(v)

        # Note that MaterialX does not use absolute path notation while Usd
        # does. This will result in an error when trying set the path
        usdPath = '/' + elem.getNamePath()

        nodeDef = None
        usdNode = None
        if elem.getType() == 'material':
            usdNode = UsdShade.Material.Define(stage, usdPath)                
        elif elem.isA(mx.Node):
            nodeDef = elem.getNodeDef()
            if materialPath:
                elemPath = '/' + materialPath + usdPath
            else:
                elemPath = usdPath
            usdNode = UsdShade.Shader.Define(stage, elemPath)
        elif elem.isA(mx.NodeGraph):
            if materialPath:
                elemPath = '/' + materialPath + usdPath
            else:
                elemPath = usdPath
            usdNode = UsdShade.NodeGraph.Define(stage, elemPath)

        if usdNode:
            if nodeDef:
                usdNode.SetShaderId(nodeDef.getName())
            emitUsdValueElements(elem, usdNode, emitAllValueElements)

    # Emit connections between Usd nodes
    for v in mxnodes:
        elem = doc.getDescendant(v)
        usdPath = '/' + elem.getNamePath()

        if elem.getType() == 'material':
            emitUsdConnections(elem, stage, '/')                
        elif elem.isA(mx.Node):
            emitUsdConnections(elem, stage, '/' + materialPath + '/')                
        elif elem.isA(mx.NodeGraph):
            emitUsdConnections(elem, stage, '/' + materialPath + '/')                

Top Level Conversion Logic¶

The sample wrapper for conversion is called convertMtlxToUsd() which takes as input a MaterialX filename, creates a stage in memory and then performs the conversion.

As noted in the Documents learning material MaterialX has one working document, and the node definitions are required to be part of this document. To avoid accidentally translating those definitions, the scene nodes are first determined using a utility: findMaterialXNodes().

In [17]:
def findMaterialXNodes(doc):
    """
    Find all nodes in a MaterialX document
    """
    visitedNodes = []
    treeIter = doc.traverseTree()
    for elem in treeIter:
        path = elem.getNamePath()
        if path in visitedNodes:
            continue
        visitedNodes.append(path)
    return visitedNodes

Pruning based on source URI could also be performed as for export but it is easier to pre-parse the document without any definitions before loading in the definitions.

In [18]:
def convertMtlxToUsd(mtlxFileName, emitAllValueElements):
    """
    Read in a MaterialX file and emit it to a new Usd Stage
    Dump results for display and save to usda file.

    Parameters:
    -----------
    mtlxFileName : string
        Name of file containing MaterialX document. Assumed to end in ".mtlx"
     emitAllValueElements: bool
        Emit value elements based on node definition, even if not specified on node instance.         
    """
    stage = Usd.Stage.CreateInMemory()
    
    doc = mx.createDocument()
    mtlxFilePath = mx.FilePath(mtlxFileName)
    if not mtlxFilePath.exists():
        print('Failed to read file: ', mtlxFilePath.asString())
        return
    
    # Find nodes to transform before importing the definition library
    mx.readFromXmlFile(doc, mtlxFileName)
    mxnodes = findMaterialXNodes(doc)
    stdlib = mx.createDocument()
    libFiles = []
    searchPath = mx.getDefaultDataSearchPath()
    libFiles = mx.loadLibraries(mx.getDefaultDataLibraryFolders(), searchPath, stdlib)
    doc.importLibrary(stdlib)
    
    # Translate
    emitUsdShaderGraph(doc, stage, mxnodes, emitAllValueElements)        

    usdFile = mtlxFileName.removesuffix('.mtlx')
    usdFile = usdFile + '.usda'
    print('Export USD file: ', usdFile)
    stage.Export(usdFile, False)

    return stage

Test Files¶

Conversion to a few test files is performed, including performing the reverse translation of the Usd sample file shown previously.

Sample Marble¶

For the marble example, we turn on the option that will create a Usd node input using all the inputs specified on the definition of each MaterialX shader node instance.

In [19]:
testFile = 'data/sample_marble.mtlx'

# Convert to Usd. Indicate to include all inputs based on a MaterialX node's definition
# as opposed to just those explicitly specified on the node instance.
display_markdown('#### Sample Marble Converted from MaterialX', raw=True)
includeDefinitionInputs = True
stage = convertMtlxToUsd(testFile, includeDefinitionInputs)
stringResult = stage.GetRootLayer().ExportToString()
text = '<details><summary>Usd Results</summary>\n\n' + '```usd\n' + stringResult + '```\n' + '</details>\n' 
display_markdown(text , raw=True)

# Convert back to MaterialX
doc = convertUsdToMtlx(stage, stdlib)
result, error = doc.validate()
if error:
    print(error)
writeOptions = mx.XmlWriteOptions()
writeOptions.writeXIncludeEnable = False
writeOptions.elementPredicate = skipLibraryElement
documentContents = mx.writeToXmlString(doc, writeOptions)
text = '<details><summary>Converted Back to MaterialX</summary>\n\n' + '```xml\n' + documentContents + '```\n' + '</details>\n' 
display_markdown(text , raw=True)

Sample Marble Converted from MaterialX¶

Export USD file:  data/sample_marble.usda
Usd Results
#usda 1.0

def Material "Marble_3D"
{
    token outputs:mtlx:surface.connect = </Marble_3D/SR_marble1.outputs:out>

    def NodeGraph "NG_marble1"
    {
        color3f inputs:base_color_1 = (0.8, 0.8, 0.8)
        color3f inputs:base_color_2 = (0.1, 0.1, 0.3)
        int inputs:noise_octaves = 3
        float inputs:noise_power = 3
        float inputs:noise_scale_1 = 6
        float inputs:noise_scale_2 = 4
        color3f outputs:out.connect = </Marble_3D/NG_marble1/color_mix.outputs:out>

        def Shader "obj_pos"
        {
            uniform token info:id = "ND_position_vector3"
            string inputs:space = "object"
            vector3f outputs:out
        }

        def Shader "add_xyz"
        {
            uniform token info:id = "ND_dotproduct_vector3"
            vector3f inputs:in1 = (0, 0, 0)
            vector3f inputs:in1.connect = </Marble_3D/NG_marble1/obj_pos.outputs:out>
            vector3f inputs:in2 = (1, 1, 1)
            float outputs:out
        }

        def Shader "scale_xyz"
        {
            uniform token info:id = "ND_multiply_float"
            float inputs:in1 = 0
            float inputs:in1.connect = </Marble_3D/NG_marble1/add_xyz.outputs:out>
            float inputs:in2 = 1
            float inputs:in2.connect = </Marble_3D/NG_marble1.inputs:noise_scale_1>
            float outputs:out
        }

        def Shader "scale_pos"
        {
            uniform token info:id = "ND_multiply_vector3FA"
            vector3f inputs:in1 = (0, 0, 0)
            vector3f inputs:in1.connect = </Marble_3D/NG_marble1/obj_pos.outputs:out>
            float inputs:in2 = 1
            float inputs:in2.connect = </Marble_3D/NG_marble1.inputs:noise_scale_2>
            vector3f outputs:out
        }

        def Shader "noise"
        {
            uniform token info:id = "ND_fractal3d_float"
            float inputs:amplitude = 1
            float inputs:diminish = 0.5
            float inputs:lacunarity = 2
            int inputs:octaves = 3
            int inputs:octaves.connect = </Marble_3D/NG_marble1.inputs:noise_octaves>
            vector3f inputs:position.connect = </Marble_3D/NG_marble1/scale_pos.outputs:out>
            float outputs:out
        }

        def Shader "scale_noise"
        {
            uniform token info:id = "ND_multiply_float"
            float inputs:in1 = 0
            float inputs:in1.connect = </Marble_3D/NG_marble1/noise.outputs:out>
            float inputs:in2 = 3
            float outputs:out
        }

        def Shader "sum"
        {
            uniform token info:id = "ND_add_float"
            float inputs:in1 = 0
            float inputs:in1.connect = </Marble_3D/NG_marble1/scale_xyz.outputs:out>
            float inputs:in2 = 0
            float inputs:in2.connect = </Marble_3D/NG_marble1/scale_noise.outputs:out>
            float outputs:out
        }

        def Shader "sin"
        {
            uniform token info:id = "ND_sin_float"
            float inputs:in = 0
            float inputs:in.connect = </Marble_3D/NG_marble1/sum.outputs:out>
            float outputs:out
        }

        def Shader "scale"
        {
            uniform token info:id = "ND_multiply_float"
            float inputs:in1 = 0
            float inputs:in1.connect = </Marble_3D/NG_marble1/sin.outputs:out>
            float inputs:in2 = 0.5
            float outputs:out
        }

        def Shader "bias"
        {
            uniform token info:id = "ND_add_float"
            float inputs:in1 = 0
            float inputs:in1.connect = </Marble_3D/NG_marble1/scale.outputs:out>
            float inputs:in2 = 0.5
            float outputs:out
        }

        def Shader "power"
        {
            uniform token info:id = "ND_power_float"
            float inputs:in1 = 0
            float inputs:in1.connect = </Marble_3D/NG_marble1/bias.outputs:out>
            float inputs:in2 = 1
            float inputs:in2.connect = </Marble_3D/NG_marble1.inputs:noise_power>
            float outputs:out
        }

        def Shader "color_mix"
        {
            uniform token info:id = "ND_mix_color3"
            color3f inputs:bg = (0, 0, 0)
            color3f inputs:bg.connect = </Marble_3D/NG_marble1.inputs:base_color_1>
            color3f inputs:fg = (0, 0, 0)
            color3f inputs:fg.connect = </Marble_3D/NG_marble1.inputs:base_color_2>
            float inputs:mix = 0
            float inputs:mix.connect = </Marble_3D/NG_marble1/power.outputs:out>
            color3f outputs:out
        }
    }

    def Shader "SR_marble1"
    {
        uniform token info:id = "ND_standard_surface_surfaceshader"
        float inputs:base = 1
        color3f inputs:base_color = (0.8, 0.8, 0.8)
        color3f inputs:base_color.connect = </Marble_3D/NG_marble1.outputs:out>
        float inputs:coat = 0
        float inputs:coat_affect_color = 0
        float inputs:coat_affect_roughness = 0
        float inputs:coat_anisotropy = 0
        color3f inputs:coat_color = (1, 1, 1)
        float inputs:coat_IOR = 1.5
        vector3f inputs:coat_normal
        float inputs:coat_rotation = 0
        float inputs:coat_roughness = 0.1
        float inputs:diffuse_roughness = 0
        float inputs:emission = 0
        color3f inputs:emission_color = (1, 1, 1)
        float inputs:metalness = 0
        vector3f inputs:normal
        color3f inputs:opacity = (1, 1, 1)
        float inputs:sheen = 0
        color3f inputs:sheen_color = (1, 1, 1)
        float inputs:sheen_roughness = 0.3
        float inputs:specular = 1
        float inputs:specular_anisotropy = 0
        color3f inputs:specular_color = (1, 1, 1)
        float inputs:specular_IOR = 1.5
        float inputs:specular_rotation = 0
        float inputs:specular_roughness = 0.1
        float inputs:subsurface = 0.4
        float inputs:subsurface_anisotropy = 0
        color3f inputs:subsurface_color = (1, 1, 1)
        color3f inputs:subsurface_color.connect = </Marble_3D/NG_marble1.outputs:out>
        color3f inputs:subsurface_radius = (1, 1, 1)
        float inputs:subsurface_scale = 1
        vector3f inputs:tangent
        float inputs:thin_film_IOR = 1.5
        float inputs:thin_film_thickness = 0
        bool inputs:thin_walled = 0
        float inputs:transmission = 0
        color3f inputs:transmission_color = (1, 1, 1)
        float inputs:transmission_depth = 0
        float inputs:transmission_dispersion = 0
        float inputs:transmission_extra_roughness = 0
        color3f inputs:transmission_scatter = (0, 0, 0)
        float inputs:transmission_scatter_anisotropy = 0
        token outputs:out
    }
}
Converted Back to MaterialX
<?xml version="1.0"?>
<materialx version="1.39">
  <surfacematerial name="Marble_3D" type="material">
    <input name="surfaceshader" type="surfaceshader" nodename="SR_marble1" />
  </surfacematerial>
  <nodegraph name="NG_marble1">
    <input name="base_color_1" type="color3" value="0.800000011920929,0.800000011920929,0.800000011920929" />
    <input name="base_color_2" type="color3" value="0.10000000149011612,0.10000000149011612,0.30000001192092896" />
    <input name="noise_octaves" type="integer" value="3" />
    <input name="noise_power" type="float" value="3.0" />
    <input name="noise_scale_1" type="float" value="6.0" />
    <input name="noise_scale_2" type="float" value="4.0" />
    <output name="out" type="color3" nodename="color_mix" />
    <position name="obj_pos" type="vector3" nodedef="ND_position_vector3">
      <input name="space" type="string" value="object" />
    </position>
    <dotproduct name="add_xyz" type="float" nodedef="ND_dotproduct_vector3">
      <input name="in1" type="vector3" nodename="obj_pos" />
      <input name="in2" type="vector3" value="1.0,1.0,1.0" />
    </dotproduct>
    <multiply name="scale_xyz" type="float" nodedef="ND_multiply_float">
      <input name="in1" type="float" nodename="add_xyz" />
      <input name="in2" type="float" interfacename="noise_scale_1" />
    </multiply>
    <multiply name="scale_pos" type="vector3" nodedef="ND_multiply_vector3FA">
      <input name="in1" type="vector3" nodename="obj_pos" />
      <input name="in2" type="float" interfacename="noise_scale_2" />
    </multiply>
    <fractal3d name="noise" type="float" nodedef="ND_fractal3d_float">
      <input name="amplitude" type="float" value="1.0" />
      <input name="diminish" type="float" value="0.5" />
      <input name="lacunarity" type="float" value="2.0" />
      <input name="octaves" type="integer" interfacename="noise_octaves" />
      <input name="position" type="vector3" nodename="scale_pos" />
    </fractal3d>
    <multiply name="scale_noise" type="float" nodedef="ND_multiply_float">
      <input name="in1" type="float" nodename="noise" />
      <input name="in2" type="float" value="3.0" />
    </multiply>
    <add name="sum" type="float" nodedef="ND_add_float">
      <input name="in1" type="float" nodename="scale_xyz" />
      <input name="in2" type="float" nodename="scale_noise" />
    </add>
    <sin name="sin" type="float" nodedef="ND_sin_float">
      <input name="in" type="float" nodename="sum" />
    </sin>
    <multiply name="scale" type="float" nodedef="ND_multiply_float">
      <input name="in1" type="float" nodename="sin" />
      <input name="in2" type="float" value="0.5" />
    </multiply>
    <add name="bias" type="float" nodedef="ND_add_float">
      <input name="in1" type="float" nodename="scale" />
      <input name="in2" type="float" value="0.5" />
    </add>
    <power name="power" type="float" nodedef="ND_power_float">
      <input name="in1" type="float" nodename="bias" />
      <input name="in2" type="float" interfacename="noise_power" />
    </power>
    <mix name="color_mix" type="color3" nodedef="ND_mix_color3">
      <input name="bg" type="color3" interfacename="base_color_1" />
      <input name="fg" type="color3" interfacename="base_color_2" />
      <input name="mix" type="float" nodename="power" />
    </mix>
  </nodegraph>
  <standard_surface name="SR_marble1" type="surfaceshader" nodedef="ND_standard_surface_surfaceshader">
    <input name="base" type="float" value="1.0" />
    <input name="base_color" type="color3" nodegraph="NG_marble1" />
    <input name="coat" type="float" value="0" />
    <input name="coat_affect_color" type="float" value="0" />
    <input name="coat_affect_roughness" type="float" value="0" />
    <input name="coat_anisotropy" type="float" value="0" />
    <input name="coat_color" type="color3" value="1.0,1.0,1.0" />
    <input name="coat_IOR" type="float" value="1.5" />
    <input name="coat_rotation" type="float" value="0" />
    <input name="coat_roughness" type="float" value="0.10000000149011612" />
    <input name="diffuse_roughness" type="float" value="0" />
    <input name="emission" type="float" value="0" />
    <input name="emission_color" type="color3" value="1.0,1.0,1.0" />
    <input name="metalness" type="float" value="0" />
    <input name="opacity" type="color3" value="1.0,1.0,1.0" />
    <input name="sheen" type="float" value="0" />
    <input name="sheen_color" type="color3" value="1.0,1.0,1.0" />
    <input name="sheen_roughness" type="float" value="0.30000001192092896" />
    <input name="specular" type="float" value="1.0" />
    <input name="specular_anisotropy" type="float" value="0" />
    <input name="specular_color" type="color3" value="1.0,1.0,1.0" />
    <input name="specular_IOR" type="float" value="1.5" />
    <input name="specular_rotation" type="float" value="0" />
    <input name="specular_roughness" type="float" value="0.10000000149011612" />
    <input name="subsurface" type="float" value="0.4000000059604645" />
    <input name="subsurface_anisotropy" type="float" value="0" />
    <input name="subsurface_color" type="color3" nodegraph="NG_marble1" />
    <input name="subsurface_radius" type="color3" value="1.0,1.0,1.0" />
    <input name="subsurface_scale" type="float" value="1.0" />
    <input name="thin_film_IOR" type="float" value="1.5" />
    <input name="thin_film_thickness" type="float" value="0" />
    <input name="thin_walled" type="boolean" value="false" />
    <input name="transmission" type="float" value="0" />
    <input name="transmission_color" type="color3" value="1.0,1.0,1.0" />
    <input name="transmission_depth" type="float" value="0" />
    <input name="transmission_dispersion" type="float" value="0" />
    <input name="transmission_extra_roughness" type="float" value="0" />
    <input name="transmission_scatter" type="color3" value="0.0,0.0,0.0" />
    <input name="transmission_scatter_anisotropy" type="float" value="0" />
  </standard_surface>
</materialx>

Sample Nodegraph from NodeGraph Tutorial¶

Here the example MaterialX file produced from the Nodegraph book is converted.

In [20]:
testFile = 'data/sample_nodegraph.mtlx'
display_markdown('#### Sample Tutorial Nodegraph Converted from MaterialX', raw=True)
stage = convertMtlxToUsd(testFile, False)
stringResult = stage.GetRootLayer().ExportToString()
text = '<details><summary>Usd Results</summary>\n\n' + '```usd\n' + stringResult + '```\n' + '</details>\n' 
display_markdown(text , raw=True)

# Convert back to MaterialX
doc = convertUsdToMtlx(stage, stdlib)
writeOptions = mx.XmlWriteOptions()
writeOptions.writeXIncludeEnable = False
writeOptions.elementPredicate = skipLibraryElement
documentContents = mx.writeToXmlString(doc, writeOptions)
text = '<details><summary>Converted Back to MaterialX</summary>\n\n' + '```xml\n' + documentContents + '```\n' + '</details>\n' 
display_markdown(text , raw=True)

Sample Tutorial Nodegraph Converted from MaterialX¶

Export USD file:  data/sample_nodegraph.usda
Usd Results
#usda 1.0

def Material "my_material"
{
    token outputs:mtlx:surface.connect = </my_material/test_nodegraph.outputs:out>

    def NodeGraph "test_nodegraph"
    {
        float inputs:color_scale = 0.2
        asset inputs:input_file = @checker.png@
        token outputs:out.connect = </my_material/test_nodegraph/test_shader.outputs:out>

        def Shader "test_shader"
        {
            uniform token info:id = "ND_standard_surface_surfaceshader"
            float inputs:base.connect = </my_material/test_nodegraph.inputs:color_scale>
            color3f inputs:base_color.connect = </my_material/test_nodegraph/test_image.outputs:out>
            token outputs:out
        }

        def Shader "test_image"
        {
            uniform token info:id = "ND_image_color3"
            asset inputs:file.connect = </my_material/test_nodegraph.inputs:input_file>
            color3f outputs:out
        }
    }
}
Converted Back to MaterialX
<?xml version="1.0"?>
<materialx version="1.39">
  <surfacematerial name="my_material" type="material">
    <input name="surfaceshader" type="surfaceshader" nodegraph="test_nodegraph" />
  </surfacematerial>
  <nodegraph name="test_nodegraph">
    <input name="color_scale" type="float" value="0.20000000298023224" />
    <input name="input_file" type="filename" value="checker.png" />
    <output name="out" type="token" nodename="test_shader" />
    <standard_surface name="test_shader" type="surfaceshader" nodedef="ND_standard_surface_surfaceshader">
      <input name="base" type="float" interfacename="color_scale" />
      <input name="base_color" type="color3" nodename="test_image" />
    </standard_surface>
    <image name="test_image" type="color3" nodedef="ND_image_color3">
      <input name="file" type="filename" interfacename="input_file" />
    </image>
  </nodegraph>
</materialx>

Re-import Usd Example Converted to MaterialX¶

Finally, the MaterialX file converted from Usd previously is re-converted back into Usd.

For validation purposes bi-directional conversion and compare is useful to ensure there is no loss of data when performing data model interop. At time of writing, this round trip logic is not easily accessible.

In [21]:
testFile = 'data/test_usd_mtlx.mtlx'
display_markdown('#### Nested Nodegraph Converted from MaterialX', raw=True)
stage = convertMtlxToUsd(testFile, False)
stringResult = stage.GetRootLayer().ExportToString()
text = '<details><summary>Usd Results</summary>\n\n' + '```usd\n' + stringResult + '```\n' + '</details>\n' 
display_markdown(text , raw=True)

Nested Nodegraph Converted from MaterialX¶

Export USD file:  data/test_usd_mtlx.usda
Usd Results
#usda 1.0

def Material "collect1"
{
    token outputs:mtlx:displacement.connect = </collect1/my_materialx_subnet.outputs:displacement>
    token outputs:mtlx:surface.connect = </collect1/my_materialx_subnet.outputs:surface>

    def NodeGraph "my_materialx_subnet"
    {
        color3f inputs:base_color
        token outputs:displacement.connect = </collect1/my_materialx_subnet/mtlxdisplacement.outputs:out>
        token outputs:surface.connect = </collect1/my_materialx_subnet/mtlxstandard_surface1.outputs:out>

        def Shader "mtlxstandard_surface1"
        {
            uniform token info:id = "ND_standard_surface_surfaceshader"
            float inputs:base = 1
            color3f inputs:base_color.connect = </collect1/my_materialx_subnet/image_readers.outputs:out>
            float inputs:coat = 0
            float inputs:coat_roughness = 0.1
            float inputs:emission = 0
            color3f inputs:emission_color = (1, 1, 1)
            float inputs:metalness = 0
            float inputs:specular = 1
            color3f inputs:specular_color = (1, 1, 1)
            float inputs:specular_IOR = 1.5
            float inputs:specular_roughness.connect = </collect1/my_materialx_subnet/image_readers.outputs:out_2>
            float inputs:transmission = 0
            token outputs:out
        }

        def NodeGraph "image_readers"
        {
            color3f inputs:_base_color.connect = </collect1/my_materialx_subnet.inputs:base_color>
            color3f outputs:out.connect = </collect1/my_materialx_subnet/image_readers/mtlximage1.outputs:out>
            float outputs:out_2.connect = </collect1/my_materialx_subnet/image_readers/mtlximage2.outputs:out>

            def Shader "mtlximage1"
            {
                uniform token info:id = "ND_image_color3"
                color3f inputs:default.connect = </collect1/my_materialx_subnet/image_readers.inputs:_base_color>
                asset inputs:file = @file1.png@
                color3f outputs:out
            }

            def Shader "mtlximage2"
            {
                uniform token info:id = "ND_image_float"
                asset inputs:file = @file2.png@
                float outputs:out
            }
        }

        def Shader "mtlxdisplacement"
        {
            uniform token info:id = "ND_displacement_float"
            token outputs:out
        }
    }

    def NodeGraph "usdpreview_subnet"
    {
        color3f inputs:base_color
        token outputs:surface.connect = </collect1/usdpreview_subnet/usdpreviewsurface1.outputs:out>

        def Shader "usdpreviewsurface1"
        {
            uniform token info:id = "ND_UsdPreviewSurface_surfaceshader"
            color3f inputs:diffuseColor.connect = </collect1/usdpreview_subnet.inputs:base_color>
            token outputs:out
        }
    }
}

Appendix: Mapping Usd Types To MaterialX Types¶

For a completeness a full mapping of the following applicable Usd types should be performed. Most are mappable but type mapping can be "lossy" as there is no concept of type precision (half, float, double) for instance.

In [22]:
typestring = ''
for t in dir(Sdf.ValueTypeNames):
    if t.startswith('__'):
        continue
    typestring = typestring + '- Type : ' + str(t) + '\n'
text = '<details><summary>Usd Types</summary>\n\n' + typestring + '</details>\n' 
display_markdown(text , raw=True)
Usd Types
  • Type : Asset
  • Type : AssetArray
  • Type : Bool
  • Type : BoolArray
  • Type : Color3d
  • Type : Color3dArray
  • Type : Color3f
  • Type : Color3fArray
  • Type : Color3h
  • Type : Color3hArray
  • Type : Color4d
  • Type : Color4dArray
  • Type : Color4f
  • Type : Color4fArray
  • Type : Color4h
  • Type : Color4hArray
  • Type : Double
  • Type : Double2
  • Type : Double2Array
  • Type : Double3
  • Type : Double3Array
  • Type : Double4
  • Type : Double4Array
  • Type : DoubleArray
  • Type : Find
  • Type : Float
  • Type : Float2
  • Type : Float2Array
  • Type : Float3
  • Type : Float3Array
  • Type : Float4
  • Type : Float4Array
  • Type : FloatArray
  • Type : Frame4d
  • Type : Frame4dArray
  • Type : Group
  • Type : Half
  • Type : Half2
  • Type : Half2Array
  • Type : Half3
  • Type : Half3Array
  • Type : Half4
  • Type : Half4Array
  • Type : HalfArray
  • Type : Int
  • Type : Int2
  • Type : Int2Array
  • Type : Int3
  • Type : Int3Array
  • Type : Int4
  • Type : Int4Array
  • Type : Int64
  • Type : Int64Array
  • Type : IntArray
  • Type : Matrix2d
  • Type : Matrix2dArray
  • Type : Matrix3d
  • Type : Matrix3dArray
  • Type : Matrix4d
  • Type : Matrix4dArray
  • Type : Normal3d
  • Type : Normal3dArray
  • Type : Normal3f
  • Type : Normal3fArray
  • Type : Normal3h
  • Type : Normal3hArray
  • Type : Opaque
  • Type : PathExpression
  • Type : PathExpressionArray
  • Type : Point3d
  • Type : Point3dArray
  • Type : Point3f
  • Type : Point3fArray
  • Type : Point3h
  • Type : Point3hArray
  • Type : Quatd
  • Type : QuatdArray
  • Type : Quatf
  • Type : QuatfArray
  • Type : Quath
  • Type : QuathArray
  • Type : String
  • Type : StringArray
  • Type : TexCoord2d
  • Type : TexCoord2dArray
  • Type : TexCoord2f
  • Type : TexCoord2fArray
  • Type : TexCoord2h
  • Type : TexCoord2hArray
  • Type : TexCoord3d
  • Type : TexCoord3dArray
  • Type : TexCoord3f
  • Type : TexCoord3fArray
  • Type : TexCoord3h
  • Type : TexCoord3hArray
  • Type : TimeCode
  • Type : TimeCodeArray
  • Type : Token
  • Type : TokenArray
  • Type : UChar
  • Type : UCharArray
  • Type : UInt
  • Type : UInt64
  • Type : UInt64Array
  • Type : UIntArray
  • Type : Vector3d
  • Type : Vector3dArray
  • Type : Vector3f
  • Type : Vector3fArray
  • Type : Vector3h
  • Type : Vector3hArray