{ "cells": [ { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "# USD and MaterialX NodeGraphs\n", "\n", "This notebook will look at some of the basic interop between MaterialX and USD focusing on nodegraphs and nodes.\n", "Material assignment will not be examined, nor is the intent to provide a tutorial about Usd, which can be found in other places\n", "such as from Pixar, NVIDIA, and Houdini\n", "\n", "Topics covered include:\n", "1. Usd and MaterialX Package Setup\n", "2. Translating a Usd file with MaterialX materials to MaterialX format, focusing on traversal, pathing, and connection mapping. \n", "3. Translating a MaterialX into Usd format with the same focus.\n", "\n", "> Neither translators are intended to be a full importer or exporter but rather usable for learning purposes, noting that\n", "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." ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## 1. Usd Setup\n", "\n", "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. " ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "vscode": { "languageId": "shellscript" } }, "outputs": [], "source": [ "#pip install usd-core" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "After installation various packages can be imported. `Usd`, `UsdShade`, `Sdf`, and `Gf` are the main packages used. The MaterialX package is also imported. \n", "| Note that the definition registry (`Sdr`) does not contain any MaterialX definitions." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Using Usd Version: 0.23.11\n", "- 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 }\n", "Using MaterialX Version: 1.39.0\n" ] } ], "source": [ "from pxr import Usd\n", "from pxr import UsdShade\n", "from pxr import Sdf\n", "from pxr import Gf\n", "from pxr import UsdGeom\n", "from pxr import Sdr\n", "\n", "# For Markdown output display\n", "from IPython.display import display_markdown\n", "\n", "import MaterialX as mx\n", "\n", "major, minor, build = Usd.GetVersion() \n", "print('Using Usd Version:', str(major) + \".\" + str(minor) + \".\" + str(build))\n", "if Sdr.Registry():\n", " print('- Sdr nodes {', ', '.join(Sdr.Registry().GetNodeNames()), '}')\n", "print('Using MaterialX Version:', mx.getVersionString())" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "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()` " ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "
Flattened Usd File\n", "\n", "```usd\n", "#usda 1.0\n", "(\n", " doc = \"\"\"Generated from Composed Stage of root layer d:\\\\Work\\\\materialx\\\\MaterialX_Learn_Private\\\\pymaterialx\\\\data\\\\sphere_with_nodegraphs.usda\n", "\"\"\"\n", " endTimeCode = 1\n", " framesPerSecond = 24\n", " metersPerUnit = 1\n", " startTimeCode = 1\n", " timeCodesPerSecond = 24\n", " upAxis = \"Y\"\n", ")\n", "\n", "def Xform \"mySphere\" (\n", " kind = \"component\"\n", ")\n", "{\n", " def Sphere \"geo\"\n", " {\n", " float3[] extent = [(-1, -1, -1), (1, 1, 1)]\n", " double radius = 1\n", " matrix4d xformOp:transform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) )\n", " uniform token[] xformOpOrder = [\"xformOp:transform\"]\n", " }\n", "\n", " def Scope \"mtl\"\n", " {\n", " def Material \"collect1\"\n", " {\n", " color3f inputs:base_color = (1, 0, 0) (\n", " displayName = \"Base Color for Material Interface\"\n", " )\n", " token outputs:mtlx:displacement.connect = \n", " token outputs:mtlx:surface.connect = \n", " token outputs:surface.connect = \n", "\n", " def NodeGraph \"my_materialx_subnet\"\n", " {\n", " color3f inputs:base_color.connect = \n", " token outputs:displacement.connect = \n", " token outputs:surface.connect = \n", "\n", " def Shader \"mtlxstandard_surface1\"\n", " {\n", " uniform token info:id = \"ND_standard_surface_surfaceshader\"\n", " float inputs:base = 1\n", " color3f inputs:base_color.connect = \n", " float inputs:coat = 0\n", " float inputs:coat_roughness = 0.1\n", " float inputs:emission = 0\n", " color3f inputs:emission_color = (1, 1, 1)\n", " float inputs:metalness = 0\n", " float inputs:specular = 1\n", " color3f inputs:specular_color = (1, 1, 1)\n", " float inputs:specular_IOR = 1.5\n", " float inputs:specular_roughness = 0.2\n", " float inputs:specular_roughness.connect = \n", " float inputs:transmission = 0\n", " token outputs:out\n", " }\n", "\n", " def NodeGraph \"image_readers\"\n", " {\n", " color3f inputs:_base_color.connect = \n", " color3f outputs:out.connect = \n", " float outputs:out_2.connect = \n", "\n", " def Shader \"mtlximage1\"\n", " {\n", " uniform token info:id = \"ND_image_color3\"\n", " color3f inputs:default.connect = \n", " asset inputs:file = @file1.png@\n", " color3f outputs:out\n", " }\n", "\n", " def Shader \"mtlximage2\"\n", " {\n", " uniform token info:id = \"ND_image_float\"\n", " asset inputs:file = @file2.png@\n", " float outputs:out\n", " }\n", " }\n", "\n", " def Shader \"mtlxdisplacement\"\n", " {\n", " uniform token info:id = \"ND_displacement_float\"\n", " token outputs:out\n", " }\n", " }\n", "\n", " def NodeGraph \"usdpreview_subnet\"\n", " {\n", " color3f inputs:base_color.connect = \n", " token outputs:surface.connect = \n", "\n", " def Shader \"usdpreviewsurface1\"\n", " {\n", " uniform token info:id = \"UsdPreviewSurface\"\n", " color3f inputs:diffuseColor.connect = \n", " token outputs:surface\n", " }\n", " }\n", " }\n", " }\n", "}\n", "\n", "```\n", "
\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Load in a sample file\n", "stage_unflattend = Usd.Stage.Open('data/sphere_with_nodegraphs.usda') \n", "\n", "# Flatten layers\n", "layer = stage_unflattend.Flatten()\n", "stage = Usd.Stage.Open(layer)\n", "\n", "# Print as String\n", "stringResult = layer.ExportToString()\n", "text = '
Flattened Usd File\\n\\n' + '```usd\\n' + stringResult + '```\\n' + '
\\n' \n", "display_markdown(text , raw=True)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## 2. Usd Traversal\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " - Name mySphere, Type node Path /mySphere\n", " - purpose : default\n", " - visibility : inherited\n", " - Name geo, Type node Path /mySphere/geo\n", " - extent : [(-1, -1, -1), (1, 1, 1)]\n", " - orientation : rightHanded\n", " - purpose : default\n", " - radius : 1.0\n", " - visibility : inherited\n", " - xformOp:transform : ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) )\n", " - xformOpOrder : [xformOp:transform]\n", " - Name mtl, Type node Path /mySphere/mtl\n", " - purpose : default\n", " - visibility : inherited\n", " - Name collect1, Type material Path /mySphere/mtl/collect1\n", " - inputs:base_color : (1, 0, 0)\n", " - Name my_materialx_subnet, Type graph Path /mySphere/mtl/collect1/my_materialx_subnet\n", " - Name mtlxstandard_surface1, Type shader Path /mySphere/mtl/collect1/my_materialx_subnet/mtlxstandard_surface1\n", " - info:id : ND_standard_surface_surfaceshader\n", " - info:implementationSource : id\n", " - inputs:base : 1.0\n", " - inputs:coat_roughness : 0.10000000149011612\n", " - inputs:emission_color : (1, 1, 1)\n", " - inputs:specular : 1.0\n", " - inputs:specular_color : (1, 1, 1)\n", " - inputs:specular_IOR : 1.5\n", " - inputs:specular_roughness : 0.20000000298023224\n", " - Name image_readers, Type graph Path /mySphere/mtl/collect1/my_materialx_subnet/image_readers\n", " - Name mtlximage1, Type shader Path /mySphere/mtl/collect1/my_materialx_subnet/image_readers/mtlximage1\n", " - info:id : ND_image_color3\n", " - info:implementationSource : id\n", " - inputs:file : @file1.png@\n", " - Name mtlximage2, Type shader Path /mySphere/mtl/collect1/my_materialx_subnet/image_readers/mtlximage2\n", " - info:id : ND_image_float\n", " - info:implementationSource : id\n", " - inputs:file : @file2.png@\n", " - Name mtlxdisplacement, Type shader Path /mySphere/mtl/collect1/my_materialx_subnet/mtlxdisplacement\n", " - info:id : ND_displacement_float\n", " - info:implementationSource : id\n", " - Name usdpreview_subnet, Type graph Path /mySphere/mtl/collect1/usdpreview_subnet\n", " - Name usdpreviewsurface1, Type shader Path /mySphere/mtl/collect1/usdpreview_subnet/usdpreviewsurface1\n", " - info:id : UsdPreviewSurface\n", " - info:implementationSource : id\n" ] } ], "source": [ "# Start from the root\n", "prim = stage.GetPrimAtPath('/')\n", "\n", "# Utility to recursive traverse and print out children, and their attributes\n", "def printChildren(indent, prim):\n", " children = prim.GetChildren()\n", " for child in children:\n", " if child:\n", " primtype = 'node'\n", " if child.IsA(UsdShade.Material):\n", " primtype = 'material'\n", " elif child.IsA(UsdShade.NodeGraph):\n", " primtype = 'graph'\n", " elif child.IsA(UsdShade.Shader):\n", " primtype = 'shader'\n", " print('%s - Name %s, Type %s Path %s' % (indent, child.GetName(), primtype, child.GetPrimPath())) \n", " for attr in child.GetAttributes():\n", " if attr.Get():\n", " print(indent, ' -', attr.GetName() + ' : ', attr.Get())\n", " printChildren(indent + ' ', child) \n", "\n", "# Print out tree\n", "printChildren(' ', prim)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## 3. Examining Shader Graphs\n", "\n", "This can be refined to only examine shading nodes and ports.\n", "\n", "Two utility functions are added:\n", " \n", "* `printValueElements()` selectively examines inputs and outputs using `GetInputs()` and `GetOutputs` on a UsdShadeShader, or UsdShadeNodeGraph. Tokens are not considered in this example. \n", "\n", "* `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. " ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " - Material collect1, Path /mySphere/mtl/collect1\n", " - Input: base_color\n", " - Output: mtlx:displacement\n", " - Output: mtlx:surface\n", " - Output: surface\n", " - Nodegraph: my_materialx_subnet, Path /mySphere/mtl/collect1/my_materialx_subnet\n", " - Input: base_color\n", " - Output: displacement\n", " - Output: surface\n", " - Shader mtlxstandard_surface1, Path /mySphere/mtl/collect1/my_materialx_subnet/mtlxstandard_surface1\n", " - Nodedef: ND_standard_surface_surfaceshader\n", " - Input: base\n", " - Input: base_color\n", " - Input: coat\n", " - Input: coat_roughness\n", " - Input: emission\n", " - Input: emission_color\n", " - Input: metalness\n", " - Input: specular\n", " - Input: specular_color\n", " - Input: specular_IOR\n", " - Input: specular_roughness\n", " - Input: transmission\n", " - Output: out\n", " - Nodegraph: image_readers, Path /mySphere/mtl/collect1/my_materialx_subnet/image_readers\n", " - Input: _base_color\n", " - Output: out\n", " - Output: out_2\n", " - Shader mtlximage1, Path /mySphere/mtl/collect1/my_materialx_subnet/image_readers/mtlximage1\n", " - Nodedef: ND_image_color3\n", " - Input: default\n", " - Input: file\n", " - Output: out\n", " - Shader mtlximage2, Path /mySphere/mtl/collect1/my_materialx_subnet/image_readers/mtlximage2\n", " - Nodedef: ND_image_float\n", " - Input: file\n", " - Output: out\n", " - Shader mtlxdisplacement, Path /mySphere/mtl/collect1/my_materialx_subnet/mtlxdisplacement\n", " - Nodedef: ND_displacement_float\n", " - Output: out\n", " - Nodegraph: usdpreview_subnet, Path /mySphere/mtl/collect1/usdpreview_subnet\n", " - Input: base_color\n", " - Output: surface\n", " - Shader usdpreviewsurface1, Path /mySphere/mtl/collect1/usdpreview_subnet/usdpreviewsurface1\n", " - Nodedef: UsdPreviewSurface\n", " - Input: diffuseColor\n", " - Output: surface\n" ] } ], "source": [ "def printValueElements(shaderInterface, indent):\n", " \"\"\"\n", " Print out the inputs and outputs \n", " \"\"\"\n", " for input in shaderInterface.GetInputs():\n", " if input:\n", " print(indent, '- Input:', input.GetBaseName())\n", " for output in shaderInterface.GetOutputs():\n", " if output:\n", " print(indent, '- Output:', output.GetBaseName())\n", "\n", "def printShaderNodes(indent, prim):\n", " \"\"\"\n", " Print out shader node information\n", " \"\"\"\n", " # Use RTTI to find the desired types\n", " if prim.IsA(UsdShade.Material): \n", " print(indent, '- Material %s, Path %s' % (prim.GetName(), prim.GetPrimPath()))\n", " material = UsdShade.Material(prim)\n", " printValueElements(material, indent + ' ')\n", "\n", " elif prim.IsA(UsdShade.NodeGraph):\n", " print(indent, '- Nodegraph: %s, Path %s' % (prim.GetName(), prim.GetPrimPath()))\n", " nodegraph = UsdShade.NodeGraph(prim)\n", " printValueElements(nodegraph, indent + ' ')\n", "\n", "\n", " elif prim.IsA(UsdShade.Shader): \n", " shader = UsdShade.Shader(prim)\n", " print(indent, '- Shader %s, Path %s' % (prim.GetName(), prim.GetPrimPath()))\n", " print(indent, ' - Nodedef: ', (shader.GetIdAttr().Get()))\n", " printValueElements(shader, indent + ' ')\n", "\n", " # Visit children\n", " children = prim.GetChildren()\n", " if children:\n", " childIndent = indent+' '\n", " for child in children:\n", " printShaderNodes(childIndent, child)\n", "\n", "# Traverse and print output \"shader\" contents\n", "prim = stage.GetPrimAtPath('/')\n", "printShaderNodes(' ', prim)\n", "\n", "materials = [x for x in stage.Traverse() if x.IsA(UsdShade.Material)]\n" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## 4. Usd to MaterialX Translation\n", "\n", "The previous example is modified to create MaterialX graphs, shaders and materials. \n", "\n", "Note that the shading network contains nested nodegraphs. That is a nodegraph can contain another nodegraph. \n", "While this is part of the MaterialX specification, full support for this does not currently exist at time of writing. \n", "For the purposes of translation / interop, logic is included which can general enough to support any level of graph nesting.\n", "\n", "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.\n", "\n", "### 4.1 Setup\n", "The first step is to add in a basic setup for MaterialX to create a working document and load in standard definitions." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Created working document. Loaded in 750 definitions\n" ] } ], "source": [ "# Perform basic setup\n", "stdlib = mx.createDocument()\n", "libFiles = []\n", "searchPath = mx.getDefaultDataSearchPath()\n", "libFiles = mx.loadLibraries(mx.getDefaultDataLibraryFolders(), searchPath, stdlib)\n", " \n", "doc = mx.createDocument()\n", "doc.importLibrary(stdlib)\n", "print('Created working document. Loaded in %d definitions' % len(doc.getNodeDefs()))\n", "\n", "# Write predicate\n", "def skipLibraryElement(elem):\n", " return not elem.hasSourceUri()" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "### 4.2 Translation Logic\n", "\n", "Next, translation logic is broken up into a series of utilities which perform Usd to MaterialX mappings.\n", "\n", "#### 4.2.1 Type and Value Mapping\n", "\n", "The first of these are utilities for value and type mapping:\n", "* The utility `mapUsdTypeToMtlx()` maps native Usd type strings to MaterialX native type strings. \n", "* The utility `mapUsdValueToMtlx()` is used to map a Usd `Gf` value to a MaterialX string value.\n", "\n", "> Note: Both mappings only handle a subset of all possible mappings. " ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "def mapUsdTypeToMtlx(usdType):\n", " \"\"\" \n", " Map a Usd type string to a MaterialX type string.\n", " Note this is not a complete mapping.\n", " \"\"\"\n", " usdTypeString = str(usdType)\n", " mtlxType = 'color3'\n", " if 'color3' in usdTypeString:\n", " mtlxType ='color3'\n", " elif 'color4' in usdTypeString:\n", " mtlxType ='color4'\n", " elif 'float4' in usdTypeString:\n", " mtlxType ='vector4'\n", " elif 'vector3' in usdTypeString:\n", " mtlxType ='vector3'\n", " elif 'float2' in usdTypeString:\n", " mtlxType ='vector2'\n", " elif 'float' == usdType:\n", " mtlxType ='float'\n", " elif 'string' in usdTypeString:\n", " mtlxType = 'string'\n", " elif 'int' in usdTypeString:\n", " mtlxType = 'integer'\n", " elif 'bool' in usdTypeString:\n", " mtlxType = 'boolean'\n", " elif 'asset' in usdTypeString:\n", " mtlxType = 'filename'\n", " elif 'token' in usdTypeString:\n", " mtlxType = 'token'\n", " else: \n", " mtlxType = usdTypeString\n", " print('--> Mapping of Usd type failed:', usdTypeString)\n", " return mtlxType\n", "\n", "def mapUsdValueToMtlx(mtlxType, usdValue):\n", " \"\"\"\n", " Map a Usd value to a MaterialX value. \n", " Note this is not a complete mapping. Ideally, if this is a value on a node\n", " input / output, then the definition can be queried to get the default value.\n", " \"\"\"\n", " mtlxValue = None\n", " if mtlxType == 'float':\n", " if not usdValue:\n", " mtlxValue = '0'\n", " else:\n", " mtlxValue = str(usdValue)\n", " elif mtlxType == 'integer':\n", " if not usdValue:\n", " mtlxValue = '0'\n", " else:\n", " mtlxValue = str(usdValue)\n", " elif mtlxType == 'boolean': \n", " if not usdValue:\n", " mtlxValue = 'false'\n", " else:\n", " mtlxValue = str(usdValue).lower() \n", " elif mtlxType == 'string' or mtlxType == 'displacementshader' or mtlxType == 'surfaceshader': \n", " if not usdValue:\n", " mtlxValue = ''\n", " else:\n", " mtlxValue = usdValue\n", " elif mtlxType == 'filename': \n", " if not usdValue:\n", " mtlxValue = ''\n", " else:\n", " mtlxValue = str(usdValue).removeprefix('@').removesuffix('@')\n", " elif mtlxType == 'vector2':\n", " if not usdValue:\n", " mtlxValue = '0, 0'\n", " else:\n", " mtlxValue = str(usdValue[0]) + ',' + str(usdValue[1])\n", " elif mtlxType == 'color3' or mtlxType == 'vector3':\n", " if not usdValue:\n", " mtlxValue = '0, 0, 0'\n", " else:\n", " mtlxValue = str(usdValue[0]) + ',' + str(usdValue[1]) + ',' + str(usdValue[2])\n", " elif mtlxType == 'color4' or mtlxType == 'vector4':\n", " if usdValue is None:\n", " mtlxValue = '0, 0, 0, 0'\n", " else:\n", " mtlxValue = str(usdValue[0]) + ',' + str(usdValue[1]) + ',' + str(usdValue[2]) + ',' + str(usdValue[3])\n", "\n", " if mtlxValue is None:\n", " print('--> Mapping of Usd Value %s failed for MaterialX type %s' % (usdValue, mtlxType))\n", "\n", " return mtlxValue\n", "\n", "# Mapping from Sdf type to MaterialX type.\n", "# It is possible to map using Sdf type. This function is unused in this example \n", "def mapUsdSdfTypeToMtlx(usdType):\n", " mtlxUsdMap = dict()\n", " mtlxUsdMap[Sdf.ValueTypeNames.Asset] = 'filename' \n", " mtlxUsdMap[Sdf.ValueTypeNames.String] = 'string'\n", " mtlxUsdMap[Sdf.ValueTypeNames.Bool] = 'boolean' \n", " mtlxUsdMap[Sdf.ValueTypeNames.Int] = 'integer' \n", " mtlxUsdMap[Sdf.ValueTypeNames.Color3f] = 'color3' \n", " mtlxUsdMap[Sdf.ValueTypeNames.Color4f] = 'color4' \n", " mtlxUsdMap[Sdf.ValueTypeNames.Float] = 'float' \n", " mtlxUsdMap[Sdf.ValueTypeNames.Float2] = 'vector2' \n", " mtlxUsdMap[Sdf.ValueTypeNames.Float3] = 'vector3' \n", " mtlxUsdMap[Sdf.ValueTypeNames.Vector3f] = 'vector3' \n", " mtlxUsdMap[Sdf.ValueTypeNames.Float4] = 'vector4' \n", " \n", " if usdType in mtlxUsdMap:\n", " return mtlxUsdMap[usdType]\n", " return 'string'" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "#### 4.2.2 Multiple Output Detection\n", "\n", "`isMultiOutput` is used to determine if the Usd prim (nodegraph, shader or material)\n", "has multiple outputs. \n", "\n", "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') " ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "def isMultiOutput(prim):\n", " \"\"\" Test if the Usd prim has multiple outputs \"\"\"\n", " outputCount = 0\n", " if prim.IsA(UsdShade.Material): \n", " usdMaterial = UsdShade.Material(prim)\n", " outputCount = len(usdMaterial.GetOutputs())\n", " elif prim.IsA(UsdShade.NodeGraph):\n", " usdNodegraph = UsdShade.NodeGraph(prim)\n", " outputCount = len(usdNodegraph.GetOutputs())\n", " elif prim.IsA(UsdShade.Shader): \n", " usdShader = UsdShade.Shader(prim)\n", " outputCount = len(usdShader.GetOutputs())\n", "\n", " return outputCount > 1" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "#### 4.2.3 Value Element (Input / Output) Mapping\n", "\n", "`emitMtlxValueELements` handles the mapping of Usd inputs and outputs to MaterialX inputs and outputs. \n", "\n", "This includes: \n", "* Creating the input / output. \n", " * Unlike Usd which has explicit outputs, **MaterialX never specifies outputs on nodes**, only on nodegraphs. \n", " * This difference is handled when visiting Usd outputs.\n", "* Setting a value **or** (*)\n", "* Setting connection attributes. \n", " * `GetConnectedSources()` in Usd is roughly requivalent to `getConnectedNode()` in MaterialX. One interesting difference is that \"valid\" vs \"invalid\" sources can be returned.\n", " * Unlike Usd which has a single `connect` syntax and corresponding API for connection logic and behaviour, MaterialX can require multiple attributes to specified to when creating / modifying a connection. \n", " * This depends on: \n", " * if the upstream element is a `nodegraph` or `node`, or `interface input` then 1 of 3 different attributes are set; \n", " * if the upstream element has multiple outputs (`multioutput` type), an additional `output` attribute is required; and\n", " * if there is a specific channel extracted from the upstream port, an additional `channel` attribute is required. Logic for channels is not included as part of this example.\n", " * **Ideally if MaterialX adopted a similar syntax to Usd then the mapping would be vastly simplified.**\n", "\n", "__Notes__\n", "\n", "1. Additional layer nesting in Usd not directly related to the shading network is not preserved in this example. This could be handled by additional nodegraph nesting or something like `namepspace` nesting could be used. The former is less lossy, as `namespace`s are flattened on import from MaterialX in `UsdMtlx` at the current time. \n", "\n", "2. It is assumed that the Usd string representation for a value can be mapped to a MaterialX one. For example, the string representation for a vector3 (`(v1, v2, v3)`) in Usd is valid syntax in MaterialX (`v1, v2, v3`).\n", "\n", "3. For a Usd port with a `token` type the type of the created MaterialX input / output is set based on the port's name if the Usd Port name is a 'surface' or 'displacement' shader. This logic is encapsulated in the utility function `mapUsdTokenToStype()` (There appears to be no way by just examining the Usd shading network to determine the type without this assumption at this time).\n", "\n", "(*) MaterialX only allows either a connection or value to be specified on a port." ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "def mapUsdTokenToType(mtlxType, usdBaseName, mtlxPrefix=False):\n", " \"\"\"\n", " Utility to test the base name for a semantic match to a surface or displacement shader\n", " If found return the appropriate MaterialX type. Othewise the type is simply `token`.\n", " Note: Only types specified with 'mtlx' are considered to be MaterialX shader, if mtlxPrefix is set to True\n", " \"\"\"\n", " usdBaseNameSplit = mx.splitString(usdBaseName, ':')\n", " testName = usdBaseNameSplit[len(usdBaseNameSplit)-1] \n", " if not mtlxPrefix or (mtlxPrefix and 'mtlx' in usdBaseNameSplit): \n", " if 'displacement' == str(testName) or 'displacementshader' == str(testName):\n", " mtlxType = 'displacementshader'\n", " elif 'surface' == str(testName) or 'surfaceshader' == str(testName):\n", " mtlxType = 'surfaceshader'\n", " elif 'volume' == str(testName) or 'volumeshader' == str(testName):\n", " mtlxType = 'volumeshader'\n", " return mtlxType\n", "\n", "def emitMtlxValueElements(shader, parent, emitInputs, emitOutputs):\n", " \"\"\"\n", " Emit MaterialX value elements (currently only Inputs and Outputs)\n", " This is not a complete translation of all value element attributes.\n", " \"\"\"\n", " if emitInputs:\n", " for input in shader.GetInputs():\n", "\n", " # Only output if there is a value or a connection\n", " if input:\n", "\n", " # Map Usd type to Mtlx type and create an input\n", " usdType = input.GetTypeName()\n", " mtlxType = mapUsdTypeToMtlx(usdType)\n", " usdBaseName = input.GetBaseName()\n", " mtlxType = mapUsdTokenToType(mtlxType, usdBaseName)\n", " usdBaseName = usdBaseName.replace(':', '_') \n", "\n", " # Add a connection if encountered\n", " if input.HasConnectedSource():\n", " newInput = parent.addInput(usdBaseName, mtlxType)\n", "\n", " # Only consider \"valid\" inputs.\n", " usdSources, invalidSources = input.GetConnectedSources() \n", " if usdSources and usdSources[0]:\n", " # Check UsdShadeConnectionSourceInfo to extract\n", " # out the upstream information\n", " usdSource1 = usdSources\n", " sourcePrim = usdSource1[0].source.GetPrim()\n", " sourcePort = usdSource1[0].sourceName # e.g. out\n", " sourceDirection = usdSource1[0].sourceType # e.g. Input / Output\n", " sourceType = usdSource1[0].typeName # e.g. color3f\n", "\n", " # Handle the complex MaterialX attribute syntax\n", " # for specifying a connection.\n", " # ---------------------------------------------\n", " # Assume a node->input connection to start\n", " mtlxConnectString = 'nodename'\n", " mtlxConnectItem = sourcePrim.GetName()\n", "\n", " # An input->input connection is denoted using\n", " # \"interfacename\", but no \"node\", or \"nodegraph\"\n", " if sourceDirection == UsdShade.AttributeType.Input:\n", " mtlxConnectString = 'interfacename'\n", " mtlxConnectItem = sourcePort\n", "\n", " # Set the connection\n", " newInput.setAttribute(mtlxConnectString, mtlxConnectItem)\n", "\n", " else:\n", " # A nodegraph->output connect uses \"nodegraph\" vs \"node\" \n", " if sourcePrim.IsA(UsdShade.NodeGraph):\n", " mtlxConnectString = 'nodegraph'\n", "\n", " # Set the connection\n", " newInput.setAttribute(mtlxConnectString, mtlxConnectItem)\n", "\n", " # An output->intput connection is denoted using\n", " # an additional `output` attribute` if the source is \n", " # does not have multiple outputs\n", " if sourceDirection == UsdShade.AttributeType.Output:\n", " if isMultiOutput(sourcePrim):\n", " newInput.setAttribute('output', sourcePort) \n", " \n", " # Set value if not connected.\n", " else:\n", " usdVal = input.Get()\n", " if usdVal is not None:\n", " newInput = parent.addInput(usdBaseName, mtlxType)\n", " if newInput:\n", " mtlxVal = mapUsdValueToMtlx(mtlxType, usdVal)\n", " if mtlxVal is not None:\n", " newInput.setValueString(mtlxVal)\n", " \n", " # Emit outputs if specified. Unlike Usd, outputs are not explicitly defined\n", " # except for nodegraph. The branching toggle `emitOuputs` allows for outputs to be selectively emitted.\n", " if emitOutputs:\n", " for output in shader.GetOutputs():\n", " if output:\n", "\n", " usdType = output.GetTypeName()\n", "\n", " mtlxType = mapUsdTypeToMtlx(usdType)\n", " usdBaseName = output.GetBaseName()\n", " #usdFullName = output.GetFullName()\n", " \n", " newOutput = None\n", " # Note that MaterialX materials specify connections as input and NOT as outputs\n", " # as with Usd. Additionally only a subset of types. If there is more than one\n", " # input with the same type, only the first will be recorded.\n", " if parent.getType() == 'material':\n", " mtlxType = mapUsdTokenToType(mtlxType, usdBaseName, True)\n", " if mtlxType in ['surfaceshader', 'volumeshader', 'displacementshader']:\n", " if parent.getInput(mtlxType):\n", " print('Skip connecting > 1 shader of type %s on material %s' % (mtlxType, parent.getNamePath()))\n", " else:\n", " newOutput = parent.addInput(mtlxType, mtlxType)\n", " else:\n", " mtlxType = mapUsdTokenToType(mtlxType, usdBaseName)\n", " usdBaseName = usdBaseName.replace(':', '_') \n", " newOutput = parent.addOutput(usdBaseName, mtlxType)\n", "\n", " if newOutput and output.HasConnectedSource():\n", " usdSources, invalidSources = output.GetConnectedSources() \n", " if usdSources and usdSources[0]:\n", " # Check UsdShadeConnectionSourceInfo\n", " usdSource1 = usdSources[0]\n", " sourcePrim = usdSource1.source.GetPrim()\n", " sourcePort = usdSource1.sourceName\n", " sourceDirection = usdSource1.sourceType\n", " sourceType = usdSource1.typeName\n", "\n", " mtlxConnectString = 'nodename'\n", " mtlxConnectItem = sourcePrim.GetName()\n", "\n", " # An input->output connection should never occur\n", " # and is ignored\n", " #if sourceDirection == UsdShade.AttributeType.Input:\n", "\n", " # Handle adding in node or nodegraph depending on source\n", " # prim type. \n", " if sourcePrim.IsA(UsdShade.NodeGraph):\n", " mtlxConnectString = 'nodegraph'\n", "\n", " newOutput.setAttribute(mtlxConnectString, mtlxConnectItem)\n", "\n", " # Handle output->output connection\n", " if sourceDirection == UsdShade.AttributeType.Output:\n", " if isMultiOutput(sourcePrim):\n", " newOutput.setAttribute('output', sourcePort)\n" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "#### 4.2.4 Top Level Translation Logic\n", "\n", "A final utility interface called `emitMaterialX()` wraps up the top level translation logic.\n", "\n", "As in the previous example a tree traversal is performed. \n", "\n", "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.\n", "\n", "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(). \n", "\n", "Notes:\n", "1. Usd materials are considered to be *node graphs*, while in MaterialX materials are *nodes* which connect to surface, volume or displacement shaders. During conversion anything else must be located at the same level as the material and not nested within the material graph as in Usd. Previously to version 1.38.6, MaterialX materials were closer in nature to Usd materials as they also embedded shader associations as part of the material and materials were not nodes.\n", "2. Usd separates out the functional API from the primitive and as such an interface needs to be instantiated given a `UsdPrim`. This differs from MaterialX which does not separate out the functional API, with all types deriving from a common `Element` class.\n", "3. Saved paths in Usd are *absolute* while paths in MaterialX are *relative* to the current parent scope. Usd has a root path specifier '/' while MaterialX does not. \n", "4. No specific logic is required to handle different definition versions as long as a different `nodedef` identifier is used for different versions. This should be the case within MaterialX and when MaterialX `nodedefs` are loaded into the Usd shader registriy (`Sdr`) \n", "\n", "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. " ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Loaded in 750 definitions\n" ] }, { "data": { "text/markdown": [ "
Resulting Generated MaterialX\n", "\n", "```xml\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", "```\n", "
\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "\n", "def emitMaterialX(stage, indent, prim, parent):\n", " \"\"\"\n", " Emit MaterialX for a given Usd Stage starting at a given root.\n", " Currently only nodegraphs, material and shader nodes are supported.\n", " \"\"\"\n", " if prim:\n", " # Test if it's a material first as a material is a nodegraph\n", " # Ignore inputs as they have no meaning on a MaterialX material.\n", " if prim.IsA(UsdShade.Material): \n", " doc = parent.getDocument()\n", " usdMaterial = UsdShade.Material(prim)\n", " mtlxName = parent.createValidChildName(prim.GetName())\n", " mtlxMaterial = parent.addMaterialNode(mtlxName)\n", " emitMtlxValueElements(usdMaterial, mtlxMaterial, False, True)\n", "\n", " elif prim.IsA(UsdShade.NodeGraph):\n", " doc = parent.getDocument()\n", " usdNodegraph = UsdShade.NodeGraph(prim)\n", " mtlxName = parent.createValidChildName(prim.GetName())\n", " mtlxNodeGraph = parent.addChildOfCategory('nodegraph', mtlxName)\n", " parent = mtlxNodeGraph\n", " emitMtlxValueElements(usdNodegraph, mtlxNodeGraph, True, True)\n", "\n", " elif prim.IsA(UsdShade.Shader): \n", " usdShader = UsdShade.Shader(prim)\n", " mtlxNodeDefId = ''\n", " \n", " # Note: Only consider when the definition is specified in the identifier\n", " usdImplAttr = usdShader.GetImplementationSourceAttr()\n", " if usdImplAttr.Get() == 'id':\n", " mtlxNodeDefId = usdShader.GetIdAttr().Get()\n", "\n", " # Do a manual rename for built in UsdPreviewSurface\n", " # Could be done for other built-ins which have MaterialX\n", " # definitions.\n", " if mtlxNodeDefId == 'UsdPreviewSurface':\n", " mtlxNodeDefId = 'ND_UsdPreviewSurface_surfaceshader'\n", "\n", " # Look for an existing definition. If found add an instance and populate\n", " # it's inputs and outputs.\n", " doc = parent.getDocument()\n", " mtlxNodeDef = None\n", " if mtlxNodeDefId:\n", " mtlxNodeDef = doc.getNodeDef(mtlxNodeDefId)\n", " if mtlxNodeDef:\n", " mtlxShadername = parent.createValidChildName(prim.GetName())\n", " shaderNode = parent.addNodeInstance(mtlxNodeDef, mtlxShadername) \n", " emitMtlxValueElements(usdShader, shaderNode, True, False)\n", " else:\n", " print('Skipping shader node %s: No MaterialX definition found.' % prim.GetName()) \n", "\n", " children = prim.GetChildren()\n", " for child in children:\n", " emitMaterialX(stage, indent+indent, child, parent)\n", "\n", "def convertUsdToMtlx(stage, stdlib):\n", "\n", " doc = mx.createDocument()\n", " doc.importLibrary(stdlib)\n", "\n", " # Start at the root and emit child nodes \n", " prim = stage.GetPrimAtPath('/')\n", " emitMaterialX(stage, ' ', prim, doc)\n", "\n", " return doc\n", "\n", "stdlib = mx.createDocument()\n", "libFiles = []\n", "searchPath = mx.getDefaultDataSearchPath()\n", "libFiles = mx.loadLibraries(mx.getDefaultDataLibraryFolders(), searchPath, stdlib)\n", "print('Loaded in %d definitions' % len(stdlib.getNodeDefs()))\n", "doc = convertUsdToMtlx(stage, stdlib)\n", "\n", "# Write results to Markdown / file\n", "writeOptions = mx.XmlWriteOptions()\n", "writeOptions.writeXIncludeEnable = False\n", "writeOptions.elementPredicate = skipLibraryElement\n", "documentContents = mx.writeToXmlString(doc, writeOptions)\n", "\n", "text = '
Resulting Generated MaterialX\\n\\n' + '```xml\\n' + documentContents + '```\\n' + '
\\n' \n", "display_markdown(text , raw=True)\n", "\n", "mx.writeToXmlFile(doc, 'data/test_usd_mtlx.mtlx', writeOptions)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## 5. Updating MaterialX / Usd Inputs \n", "\n", "There are different ways to approach handling an edit in Usd and then updating the corresponding MaterialX.\n", "This example only handles **value** changes by updating matching inputs via `path` lookups in Usd and MaterialX.\n", "\n", "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`. \n", "* The stage interface GetPrimAtPath() is used to lookup the node to edit in Usd, and \n", "* The document interface getDescendent() used for MaterialX.\n", "* The input on each node is then found using GetInput() and getInput() for Usd and MaterialX respectively.\n", "\n", "Note that the Usd `path` differs from the MaterialX `path` as MaterialX does not accept a path that starts with\n", "'/' in it's path related interfaces. \n", "> *This would be a good discrepancy to address, which could just be an implementation issue*.\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Modified Usd from: 0.1 to 0.9\n", "Modified MaterialX from: 0.10000000149011612 to 0.9\n" ] } ], "source": [ "# Note that the MaterialX path cannot start with '/' otherwise `getDescendent)` will fail to\n", "# find the element. \n", "mtlxPath = 'my_materialx_subnet/mtlxstandard_surface1' \n", "\n", "# Add additional path nesting in the Usd stage including parenting of the shader\n", "# graph under the material `collect1`` which does not exist in a MaterialX graph.\n", "usdPath = '/mySphere/mtl/collect1/' + mtlxPath \n", "\n", "# Input to modify\n", "inputName = 'coat_roughness'\n", "\n", "# Update the input in Usd\n", "currentValue = 999\n", "prim = stage.GetPrimAtPath(usdPath)\n", "if prim:\n", " stdsurf = UsdShade.Shader(prim)\n", " surfInput = stdsurf.GetInput(inputName)\n", " if surfInput:\n", " currentValue = surfInput.Get()\n", " surfInput.Set(0.9)\n", "\n", " print('Modified Usd from: %g to %g' % (currentValue, surfInput.Get()))\n", "\n", "# Update the input in MaterialX\n", "currentValue = 999\n", "mtxlStdSurf = doc.getDescendant(mtlxPath)\n", "if mtxlStdSurf:\n", " mtlxSurfInput = mtxlStdSurf.getInput(inputName)\n", " if mtlxSurfInput:\n", " currentValue = mtlxSurfInput.getValueString()\n", " mtlxSurfInput.setValue(0.9)\n", "\n", " print('Modified MaterialX from: %s to %s' % (currentValue, mtlxSurfInput.getValueString()))" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## 6. MaterialX to Usd Example\n", "\n", "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\n", "so is not available unless a custom / local build is used.\n", "\n", "To start, manual creation of Usd nodes based on the `marble` example demonstrates usage of some basic interfaces of shaders, materials, graphs, and ports.\n", "\n", "Logic to consider includes creating the appropriate UsdShade type, setting a definition (`nodedef`) association, translating value and type constructs, and forming port connections. \n", "\n", "Of note:\n", "1. Outputs are explicitly created on nodes as well as nodegraphs (unlike MaterialX)\n", "2. The explicit setting of the node definition name as the identifier to the SdrRegistry\n", "3. No Usd scopes are created as the MaterialX defines no scope." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "```usd\n", "#usda 1.0\n", "\n", "def Scope \"mtl\"\n", "{\n", " def Material \"Marble_3D\"\n", " {\n", " token outputs:mtlx:surface.connect = \n", "\n", " def Shader \"SR_marble1\"\n", " {\n", " uniform token info:id = \"ND_standard_surface_surfaceshader\"\n", " color3f inputs:base_color = (1, 1, 1)\n", " color3f inputs:base_color.connect = \n", " float inputs:specular_roughness = 0.1\n", " float inputs:subsurface = 0.4\n", " float inputs:subsurface_color = 0\n", " token outputs:surface\n", " }\n", "\n", " def NodeGraph \"NG_marble1\"\n", " {\n", " color3f outputs:out\n", " }\n", " }\n", "}\n", "\n", "\n", "```\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "\n", "def createSimpleMtlx(stage, materialScope):\n", " \"\"\"\n", " Hard coded simple MaterialX to Usd example which produces \n", " part of the marble example. \n", " \"\"\"\n", " if materialScope:\n", " UsdGeom.Scope.Define(stage, materialScope)\n", " \n", " materialPath = Sdf.Path(materialScope).AppendPath('Marble_3D')\n", " material = UsdShade.Material.Define(stage, materialPath)\n", "\n", " # Create a standard surface shader\n", " shaderPath = materialPath.AppendPath('SR_marble1')\n", " stdSurfShader = UsdShade.Shader.Define(stage, shaderPath)\n", " stdSurfShader.CreateIdAttr(\"ND_standard_surface_surfaceshader\")\n", " stdSurfShader.CreateInput(\"base_color\", Sdf.ValueTypeNames.Color3f).Set(Gf.Vec3f(0.8, 0.8, 0.8))\n", " stdSurfShader.CreateInput(\"specular_roughness\", Sdf.ValueTypeNames.Float).Set(0.1)\n", " stdSurfShader.CreateInput(\"subsurface\", Sdf.ValueTypeNames.Float).Set(0.4)\n", " stdSurfShader.CreateInput(\"subsurface_color\", Sdf.ValueTypeNames.Float).Set(0.0)\n", "\n", " # Connect shader to material. Note that an output is explicitly created.\n", " nodeOutput = material.CreateOutput('mtlx:surface', Sdf.ValueTypeNames.Token)\n", " if nodeOutput:\n", " #nodeOutput.SetTypeName('mtlx:surface')\n", " nodeOutput.ConnectToSource(stdSurfShader.ConnectableAPI(), \"surface\")\n", "\n", " # Create upstream pattern graph\n", " patternGraphPath = materialPath.AppendPath('NG_marble1')\n", " patternGraph = UsdShade.NodeGraph.Define(stage, patternGraphPath)\n", " graphOutput = patternGraph.CreateOutput('out', Sdf.ValueTypeNames.Color3f)\n", "\n", " # Connect graph to shader input. Note that as with MaterialX, the existing value is not removed,\n", " base_color = stdSurfShader.GetInput('base_color')\n", " if base_color:\n", " base_color.ConnectToSource(patternGraph.ConnectableAPI(), \"out\")\n", " base_color.Set(Gf.Vec3f(1, 1, 1))\n", "\n", "marbleStage = Usd.Stage.CreateInMemory()\n", "mtlxScope = \"/mtl\"\n", "createSimpleMtlx(marbleStage, mtlxScope)\n", "stringResult = marbleStage.GetRootLayer().ExportToString()\n", "display_markdown('```usd\\n' + stringResult + '\\n```\\n', raw=True)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "### 6.1 MaterialX to Usd Utilities\n", "\n", "For arbitrary MaterialX graphs, a series of utilities is provided to perform the translation.\n", "\n", "This is again to show any notable differences in nomenclature, API, and mappings between Usd and MaterialX\n", "but in this case for the reverse mapping from MaterialX to Usd. \n", "\n", "All logic creates the minimal amount of nesting to reflect how MaterialX does not support nesting via non-nested node graphs." ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "#### 6.1.1 MaterialX to Usd : Type and Value Mapping\n", "\n", "The `mapMtxToUsdType()` and `mapMtxToUsdValue()` utilities provide mappings for type and value respectively. \n", "The mapping is from MaterialX type name to an Usd Sdf type, and from a MaterialX `Value` to a Usd `Gf` value. " ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "def mapMtxToUsdType(mtlxType):\n", " \"\"\"\n", " Map a MaterialX type to an Usd Sdf type\n", "\n", " Parameters:\n", " -----------\n", " - mtxType : string\n", " MaterialX type \n", " \"\"\"\n", " mtlxUsdMap = dict()\n", " mtlxUsdMap['filename'] = Sdf.ValueTypeNames.Asset\n", " mtlxUsdMap['string'] = Sdf.ValueTypeNames.String\n", " mtlxUsdMap['boolean'] = Sdf.ValueTypeNames.Bool\n", " mtlxUsdMap['integer'] = Sdf.ValueTypeNames.Int\n", " mtlxUsdMap['float'] = Sdf.ValueTypeNames.Float\n", " mtlxUsdMap['color3'] = Sdf.ValueTypeNames.Color3f\n", " mtlxUsdMap['color4'] = Sdf.ValueTypeNames.Color4f\n", " mtlxUsdMap['vector2'] = Sdf.ValueTypeNames.Float2 \n", " mtlxUsdMap['vector3'] = Sdf.ValueTypeNames.Vector3f \n", " mtlxUsdMap['vector4'] = Sdf.ValueTypeNames.Float4 \n", " mtlxUsdMap['surfaceshader'] = Sdf.ValueTypeNames.Token\n", "\n", " if mtlxType in mtlxUsdMap:\n", " return mtlxUsdMap[mtlxType]\n", " return Sdf.ValueTypeNames.Token\n", "\n", "def mapMtxToUsdValue(mtlxType, mtlxValue):\n", " \"\"\"\n", " Map a MaterialX value of a given type to a Usd value.\n", " Note: Not all types are included here.\n", " \"\"\"\n", " usdValue = '__'\n", " if mtlxType == 'float':\n", " usdValue = mtlxValue\n", " elif mtlxType == 'integer':\n", " usdValue = mtlxValue\n", " elif mtlxType == 'boolean': \n", " usdValue = mtlxValue\n", " elif mtlxType == 'string': \n", " usdValue = mtlxValue\n", " elif mtlxType == 'filename': \n", " usdValue = mtlxValue\n", " elif mtlxType == 'vector2':\n", " usdValue = Gf.Vec2f( mtlxValue[0], mtlxValue[1] )\n", " elif mtlxType == 'color3' or mtlxType == 'vector3':\n", " usdValue = Gf.Vec3f( mtlxValue[0], mtlxValue[1], mtlxValue[2] )\n", " elif mtlxType == 'color4' or mtlxType == 'vector4':\n", " usdValue = Gf.Vec4f( mtlxValue[0], mtlxValue[1], mtlxValue[2], mtlxValue[3] )\n", "\n", " return usdValue" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "#### 6.1.2 MaterialX to Usd Connection Mapping\n", "\n", "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.\n", "\n", "Similar to the logic shown for the *Nodegraph Traversal* book, node and interface discovery needs to be performed\n", "by parsing the `node`, `nodegraph`, `interface` and `output` attributes. \n", "\n", "Note that when looking for the node to connect to, the document root has an empty path string so logic must be\n", "added to insert the required Usd root string '/'. **Again this would not be required if the root '/' specifier\n", "was supported in MaterialX, and the document path (from `getNamePath()` return '/' instead of an empty string)** \n", "\n", "Also note that for this example, geometric bindings including `defaultgeomprop` are not handled to create upstream input streams as they are in `UsdMtlx`." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "def mapMtlxToUsdShaderNotation(name):\n", " '''\n", " Utility to map from a MaterialX shader notation to Usd.\n", " It would be easier if the same notation was used.\n", " '''\n", " if name == 'surfaceshader': \n", " name = 'surface'\n", " elif name == 'displacementshader':\n", " name = 'displacement'\n", " elif name == 'volumshader':\n", " name = 'volume'\n", " return name\n", "\n", "def emitUsdConnections(node, stage, rootPath):\n", " \"\"\" \n", " Emit connections between MaterialX elements as Usd connections for \n", " a given MaterialX node.\n", "\n", " Paramters:\n", " - node : \n", " MaterialX node to examine\n", " - stage :\n", " Usd stage to write connection to\n", " \"\"\"\n", " if not node:\n", " return\n", " \n", " materialPath = None\n", " if node.getType() == 'material':\n", " materialPath = node.getName()\n", "\n", " for valueElement in node.getActiveValueElements():\n", " isInput = valueElement.isA(mx.Input) \n", " isOutput = valueElement.isA(mx.Output)\n", " if isInput or isOutput:\n", "\n", " interfacename = ''\n", "\n", " # Find out what type of element is connected to upstream:\n", " # node, nodegraph, or interface input.\n", " mtlxConnection = valueElement.getAttribute('nodename')\n", " if not mtlxConnection:\n", " mtlxConnection = valueElement.getAttribute('nodegraph')\n", " if not isOutput:\n", " if not mtlxConnection:\n", " mtlxConnection = valueElement.getAttribute('interfacename')\n", " interfacename = mtlxConnection \n", "\n", " connectionPath = ''\n", " if mtlxConnection:\n", "\n", " # Handle input connection by searching for the appropriate parent node.\n", " # - If it's an interface input we want the parent nodegraph. Otherwise\n", " # we want the node or nodegraph specified above.\n", " # - If the parent path is the root (getNamePath() is empty), then this is to \n", " # nodes at the root document level. \n", " if isInput:\n", " parent = node.getParent()\n", " if parent.getNamePath():\n", " if interfacename:\n", " connectionPath = rootPath + parent.getNamePath()\n", " else:\n", " connectionPath = rootPath + parent.getNamePath() + '/' + mtlxConnection\n", " else:\n", " # The connectio is to a prim at the root level so insert a '/' identifier\n", " # as getNamePath() will return an empty string at the root Document level.\n", " if interfacename:\n", " connectionPath = rootPath\n", " else:\n", " connectionPath = rootPath + mtlxConnection\n", "\n", " # Handle output connection by looking for sibling elements\n", " else:\n", " parent = node.getParent() \n", " \n", " # Connection is to sibling under the same nodegraph\n", " if node.isA(mx.NodeGraph):\n", " connectionPath = rootPath + node.getNamePath() + '/' + mtlxConnection\n", " else:\n", " # Connection is to a nodegraph parent of the current node \n", " if parent.getNamePath():\n", " connectionPath = rootPath + parent.getNamePath() + '/' + mtlxConnection\n", " # Connection is to the root document.\n", " else:\n", " connectionPath = rootPath + mtlxConnection\n", "\n", " # Find the source prim\n", " # Assumes that the source is either a nodegraph, a material or a shader\n", " connectionPath = connectionPath.removesuffix('/')\n", " sourcePrim = None\n", " sourcePort = 'out'\n", " source = stage.GetPrimAtPath(connectionPath)\n", " if not source:\n", " if materialPath:\n", " connectionPath = '/' + materialPath + connectionPath\n", " source = stage.GetPrimAtPath(connectionPath)\n", " if not source:\n", " source = stage.GetPrimAtPath('/' + materialPath)\n", " if source:\n", " if source.IsA(UsdShade.Material): \n", " sourcePrim = UsdShade.Material(source)\n", " elif source.IsA(UsdShade.NodeGraph):\n", " sourcePrim = UsdShade.NodeGraph(source)\n", " elif source.IsA(UsdShade.Shader): \n", " sourcePrim = UsdShade.Shader(source)\n", "\n", " # Special case handle interface input vs an output\n", " if interfacename:\n", " sourcePort = interfacename\n", " else: \n", " sourcePort = valueElement.getAttribute('output')\n", " if not sourcePort:\n", " sourcePort = 'out'\n", " if sourcePort:\n", " mtlxConnection = mtlxConnection + '. Port:' + sourcePort\n", "\n", " else:\n", " print('> Failed to find source at path:', connectionPath)\n", "\n", " # Find destination prim and port and make the appropriate connection.\n", " # Assumes that the destination is either a nodegraph, a material or a shader\n", " destInput = None\n", " if sourcePrim:\n", " dest = stage.GetPrimAtPath(rootPath + node.getNamePath())\n", " if not dest:\n", " print('> Failed to find dest at path:', rootPath + node.getNamePath())\n", " else:\n", " destPort = None\n", " portName = valueElement.getName()\n", " destNode = None\n", " if dest.IsA(UsdShade.Material): \n", " destNode = UsdShade.Material(dest)\n", " elif dest.IsA(UsdShade.NodeGraph):\n", " destNode = UsdShade.NodeGraph(dest)\n", " elif dest.IsA(UsdShade.Shader): \n", " destNode = UsdShade.Shader(dest)\n", " else:\n", " print('> Encountered unsupport destinion type')\n", "\n", " # Find downstream port (input or output)\n", " if destNode:\n", " if isInput:\n", " # Map from MaterialX to Usd connection syntax\n", " if dest.IsA(UsdShade.Material):\n", " portName = mapMtlxToUsdShaderNotation(portName)\n", " portName = 'mtlx:' + portName\n", " destPort = destNode.GetOutput(portName) \n", " else:\n", " destPort = destNode.GetInput(portName) \n", " else:\n", " destPort = destNode.GetOutput(portName) \n", "\n", " # Make connection to interface input, or node/nodegraph output\n", " if destPort:\n", " if interfacename:\n", " interfaceInput = sourcePrim.GetInput(sourcePort) \n", " if interfaceInput:\n", " if not destPort.ConnectToSource(interfaceInput):\n", " print('> Failed to connect: ', source.GetPrimPath(), '-->', destPort.GetFullName())\n", " else:\n", " sourcePrimAPI = sourcePrim.ConnectableAPI()\n", " if not destPort.ConnectToSource(sourcePrimAPI, sourcePort):\n", " print('> Failed to connect: ', source.GetPrimPath(), '-->', destPort.GetFullName())\n", " else:\n", " print('> Failed to find destination port:', portName)\n" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "#### 6.1.3 MaterialX to Usd Value Element Mapping\n", "\n", "Similar to how MaterialX ValueElements are created from Usd, the `emitUsdValueElements()` utility parses `ValueElements` to create Usd inputs and outputs. \n", "\n", "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`). \n", "\n", "Any inputs created from definitions will have default values which are overwritten by any values explicitly specified on the node instance. \n", "\n", "getActiveValueElements() instead of `getValueElements()` is used when examining definitions and instances to ensure that inherited inputs or outputs are included." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "def emitUsdValueElements(node, usdNode, emitAllValueElements):\n", " \"\"\"\n", " Emit MaterialX value elements in Usd.\n", "\n", " Parameters\n", " ------------ \n", " node: \n", " MaterialX node with value elements to scan\n", " usdNode:\n", " UsdShade node to create value elements on.\n", " emitAllValueElements: bool\n", " Emit value elements based on node definition, even if not specified on node instance. \n", " \"\"\"\n", " if not node:\n", " return \n", " \n", " isMaterial = node.getType() == 'material'\n", " \n", " # Instantiate with all the nodedef inputs (if emitAllValueELements is True).\n", " # Note that outputs are always created.\n", " nodedef = node.getNodeDef()\n", " if nodedef and not isMaterial:\n", " for valueElement in nodedef.getActiveValueElements():\n", " if valueElement.isA(mx.Input):\n", " if emitAllValueElements:\n", " mtlxType = valueElement.getType()\n", " usdType = mapMtxToUsdType(mtlxType)\n", "\n", " portName = valueElement.getName()\n", " usdInput = usdNode.CreateInput(portName, usdType)\n", "\n", " if len(valueElement.getValueString()) > 0:\n", " mtlxValue = valueElement.getValue()\n", " usdValue = mapMtxToUsdValue(mtlxType, mtlxValue)\n", " if usdValue != '__':\n", " usdInput.Set(usdValue)\n", "\n", " elif not isMaterial and valueElement.isA(mx.Output):\n", " usdOutput = usdNode.CreateOutput(valueElement.getName(), mapMtxToUsdType(valueElement.getType()))\n", "\n", " else:\n", " print('- Skip mapping of definition element: ', valueElement.getName(), '. Type: ', valueElement.getCategory())\n", "\n", " # From the given instance add inputs and outputs and set values.\n", " # This may override the default value specified on the definition.\n", " for valueElement in node.getActiveValueElements():\n", " if valueElement.isA(mx.Input):\n", " mtlxType = valueElement.getType()\n", " usdType = mapMtxToUsdType(mtlxType)\n", " portName = valueElement.getName()\n", " if isMaterial:\n", " # Map from Materials to Usd notation\n", " portName = mapMtlxToUsdShaderNotation(portName) \n", " usdInput = usdNode.CreateOutput('mtlx:' + portName, usdType)\n", " else: \n", " usdInput = usdNode.CreateInput(portName, usdType)\n", "\n", " # Set value. Note that we check the length of the value string\n", " # instead of getValue() as a 0 value will be skipped.\n", " if len(valueElement.getValueString()) > 0:\n", " mtlxValue = valueElement.getValue()\n", " usdValue = mapMtxToUsdValue(mtlxType, mtlxValue)\n", " if usdValue != '__':\n", " usdInput.Set(usdValue)\n", "\n", " elif not isMaterial and valueElement.isA(mx.Output):\n", " usdOutput = usdNode.GetInput(valueElement.getName())\n", " if not usdOutput:\n", " usdOutput = usdNode.CreateOutput(valueElement.getName(), mapMtxToUsdType(valueElement.getType()))\n", "\n", " else:\n", " print('- Skip mapping of element: ', valueElement.getNamePath(), '. Type: ', valueElement.getCategory())\n" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "#### 6.1.4 Emitting Usd Shading Graphs\n", "\n", "To emit the Usd shading network a utility function called `emitUsdShaderGraph()` is added.\n", "\n", "* For each node, nodegraph, or material in the MaterialX document a corresponding node is created in Usd using:\n", " * `UsdShade.Shader.Define()`, \n", " * `UsdShade.NodeGraph.Define()`, and \n", " * `UsdShade.Material.Define()` respectively. \n", "* All MaterialX node instances are checked for a corresponding MaterialX definition (`nodedef`), and if found will set\n", "the identifier as the shader id for the Usd shader node.\n", "* Connections are then made between Usd nodes based on the connections found on MaterialX nodes." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [], "source": [ "def moveChild(newParent, child):\n", " newChild = newParent.addChildOfCategory(child.getCategory(), child.getName())\n", " print(newChild.getNamePath())\n", " newChild.copyContentFrom(child)\n", " oldParent = child.getParent()\n", " oldParent.removeChild(child.getName())\n", "\n", "def emitUsdShaderGraph(doc, stage, mxnodes, emitAllValueElements):\n", " \"\"\"\n", " Emit Usd shader graph to a given stage from a list of MaterialX nodes.\n", "\n", " Parameters\n", " ------------ \n", " doc: \n", " MaterialX source document\n", " stage:\n", " Usd target stage\n", " mxnodes:\n", " MaterialX shader nodes.\n", " emitAllValueElements: bool\n", " Emit value elements based on node definition, even if not specified on node instance. \n", " \"\"\"\n", " materialPath = None\n", "\n", " for v in mxnodes:\n", " elem = doc.getDescendant(v)\n", " if elem.getType() == 'material': \n", " materialPath = elem.getName()\n", " break\n", " \n", " # Emit Usd nodes\n", " for v in mxnodes:\n", " elem = doc.getDescendant(v)\n", "\n", " # Note that MaterialX does not use absolute path notation while Usd\n", " # does. This will result in an error when trying set the path\n", " usdPath = '/' + elem.getNamePath()\n", "\n", " nodeDef = None\n", " usdNode = None\n", " if elem.getType() == 'material':\n", " usdNode = UsdShade.Material.Define(stage, usdPath) \n", " elif elem.isA(mx.Node):\n", " nodeDef = elem.getNodeDef()\n", " if materialPath:\n", " elemPath = '/' + materialPath + usdPath\n", " else:\n", " elemPath = usdPath\n", " usdNode = UsdShade.Shader.Define(stage, elemPath)\n", " elif elem.isA(mx.NodeGraph):\n", " if materialPath:\n", " elemPath = '/' + materialPath + usdPath\n", " else:\n", " elemPath = usdPath\n", " usdNode = UsdShade.NodeGraph.Define(stage, elemPath)\n", "\n", " if usdNode:\n", " if nodeDef:\n", " usdNode.SetShaderId(nodeDef.getName())\n", " emitUsdValueElements(elem, usdNode, emitAllValueElements)\n", "\n", " # Emit connections between Usd nodes\n", " for v in mxnodes:\n", " elem = doc.getDescendant(v)\n", " usdPath = '/' + elem.getNamePath()\n", "\n", " if elem.getType() == 'material':\n", " emitUsdConnections(elem, stage, '/') \n", " elif elem.isA(mx.Node):\n", " emitUsdConnections(elem, stage, '/' + materialPath + '/') \n", " elif elem.isA(mx.NodeGraph):\n", " emitUsdConnections(elem, stage, '/' + materialPath + '/') \n" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "#### Top Level Conversion Logic\n", "\n", "The sample wrapper for conversion is called `convertMtlxToUsd()` which takes as input a MaterialX filename,\n", "creates a stage in memory and then performs the conversion.\n", "\n", "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()`." ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [], "source": [ "def findMaterialXNodes(doc):\n", " \"\"\"\n", " Find all nodes in a MaterialX document\n", " \"\"\"\n", " visitedNodes = []\n", " treeIter = doc.traverseTree()\n", " for elem in treeIter:\n", " path = elem.getNamePath()\n", " if path in visitedNodes:\n", " continue\n", " visitedNodes.append(path)\n", " return visitedNodes" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "\n", "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." ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "def convertMtlxToUsd(mtlxFileName, emitAllValueElements):\n", " \"\"\"\n", " Read in a MaterialX file and emit it to a new Usd Stage\n", " Dump results for display and save to usda file.\n", "\n", " Parameters:\n", " -----------\n", " mtlxFileName : string\n", " Name of file containing MaterialX document. Assumed to end in \".mtlx\"\n", " emitAllValueElements: bool\n", " Emit value elements based on node definition, even if not specified on node instance. \n", " \"\"\"\n", " stage = Usd.Stage.CreateInMemory()\n", " \n", " doc = mx.createDocument()\n", " mtlxFilePath = mx.FilePath(mtlxFileName)\n", " if not mtlxFilePath.exists():\n", " print('Failed to read file: ', mtlxFilePath.asString())\n", " return\n", " \n", " # Find nodes to transform before importing the definition library\n", " mx.readFromXmlFile(doc, mtlxFileName)\n", " mxnodes = findMaterialXNodes(doc)\n", " stdlib = mx.createDocument()\n", " libFiles = []\n", " searchPath = mx.getDefaultDataSearchPath()\n", " libFiles = mx.loadLibraries(mx.getDefaultDataLibraryFolders(), searchPath, stdlib)\n", " doc.importLibrary(stdlib)\n", " \n", " # Translate\n", " emitUsdShaderGraph(doc, stage, mxnodes, emitAllValueElements) \n", "\n", " usdFile = mtlxFileName.removesuffix('.mtlx')\n", " usdFile = usdFile + '.usda'\n", " print('Export USD file: ', usdFile)\n", " stage.Export(usdFile, False)\n", "\n", " return stage" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "### Test Files\n", "\n", "Conversion to a few test files is performed, including performing the reverse translation of the Usd sample file shown previously." ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "#### Sample Marble\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "#### Sample Marble Converted from MaterialX" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "Export USD file: data/sample_marble.usda\n" ] }, { "data": { "text/markdown": [ "
Usd Results\n", "\n", "```usd\n", "#usda 1.0\n", "\n", "def Material \"Marble_3D\"\n", "{\n", " token outputs:mtlx:surface.connect = \n", "\n", " def NodeGraph \"NG_marble1\"\n", " {\n", " color3f inputs:base_color_1 = (0.8, 0.8, 0.8)\n", " color3f inputs:base_color_2 = (0.1, 0.1, 0.3)\n", " int inputs:noise_octaves = 3\n", " float inputs:noise_power = 3\n", " float inputs:noise_scale_1 = 6\n", " float inputs:noise_scale_2 = 4\n", " color3f outputs:out.connect = \n", "\n", " def Shader \"obj_pos\"\n", " {\n", " uniform token info:id = \"ND_position_vector3\"\n", " string inputs:space = \"object\"\n", " vector3f outputs:out\n", " }\n", "\n", " def Shader \"add_xyz\"\n", " {\n", " uniform token info:id = \"ND_dotproduct_vector3\"\n", " vector3f inputs:in1 = (0, 0, 0)\n", " vector3f inputs:in1.connect = \n", " vector3f inputs:in2 = (1, 1, 1)\n", " float outputs:out\n", " }\n", "\n", " def Shader \"scale_xyz\"\n", " {\n", " uniform token info:id = \"ND_multiply_float\"\n", " float inputs:in1 = 0\n", " float inputs:in1.connect = \n", " float inputs:in2 = 1\n", " float inputs:in2.connect = \n", " float outputs:out\n", " }\n", "\n", " def Shader \"scale_pos\"\n", " {\n", " uniform token info:id = \"ND_multiply_vector3FA\"\n", " vector3f inputs:in1 = (0, 0, 0)\n", " vector3f inputs:in1.connect = \n", " float inputs:in2 = 1\n", " float inputs:in2.connect = \n", " vector3f outputs:out\n", " }\n", "\n", " def Shader \"noise\"\n", " {\n", " uniform token info:id = \"ND_fractal3d_float\"\n", " float inputs:amplitude = 1\n", " float inputs:diminish = 0.5\n", " float inputs:lacunarity = 2\n", " int inputs:octaves = 3\n", " int inputs:octaves.connect = \n", " vector3f inputs:position.connect = \n", " float outputs:out\n", " }\n", "\n", " def Shader \"scale_noise\"\n", " {\n", " uniform token info:id = \"ND_multiply_float\"\n", " float inputs:in1 = 0\n", " float inputs:in1.connect = \n", " float inputs:in2 = 3\n", " float outputs:out\n", " }\n", "\n", " def Shader \"sum\"\n", " {\n", " uniform token info:id = \"ND_add_float\"\n", " float inputs:in1 = 0\n", " float inputs:in1.connect = \n", " float inputs:in2 = 0\n", " float inputs:in2.connect = \n", " float outputs:out\n", " }\n", "\n", " def Shader \"sin\"\n", " {\n", " uniform token info:id = \"ND_sin_float\"\n", " float inputs:in = 0\n", " float inputs:in.connect = \n", " float outputs:out\n", " }\n", "\n", " def Shader \"scale\"\n", " {\n", " uniform token info:id = \"ND_multiply_float\"\n", " float inputs:in1 = 0\n", " float inputs:in1.connect = \n", " float inputs:in2 = 0.5\n", " float outputs:out\n", " }\n", "\n", " def Shader \"bias\"\n", " {\n", " uniform token info:id = \"ND_add_float\"\n", " float inputs:in1 = 0\n", " float inputs:in1.connect = \n", " float inputs:in2 = 0.5\n", " float outputs:out\n", " }\n", "\n", " def Shader \"power\"\n", " {\n", " uniform token info:id = \"ND_power_float\"\n", " float inputs:in1 = 0\n", " float inputs:in1.connect = \n", " float inputs:in2 = 1\n", " float inputs:in2.connect = \n", " float outputs:out\n", " }\n", "\n", " def Shader \"color_mix\"\n", " {\n", " uniform token info:id = \"ND_mix_color3\"\n", " color3f inputs:bg = (0, 0, 0)\n", " color3f inputs:bg.connect = \n", " color3f inputs:fg = (0, 0, 0)\n", " color3f inputs:fg.connect = \n", " float inputs:mix = 0\n", " float inputs:mix.connect = \n", " color3f outputs:out\n", " }\n", " }\n", "\n", " def Shader \"SR_marble1\"\n", " {\n", " uniform token info:id = \"ND_standard_surface_surfaceshader\"\n", " float inputs:base = 1\n", " color3f inputs:base_color = (0.8, 0.8, 0.8)\n", " color3f inputs:base_color.connect = \n", " float inputs:coat = 0\n", " float inputs:coat_affect_color = 0\n", " float inputs:coat_affect_roughness = 0\n", " float inputs:coat_anisotropy = 0\n", " color3f inputs:coat_color = (1, 1, 1)\n", " float inputs:coat_IOR = 1.5\n", " vector3f inputs:coat_normal\n", " float inputs:coat_rotation = 0\n", " float inputs:coat_roughness = 0.1\n", " float inputs:diffuse_roughness = 0\n", " float inputs:emission = 0\n", " color3f inputs:emission_color = (1, 1, 1)\n", " float inputs:metalness = 0\n", " vector3f inputs:normal\n", " color3f inputs:opacity = (1, 1, 1)\n", " float inputs:sheen = 0\n", " color3f inputs:sheen_color = (1, 1, 1)\n", " float inputs:sheen_roughness = 0.3\n", " float inputs:specular = 1\n", " float inputs:specular_anisotropy = 0\n", " color3f inputs:specular_color = (1, 1, 1)\n", " float inputs:specular_IOR = 1.5\n", " float inputs:specular_rotation = 0\n", " float inputs:specular_roughness = 0.1\n", " float inputs:subsurface = 0.4\n", " float inputs:subsurface_anisotropy = 0\n", " color3f inputs:subsurface_color = (1, 1, 1)\n", " color3f inputs:subsurface_color.connect = \n", " color3f inputs:subsurface_radius = (1, 1, 1)\n", " float inputs:subsurface_scale = 1\n", " vector3f inputs:tangent\n", " float inputs:thin_film_IOR = 1.5\n", " float inputs:thin_film_thickness = 0\n", " bool inputs:thin_walled = 0\n", " float inputs:transmission = 0\n", " color3f inputs:transmission_color = (1, 1, 1)\n", " float inputs:transmission_depth = 0\n", " float inputs:transmission_dispersion = 0\n", " float inputs:transmission_extra_roughness = 0\n", " color3f inputs:transmission_scatter = (0, 0, 0)\n", " float inputs:transmission_scatter_anisotropy = 0\n", " token outputs:out\n", " }\n", "}\n", "\n", "```\n", "
\n" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/markdown": [ "
Converted Back to MaterialX\n", "\n", "```xml\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", "```\n", "
\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "testFile = 'data/sample_marble.mtlx'\n", "\n", "# Convert to Usd. Indicate to include all inputs based on a MaterialX node's definition\n", "# as opposed to just those explicitly specified on the node instance.\n", "display_markdown('#### Sample Marble Converted from MaterialX', raw=True)\n", "includeDefinitionInputs = True\n", "stage = convertMtlxToUsd(testFile, includeDefinitionInputs)\n", "stringResult = stage.GetRootLayer().ExportToString()\n", "text = '
Usd Results\\n\\n' + '```usd\\n' + stringResult + '```\\n' + '
\\n' \n", "display_markdown(text , raw=True)\n", "\n", "# Convert back to MaterialX\n", "doc = convertUsdToMtlx(stage, stdlib)\n", "result, error = doc.validate()\n", "if error:\n", " print(error)\n", "writeOptions = mx.XmlWriteOptions()\n", "writeOptions.writeXIncludeEnable = False\n", "writeOptions.elementPredicate = skipLibraryElement\n", "documentContents = mx.writeToXmlString(doc, writeOptions)\n", "text = '
Converted Back to MaterialX\\n\\n' + '```xml\\n' + documentContents + '```\\n' + '
\\n' \n", "display_markdown(text , raw=True)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "#### Sample Nodegraph from NodeGraph Tutorial\n", "\n", "Here the example MaterialX file produced from the *Nodegraph* book is converted." ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "#### Sample Tutorial Nodegraph Converted from MaterialX" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "Export USD file: data/sample_nodegraph.usda\n" ] }, { "data": { "text/markdown": [ "
Usd Results\n", "\n", "```usd\n", "#usda 1.0\n", "\n", "def Material \"my_material\"\n", "{\n", " token outputs:mtlx:surface.connect = \n", "\n", " def NodeGraph \"test_nodegraph\"\n", " {\n", " float inputs:color_scale = 0.2\n", " asset inputs:input_file = @checker.png@\n", " token outputs:out.connect = \n", "\n", " def Shader \"test_shader\"\n", " {\n", " uniform token info:id = \"ND_standard_surface_surfaceshader\"\n", " float inputs:base.connect = \n", " color3f inputs:base_color.connect = \n", " token outputs:out\n", " }\n", "\n", " def Shader \"test_image\"\n", " {\n", " uniform token info:id = \"ND_image_color3\"\n", " asset inputs:file.connect = \n", " color3f outputs:out\n", " }\n", " }\n", "}\n", "\n", "```\n", "
\n" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/markdown": [ "
Converted Back to MaterialX\n", "\n", "```xml\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", "```\n", "
\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "testFile = 'data/sample_nodegraph.mtlx'\n", "display_markdown('#### Sample Tutorial Nodegraph Converted from MaterialX', raw=True)\n", "stage = convertMtlxToUsd(testFile, False)\n", "stringResult = stage.GetRootLayer().ExportToString()\n", "text = '
Usd Results\\n\\n' + '```usd\\n' + stringResult + '```\\n' + '
\\n' \n", "display_markdown(text , raw=True)\n", "\n", "# Convert back to MaterialX\n", "doc = convertUsdToMtlx(stage, stdlib)\n", "writeOptions = mx.XmlWriteOptions()\n", "writeOptions.writeXIncludeEnable = False\n", "writeOptions.elementPredicate = skipLibraryElement\n", "documentContents = mx.writeToXmlString(doc, writeOptions)\n", "text = '
Converted Back to MaterialX\\n\\n' + '```xml\\n' + documentContents + '```\\n' + '
\\n' \n", "display_markdown(text , raw=True)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "#### Re-import Usd Example Converted to MaterialX\n", "\n", "Finally, the MaterialX file converted from Usd previously is re-converted back into Usd.\n", "\n", "For validation purposes bi-directional conversion and compare is useful to ensure there is no loss of data when\n", "performing data model interop. At time of writing, this round trip logic is not easily accessible." ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "#### Nested Nodegraph Converted from MaterialX" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "Export USD file: data/test_usd_mtlx.usda\n" ] }, { "data": { "text/markdown": [ "
Usd Results\n", "\n", "```usd\n", "#usda 1.0\n", "\n", "def Material \"collect1\"\n", "{\n", " token outputs:mtlx:displacement.connect = \n", " token outputs:mtlx:surface.connect = \n", "\n", " def NodeGraph \"my_materialx_subnet\"\n", " {\n", " color3f inputs:base_color\n", " token outputs:displacement.connect = \n", " token outputs:surface.connect = \n", "\n", " def Shader \"mtlxstandard_surface1\"\n", " {\n", " uniform token info:id = \"ND_standard_surface_surfaceshader\"\n", " float inputs:base = 1\n", " color3f inputs:base_color.connect = \n", " float inputs:coat = 0\n", " float inputs:coat_roughness = 0.1\n", " float inputs:emission = 0\n", " color3f inputs:emission_color = (1, 1, 1)\n", " float inputs:metalness = 0\n", " float inputs:specular = 1\n", " color3f inputs:specular_color = (1, 1, 1)\n", " float inputs:specular_IOR = 1.5\n", " float inputs:specular_roughness.connect = \n", " float inputs:transmission = 0\n", " token outputs:out\n", " }\n", "\n", " def NodeGraph \"image_readers\"\n", " {\n", " color3f inputs:_base_color.connect = \n", " color3f outputs:out.connect = \n", " float outputs:out_2.connect = \n", "\n", " def Shader \"mtlximage1\"\n", " {\n", " uniform token info:id = \"ND_image_color3\"\n", " color3f inputs:default.connect = \n", " asset inputs:file = @file1.png@\n", " color3f outputs:out\n", " }\n", "\n", " def Shader \"mtlximage2\"\n", " {\n", " uniform token info:id = \"ND_image_float\"\n", " asset inputs:file = @file2.png@\n", " float outputs:out\n", " }\n", " }\n", "\n", " def Shader \"mtlxdisplacement\"\n", " {\n", " uniform token info:id = \"ND_displacement_float\"\n", " token outputs:out\n", " }\n", " }\n", "\n", " def NodeGraph \"usdpreview_subnet\"\n", " {\n", " color3f inputs:base_color\n", " token outputs:surface.connect = \n", "\n", " def Shader \"usdpreviewsurface1\"\n", " {\n", " uniform token info:id = \"ND_UsdPreviewSurface_surfaceshader\"\n", " color3f inputs:diffuseColor.connect = \n", " token outputs:out\n", " }\n", " }\n", "}\n", "\n", "```\n", "
\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "testFile = 'data/test_usd_mtlx.mtlx'\n", "display_markdown('#### Nested Nodegraph Converted from MaterialX', raw=True)\n", "stage = convertMtlxToUsd(testFile, False)\n", "stringResult = stage.GetRootLayer().ExportToString()\n", "text = '
Usd Results\\n\\n' + '```usd\\n' + stringResult + '```\\n' + '
\\n' \n", "display_markdown(text , raw=True)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Appendix: Mapping Usd Types To MaterialX Types\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "
Usd Types\n", "\n", "- Type : Asset\n", "- Type : AssetArray\n", "- Type : Bool\n", "- Type : BoolArray\n", "- Type : Color3d\n", "- Type : Color3dArray\n", "- Type : Color3f\n", "- Type : Color3fArray\n", "- Type : Color3h\n", "- Type : Color3hArray\n", "- Type : Color4d\n", "- Type : Color4dArray\n", "- Type : Color4f\n", "- Type : Color4fArray\n", "- Type : Color4h\n", "- Type : Color4hArray\n", "- Type : Double\n", "- Type : Double2\n", "- Type : Double2Array\n", "- Type : Double3\n", "- Type : Double3Array\n", "- Type : Double4\n", "- Type : Double4Array\n", "- Type : DoubleArray\n", "- Type : Find\n", "- Type : Float\n", "- Type : Float2\n", "- Type : Float2Array\n", "- Type : Float3\n", "- Type : Float3Array\n", "- Type : Float4\n", "- Type : Float4Array\n", "- Type : FloatArray\n", "- Type : Frame4d\n", "- Type : Frame4dArray\n", "- Type : Group\n", "- Type : Half\n", "- Type : Half2\n", "- Type : Half2Array\n", "- Type : Half3\n", "- Type : Half3Array\n", "- Type : Half4\n", "- Type : Half4Array\n", "- Type : HalfArray\n", "- Type : Int\n", "- Type : Int2\n", "- Type : Int2Array\n", "- Type : Int3\n", "- Type : Int3Array\n", "- Type : Int4\n", "- Type : Int4Array\n", "- Type : Int64\n", "- Type : Int64Array\n", "- Type : IntArray\n", "- Type : Matrix2d\n", "- Type : Matrix2dArray\n", "- Type : Matrix3d\n", "- Type : Matrix3dArray\n", "- Type : Matrix4d\n", "- Type : Matrix4dArray\n", "- Type : Normal3d\n", "- Type : Normal3dArray\n", "- Type : Normal3f\n", "- Type : Normal3fArray\n", "- Type : Normal3h\n", "- Type : Normal3hArray\n", "- Type : Opaque\n", "- Type : PathExpression\n", "- Type : PathExpressionArray\n", "- Type : Point3d\n", "- Type : Point3dArray\n", "- Type : Point3f\n", "- Type : Point3fArray\n", "- Type : Point3h\n", "- Type : Point3hArray\n", "- Type : Quatd\n", "- Type : QuatdArray\n", "- Type : Quatf\n", "- Type : QuatfArray\n", "- Type : Quath\n", "- Type : QuathArray\n", "- Type : String\n", "- Type : StringArray\n", "- Type : TexCoord2d\n", "- Type : TexCoord2dArray\n", "- Type : TexCoord2f\n", "- Type : TexCoord2fArray\n", "- Type : TexCoord2h\n", "- Type : TexCoord2hArray\n", "- Type : TexCoord3d\n", "- Type : TexCoord3dArray\n", "- Type : TexCoord3f\n", "- Type : TexCoord3fArray\n", "- Type : TexCoord3h\n", "- Type : TexCoord3hArray\n", "- Type : TimeCode\n", "- Type : TimeCodeArray\n", "- Type : Token\n", "- Type : TokenArray\n", "- Type : UChar\n", "- Type : UCharArray\n", "- Type : UInt\n", "- Type : UInt64\n", "- Type : UInt64Array\n", "- Type : UIntArray\n", "- Type : Vector3d\n", "- Type : Vector3dArray\n", "- Type : Vector3f\n", "- Type : Vector3fArray\n", "- Type : Vector3h\n", "- Type : Vector3hArray\n", "
\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "typestring = ''\n", "for t in dir(Sdf.ValueTypeNames):\n", " if t.startswith('__'):\n", " continue\n", " typestring = typestring + '- Type : ' + str(t) + '\\n'\n", "text = '
Usd Types\\n\\n' + typestring + '
\\n' \n", "display_markdown(text , raw=True)" ] } ], "metadata": { "kernelspec": { "display_name": ".venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.10" }, "orig_nbformat": 4 }, "nbformat": 4, "nbformat_minor": 2 }