MaterialX Shader Translators¶

There are a series of BXDF shader translation definitions which are implemented as nodegraphs. They can be found in libraries/bxdf/translators/.

They do not map from all shading models to all other shading models, and have been created based on the what is considered to tbe the "standard" shading model and what other shading models this standard model would typically be translated to. Only surface shading models are currently supported.

The current proposed standard is the "Open PBR Surface" model. See this link for more information: OpenPBR. At time of writing however the "Autodesk Standard Surface" model has the most translation support.

Translators¶

Translation graphs are not limited purely for mapping BXDF nodes but could be used to map any node to any other node type. Additionally it would be useful to map from one version of a given node to another version of the same node type. There is currently no support for this but it is a planned future enhancement (at time of writing this is version 1.39.5 or later).

API Support¶

The MaterialX API provides support for using these translators via the ShaderTranslator class.

An example Python script is available as part of the Python package and can be found under python/Scripts/translateshader.py. The script is overly packaged to include texture baking as well which is not a requirement for remapping.

For clarity we pull out only the required setup here.

Setup¶

First we need to create a document with the appropriate definition libraries loaded. This will include the translator definitions.

In [48]:
import MaterialX as mx

def creeate_working_document() -> dict[str, mx.Document]:
    doc : mx.Document = mx.createDocument()
    stdlib : mx.Document = mx.createDocument()

    searchPath : mx.FileSearchPath = mx.getDefaultDataSearchPath()
    libraryFolders : list[mx.FilePath]= mx.getDefaultDataLibraryFolders()
    libraryFiles : set[str] = mx.loadLibraries(libraryFolders, searchPath, stdlib)
    doc.setDataLibrary(stdlib)
    nodedefs : list[mx.NodeDef] = doc.getNodeDefs()
    print(f"Created working doc with {len(nodedefs)} nodedefs from standard library.")

    result = { "doc": doc, "stdlib": stdlib }
    return result

result = creeate_working_document()
doc : mx.Document = result["doc"]
stdlib : mx.Document = result["stdlib"]
Created working doc with 803 nodedefs from standard library.

All translators have the nodegroup attribute set as translation.

In [49]:
# Look for nodedefs which are translators.
translators : list[mx.NodeDef] = [nd for nd in doc.getNodeDefs() if nd.getNodeGroup() == "translation"]
print(f"Found {len(translators)} translators in the standard library.")
for translator in translators:
    print(f"Translator: {translator.getName()} for target: {translator.getAttribute('target')}")    
Found 4 translators in the standard library.
Translator: ND_open_pbr_surface_to_standard_surface for target: 
Translator: ND_standard_surface_to_gltf_pbr for target: 
Translator: ND_standard_surface_to_open_pbr_surface for target: 
Translator: ND_standard_surface_to_UsdPreviewSurface for target: 

Finding out what is being translated is obfiscated as it's encoded into the name of the node definition (nodedef).

For example "OpenPBR to standard surface is defined as ND_open_pbr_surface_to_standard_surface.

  • Given a definition to extract the "from" and "to" parts we can split the name at _to_ and then remove the ND_ prefix.
  • Given a desired "from" and "to" we need to build the name by inserting _to_ between the two and adding the ND_ prefix.
In [50]:
def derive_translator_name_from_targets(source : str, target : str) -> str:
    return f"ND_{source}_to_{target}"

def find_targets_in_translator_name(name : str) -> tuple[str, str] | None:
    prefix = "ND_"
    infix = "_to_"
    if not name.startswith(prefix):
        return None
    parts = name[len(prefix):].split(infix)
    if len(parts) != 2:
        return None
    return (parts[0], parts[1])
In [51]:
# Get source and target from name
targets_list : list[tuple[str, str]] = []
for translator in translators:
    translator_name = translator.getName()
    targets = find_targets_in_translator_name(translator_name)
    if targets:
        targets_list.append(targets)
        print(f"Translator name '{translator_name}' maps source: '{targets[0]}' to target: '{targets[1]}'")
