UsdShade : USD Shading Schema

UsdShade provides schemas and behaviors for creating and binding materials, which encapsulate shading networks.

UsdShade Networks

UsdShade provides schemas and behaviors for creating shading networks (UsdShadeNodeGraph) and materials (UsdShadeMaterial). The networks are composed of UsdShadeShader objects, as well as other UsdShadeNodeGraph.

Objects in a network are connected together and to their encapsulating Material using the UsdShadeConnectableAPI schema, which allows one to create UsdShadeInput and UsdShadeOutput (which are UsdAttribute schemas), and connect them using UsdAttribute connections.

Here's a python example.

# create material
materialPath = Sdf.Path('/Model/Materials/MyMaterial')
material = UsdShade.Material.Define(stage, materialPath)
# create shaders
downstreamShader = UsdShade.Shader.Define(
stage, materialPath.AppendChild('Downstream'))
upstreamShader = UsdShade.Shader.Define(
stage, materialPath.AppendChild('Upstream'))
# Connect
inputPort = downstreamShader.CreateInput(
'DownstreamInput', Sdf.ValueTypeNames.Float)
inputPort.ConnectToSource(upstreamShader, 'UpstreamOutput')

This will yield a material with two connected nodes.

#usda 1.0
def "Model"
{
def "Materials"
{
def Material "MyMaterial"
{
def Shader "Downstream"
{
float inputs:DownstreamInput.connect =
</Model/Materials/MyMaterial/Upstream.outputs:UpstreamOutput>
}
def Shader "Upstream"
{
float outputs:UpstreamOutput
}
}
}
}

Encapsulation and Sharing

Note
In UsdShade, all shaders are UsdPrims or just "prims". However, in deference to the larger body of technical discourse on shading, we will refer to them as "nodes" in this discussion.

Shading nodes should be encapsulated in a containing object, and are not generally used in isolation.

Shading networks can be organized into coherent packaged units (UsdShadeNodeGraph), with their own public parameters exposed and connected to the internal nodes. In this scenario, the UsdShadeNodeGraph is a parent or ancestor prim (UsdShadeNodeGraph can be nested) to all of the UsdShadeShader prims in the network, and serves as the point of encapsulation - the UsdShadeNodeGraph prim can then be referenced into other, larger networks as a building block, with its entire network intact. When referenced into larger networks, NodeGraphs can also be instanced so that they appear as a single prim in the network, and can be processed more efficiently when referenced from multiple locations.

If the network of shading nodes is directly consumable as a "shader" of a type known to some client renderer (e.g. a surface shader), then the encapsulating parent/ancestor should be declared as a UsdShadeMaterial, which is a container that can also be bound to geometries or collections. Materials can also be reused and instanced, retaining the same network but allowing top-level "Material Interface" parameters to be authored uniquely.

Containers vs Primitive Shading Nodes

Containers are prims that are designed to have child prims, like UsdShadeMaterial and UsdShadeNodeGraph, and that encapsulate parts or a whole network. These containers are differentiated against non-containers, which are are shader nodes like UsdShadeShader prims.

Container types are identified by their UsdShadeConnectableAPI::IsContainer() implementation, which is an extensible API, such that other types can work as containers with the other UsdShade APIs, by implementing the UsdShadeConnectableAPIBehavior plugin interface.

Containers usually define an interface that establishes input and output attributes, which allows exposing functionality of the encapsulated part of the network. With such an interface definition they can be treated in many ways like a primitive shading node, but have an implementation that can be inspected in Usd.

Exposing parameters on containers

To expose a parameter to the container, we use the same mechanism that connects nodes.

# Expose a parameter to the public interface
internalPort = upstreamShader.CreateInput(
'internalPort', Sdf.ValueTypeNames.Float)
exposedPort = material.CreateInput(
'ExposedPort', Sdf.ValueTypeNames.Float)
exposedPort.Set(1.0)
internalPort.ConnectToSource(exposedPort)

Which will yield a public interface parameter called 'ExposedPort' on the UsdShadeMaterial called 'MyMaterial', and set its default value to 1.0

