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. Some useful additions would include:

  • Adding new graphs to map any node to any other node type as appropriate.
  • Giving more control over how translation is performed, suggesting to never use the API to translate every shader in a document.
    • Translation without addition graph grouping. This can create unsupported nested graphs, and lacks control over graph attributes.
    • 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).

Ungrouped and versioned translation are demonstrated in this notebook.

Current 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 [174]:
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 [175]:
# 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(mx.prettyPrint(translator))
    name = translator.getName()
    #source_target = name.split("_to_")
    #source = source_target[0] if len(source_target) > 1 else "unknown"
    #source = source.split("_", 1)[-1]
    #target = source_target[1] if len(source_target) > 1 else "unknown" 
    print(f"Translator: {translator.getName()} found.")
Found 4 translators in the standard library.
Translator: ND_open_pbr_surface_to_standard_surface found.
Translator: ND_standard_surface_to_gltf_pbr found.
Translator: ND_standard_surface_to_open_pbr_surface found.
Translator: ND_standard_surface_to_UsdPreviewSurface found.

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 [176]:
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:
    source_target = name.split("_to_")
    source = source_target[0] if len(source_target) > 1 else "unknown"
    source = source.split("_", 1)[-1]
    target = source_target[1] if len(source_target) > 1 else "unknown" 
    #prefix = "ND_"
    #infix = "_to_"
    #if not name.startswith(prefix):
    #    return None
    #parts = name[len(prefix):].split(infix)
    #if len(parts) != 2:
    #    return None
    return (source, target)
