Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions documents/Specification/MaterialX.Proposals.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,115 @@ Materials can inherit from other materials, to add or change shaders connected t
Inheritance of material-type custom nodes is also allowed, so that new or changed input values can be applied on top of those specified in the inherited material.



### TypeDef Token Elements

TypeDef token elements allow the definition of value strings with a consistent meaning across types, e.g. a `half` value that corresponds to "0.5" for the `float` type and "0.5, 0.5, 0.5" for the `vector3` type. References to TypeDef tokens are restricted to typed `value` attributes, and are bracketed by the `[` and `]` characters, e.g. `[half]`. Within a typed `value` attribute, TypeDef token substitutions are applied before all other token substitutions.

```xml
<typedef name="float">
<token name="zero" type="float" value="0.0" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type specification here feels unnecessary, given the scope of the element.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a completely fair point, and I mainly added this to be consistent with the concept of "typed values" in MaterialX, where the element itself has enough information for an accessor to interpret its combined type and value attributes as a typed value in the engine.

It's certainly not a requirement here, but it seems consistent with how typed values are handled elsewhere in the MaterialX specification and API.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are any of those other typed value use-cases encapsulated in a container element that concretely defines the type? if they are, then I think maybe there is precedent enough to retain this, otherwise I feel we should remove it for both brevity and lack of potential error/confusion.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although I see the perspective that you're outlining, it's worth noting that there are no typed MaterialX elements that state a value attribute without also stating the type attribute that clarifies its interpretation. This is what allows the API calls in TypedElement to be shared across a wide set of Element subclasses, since they all build upon the same expression of a typed value.

From that perspective, it seems more robust to state both value and type within the token element itself, rather than making an exception when the parent of the element has a type-like name.

<token name="half" type="float" value="0.5" />
<token name="one" type="float" value="1.0" />
</typedef>
<typedef name="color3" semantic="color" >
<token name="zero" type="color3" value="0.0, 0.0, 0.0" />
<token name="half" type="color3" value="0.5, 0.5, 0.5" />
<token name="one" type="color3" value="1.0, 1.0, 1.0" />
</typedef>
<typedef name="color4" semantic="color">
<token name="zero" type="color4" value="0.0, 0.0, 0.0, 0.0" />
<token name="half" type="color4" value="0.5, 0.5, 0.5, 0.5" />
<token name="one" type="color4" value="1.0, 1.0, 1.0, 1.0" />
</typedef>
<typedef name="vector2" >
<token name="zero" type="vector2" value="0.0, 0.0" />
<token name="half" type="vector2" value="0.5, 0.5" />
<token name="one" type="vector2" value="1.0, 1.0" />
</typedef>
<typedef name="vector3" >
<token name="zero" type="vector3" value="0.0, 0.0, 0.0" />
<token name="half" type="vector3" value="0.5, 0.5, 0.5" />
<token name="one" type="vector3" value="1.0, 1.0, 1.0" />
</typedef>
<typedef name="vector4">
<token name="zero" type="vector4" value="0.0, 0.0, 0.0, 0.0" />
<token name="half" type="vector4" value="0.5, 0.5, 0.5, 0.5" />
<token name="one" type="vector4" value="1.0, 1.0, 1.0, 1.0" />
</typedef>
```

### Template Elements
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The great news is that I think our proposals look pretty similar, so that should make things easy!

For clarity perhaps I can enumerate what I see as the differences and we can discuss from there....

1) ( and ) vs @ to wrap token

This is something that would be straight forward to adopt in my PR if thats desired. I have no objections here, the choice of @ was always somewhat arbitrary - I agree that the visual appearance of the key being surrounded by ( and ) appears visually more appealing.

2) Simple variable replacement

You are choosing to restrict the template replacement mechanism to just a single key. In my investigations while templatizing the entire standard data library I found this was insufficient. You can look at my PR for the concrete examples where I have more than one name specified in typenames. I am more than happy to conform on naming from typenames to keys in thats preferred - that seems like a nice improvement.

3) Defining the constant values