#usda 1.0
def "Model"
{
def "Materials"
{
def Material "MyMaterial"
{
float inputs:ExposedPort = 1
def Shader "Downstream"
{
float inputs:DownstreamInput.connect =
</Model/Materials/MyMaterial/Upstream.outputs:UpstreamOutput>
}
def Shader "Upstream"
{
float inputs:internalPort.connect =
</Model/Materials/MyMaterial.inputs:ExposedPort>
float outputs:UpstreamOutput
}
}
}
}

To expose an output of a node network as an output of a NodeGraph, or as a "terminal output" of a Material, we again use the same connection API, except that now we are connecting an Output to another Output (in effect, forwarding the Output from a node to its encapsulating container):

# The output represents the result of the shader's computation. For
# complex types like "surface illumination" we use the type Token as
# a standin for the type specific to the renderer
outPort = surfaceShader.CreateOutput(
'out', Sdf.ValueTypeNames.Token)
surfaceTerminal = material.CreateOutput(
'surface', Sdf.ValueTypeNames.Token)
# For outputs, it is the container's Output that connect's to the Node's
# output
surfaceTerminal.ConnectToSource(outPort)

Which will yield a public interface parameter called 'ExposedPort' on the UsdShadeMaterial called 'MyMaterial', and set its default value to 1.0

#usda 1.0
def "Model"
{
def "Materials"
{
def Material "MyMaterial"
{
token outputs:surface.connect =
</Model/Materials/MyMaterial/Surface.outputs:out>
def Shader "Surface"
{
token outputs:out
}
}
}
}

Connectability Rules for UsdShade Types

As noted above, encapsulation is critical to UsdShade connectability rules, with different UsdShade nodes providing appropriate connectivity rules described below:

  • UsdShadeShader: Inputs can be connected to any Input or Output of any other shader or NodeGraph encapsulated by the same nearest-in-namespace encapsulating NodeGraph or Nodegraph-derived container. Outputs cannot be connected.
  • UsdShadeNodeGraph: Inputs follow the same rule as Shaders. Outputs can be connected to any Output on a prim (Shader or NodeGraph) encapsulated by the NodeGraph itself, or to an Input of the same NodeGraph itself, creating a "pass through" connection.
  • Default behavior for NodeGraph-derived Types (e.g. UsdShadeMaterial) Inputs and Outputs follow the same rule, which is that they can be connected to any Output on a prim (Shader or NodeGraph) encapsulated by the Material itself. Note that "pass through" connections are not allowed for Nodegraph-derived container nodes.

    Sub-classes of UsdShadeNodeGraph can override UsdShadeConnectableAPIBehavior interface to provide a different implementation.

Note that interface-only connections can only happen between inputs and source which have "interfaceOnly" connectivity.

Connections and Dataflow in UsdShade

UsdShade uses UsdAttribute connections both to indicate dataflow from shading node outputs to inputs, and to indicate pre-rendering propagation of values authored on UsdShadeNodeGraph and UsdShadeMaterial inputs to shader node inputs. In USD, connections (and relationships) are authored on the consumer, and target the source or producer. Therefore, data in a UsdShade network flows from a connection's target to its anchor. To reliably translate UsdShade networks for consumption by renderers, we need to establish a few rules about how values propagate in the face of connections.

Valid Shader Connections Win Over Input Values

When an input on a shading node has both an authored value (default or timeSamples), and a connection to an output on another shading node, then the connection alone is transmitted to the renderer - the authored value is irrelevant. Connections that target an output that does not exist in the containing Material are ignored; if the connected input has an authored value, then in this case, and this case alone, we pass the value to the renderer and ignore the connection.

In the following example, we will provide values to the renderer for inputs valueOnly (2) and brokenConnection (4), while informing the renderer of a connection between validOutput and connected, ignoring the value authored of 42 on connected.

#usda 1.0
def "Model"
{
def "Materials"
{
def Material "MyMaterial"
{
def Shader "Downstream"
{
float inputs:brokenConnection = 4
float inputs:brokenConnection.connect =
</Model/Materials/MyMaterial/MissingShader.outputs:MissingOutput>
float inputs:connected = 42
float inputs:connected.connect =
</Model/Materials/MyMaterial/Upstream.outputs:UpstreamOutput>
float inputs:valueOnly = 2
}
def Shader "Upstream"
{
float outputs:UpstreamOutput
}
}
}
}

