AcademySoftwareFoundation / MaterialX

MaterialX is an open standard for the exchange of rich material and look-development content across applications and renderers.
http://www.materialx.org/
Apache License 2.0
1.85k stars 351 forks source link

1.39 : Shader Graph Connectivity Simplification Proposal #1091

Closed kwokcb closed 1 year ago

kwokcb commented 2 years ago

Shader Graph Connectivity

1. Introduction

This is a proposal to update the way that connections are specified in MaterialX.

Motivating factors include:

  1. Providing a single unified syntax and API entry point to specify and modify an arbitrary connection. Currently a user must take into account numerous (up to 6 different) attributes and an equivalent number of API entry points to determine what a single connection represents. Special casing is required for different subsets of attributes depending on what is upstream and downstream of a connection. This includes handling:

    • If an node input is connected to nodegraph input
    • If an node output is connected to a nodegraph output
    • If a node input is connected to a node 1 output
    • If a node input is connected to a node with multiple outputs
    • If a nodegraph input is connected to a node with 1 vs multiple outputs
    • If a node input is connected to a channel of an upstream output or input
    • If a nodegraph input is connected to a nodegraph output
    • If a nodegraph output provides a channel to a downstream input
    • If a node output provides a channel to a downstream input For the most part a user must read the specification to distill the set of attributes that need to be examined for each variant or special case. (*)
  2. Formalizing and simplifying connection rules to support more robust connection validation and avoid invalid data. Currently for proper validation of all existing connections multiple attributes must be checked for consistency. To date it is still very easy to create data which is inconsistent and thus invalid as each attribute entry point can be independently set. As the validation process is expensive, it is either not part of the connection modification logic or not for all entry points and can thus be skipped. This results in storage and transmission of existing or new documents which are invalid. As there is no known logic to make things valid, this results in the requirement to hand edit files assuming the user has sufficient knowledge to know how to do so.

  3. Simplification of traversal logic used for workflows such as graph editing or code generation by removing special case handling. The only traversal which is mostly complete is that used for code generation which must create a parallel graph with simplified connection storage and interfaces mirroring the proposed single specification point. There is no other published way to easily parse for connections. Note that it is still possible to specify a connection which is ambiguous when traversing through interface connections resulting in traversal failure and currently code generation failure.

  4. Hiding of storage complexity and single point of interaction. Much of the syntax and interfaces expose the underlying implementation details. While it is possible to wrap up the existing complexity with new interfaces the existing interfaces and data attributes still exist allowing for the same integrity issues. With their deprecation, the hope is provide a single "future-proof " interface which is reflected using a single syntax and storage location.

(*) From a standards consistency point of view a single data syntax is more consistent with how USD stores and handles connections (See implementation details at end of document).

Examples

To understand the proposal basic background is provided first. A second purpose describe the existing connect variants For the most part each variant requires different syntax and special case handling.

For those who already understand how connectivity variants works, please skip to section 3 (Proposal).

2. Background

2.1 Shader Graphs Nodes and Nodegraphs

} class GraphElement { +string name +nodes[] } class NodeGraph { +inputs +outputs } GraphElement <|-- Document : implements GraphElement <|-- NodeGraph : implements


## 2.2  Composition and Scope