We're pretty similar here, happy to debating naming of attributes and elements. Other than naming the difference is that you do include a type attribute in the element, which to me feels unnecessary given the scope of the element, the parent element is already defining the type. Including a type attribute here feels like it doesn't add anything, and would only be a source of errors of frustration if they differ with the parent type. Much like in the function nodegraph proposal is removing the node definition attribute from its description because its not needed due to element scope, I feel we should do the same here.

And here's the place where maybe we're furthest apart...

4) Reusing the syntax for constant value and template replacement

You're re-using the (key) syntax for the constant value replacement. I think they can and should be separate, because they really are different elements of functionality.

Personally I think that <input name="in" type="vector3" value="(zero)" /> is less clear in its intentions than <input name="in" type="vector3" value="Value:zero" /> or <input name="in" type="vector3" value="Constant:zero" />. I also worry that given we're re-using the template replacement syntax, it leads the users to think they are allowed to write something like. <input name="in" type="vector3" value="(zero) (zero) (zero)" /> because it is completely valid to re-use the (key) string multiple times in <template> replacement context. Leveraging a separate syntax construct for this cleanly differentiates the two ideas.

The other significant disadvantage to overloading the syntax is that we remove the ability for us to be able to process one feature and not the other. For instance it can be beneficial to expand the templates and leave the named constants in place. I think that is a lot cleaner and clearer to implement if they are separate concerns. We want to be able to raise warnings/errors if keys are mis-typed in a <template> element. If we overload the syntax and want to only expand the templates and not the constants then we won't be able to easily identify typos.


Template elements allow a single template pattern to be used in instantiating an arbitrary number of elements at runtime, with each reference to the provided `key` being replaced with one of the corresponding strings in the `values` array. Each element within the scope of the `template` will be instantiated separately for each string in the `values` array.

References to the template `key` are expressed as the string value of the `key` bracketed by the `(` and `)` characters, e.g. `(keystring)`. Substitution of key-value pairs takes precedence over any other supported substitutions within the scope of a `template` element.

To support generic typed values, TypeDef `token` strings may be used in place of any literal value within a `template` element, bracketed by the `[` and `]` characters. At template expansion time, any typed value that corresponds to a TypeDef `token` will be replaced with the corresponding literal value for the given `token` and type.

Template elements may be nested to any depth, allowing for efficient authoring of combinatorial element templates.

The following example show how the full set of `nodedef` and `nodegraph` variations for the `contrast` node in MaterialX would be expressed using two `template` elements.