Resolving Interface Connections

When we create inputs on NodeGraphs or Materials to serve as "public interface" for shading properties, it is common to create an appropriately-typed attribute, but not provide a default value for it. When USD is the document for a material shading network, this "uninitialized interface attribute" allows the Material to continue to receive updates to published shaders made available through the SdrRegistry long after the Material has been created. Why? Because of the first rule of interface value propagation:

  • If a Material or NodeGraph input provides no value, and one or more of its shader's inputs connects to the interface attribute, then the value supplied to the renderer for that shading input should be whatever value is authored on the shader input, or if none is authored, then we emit no value to the renderer, indicating it should simply follow the shader implementation's own default value.

NodeGraphs can be embedded inside Materials, and also as nested components inside other NodeGraphs. Because of this nestability, it is posible that a deeply embedded shader node input may need to travel several connection hops to find an interface attribute that provides a value for it to use. This leads to the second and final rule of interface value propagation:

  • If a shader node input is connected to a containing NodeGraph input that is in turn connected to an outer-containing NodeGraph or Material, it is the outermost authored input default in the connection chain that provides the shader input's value. This allows the "user" of a NodeGraph to always be able to drive its inputs from its own public interface.

Putting these two rules together, in the example below, we expect the following values to be passed to the renderer for each shader input:

  • spOne = 4, because neither of the interface attributes in its connection chain supply a value.
  • spTwo = 14, because matInterfaceTwo provides the strongest opinion, as the outermost value-provider in the connection chain.
  • spThree = 64, because only its directly-embedding NodeGraph's interface attribute provides a value stronger than its own default.
#usda 1.0
def "Model"
{
def "Materials"
{
def Material "MyMaterial"
{
float inputs:matInterfaceOne
float inputs:matInterfaceTwo = 14
float inputs:matInterfaceThree
def NodeGraph "Package"
{
float inputs:ngInterfaceOne.connect =
</Model/Materials/MyMaterial.inputs:matInterfaceOne>
float inputs:ngInterfaceTwo = 28
float inputs:ngInterfaceTwo.connect =
</Model/Materials/MyMaterial.inputs:matInterfaceTwo>
float inputs:ngInterfaceThree = 64
float inputs:ngInterfaceThree.connect =
</Model/Materials/MyMaterial.inputs:matInterfaceThree>
def Shader "EmbeddedInNG"
{
float inputs:spOne = 4
float inputs:spOne.connect =
</Model/Materials/MyMaterial/Package.inputs:ngInterfaceOne>
float inputs:spTwo = 5
float inputs:spTwo.connect =
</Model/Materials/MyMaterial/Package.inputs:ngInterfaceTwo>
float inputs:spThree = 6
float inputs:spThree.connect =
</Model/Materials/MyMaterial/Package.inputs:ngInterfaceThree>
}
}
}
}
}

NodeGraphs also define outputs to declare the signals that are provided for the rest of the network. From the outside, which is where the NodeGraph is connected to other shading nodes or NodeGraphs, the outputs behave conceptually like those on shading nodes. On the inside of the NodeGraph the outputs are connected to outputs of nested shading nodes or nested NodeGraphs or they can be connected to input attributes on the same NodeGraph as a pass through mechanism.

In the example below we have a NodGraph with two inputs (ngPassThruIn and ngToModifyIn) and two outputs (ngPassThruOut and ngModifiedOut). ngPassThruIn is sent straight to ngPassThruOut without modification, which means it is essentially forwarding connections and effectively result1 is connected to input1 directly. The input of ngToModifyIn is fed to the Modifier shading node, which sends a modified result to ngModifiedOut, which effectively models result2 -> toModify (on Modifier) and modified -> input2.