Translator name 'ND_open_pbr_surface_to_standard_surface' maps source: 'open_pbr_surface' to target: 'standard_surface'
Translator name 'ND_standard_surface_to_gltf_pbr' maps source: 'standard_surface' to target: 'gltf_pbr'
Translator name 'ND_standard_surface_to_open_pbr_surface' maps source: 'standard_surface' to target: 'open_pbr_surface'
Translator name 'ND_standard_surface_to_UsdPreviewSurface' maps source: 'standard_surface' to target: 'UsdPreviewSurface'
In [52]:
def find_translator(doc : mx.Document, source : str, target : str) -> mx.NodeDef:
    derived_name = derive_translator_name_from_targets(source, target)
    # Look for the translator in the document
    translator_nodedef : mx.NodeDef = doc.getNodeDef(derived_name)
    return translator_nodedef

# Derive name from source and target
for source, target in targets_list:
    translator_nodedef : mx.NodeDef = find_translator(doc, source, target)
    if translator_nodedef:
        print(f"- Found translator nodedef: '{translator_nodedef.getName()}' in document.")
- Found translator nodedef: 'ND_open_pbr_surface_to_standard_surface' in document.
- Found translator nodedef: 'ND_standard_surface_to_gltf_pbr' in document.
- Found translator nodedef: 'ND_standard_surface_to_open_pbr_surface' in document.
- Found translator nodedef: 'ND_standard_surface_to_UsdPreviewSurface' in document.

There is no clean API for finding BXDF shading models as they have not specific classification attribute. Below is a utility to find this based on existing library information.

The NodeDef.getNodeString() method provides the name of shading models that can as input to search for translators.

In [53]:
# Scan all nodedefs with output type of "surfaceshader"
def find_all_bxdf(doc : mx.Document) -> list[mx.NodeDef]:
    bxdfs : list[mx.NodeDef] = []
    for nodedef in doc.getNodeDefs():
        if nodedef.getType() == "surfaceshader":    
            if nodedef.getNodeString() not in ["convert", "surface"] and nodedef.getNodeGroup() == "pbr":   
                bxdfs.append(nodedef)
    return bxdfs

bdxfs = find_all_bxdf(doc)
print(f"Found {len(bdxfs)} shading models in the document:")
for bxdf in bdxfs:
    print(f"- Model: NodeDef identifier {bxdf.getName()}. Classification: {bxdf.getNodeString()}. Version: {bxdf.getAttribute('version')}")
Found 6 shading models in the document:
- Model: NodeDef identifier ND_disney_principled. Classification: disney_principled. Version: 
- Model: NodeDef identifier ND_gltf_pbr_surfaceshader. Classification: gltf_pbr. Version: 2.0.1
- Model: NodeDef identifier ND_open_pbr_surface_surfaceshader. Classification: open_pbr_surface. Version: 1.1
- Model: NodeDef identifier ND_standard_surface_surfaceshader. Classification: standard_surface. Version: 1.0.1
- Model: NodeDef identifier ND_standard_surface_surfaceshader_100. Classification: standard_surface. Version: 1.0.0
- Model: NodeDef identifier ND_UsdPreviewSurface_surfaceshader. Classification: UsdPreviewSurface. Version: 2.6
In [54]:
# Create one of each shading model node

for node in doc.getNodes():
    doc.removeNode(node.getName())

def create_bxdf_node(doc : mx.Document, bxdf, add_all_inputs=False) -> mx.Node:

    bxdf_node : mx.Node = doc.addNodeInstance(bxdf, "test_" + bxdf.getName())
    nodedef = doc.getNodeDef(bxdf.getName())
    if add_all_inputs:
        bxdf_node.addInputsFromNodeDef()
    for input in bxdf_node.getInputs():
        if not input.getValue():
            nodedef_input : mx.Input = nodedef.getInput(input.getName())    
            added_default = False
            #if nodedef_input:
            #    default_geom_prop = nodedef_input.getDefaultGeomPropString()
            #    if default_geom_prop:
            #        input.setDefaultGeomPropString(default_geom_prop)
            #        added_default = True
            if not added_default:
                #input.setDefaultValue(noddef.getInput(input.getName()).getDefaultValue())
                bxdf_node.removeInput(input.getName())
    return bxdf_node

