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:
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.
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.
#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.
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.23.11 - Sdr nodes { UsdTransform2d, HwPtexTexture, HwFieldReader_float, HwUvTexture, CylinderLight, HwFieldReader, HwFieldReader_float2, MeshLight, HwFieldReader_float3, HwPrimvar, DomeLight, DomeLight_1, DiskLight, UsdPrimvarReader_string, PortalLight, RectLight, GeometryLight, UsdPrimvarReader_float4, SphereLight, DistantLight, VolumeLight, UsdPreviewSurface, UsdUVTexture, UsdPrimvarReader_float, UsdPrimvarReader_float2, UsdPrimvarReader_float3, UsdPrimvarReader_int, UsdPrimvarReader_normal, UsdPrimvarReader_point, UsdPrimvarReader_vector, UsdPrimvarReader_matrix } Using MaterialX Version: 1.39.0
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()
# 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)
#usda 1.0
(
doc = """Generated from Composed Stage of root layer d:\\Work\\materialx\\MaterialX_Learn_Private\\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
}
}
}
}
}
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.
# 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
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.
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
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.
The first step is to add in a basic setup for MaterialX to create a working document and load in standard definitions.
# 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 750 definitions
Next, translation logic is broken up into a series of utilities which perform Usd to MaterialX mappings.
The first of these are utilities for value and type mapping:
mapUsdTypeToMtlx()
maps native Usd type strings to MaterialX native type strings.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.
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'
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')
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
emitMtlxValueELements
handles the mapping of Usd inputs and outputs to MaterialX inputs and outputs.
This includes:
GetConnectedSources()
in Usd is roughly requivalent to getConnectedNode()
in MaterialX. One interesting difference is that "valid" vs "invalid" sources can be returned.connect
syntax and corresponding API for connection logic and behaviour, MaterialX can require multiple attributes to specified to when creating / modifying a connection.nodegraph
or node
, or interface input
then 1 of 3 different attributes are set;multioutput
type), an additional output
attribute is required; andchannel
attribute is required. Logic for channels is not included as part of this example.Notes
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 namespace
s are flattened on import from MaterialX in UsdMtlx
at the current time.
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
).
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.
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)
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:
UsdPrim
. This differs from MaterialX which does not separate out the functional API, with all types deriving from a common Element
class.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.
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 750 definitions
<?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>
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
.
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.
# 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
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:
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
}
}
}
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.
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
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
.
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)
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.
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())
To emit the Usd shading network a utility function called emitUsdShaderGraph()
is added.
UsdShade.Shader.Define()
,UsdShade.NodeGraph.Define()
, andUsdShade.Material.Define()
respectively.nodedef
), and if found will setthe identifier as the shader id for the Usd shader node.
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 + '/')
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()
.
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.
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
Conversion to a few test files is performed, including performing the reverse translation of the Usd sample file shown previously.
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.
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)
Export USD file: data/sample_marble.usda
#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
}
}
<?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>
Here the example MaterialX file produced from the Nodegraph book is converted.
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)
Export USD file: data/sample_nodegraph.usda
#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
}
}
}
<?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>
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.
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)
Export USD file: data/test_usd_mtlx.usda
#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
}
}
}
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.
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)