* The set of **direct** child elements of a graph element are said to be in the **same scope**. 
* A _graphelement_ cannot be instantiated. It can contain 0 or more nodes or nodegraphs as direct child elements.
* A  _document_ does not allow `<input>`s and `<output>`s as direct child elements.
 A document has no data flow in or out.  Documents cannot contain or reference other documents as direct children.
   ```mermaid
   graph TD
      subgraph document
         my_node 
         my_nodegraph      
     end

2.3 <output> to <input> Connectivity

Based on the definition of scope, the rules for connecting <output>s to <input>s is as follows:

Below shows an example of the possible valid pair-wise connections.

2.4 "Interface" Connectivity

The unique interface "signature" of a nodegraph is defined by it's inputs and outputs. Though it is possible have no interface inputs nor outputs this would be of no practical use as no data can flow through the graph.

Interface inputs and outputs can be connected to direct child nodes within the it's scope. We use the term "interior" to denote that a node is within scope.

  1. A <nodegraph> <input> may be connected to one or more interior node's <input> within the same scope. ( In this and following examples, inputs are color coded. )

       graph TB
       subgraph nodegraph1
        .input1 --> node1.input1
        .input1 --> node2.input2
        .input2 --> node2.input1
        style .input1 fill:#1b1,color:#fff 
        style .input2 fill:#1b1,color:#fff 
        style node1.input1 fill:#1b1,color:#fff 
        style node2.input1 fill:#1b1,color:#fff 
        style node2.input2 fill:#1b1,color:#fff 
      end
  2. An interior node's <output>s may be connected to one or more nodegraph outputs within the same scope. ( In this and following examples, outputs are color coded. )

    graph TB
    subgraph nodegraph1
        node1.output1 --> .output1 
        node1.output1 --> .output2
        node1.output2 --> .output3
        style node1.output1 fill:#0bb,color:#fff
        style node1.output2 fill:#0bb,color:#fff
        style .output1 fill:#0bb,color:#fff
        style .output2 fill:#0bb,color:#fff
        style .output3 fill:#0bb,color:#fff
    end

    As nodegraphs can be nested within other nodegraphs, the same rules apply to connect to interior nodegraphs.

graph TB
    subgraph nodegraph1
        .input1 --> nodegraph3_input1[nodegraph3.input1]
        .input1 --> nodegraph2_input2[nodegraph2.input2]
        .input2 --> nodegraph2_input1[nodegraph2.input1]
        subgraph nodegraph3
            nodegraph3_input1
        end
        subgraph nodegraph2
            nodegraph2_input1
            nodegraph2_input2
        end
        style nodegraph3_input1 fill:#1b1,color:#fff 
        style nodegraph2_input1 fill:#1b1,color:#fff 
        style nodegraph2_input2 fill:#1b1,color:#fff 
        style .input1 fill:#1b1,color:#fff 
        style .input2 fill:#1b1,color:#fff 

        nodegraph2.output1 --> .output1 
        nodegraph2.output1 --> .output2
        nodegraph2.output2 --> .output3
        subgraph nodegraph2
          nodegraph2.output1
          nodegraph2.output2
        end
        style nodegraph2.output1 fill:#0bb,color:#fff 
        style nodegraph2.output2 fill:#0bb,color:#fff 
        style .output1 fill:#0bb,color:#fff
        style .output2 fill:#0bb,color:#fff
        style .output3 fill:#0bb,color:#fff
    end

2.5 Invalid Configurations

3 Proposal

3.1 Existing Connection Syntax

Currently connections are specified on an input using addition attributes:

3.2 New "Connection" Syntax

The proposal is to replace to replace all existing connectivity attributes with a single attribute named connection.

The assigned value will be a reference to upstream connection.

  connection=`path reference`

A path reference is always specified relative to the current scope. Thus, absolute path and parent references are disallowed. i.e. Paths starting with a / and is ../.

Inputs and outputs of a node and nodegraph are prefixed by a . (dot)

A path reference may be to an upstream node or nodegraph without explicitly specifying its output if and only if the node has only one output. Otherwise the reference is considered to be ambiguous and fail validation.

Inputs which currently reference an interface input using interfacename will use a path notation of this form:

Note that this means that nodegraph inputs can be connected to node inputs. This makes it orthogonal to how node outputs can be connected to nodegraph outputs. These type of connections are still considered to be invalid if specified outside a nodegraph.

The 1.39 channel attribute (and the existing channels attribute are represented as part of the path notation using an array notation:

where # is the channel number 0..N. Where N+1 is the number of channels.

<namespace> can still be supported as part of a node name (as it currently is now). That is, it is still allowed to connected between nodes in different namespaces.

3.3 Examples: Connections Using A Common Parent

The following are some examples of connections where the connections occur within the parent.

3.4 Example: Connections Between Nodes and Nodegraphs

This example shows:

Existing syntax: (Note the usage of 3 different attributes to denote a connect).

<nodegraph name="N1">
  <add name="add_color" type="color3">
    <input name="in1" type="color3" value="0.1, 0.1, 0.1" />
    <input name="in2" type="color3" value="0.3, 0.4, 0.5" />
  </add>
  <output name="outA" type="color3" node="add_color" />
</nodegraph>

<nodegraph name="N2">
  <input name="in_color" nodegraph="N1" output="outA" />
  <multiply name="multiply_color" type="color3">
    <input name="in1" type="color3" interfacename="in_color" />
    <input name="in2" type="color3" value="0.5, 0.5, 0.5" />
  </multiply>
  <output name="out_color" type="color3" node="multiply_color" />
</nodegraph>

<multiply name="multiply_by_2" type="color3">
  <input name="in1" type="color3" nodegraph="N2" output="out_color" />
  <input name="in2" type="color3" value="2.0, 2.0, 2.0" />
</multiply>

New syntax:

<nodegraph name="N1">
  <add name="add_color" type="color3">
    <input name="in1" type="color3" value="0.1, 0.1, 0.1" />
    <input name="in2" type="color3" value="0.3, 0.4, 0.5" />
  </add>
  <output name="outA" type="color3" connection="add_color" />
</nodegraph>

<nodegraph name="N2">
  <input name="in_color" connection="N1/outA" />
  <multiply name="multiply_color" type="color3">
    <input name="in1" type="color3" connection=".in_color" />
    <input name="in2" type="color3" value="0.5, 0.5, 0.5" />
  </multiply>
  <output name="out_color" type="color3" connection="multiply_color" />
</nodegraph>

<multiply name="multiply_by_2" type="color3">
  <input name="in1" type="color3" connection="N2/out_color" />
  <input name="in2" type="color3" value="2.0, 2.0, 2.0" />
</multiply>
graph TB;
    subgraph N1
    N1/add_color --connection=add_color--> N1/outA
    style N1/outA fill:#bfb,color:#000
    end
    subgraph N2
    N1/outA --connection=N1/outA--> N2/in_color
    N2/in_color --connection=.in_color--> N2/multiply_color.in1 
    style N2/in_color fill:#bfb,color:#000
    N2/multiply_color.in1 -.- N2/multiply_color
    style N2/multiply_color.in1 fill:#bfb,color:#000
    N2/multiply_color --connection=multiply_color--> N2/output_color
    style N2/output_color fill:#bfb,color:#000
    end
    N2/output_color --connection=N2/out_color--> multiply_by_2.in1
    style multiply_by_2.in1 fill:#bfb,color:#000

4. Integration and Implementation Notes

The primary messages are that:

  1. A lot of "special" case code which is very hard to maintain should be removed :).
  2. The change can be made "transparently" with affecting existing integrations (*)

(*) If integrations are directly parsing string attributes instead of going through an interafces, there isn't much that can be done and in general these will break regardless of what enhancement is made to attributes. e.g. Integrations use getAttribute() to poke into attribute values.

4.1 Path Representation

All connections are currently stored as raw strings with no validation. It is possible to change this storage to be a NodePath which can encapsulate all the current string manipulation and add syntax validity checking.

Note that there is the opportunity to consolidate some of the string handling logic with FilePath and GeomPath to reduce duplicate logic.

classDiagram
class NodePath {
    +string name
    +isValid()
}

4.2 Connection Representation

The proposal is create a formal definition of a NodeEdge with associated of interfaces. Any connectivity rules can be enforced via these interfaces.

These interfaces can be added to existing Port classes. Storage can be part of the Port or externalized into a cache if this is a better approach.

A base set of PortElement interfaces includes:

An Output can extend this to include these interfaces:

A NodeEdge can have these interfaces

classDiagram 
class GraphIterator {
  NodeEdge[]  
}
GraphIterator *-- NodeEdge : reference 
class PortElement {
 + bool makeConnection(Port)
 + bool breakConnection()
 + NodeEdgePtr getEdge() const
 + unsigned int getChannel() const
 + bool setChannel(unsigned int)
}
class Output  {
 + bool makeConnection(Port)
 + bool breakConnection()
 + NodeEdge& getEdge() const
 + unsigned int getChannel() const
 + bool setChannel(unsigned int)
}
PortElement <|-- Output : inherits
PortElement <|-- Input : inherits
class NodeEdge {
  + PortElementPtr getDestination(): 
  + const string& string() const
  + isValid() const
}

Any Node references can be obtained using the existing PortElement interfaces which return the parent of the Port.

The conversion from a connection attribute (NodePath) is performed only as required to make all connections. Any invalid / non-existent connection attributes are not affected. An "invalid" NodeEdge can be returned if (missing source). TBD if this should be allowed.

A connection attribute string can be extracted for Document save, or whenever desired.

4.2 Traversal

The current traversal iterator (GraphIterator) would have the internal logic modified to use the new interface. The existing Edge information which only provides downstream information can be replaced / enhanced to supply any information from a NodeEdge.

4.3 Upgrade / Legacy Interfaces

4.3.1 Attribute Storage

4.3.1 Connection / Attribute Synchronization

4.3.2 Existing Interfaces

4.4 Future Features

4.5 Consistency Case 1: USD Integration

The new connection syntax is closer in alignment with USD connection syntax. It is worth noting MaterialX pathing remains relative and that channel extraction is part of the path syntax. The latter does not add any new complication to interop, and how channel specification is handled is covered by a another issue (outside this scope of this issue).

There no is affect on input value vs connection logic as defined in USD (as both are still not allowed in MaterialX).

For import to USD, as long as the query interfaces remain to provide connection information there should be no affect here. For workflows which require export from UsdShade networks, then it will be useful to provide a single black-box "connect" interface -- which would also future proof any such conversions from underlying syntax details. For backwards compatibility existing connection set APIs could still be used as noted and just marked as deprecated.

Simple example taken from USD schema page:

#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
            }
        }
    }
}

would translate to something like this.

<shader name="Upstream>
  <output name="UpstreamOutput">
</shader>
<shader name="DownStream">
  <input name="DownStreamInput" connection="Upstream.UpstreamOutput"/>
</shader>
graph LR
   Upstream --"connection='Upstream.UpstreamOutput'"--> DownStream 
ashwinbhat commented 2 years ago

Thanks for putting this together @kwokcb , I think this makes graph connection more clear. 1: Fallback value for connection I know we consider an input with a value and a connection as ambiguous state, but would like to suggest that we allow this for "fallback" scenarios. e.g. <input name="in1" type="color3" connection="node1.out" /> could be authored as: <input name="in1" type="color3" value="1,1,1" connection="node1.out" />

There can be a valid case where a renderer may choose not to process a connection for performance reasons or it cannot process the connection because the node cannot be resolved or the asset used in the node cannot be resolved. In these cases the render can use the value as a fallback. The author of the MaterialX can publish a suitable fallback value. Note that fallback value is not the same at the nodedef default.

2; Channel identifiers Instead of using a numbered index e.g. .albedo[0], could we use named channels e.g. .albedo.r I think this would make it more clear and is probably more aligned with USD usage.

kwokcb commented 2 years ago

Thanks for putting this together @kwokcb , I think this makes graph connection more clear. 1: Fallback value for connection I know we consider an input with a value and a connection as ambiguous state, but would like to suggest that we allow this for "fallback" scenarios. e.g. <input name="in1" type="color3" connection="node1.out" /> could be authored as: <input name="in1" type="color3" value="1,1,1" connection="node1.out" />

There can be a valid case where a renderer may choose not to process a connection for performance reasons or it cannot process the connection because the node cannot be resolved or the asset used in the node cannot be resolved. In these cases the render can use the value as a fallback. The author of the MaterialX can publish a suitable fallback value. Note that fallback value is not the same at the nodedef default.

2; Channel identifiers Instead of using a numbered index e.g. .albedo[0], could we use named channels e.g. .albedo.r I think this would make it more clear and is probably more aligned with USD usage.

Both are useful, but probably beyond the scope of this issue so think we'd want to spawn them.

  1. is useful but at least based on recent discussion, this would need to be added in as a new proposal. AFAIK there are existing folks that get confused when both are specified :slightly_smiling_face: . Also how often would another non-default value be required ?
  2. This I assume is part of the continuing discussion about how channel is handled between MTLX and USD. The numeric input just reflects the agreed upon syntax for the "channel" vs "channels" spec change and believe @Doug Smythe can give you a better reply. ( I don't want to "boil the ocean" by added in too much stuff -- just "boil the syntax" :slightly_smiling_face: . )
kwokcb commented 2 years ago

More upfront reasoning has been added as well examples using new syntax have comparison with old syntax.

kwokcb commented 2 years ago

Adding comments in slack thread here:

Nick Porcino The proposal makes sense to me, and I was able to successfully puzzle out answers to all the mental questions I formed while reading it. It took some thought to deduce the motivation though! The final sentence hints at it ~ "A lot of special case code should hopefully be removed" I was wondering if the opening paragraph could lead with motivation, and perhaps give a concrete example or two of the special cases that can be removed, or perhaps pathological constructions that can be avoided with the new structure? In the spirit of continuing to line up USD and MaterialX, I've got two other questions, which I think would be nice to see addressed in the proposal itself. I'm wondering if it might be possible to add any notes on possible repercussions to UsdShadeGraph interoperability, even if the impact is trivial and in that case to note that "it's transparent"? I'm assuming the impact on Hydra rendering would be nothing, in the case of a pass through, since Hydra will just ask MaterialX to do codegen as it has done historically

Doug Smythe With a proposed change this large to a core part of MaterialX documents, I think it's extremely important to demonstrate in the proposal what clear benefits it provides, and/or what real problems with the current connection mechanisms are solved by the proposal. While I've been in discussion about improving aspects and syntax in this proposal, I must admit I'm not personally convinced yet that it's a big enough improvement to warrant such a backwards-incompatible change in syntax, but I'm very open to a well-reasoned argument in favor of the change.

Doug Smythe As for ".albedo.r" rather than ".albedo[0]", I would say that the recently-introduced numeric channel indices have the advantages of 1) being consistent for both color and vector types, and 2) we've already switched from strings and string channel names to / channel numbers for ease of parsing and implementability in shading languages that don't support string operations (e.g. "most of them").

Ashwin Bhat Regarding ".albedo.r" vs ".albedo[0]", is there an assumption that .albedo[0] should return the r channel or is it whatever the implementation chooses to use for channel 0? Bernard's proposal calls out an important point: It is not valid to have both a value and a connection specified on an input. It would be good to get some clarity on this area. Even in the current spec is the following an invalid input? input name="base_color" type="color3" value="0.5,0.3,0.2" nodegraph="NG_BrickPattern" output="base_color_output" /> The author's intent here is, base_color is the output of the node graph, but a renderer can choose to skip the nodegraph evaluation if needed and use the value as a suitable fallback. (edited)

Doug Smythe For the "It is not valid to have both a value and a connection specified on an input" point, it was always originally the intent that an input specifies a (single) connection or a value, but never both, and never more than one connection type (e.g. to both a node output and to an interfacename). I'm open to whatever clarification of this idea the community agrees to, but I think we all agree that an explicit clarification of what is allowed and what the order of precedence is if multiple input values/connections are allowed would be a very desirable thing to have. For the assumption that .albedo[0] should return the "r" channel, yes that is indeed the assumption but you make a very good point that the in-memory representation of a multi-channel value could be in some other order than "r,g,b,a" or "x,y,z,w". If we do stick with channel numbers, we should make it clear that any implementation that doesn't use "r,g,b,a" ordering will need to remap values so that e.g. .albedo[0] does in fact always mean the "r" or "x" channel.

jstone-lucasfilm commented 1 year ago

@kwokcb There are some compelling ideas in this proposal, and I particularly like the use of a connection attribute to represent connections between nodes. On the other hand, I believe some of the ideas here would have the unintended side effect of reducing developer clarity and performance, e.g. the proposed dot/bracket separators for outputs and channels, which would prevent developers from accessing or modifying these properties individually without parsing and recomposing the entire connection string.

My recommendation would be to propose these ideas incrementally, so that each of the recommended improvements can be considered on its own and discussed by the MaterialX TSC. We may find that a subset of these ideas are robust enough to proceed with, but they will need to meet a very high bar in terms of the benefits they would provide to developers and users, especially given the recent rise in the usage of MaterialX 1.38 in the industry.