for bxdf in bdxfs:
    #print('Creating instace of nodedef:', nodedef.getName())
     bxdf_node : mx.Node = create_bxdf_node(doc, bxdf)
     if bxdf_node:
         print(f"- Created node instance of '{bxdf.getName()}' with {len(bxdf_node.getInputs())} inputs.")  

valid, error = doc.validate()
print(f"- Document validation: {valid}. Errors: '{error}'")


from IPython.display import display_markdown
def print_doc(doc : mx.Document):
    doc_string : str = mx.writeToXmlString(doc)
    display_markdown('```xml\n' + doc_string + '\n```\n', raw=True)
print_doc(doc)
- Created node instance of 'ND_disney_principled' with 0 inputs.
- Created node instance of 'ND_gltf_pbr_surfaceshader' with 0 inputs.
- Created node instance of 'ND_open_pbr_surface_surfaceshader' with 0 inputs.
- Created node instance of 'ND_standard_surface_surfaceshader' with 0 inputs.
- Created node instance of 'ND_standard_surface_surfaceshader_100' with 0 inputs.
- Created node instance of 'ND_UsdPreviewSurface_surfaceshader' with 0 inputs.
- Document validation: True. Errors: ''
<?xml version="1.0"?>
<materialx version="1.39">
  <disney_principled name="test_ND_disney_principled" type="surfaceshader" nodedef="ND_disney_principled" />
  <gltf_pbr name="test_ND_gltf_pbr_surfaceshader" type="surfaceshader" nodedef="ND_gltf_pbr_surfaceshader" />
  <open_pbr_surface name="test_ND_open_pbr_surface_surfaceshader" type="surfaceshader" nodedef="ND_open_pbr_surface_surfaceshader" />
  <standard_surface name="test_ND_standard_surface_surfaceshader" type="surfaceshader" nodedef="ND_standard_surface_surfaceshader" />
  <standard_surface name="test_ND_standard_surface_surfaceshader_100" type="surfaceshader" nodedef="ND_standard_surface_surfaceshader_100" />
  <UsdPreviewSurface name="test_ND_UsdPreviewSurface_surfaceshader" type="surfaceshader" nodedef="ND_UsdPreviewSurface_surfaceshader" />
</materialx>

There are two exposed interfaces on the ShaderTranslator class:

  • translateShader()
  • translateAllMaterials()

The first translates a given shader to the target shading model. The second translates all materials in the document to the target shading model. In general this one is mostly useless as it halts if there are any failures, and there is no way to decide which materials to translate.

In [55]:
from MaterialX import PyMaterialXGenShader as mx_gen_shader
translator : mx_gen_shader.ShaderTranslator = mx_gen_shader.ShaderTranslator.create()
try:
    translator.translateAllMaterials(doc, 'standard_surface')
except LookupError as err:
    print(err)
        

Instead we find all BXDF shader nodes in the document explicitly.

In [56]:
def get_surface_shader_nodes(doc : mx.Document) -> list[mx.Node]:
    surface_shader_nodes : list[mx.Node] = []
    for node in doc.getNodes():
        nodedef : mx.NodeDef | None = node.getNodeDef()
        if nodedef and nodedef.getType() == "surfaceshader":
            surface_shader_nodes.append(node)
    return surface_shader_nodes

surface_shader_nodes = get_surface_shader_nodes(doc)
print(f"Found {len(surface_shader_nodes)} surface shader nodes in the document.")
for node in surface_shader_nodes:
    print(f"- Surface shader node: '{node.getName()}' of type '{node.getType()}' with nodedef '{node.getNodeDef().getName()}'") 