```xml
<template name="T_contrast" key="type" values="float, color3, color4, vector2, vector3, vector4">
Copy link
Contributor

@ld-kerley ld-kerley Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll note that in my PR contrast could be represented in an even more compact form.

  <template name="TP_ND_contrastFA" varnames="nodeDefExt,floatTypeName,typeList" options="(@typeName@,@typeName@FA), (@typeName@,float), ('float, color3, color4, vector2, vector3, vector4', 'color3, color4, vector2, vector3, vector4')">
    <template name="TP_ND_contrast_@nodeDefExt@" varnames="typeName" options="(@typeList@)">
      <nodedef name="ND_contrast_@nodeDefExt@" node="contrast" nodegroup="adjustment">
        <input name="in" type="@typeName@" value="Constant:zero" spec_desc="The input color stream to be adjusted" />
        <input name="amount" type="@floatTypeName@" value="Constant:one" spec_desc="Slope multiplier for contrast adjustment. Values greater than 1.0 increase contrast, values between 0.0 and 1.0 reduce contrast." spec_acceptedvalues="[__zero__, __+inf__)" />
        <input name="pivot" type="@floatTypeName@" value="Constant:half" spec_desc="Center pivot value of contrast adjustment; this is the value that will not change as contrast is adjusted. " />
        <output name="out" type="@typeName@" defaultinput="in" spec_desc="the adjusted color value" />
      </nodedef>
    </template>
  </template>

I haven't templatized the nodegraph elements yet - but I believe they could be similarly simplified.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although this would be a clever way to further compress templates, to my eye it seems less visually clear than the proposed key and values syntax, and reads more as a "macro language" convention than a core syntax for MaterialX elements.

<nodedef name="ND_contrast_(type)" node="contrast" nodegroup="adjustment">
<input name="in" type="(type)" value="[zero]" />
<input name="amount" type="(type)" value="[one]" />
<input name="pivot" type="(type)" value="[half]" />
<output name="out" type="(type)" defaultinput="in" />
</nodedef>
<nodegraph name="NG_contrast_(type)" nodedef="ND_contrast_(type)">
<subtract name="N_sub_(type)" type="(type)">
<input name="in1" type="(type)" interfacename="in" />
<input name="in2" type="(type)" interfacename="pivot" />
</subtract>
<multiply name="N_mul_vector3" type="(type)">
<input name="in1" type="(type)" nodename="N_sub_(type)" />
<input name="in2" type="(type)" interfacename="amount" />
</multiply>
<add name="N_add_(type)" type="(type)">
<input name="in1" type="(type)" nodename="N_mul_(type)" />
<input name="in2" type="(type)" interfacename="pivot" />
</add>
<output name="out" type="(type)" nodename="N_add_(type)" />
</nodegraph>
</template>

<template name="T_contrast_FA" key="type" values="color3, color4, vector2, vector3, vector4">
<nodedef name="ND_contrast_(type)FA" node="contrast" nodegroup="adjustment">
<input name="in" type="(type)" value="[zero]" />
<input name="amount" type="float" value="1.0" />
<input name="pivot" type="float" value="0.5" />
<output name="out" type="(type)" defaultinput="in" />
</nodedef>
<nodegraph name="NG_contrast_(type)FA" nodedef="ND_contrast_(type)FA">
<subtract name="N_sub_(type)" type="(type)">
<input name="in1" type="(type)" interfacename="in" />
<input name="in2" type="float" interfacename="pivot" />
</subtract>
<multiply name="N_mul_vector3" type="(type)">
<input name="in1" type="(type)" nodename="N_sub_(type)" />
<input name="in2" type="float" interfacename="amount" />
</multiply>
<add name="N_add_(type)" type="(type)">
<input name="in1" type="(type)" nodename="N_mul_(type)" />
<input name="in2" type="float" interfacename="pivot" />
</add>
<output name="out" type="(type)" nodename="N_add_(type)" />
</nodegraph>
</template>
```

#### Proposed Template Implementation

When a document containing `template` elements is loaded through the MaterialX API, its templates are expanded to their full form through a built-in `Document::expandTemplates` method, which is invoked automatically at load-time in the same fashion as the existing `Document::upgradeVersion` method. This allows the document to be authored and stored on disk in its clearest and most compact form, while MaterialX runtimes and downstream clients can assume templates are fully expanded at load time, allowing them to operate as if no templates are present.

For an initial implementation of this feature, a proposed approach is to implement the `TemplateElement` class, the TypeDef form of the `Token` class, and the `Document::expandTemplates` method in the MaterialX runtime, and to add a single example of a `template` to the MaterialX data libraries (e.g. the `contrast` example above), in order to prove out the syntax and logic before committing to a full data library upgrade. This represents roughly 2-4 days of work for a developer that is familiar with the MaterialX C++ codebase.

An important property of this proposed implementation is that downstream clients such as OpenUSD will require no changes to support it, as the template expansion logic will be fully contained within existing MaterialX API calls. Clients that define custom MaterialX nodes will have the option of using `template` elements for efficiency and clarity, but will be under no obligation to do so, and they can effectively ignore the presence of template functionality if they choose.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I'm reading this - OpenUSD would be required to call Document::expandTemplates(). So it's not quite true to say that downstream clients require no changes?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm imagining that Document::expandTemplates would be handled exactly like the existing Document::upgradeVersion, where it's automatically invoked during the load process, and never needs to be called directly by the client.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not how I read what you've written above - maybe it's worth making that idea more explicit in the proposed text.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, and I'll add a sentence to state this clearly in the proposal, since it's an important detail.



<p>&nbsp;<p><hr><p>

# Proposals: Stdlib Nodes<a id="propose-stdlib-nodes"></a>
Expand Down