#usda 1.0
def "Model"
{
def "Materials"
{
def Material "MyMaterial"
{
def Shader "Generator"
{
float outputs:result1
float outputs:result2
}
def NodeGraph "Package"
{
float inputs:ngPassThruIn.connect =
</Model/Materials/MyMaterial/Generator.outputs:result1>
float inputs:ngToModifyIn.connect =
</Model/Materials/MyMaterial/Generator.outputs:result2>
float outputs:ngPassThruOut.connect =
</Model/Materials/MyMaterial/Package.inputs:ngPassThruIn>
float outputs:ngModifiedOut.connect =
</Model/Materials/MyMaterial/Package/Modifier.outputs:modified>
def Shader "Modifier"
{
float inputs:toModify.connect =
</Model/Materials/MyMaterial/Package.inputs:ngToModifyIn>
float outputs:modified
}
}
def Shader "Consumer"
{
float inputs:input1.connect =
</Model/Materials/MyMaterial/Package.outputs:ngPassThruOut>
float inputs:input2.connect =
</Model/Materials/MyMaterial/Package.outputs:ngModifiedOut>
}
}
}
}

Connection Resolution Utilities

The resolution of the rules described above are implemented in a helper function called UsdShadeUtils::GetValueProducingAttributes, which takes either a UsdShadeInput or UsdShadeOutput and computes the UsdAttribute(s) that either carry the default value(s) that should be sent to the renderer or the UsdShadeOutput(s) of nodes to which connections should be established. For convenience and discoverability this method is also available on Inputs and Outputs directly as UsdShadeInput::GetValueProducingAttributes and UsdShadeOutput::GetValueProducingAttributes.

This handles both the rules that connections win over default values and the forwarding logic to the interfaces of containers, like Materials or NodeGraphs. This utility function can be seen as a way to turn physical connections into logical ones. This can be useful for renderers ingesting a Material network because generally the logical connections are their only concern. This conversion "flattens" the network into a simple network that only contains Shaders with their respective input values and connections.

There are situations where only valid output attributes of shading nodes are desired and hence we have a mode, where it will not report input or output attributes on containers that carry default values. This mode can be activated by the optional shaderOutputsOnly flag.

Attributes in Usd can have not just a single connection target, but potentially multiple attributes they target. UsdShade supports having multiple connections for Inputs and Outputs (of containers, only), which means that UsdShadeUtils::GetValueProducingAttributes also handles these scenarios. The function is essentially performing a depth first connection tracing, which means that when a multi-connection is encountered along the path the tracing splits and potentially multiple source attributes are found. Note that invalid connections targets are skipped. The function will report all valid upstream attributes, which can be a mix of Inputs with default values and Outputs of Shader nodes. It is up to the client to resolve and report any inconsistencies based on what is supported in the target rendering system.

UsdShade Based Shader Definition

UsdShade has an NdrParserPlugin (UsdShadeShaderDefParserPlugin) that enables shader definitions to be encoded as USD scene description using the schemas available in UsdShade. A discovery plugin can open a USD stage containing shader definitions and populate the shader registry with nodes using the API UsdShadeShaderDefUtils::GetNodeDiscoveryResults().