Found 6 surface shader nodes in the document.
- Surface shader node: 'test_ND_disney_principled' of type 'surfaceshader' with nodedef 'ND_disney_principled'
- Surface shader node: 'test_ND_gltf_pbr_surfaceshader' of type 'surfaceshader' with nodedef 'ND_gltf_pbr_surfaceshader'
- Surface shader node: 'test_ND_open_pbr_surface_surfaceshader' of type 'surfaceshader' with nodedef 'ND_open_pbr_surface_surfaceshader'
- Surface shader node: 'test_ND_standard_surface_surfaceshader' of type 'surfaceshader' with nodedef 'ND_standard_surface_surfaceshader'
- Surface shader node: 'test_ND_standard_surface_surfaceshader_100' of type 'surfaceshader' with nodedef 'ND_standard_surface_surfaceshader_100'
- Surface shader node: 'test_ND_UsdPreviewSurface_surfaceshader' of type 'surfaceshader' with nodedef 'ND_UsdPreviewSurface_surfaceshader'

Then we translate them one at a time.

Note that if a translate is not found it has a negative side-effect of leaving behind empty node graphs (nodegraph) elements. We remove them here as a post cleanup step for clarity.

In [57]:
from MaterialX import PyMaterialXGenShader as mx_gen_shader
successfull_nodes : list[mx.Node] = []
for node in surface_shader_nodes:
    target_bxdf = "standard_surface"
    translator = mx_gen_shader.ShaderTranslator.create()
    try:
        source_bxdf = node.getCategory()
        nodedef : mx.NodeDef | None = find_translator(doc, source_bxdf, target_bxdf)
        if not nodedef:
            # Throw an exception to be caught below
            raise LookupError(f"No translator found from '{source_bxdf}' to '{target_bxdf}' for node '{node.getName()}'")
        translator.translateShader(node, target_bxdf )
        successfull_nodes.append(node)
        print(f"- Translated node '{node.getName()}' to target '{target_bxdf}'")
    except LookupError as err:
        print(f"- Failed to translate node '{node.getName()}' to target '{target_bxdf}': {err}")

# Cleanup empty nodegraphs
for nodegraph in doc.getNodeGraphs():
    if len(nodegraph.getNodes()) == 0:
        print('- Removing empty nodegraph:', nodegraph.getName())
        doc.removeNodeGraph(nodegraph.getName())
#print_doc(doc)

filepath : mx.FilePath = mx.FilePath("data/translated_materials.mtlx")
print("- Writing translated document to 'data/translated_materials.mtlx'")
mx.writeToXmlFile(doc, filename=filepath)
- Failed to translate node 'test_ND_disney_principled' to target 'standard_surface': No translator found from 'disney_principled' to 'standard_surface' for node 'test_ND_disney_principled'
- Failed to translate node 'test_ND_gltf_pbr_surfaceshader' to target 'standard_surface': No translator found from 'gltf_pbr' to 'standard_surface' for node 'test_ND_gltf_pbr_surfaceshader'
- Translated node 'test_ND_open_pbr_surface_surfaceshader' to target 'standard_surface'
- Failed to translate node 'test_ND_standard_surface_surfaceshader' to target 'standard_surface': No translator found from 'standard_surface' to 'standard_surface' for node 'test_ND_standard_surface_surfaceshader'
- Failed to translate node 'test_ND_standard_surface_surfaceshader_100' to target 'standard_surface': No translator found from 'standard_surface' to 'standard_surface' for node 'test_ND_standard_surface_surfaceshader_100'
- Failed to translate node 'test_ND_UsdPreviewSurface_surfaceshader' to target 'standard_surface': No translator found from 'UsdPreviewSurface' to 'standard_surface' for node 'test_ND_UsdPreviewSurface_surfaceshader'
- Writing translated document to 'data/translated_materials.mtlx'

Below is the result shown as a Mermaid diagram:

No description has been provided for this image
In [58]:
from MaterialX import PyMaterialXGenShader as mx_gen_shader

target_bxdf = "standard_surface"
translator = mx_gen_shader.ShaderTranslator.create()
try:
    translator.translateAllMaterials(doc, target_bxdf )
except mx.Exception as err:
    print(err)

Versioning¶

At time of writing there is no support for translating between different versions of the same BXDF shader model.

One proposal is to add a source_version and target_version attribute to the translator nodedefs to allow for this functionality in the future.

e.g. We explicitly state which version the source and destination are.

<nodedef name="ND_open_pbr_surface_to_standard_surface" node="open_pbr_surface_to_standard_surface" nodegroup="translation">

would become