In [177]:
# Get source and target from name
targets_list : list[tuple[str, str]] = []
for translator in translators:
    translator_name = translator.getName()
    source_target = find_targets_in_translator_name(translator_name)
    if source_target:
        targets_list.append(source_target)
        print(f"Translator name '{translator_name}' maps source: '{source_target[0]}' to target: '{source_target[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 [178]:
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 [179]:
# 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" and nodedef.getNodeGroup() == "pbr":
            if nodedef.getNodeString() not in ["convert", "surface"] :   
                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

Create in an instance of a these nodes is performed before to use as sources for demonstrating translation usage.

In [180]:
# 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()}' ({bxdf_node.getName()}) with {len(bxdf_node.getInputs())} inputs.")  

valid, error = doc.validate()
if not valid:
    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' (test_ND_disney_principled) with 0 inputs.
- Created node instance of 'ND_gltf_pbr_surfaceshader' (test_ND_gltf_pbr_surfaceshader) with 0 inputs.
- Created node instance of 'ND_open_pbr_surface_surfaceshader' (test_ND_open_pbr_surface_surfaceshader) with 0 inputs.
- Created node instance of 'ND_standard_surface_surfaceshader' (test_ND_standard_surface_surfaceshader) with 0 inputs.
- Created node instance of 'ND_standard_surface_surfaceshader_100' (test_ND_standard_surface_surfaceshader_100) with 0 inputs.
- Created node instance of 'ND_UsdPreviewSurface_surfaceshader' (test_ND_UsdPreviewSurface_surfaceshader) with 0 inputs.
<?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 API is unpredictable as it halts if there are any failures, and there is no way to specify which materials to translate.

In [181]:
from MaterialX import PyMaterialXGenShader as mx_gen_shader

translator : mx_gen_shader.ShaderTranslator = mx_gen_shader.ShaderTranslator.create()

def translate_all():
    try:
        translator.translateAllMaterials(doc, 'standard_surface')
    except LookupError as err:
        print(err)
        

Instead we find all BXDF shader nodes in the document:

In [182]:
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'

and then translate them one at a time.

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

In [183]:
from MaterialX import PyMaterialXGenShader as mx_gen_shader

doc2 = mx.createDocument()
doc2.copyContentFrom(doc)
doc2.setDataLibrary(doc.getDataLibrary())
#print(mx.prettyPrint(doc2))

surface_shader_nodes = get_surface_shader_nodes(doc2)
#print(f"Found {len(surface_shader_nodes)} surface shader nodes in the copied document.")

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(doc2, 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 doc2.getNodeGraphs():
    if len(nodegraph.getNodes()) == 0:
        print('- Removing empty nodegraph:', nodegraph.getName())
        doc2.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(doc2, 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

There is no way to just create a translator without also creating a graph with some generic name which must be discovered after the fact.

We create a utility to do so for greater control.

In [184]:
def translate_ungrouped(doc : mx.Document, source_bxdf : str, target_bxdf : str) -> dict[str, mx.Node] | None: 
    '''
    @brief Translate a shader node of source_bxdf to target_bxdf using ungrouped nodes.
    @detail This function creates a target node and a translation node based on the translator nodedef, then 
    makes upstream and downstream connections.
    @param doc The document to operate on.
    @param source_bxdf The source BXDF shading model name.
    @param target_bxdf The target BXDF shading model name.
    @return A dictionary with 'translationNode' and 'targetNode' if successful, None otherwise.
    '''

    # Look for a translator if one exists.
    nodedef : mx.NodeDef | None = find_translator(doc3, source_bxdf, target_bxdf)
    if not nodedef:
        print(f"- No translator found from '{source_bxdf}' to '{target_bxdf}' for node '{node.getName()}'")
        return None

    # Create a target node of the target_bxdf category.
    print('> Add target node of category:', target_bxdf)
    targetNode = doc3.addChildOfCategory(target_bxdf, node.getName() + "_target")
    if not targetNode:
        print(f"- Failed to create target node of category '{target_bxdf}' for node '{node.getName()}'")
        return None    
    targetNode.setType("surfaceshader")
    targetNode.addInputsFromNodeDef()

    # Create a translation node based on the translator nodedef.
    print('> Add translation node of category:', nodedef.getName())
    translationNode = doc3.addNodeInstance(nodedef, node.getName() + "_translator")
    translationNode.addInputsFromNodeDef()

    # Connect translation outputs to target inputs.
    print('> Add translation outputs')
    for output in nodedef.getActiveOutputs():
        #print('Add output:', output.getName())
        translationOutput = translationNode.addOutput(output.getName(), output.getType())
        translationOutput.copyContentFrom(output)  
        target_input_name = output.getName()
        # Remove trailing '_out' from name
        target_input_name = target_input_name[:-4] if target_input_name.endswith('_out') else target_input_name
        target_input = targetNode.getInput(target_input_name)
        if not target_input:
            print(f" - Warning: Target node '{targetNode.getName()}' has no input named '{target_input_name}' for output '{output.getName()}'")
            continue
        else:
            #print('Target input name:', target_input_name)
            target_input.setNodeName(translationNode.getName())
            target_input.setOutputString(translationOutput.getName())
            target_input.removeAttribute('value')

    # Copy over inputs from the source node to the translation node.
    # Note that this will copy over all attributes including upstream connections.
    print('> Add translation inputs.')
    for input in node.getActiveInputs():
        translationInput = translationNode.getInput(input.getName())
        print('>> Overwrite input:', translationInput.getName())
        if translationInput:
            # Thish will copy over all attributes including
            # updstream connections
            translationInput.copyContentFrom(input)

    return {'translationNode' : translationNode, 
            'targetNode' : targetNode }

In the code below we use the previously created document with source nodes to translate. We add in a material node for rendering purposes.

In [185]:
def add_material_node(doc : mx.Document, node : mx.Node) -> mx.Node:
    mat_node_name = doc.createValidChildName(node.getCategory() + '_material')
    mat_node = doc.addNode("surfacematerial", mat_node_name, 'material')
    mat_input = mat_node.addInput('surfaceshader', 'surfaceshader')
    mat_input.setNodeName(node.getName())
    return mat_node


doc3 = mx.createDocument()
doc3.copyContentFrom(doc)
doc3.setDataLibrary(doc.getDataLibrary())

surface_shader_nodes = get_surface_shader_nodes(doc3)
print(f"Found {len(surface_shader_nodes)} surface shader nodes in the copied document.")

successfull_nodes : list[mx.Node] = []
for node in surface_shader_nodes:
    target_bxdf = "standard_surface"
    #translator = mx_gen_shader.ShaderTranslator.create()

    source_bxdf = node.getCategory()

    result = translate_ungrouped(doc3, source_bxdf, target_bxdf)
    if result:
        print('Add translation node:', result['translationNode'].getName())
        print('Add target node:', result['targetNode'].getName())

        # (Optional) Add `surfacematerial` node.
        add_material_node(doc3, result['targetNode'])

    doc3.removeChild(node.getName())

    successfull_nodes.append(node)

status, errors = doc3.validate()
if not status:
    print('- Document validation failed with errors:')
    print(errors)

filepath : mx.FilePath = mx.FilePath("data/translated_materials_no_graph.mtlx")
print(f"- Writing translated document to '{filepath.asString()}")
mx.writeToXmlFile(doc3, filename=filepath)
Found 6 surface shader nodes in the copied document.
- No translator found from 'disney_principled' to 'standard_surface' for node 'test_ND_disney_principled'
- No translator found from 'gltf_pbr' to 'standard_surface' for node 'test_ND_gltf_pbr_surfaceshader'
> Add target node of category: standard_surface
> Add translation node of category: ND_open_pbr_surface_to_standard_surface
> Add translation outputs
> Add translation inputs.
Add translation node: test_ND_open_pbr_surface_surfaceshader_translator
Add target node: test_ND_open_pbr_surface_surfaceshader_target
- No translator found from 'standard_surface' to 'standard_surface' for node 'test_ND_standard_surface_surfaceshader'
- No translator found from 'standard_surface' to 'standard_surface' for node 'test_ND_standard_surface_surfaceshader_100'
- No translator found from 'UsdPreviewSurface' to 'standard_surface' for node 'test_ND_UsdPreviewSurface_surfaceshader'
- Document validation failed with errors:
Node input binds no value or connection: <input name="coat_normal" type="vector3">
Node input binds no value or connection: <input name="normal" type="vector3">
Node input binds no value or connection: <input name="tangent" type="vector3">

- Writing translated document to 'data\translated_materials_no_graph.mtlx

Resulting graph shows new translation node for OpenPBR to a new standard surface target node.

Direct usage of translation node to target in MXLab Editor.

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.
  6. For convenience if it is known that a source nodedef's input can be (directly) routed to a target nodedef's input, a name mapping can be provided. This will result in a default direct routing via a dot node. Users can modify the computation as needed after the fact.

We will take as an example open_pbr_surface to gltf_pbr.

In [186]:
def create_translator(doc : mx.Document, 
                      source : str, target : str, 
                      source_version = "", target_version = "", 
                      mappings = None,
                      output_doc : mx.Document | None = None) -> mx.NodeDef | None:
    '''
    @brief Create a translator nodedef and nodegraph from source to target definitions.
    @param doc The source document containing the definition.
    @param source The source definition category.
    @param target The target definition category.
    @param source_version The source version string. If empty, use the first source definition version found.
    @param target_version The target version string. If empty, use the first target definition version found.
    @param mappings A dictionary mapping source input names to target input names.
    @param output_doc The document to add the translator to. If None, use the source doc.
    @return The created translator definition.
    '''
    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())

    for source_input_name, target_input_name in mappings.items():
        source_input = translator_nodedef.getInput(source_input_name)
        output_name = target_input_name + "_out"
        target_output = nodegraph.getOutput(output_name)
        if source_input and target_output:
            dot_name = nodegraph.createValidChildName(target_input_name)
            comment = nodegraph.addChildOfCategory("comment")
            comment.setDocString(f"Routing source input: '{source_input_name}' to target input: '{target_input_name}'")
            dot_node = nodegraph.addNode('dot', dot_name)
            dot_inpput = dot_node.addInput('in', source_input.getType())
            dot_inpput.setInterfaceName(source_input.getName())
            target_output.setNodeName(dot_node.getName()) 
            print(f" - Added connection from input '{source_input.getName()}' to output '{target_output.getName()}'")

    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 [187]:
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.

We also provide an example of mapping usage. Here we map base_color from the source definition toe base_color on the target definition.

In [188]:
# Source and target versions
source_version = '1.1'
target_version = '2.0.1'

# Input mappings
mappings = dict()
mappings['base_color']  = 'base_color'

# Create translator with mappings
new_translator_nodedef : mx.NodeDef | None = create_translator(doc, source_target, dest_target, 
                                                        source_version, target_version, mappings, 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
 - Added connection from input 'base_color' to output 'base_color_out'
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" nodename="base_color" />
    <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" />
    <!--Routing source input: 'base_color' to target input: 'base_color'-->
    <dot name="base_color" type="color3">
      <input name="in" type="color3" interfacename="base_color" />
    </dot>
  </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 [189]:
def find_translator_versioned(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_versioned(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_versioned(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.