A USD file containing UsdShade-based shader definitions must adhere to the following rules, in order to produce valid SdrShaderNode s in the shader registry:

  • Every concrete shader prim at the root of the composed UsdStage should represent a new and complete shader definition. Inherits, references and other composition arcs may be used to avoid redundant scene description.
  • The shader prim's name becomes the unique identifier of the corresponding shader node in the registry. A shader's identifier is a concatenation of the
    1. family name of the shader,
    2. any type variations pertaining to the shader and
    3. the shader version, which can contain one or two ints representing the major number and an optional minor number. The type variations and shader version are optional parts of a shader identifier (i.e. not all shader identifiers may include them). If present, the different parts of the identifier are delimited by an underscore. Using UsdShadeShaderDefUtils::SplitShaderIdentifier, a shader's identifier can be split into the family name, implementation-name of the shader node (which includes the family name and the type information) and the shader version. For example,
    • if the shader prim is named "MultiTexture", the family name of the SdrShaderNode will be "MultiTexture". The corresponding shader-node's implementation name will also be "MultiTexture" and its version will be empty.
    • if the shader prim is named "MultiTexture_float2", the family name of the shader will be "MultiTexture" and its implementation name will be "MultiTexture_float2". Its version will be empty.
    • if the shader prim is named "MultiTexture_3", the family name of the shader will be "MultiTexture". It's implementation name will also be "MultiTexture" and its version will be 3.
    • if the shader prim is named "MultiTexture_float2_3_1", the family name of the shader will be "MultiTexture". The implementation name will include the type information and be set to "Primvar_float2".
  • The info:id attribute value of the shader, if authored, must match the name of the shader prim (i.e. the identifier of the SdrShaderNode).
  • The info:implementationSource of the shader must be UsdShadeTokens-> sourceAsset. There must be one or more "info:SOURCE_TYPE:sourceAsset" attributes that point to resolvable shader implementations for different source types (eg, glslfx, OSL etc.).
  • Shader prims, their inputs and outputs can contain sdrMetadata values meant to be recorded in the shader registry. The keys in the sdrMetadata dictionary correspond to the keys in SdrNodeMetadata and SdrPropertyMetadata. The only exceptions are as follows:
    • defaultInput metadatum on shader inputs gets translated to a more obscure key value of __SDR__defaultInput (which is the value of SdrPropertyMetadata->DefaultInput) in the metadata dictionary recorded by SdrRegistry.
    • Setting sdrMetadata["primvarProperty"]="1" on a shader input implies that the input names a primvar to be consumed by the shader. This causes '$' + inputName to be included in the SdrShaderNode->Primvars metadata on the SdrShaderNode. Note that it's not translated to metadata on the property itself.
    • connectability metadata authored on UsdShadeInputs gets translated to SdrPropertyMetadata->Connectable. Connectability value of "interfaceOnly" is converted to connectable="0". Connectability value of "full" is converted to connectable="1".
    • SdfAssetPath (or asset) valued shader inputs are automatically tagged with sdr metadata SdrPropertyMetadata->IsAssetIdentifier="1".
    • sdrMetadata["swizzle"] is metadata that can be specified for properties in SdrShaderProperty output definitions that describes the component(s) of the full color/vector output value produced by the shader property, and is necessary for shading systems that rely on dynamic code generation rather than self-contained shader-objects/closures. swizzle metadata is not meant to ever appear in user documents, and does not provide the ability to swizzle data on input connections.
    • sdrMetadata["implementationName"] specifies the name that will be returned by SdrShaderProperty::GetImplementationName().

Here's an example shader definition file with comments explaining the various bits.

#usda 1.0
# The prim name becomes the SdrShaderNode's identifier.
def Shader "Primvar_float_2" (
doc = "Version 2 of a Primvar node that outputs a float"
sdrMetadata = {
# This identifies the shader's role in the shading network as being
# a primvar reader.
token role = "primvar"
# The following sdr-metadatum could be authored on the node directly
# <b>in lieu of</b> authoring primvarProperty="1" on
# inputs:primvarName.
# string primvars = "$primvarName"
}
)
{
uniform token info:implementationSource = "sourceAsset"
# If primvarReader.oso can be resolved to an existent asset, then a
# SdrShaderNode is created with sourceType=OSL and sourceUri pointing
# to the resolved primvarReader.oso file.
uniform asset info:OSL:sourceAsset = @primvarReader.oso@
# If primvarReader.glslfx can be resolved to an existent asset, then
# another SdrShaderNode is created with sourceType=glslfx and sourceUri
# pointing to the resolved primvarReader.glslfx file.
uniform asset info:glslfx:sourceAsset = @primvarReader.glslfx@
token inputs:primvarName (
connectability = "interfaceOnly"
sdrMetadata = {
# This causes '$primvarName' to be appended to the
# SdrNodeMetadata->Primvars metadata on the SdrShaderNode.
string primvarProperty = "1"
}
doc = """Name of the primvar to be fetched from the geometry."""
)
# Asset valued inputs are automatically tagged with
# sdrMetadata[SdrPropertyMetadata->IsAssetIdentifier] = "1".
asset inputs:primvarFile = @@ (
connectability = "interfaceOnly"
doc = """File containing some primvar info."""
)
float inputs:fallback = 0.0 (
doc = """Fallback value to be returned when fetch failed."""
sdrMetadata = {
# This gets translated to SdrPropertyMetadata->DefaultInput="1"
# on the "fallback" SdrShaderProperty.
token defaultInput = "1"
}
)
float outputs:result
}