<nodedef name="ND_open_pbr_surface_to_standard_surface" node="open_pbr_surface_to_standard_surface" nodegroup="translation"
        source_version="1.1" target_version="1.01" >

To avoid the brittle nature of encoding the source and target names with a "_to" separator in the nodedef name, it would be better to have explicit source and target attributes as well.

<nodedef name="ND_open_pbr_surface_to_standard_surface" node="open_pbr_surface_to_standard_surface" nodegroup="translation"
         source_version="1.1" target_version="1.01" 
         source="open_pbr_surface" target="standard_surface" >

Creating New Translators¶

There are currently no tools or APIs to assist in creating new translators.

Steps:

  1. Get the input from the source nodedef. We also check for a version if specified.
  2. Get the inputs from the destination nodedef. We also check for a version if specified.
  3. Create a new nodegraph definition with the nodegroup attribute set to translation
  4. The inputs from the destination target become the outputs of the nodegraph.
  5. The inputs from the source target become the inputs of the nodegraph.

We will take as an example open_pbr_surface to gltf_pbr.

In [59]:
def create_translator(doc : mx.Document, 
                      source : str, target : str, 
                      source_version = "", target_version = "", 
                      output_doc : mx.Document | None = None) -> mx.NodeDef | None:
    '''
    @brief Create a translator nodedef and nodegraph from source to target.
    @param doc The source document containing the nodedefs.
    @param source The source nodedef nodeString.
    @param source_version The source version string. If empty, use the first source nodedef version found.
    @param target_version The target version string. If empty, use the first target nodedef version found.
    @param target The target nodedef nodeString.
    @param output_doc The document to add the translator to. If None, use the source doc.
    @return The created translator nodedef.
    '''
    if not output_doc:
        output_doc = doc
    
    # Get source and target nodedefs
    nodedefs = find_all_bxdf(doc)

    #nodedefs : list[mx.NodeDef] = doc.getNodeDefs()
    #nodedefs_set = dict((nd.getNodeString() + nd.getVersionString(), nd) for nd in nodedefs)
    #for key, value in nodedefs_set.items():
    #    print(f"Key: '{key}' -> Nodedef: '{value.getNodeString()}'")
    source_nodedef = None
    target_nodedef = None
    for nodedef in nodedefs:
        if nodedef.getNodeString() == source:
            if source_version == "" or nodedef.getVersionString() == source_version:
                source_nodedef = nodedef
        if nodedef.getNodeString() == target:
            if target_version == "" or nodedef.getVersionString() == target_version:
                target_nodedef = nodedef

    if not source_nodedef or not target_nodedef:
        #raise ValueError(f"Source or target nodedef not found for '{source}' to '{target}'")
        if not source_nodedef:
            print(f"Source nodedef not found for '{source}' with version '{source_version}'")
        if not target_nodedef:
            print(f"Target nodedef not found for '{target}' with version '{target_version}'")
        return None
    else: 
        print("Found source nodedef:", source_nodedef.getNodeString(), "version:", source_nodedef.getVersionString())
        print("Found target nodedef:", target_nodedef.getNodeString(), "version:", target_nodedef.getVersionString())

    # 1. Add a new nodedef for the translator    
    derived_name = derive_translator_name_from_targets(source, target)
    nodename = derived_name[3:] if derived_name.startswith("ND_") else derived_name
    translator_nodedef : mx.NodeDef = output_doc.addNodeDef(derived_name)
    translator_nodedef.removeOutput("out")
    translator_nodedef.setNodeString(nodename)
    translator_nodedef.setNodeGroup("translation")
    translator_nodedef.setDocString(f"Translator from '{source}' to '{target}'")

    version1 = source_nodedef.getVersionString()
    if not version1:
        version1 = "1.0"
    translator_nodedef.setAttribute('source_version', version1)
    translator_nodedef.setAttribute('source', source)
    version2 = target_nodedef.getVersionString()
    if not version2:
        version2 = "1.0"
    translator_nodedef.setAttribute('target_version', version2)
    translator_nodedef.setAttribute('target', target)
    
    # Add inputs from source as inputs to the translator
    comment = translator_nodedef.addChildOfCategory("comment")
    comment.setDocString(f"Inputs (inputs from source '{source}')")
    for input in source_nodedef.getActiveInputs():
        #print('add input:', input.getName(), input.getType())
        nodedef_input = translator_nodedef.addInput(input.getName(), input.getType())
        if input.hasValueString():
            nodedef_input.setValueString(input.getValueString())            
    
    # Add inputs from target as outputs to the translator
    comment = translator_nodedef.addChildOfCategory("comment")
    comment.setDocString(f"Outputs (inputs from target '{target}' with '_out' suffix)")
    for input in target_nodedef.getActiveInputs():
        output_name = input.getName() + "_out"
        #print('add output:', output_name, input.getType())
        translator_nodedef.addOutput(output_name, input.getType())
    
    # 2 Create a new functional nodegraph
    comment = doc.addChildOfCategory("comment")
    comment.setDocString(f"NodeGraph implementation for translator '{nodename}'")
    nodegraph_id = 'NG_' + nodename
    nodegraph : mx.NodeGraph = output_doc.addNodeGraph(nodegraph_id)
    nodegraph.setNodeDefString(derived_name)
    nodegraph.setDocString(f"NodeGraph implementation of translator from '{source}' to '{target}'")
    nodegraph.setAttribute('source_version', version1)
    nodegraph.setAttribute('source', source)
    nodegraph.setAttribute('target_version', version2)
    nodegraph.setAttribute('target', target)
    for output in translator_nodedef.getActiveOutputs():
        nodegraph.addOutput(output.getName(), output.getType())

    return translator_nodedef

In the first test we specify a version for the source which does not exist. In this case no translator is created.

In [60]:
source_target = "open_pbr_surface"
dest_target ='gltf_pbr'

new_doc = mx.createDocument()
source_version = '2.4'
target_version = ''
new_translator_nodedef : mx.NodeDef | None = create_translator(doc, source_target, dest_target, 
                                                        source_version, target_version, new_doc)
if new_translator_nodedef:
    print(f"Created new translator nodedef: '{new_translator_nodedef.getName()}' from '{source_target}' to '{dest_target}'")
    print_doc(new_doc)

    valid, error = doc.validate()
    print(f"- Document validation: {valid}. Errors: '{error}'")
else:
    print("Failed to create translator nodedef.")
Source nodedef not found for 'open_pbr_surface' with version '2.4'
Failed to create translator nodedef.

In this second test we specify existing versions.

If no version is specified and the first version found is used. It is not recommended to not specify a version as the ordering of definitions stored could be arbitrary.

In [61]:
source_version = '1.1'
target_version = '2.0.1'
new_translator_nodedef : mx.NodeDef | None = create_translator(doc, source_target, dest_target, 
                                                        source_version, target_version, new_doc)
if new_translator_nodedef:
    print(f"Created new translator nodedef: '{new_translator_nodedef.getName()}' from '{source_target}' to '{dest_target}'")
    print_doc(new_doc)

    valid, error = doc.validate()
    print(f"- Document validation: {valid}. Errors: '{error}'")  

    mx.writeToXmlFile(new_doc, filename=mx.FilePath("data/open_pbr_to_gltf_pbr.mtlx"))  
else:
    print("Failed to create translator nodedef.")
Found source nodedef: open_pbr_surface version: 1.1
Found target nodedef: gltf_pbr version: 2.0.1
Created new translator nodedef: 'ND_open_pbr_surface_to_gltf_pbr' from 'open_pbr_surface' to 'gltf_pbr'
<?xml version="1.0"?>
<materialx version="1.39">
  <nodedef name="ND_open_pbr_surface_to_gltf_pbr" node="open_pbr_surface_to_gltf_pbr" nodegroup="translation" doc="Translator from 'open_pbr_surface' to 'gltf_pbr'" source_version="1.1" source="open_pbr_surface" target_version="2.0.1" target="gltf_pbr">
    <!--Inputs (inputs from source 'open_pbr_surface')-->
    <input name="base_weight" type="float" value="1.0" />
    <input name="base_color" type="color3" value="0.8, 0.8, 0.8" />
    <input name="base_diffuse_roughness" type="float" value="0.0" />
    <input name="base_metalness" type="float" value="0.0" />
    <input name="specular_weight" type="float" value="1.0" />
    <input name="specular_color" type="color3" value="1, 1, 1" />
    <input name="specular_roughness" type="float" value="0.3" />
    <input name="specular_ior" type="float" value="1.5" />
    <input name="specular_roughness_anisotropy" type="float" value="0.0" />
    <input name="transmission_weight" type="float" value="0.0" />
    <input name="transmission_color" type="color3" value="1, 1, 1" />
    <input name="transmission_depth" type="float" value="0.0" />
    <input name="transmission_scatter" type="color3" value="0, 0, 0" />
    <input name="transmission_scatter_anisotropy" type="float" value="0.0" />
    <input name="transmission_dispersion_scale" type="float" value="0.0" />
    <input name="transmission_dispersion_abbe_number" type="float" value="20.0" />
    <input name="subsurface_weight" type="float" value="0" />
    <input name="subsurface_color" type="color3" value="0.8, 0.8, 0.8" />
    <input name="subsurface_radius" type="float" value="1.0" />
    <input name="subsurface_radius_scale" type="color3" value="1.0, 0.5, 0.25" />
    <input name="subsurface_scatter_anisotropy" type="float" value="0.0" />
    <input name="fuzz_weight" type="float" value="0.0" />
    <input name="fuzz_color" type="color3" value="1, 1, 1" />
    <input name="fuzz_roughness" type="float" value="0.5" />
    <input name="coat_weight" type="float" value="0.0" />
    <input name="coat_color" type="color3" value="1, 1, 1" />
    <input name="coat_roughness" type="float" value="0.0" />
    <input name="coat_roughness_anisotropy" type="float" value="0.0" />
    <input name="coat_ior" type="float" value="1.6" />
    <input name="coat_darkening" type="float" value="1.0" />
    <input name="thin_film_weight" type="float" value="0" />
    <input name="thin_film_thickness" type="float" value="0.5" />
    <input name="thin_film_ior" type="float" value="1.4" />
    <input name="emission_luminance" type="float" value="0.0" />
    <input name="emission_color" type="color3" value="1, 1, 1" />
    <input name="geometry_opacity" type="float" value="1" />
    <input name="geometry_thin_walled" type="boolean" value="false" />
    <input name="geometry_normal" type="vector3" />
    <input name="geometry_coat_normal" type="vector3" />
    <input name="geometry_tangent" type="vector3" />
    <input name="geometry_coat_tangent" type="vector3" />
    <!--Outputs (inputs from target 'gltf_pbr' with '_out' suffix)-->
    <output name="base_color_out" type="color3" />
    <output name="metallic_out" type="float" />
    <output name="roughness_out" type="float" />
    <output name="normal_out" type="vector3" />
    <output name="tangent_out" type="vector3" />
    <output name="occlusion_out" type="float" />
    <output name="transmission_out" type="float" />
    <output name="specular_out" type="float" />
    <output name="specular_color_out" type="color3" />
    <output name="ior_out" type="float" />
    <output name="alpha_out" type="float" />
    <output name="alpha_mode_out" type="integer" />
    <output name="alpha_cutoff_out" type="float" />
    <output name="iridescence_out" type="float" />
    <output name="iridescence_ior_out" type="float" />
    <output name="iridescence_thickness_out" type="float" />
    <output name="sheen_color_out" type="color3" />
    <output name="sheen_roughness_out" type="float" />
    <output name="clearcoat_out" type="float" />
    <output name="clearcoat_roughness_out" type="float" />
    <output name="clearcoat_normal_out" type="vector3" />
    <output name="emissive_out" type="color3" />
    <output name="emissive_strength_out" type="float" />
    <output name="thickness_out" type="float" />
    <output name="attenuation_distance_out" type="float" />
    <output name="attenuation_color_out" type="color3" />
    <output name="anisotropy_strength_out" type="float" />
    <output name="anisotropy_rotation_out" type="float" />
    <output name="dispersion_out" type="float" />
  </nodedef>
  <nodegraph name="NG_open_pbr_surface_to_gltf_pbr" nodedef="ND_open_pbr_surface_to_gltf_pbr" doc="NodeGraph implementation of translator from 'open_pbr_surface' to 'gltf_pbr'" source_version="1.1" source="open_pbr_surface" target_version="2.0.1" target="gltf_pbr">
    <output name="base_color_out" type="color3" />
    <output name="metallic_out" type="float" />
    <output name="roughness_out" type="float" />
    <output name="normal_out" type="vector3" />
    <output name="tangent_out" type="vector3" />
    <output name="occlusion_out" type="float" />
    <output name="transmission_out" type="float" />
    <output name="specular_out" type="float" />
    <output name="specular_color_out" type="color3" />
    <output name="ior_out" type="float" />
    <output name="alpha_out" type="float" />
    <output name="alpha_mode_out" type="integer" />
    <output name="alpha_cutoff_out" type="float" />
    <output name="iridescence_out" type="float" />
    <output name="iridescence_ior_out" type="float" />
    <output name="iridescence_thickness_out" type="float" />
    <output name="sheen_color_out" type="color3" />
    <output name="sheen_roughness_out" type="float" />
    <output name="clearcoat_out" type="float" />
    <output name="clearcoat_roughness_out" type="float" />
    <output name="clearcoat_normal_out" type="vector3" />
    <output name="emissive_out" type="color3" />
    <output name="emissive_strength_out" type="float" />
    <output name="thickness_out" type="float" />
    <output name="attenuation_distance_out" type="float" />
    <output name="attenuation_color_out" type="color3" />
    <output name="anisotropy_strength_out" type="float" />
    <output name="anisotropy_rotation_out" type="float" />
    <output name="dispersion_out" type="float" />
  </nodegraph>
</materialx>
- Document validation: True. Errors: ''

Updated Translation Search¶

To improve the translation search to account for versions, we can implement a more robust search mechanism such as the following:

This could be incorporated into the ShaderTranslator API which performs the translation replacing the existing literal string search.

In [62]:
def find_translator(doc : mx.Document, 
                    source : str, target : str, 
                    source_version : str, target_version: str) -> mx.NodeDef | None:
    '''
    @brief Find a translator nodedef from source to target with specific versions.
    @param doc The document to search in.
    @param source The source nodedef nodeString.
    @param target The target nodedef nodeString.
    @param source_version The source version string.
    @param target_version The target version string.
    @return The found translator nodedef or None.
    '''
    nodedefs = doc.getNodeDefs()
    for nodedef in nodedefs:
        if nodedef.getNodeGroup() == "translation":
            nodedef_source = nodedef.getAttribute('source')
            nodedef_target = nodedef.getAttribute('target')
            nodedef_source_version = nodedef.getAttribute('source_version')
            nodedef_target_version = nodedef.getAttribute('target_version')
            if (nodedef_source == source and nodedef_target == target and
                nodedef_source_version == source_version and
                nodedef_target_version == target_version):
                return nodedef
    return None


nodedef = find_translator(new_doc, source_target, dest_target, source_version, target_version)
if nodedef:
    print(f"Found translator nodedef: '{nodedef.getName()}' from '{source_target}' to '{dest_target}' with versions '{source_version}' to '{target_version}'")
else:
    print(f"Translator not found for '{source_target}' to '{dest_target}' with versions '{source_version}' to {target_version}.")

target_version = "0.9"
nodedef = find_translator(new_doc, source_target, dest_target, source_version, target_version)
if nodedef:
    print(f"Found translator nodedef: '{nodedef.getName()}' from '{source_target}' to '{dest_target}' with versions '{source_version}' to '{target_version}'")
else:
    print(f"Translator not found for '{source_target}' to '{dest_target}' with versions '{source_version}' to {target_version}.")
Found translator nodedef: 'ND_open_pbr_surface_to_gltf_pbr' from 'open_pbr_surface' to 'gltf_pbr' with versions '1.1' to '2.0.1'
Translator not found for 'open_pbr_surface' to 'gltf_pbr' with versions '1.1' to 0.9.