MaterialX icon indicating copy to clipboard operation
MaterialX copied to clipboard

Feature Proposal: Support Nested NodeGraphs

Open kwokcb opened this issue 2 years ago • 4 comments

MaterialX Nested Nodegraphs

This document outlines the proposal to support a <nodegraph> which is a child of another <nodegraph>. As there are no syntax changes, the proposal is for a 1.38.x incremental release.

Authors

Niklas Harrysson, Bernard Kwok, Jonathan Stone

Motivation

Currently compound nodegraphs can be specified at the document level. The extension to this is to allow nesting at any level in a graph hierarchy.

This allows for packaging of sub-parts of a graph into logical units for better organization and readability. When a graph becomes large it becomes convenient to have the ability to wrap parts of it into compounds where it make sense.

For example, in the nodegraph for standard_surface there are several parts of the graph that can be separated into child compounds that calculate a particular piece of logic such as:

  • Tangent rotations
  • How coat affect roughness
  • How coat effect diffuse and sss colors
  • How fresnel effects the EDF

Even the logical layering of BSDFs can probably have been separated out into compounds for better readability in a graph editor.

As an artist this becomes a useful tool in the toolbox for organizing graphs.

In addition, when such a compound is found to be reusable it can be published as a new definition (nodedef) with a functional nodegraph allowing it to be instantiated and reused. Without the ability to create child compounds an artist currently has to first move the graph up to the document level, then recreate the logic there, and then perform the "publish" to a nodedef.

Another motivation is that a user is presented with the ability to construct nested nodegraphs in the Graph Editor, but cannot save the result to a document.

Syntax Changes

None

Connection Logic Changes

Connection handling becomes more consistent between nodes and nodegraphs by generalizing connections. This is achieved by removing the restriction that some things you can do on nodes are not allowed on nodegraphs. Namely:

  • interfacename can be specified on nodegraph <input>s to allow connections between inputs on a child nodegraph and a parent nodegraph.

Possible Configurations

Given a parent <nodegraph> called parentNG and a child nodegraph childNG, and child nodes childNodeUp and childNodeDown:

  1. The <input> on childNG is connected to the output of an upstream node childNodeUp

    <input ... node="childNodeUp"> [output="output on childNodeUp"]>
    
     graph TB;
     subgraph parentNG
         childNodeUp
         childNG 
     end
     subgraph childNG
       input([input])
     end
     childNodeUp --> input
     style input fill:#0bb, color:#111
    

    The output is specified if childNodeUp has multiple outputs. If the upstream node was a nodegraph then syntax would be nodegraph= instead of node=. (See *)

  2. The <output> on childNG is connected to the input of a downstream node childNodeDown

    <input ... nodegraph="childNG"> [output="output on childNG"]>
    
     graph TB;
     subgraph parentNG
         childNodeDown
         childNG 
     end
     subgraph childNG
       output([output])
     end
     output --> childNodeDown
     style output fill:#0b0, color:#111
    

    The output is specified if childNG has multiple outputs.

  3. The <input> on childNG is connected to the interface <input> called parentNGInput of the parent nodegraph parentNG

    <input ... interfacename="parentNGInput"`>
    
     graph TB;
     subgraph parentNG
         parentNGInput([parentNGInput])
         childNG 
     end
     subgraph childNG
       input([input])
     end
     parentNGInput  --interfacename--> input
     style input fill:#0bb, color:#111
     style parentNGInput fill:#0bb, color:#111
    
  4. The <output> on childNG is connected to the <output> of the parent nodegraph parentNG

    <output ... nodegraph="childNG" [output="output on childNG"]>
    
     graph TB;
     subgraph parentNG
         outputparentNG([output])
         childNG 
     end
     subgraph childNG
       output([output])
     end
     output  --> outputparentNG
     style output fill:#0b0, color:#111
     style outputparentNG fill:#0b0, color:#111
    

    with the output being specified if childNG has multiple outputs.

(*) Note that <nodegraph> to <nodegraph> connections are already supported so is similar to the node to nodegraph connection scenario.

Interface Example

Example shows interfaces being connected between parent and child nodegraphs

<?xml version="1.0"?>
<materialx version="1.38" colorspace="lin_rec709">
  <nodegraph name="parentNG">
    <input name="parentNGInput" type="color3" value="1, 1, 0.2"  />
    <input name="parentNGInput2" type="color3" value="0.2, 1, 1" />
    <nodegraph name="childNG">
      <input name="childNGInput" type="color3" interfacename="parentNGInput" />
      <input name="childNGInput2" type="color3" interfacename="parentNGInput2" />
      <multiply name="multiplyNode" type="color3">
        <input name="in1" type="color3" interfacename="childNGInput" />
        <input name="in2" type="color3" interfacename="childNGInput2" />
      </multiply>
      <output name="childNGOutput" type="color3" nodename="multiplyNode" />
    </nodegraph>
    <output name="parentNGOutput" type="color3" nodegraph="childNG" />
  </nodegraph>
</materialx>
graph TD; 
    top_shader[top_shader]
    parentNG_childNG_multiplyNode[multiplyNode] --> childNGOutput 
    parentNG_childNG_childNGInputINT([childNGInput]) ==.in1==> parentNG_childNG_multiplyNode[multiplyNode]
    style parentNG_childNG_childNGInputINT fill:#0bb, color:#111
    parentNG_childNG_childNGInput2INT([childNGInput2]) ==.in2==> parentNG_childNG_multiplyNode[multiplyNode]
    style parentNG_childNG_childNGInput2INT fill:#0bb, color:#111
    parentNGOutput --".base_color"--> top_shader[top_shader]

  subgraph parentNG
    parentNG_childNG
    parentNG_parentInput([parentInput]) 
    parentNG_parentInput2([parentInput2])
    style parentNG_parentInput2 fill:#0bb, color:#111
    style parentNG_parentInput fill:#0bb, color:#111
    parentNGOutput([parentNGOutput]) 
    style parentNGOutput fill:#0b0, color:#111    
   end
  subgraph parentNG_childNG[childNG]
    parentNG_childNG_childNGInput2INT
    parentNG_childNG_childNGInputINT
    parentNG_childNG_multiplyNode
    childNGOutput([childNGOutput]) --> parentNGOutput
    style childNGOutput fill:#0b0, color:#111    
  end

    parentNG_parentInput([parentInput]) --> parentNG_childNG_childNGInputINT
    parentNG_parentInput2([parentInput2]) --> parentNG_childNG_childNGInput2INT

The resulting rendering in MaterialXView would look something like this, where the parent nodegraph inputs (parentNGInput, parentNGInput2) are exposed as input uniforms via shader code generation. MaterialX_Nested_NodeGraphs_1

Implementation Requirements

  • Graph traversal logic, shader generation, and value evaluation handles interfacename on nodegraph inputs, and traversal from a parent nodegraph output to a child nodegraph output
  • "Inherited" properties (such as fileprefix, colorspace) evaluate properly through parent/child nodegraphs. This should already be the case.
  • No "upgrade" path is required.

kwokcb avatar Mar 08 '23 21:03 kwokcb

Based on this Slack thread, for interop this would allow for future support so that USD (hdMtlx) would no longer need to pre-flatten it's graphs for conversion to MaterialX. The flip side would be that conversion to USD could support nested graphs, with a possible utility pre-flatten within MaterialX.

This is implied that only connections within the current scope are allowed as re-confirmed by Doug Smythe.

kwokcb avatar Mar 08 '23 21:03 kwokcb

See this Slack thread for additional discussion.

kwokcb avatar Mar 09 '23 13:03 kwokcb

Looks really good to me!

Just some thoughts on the connection:

<input ... nodegraph="childNG"> [output="output on childNG"]>

It's logical to use nodegraph= to specify the "childNG" as input, since node graphs are not quite nodes (shaders) in MaterialX. Though in DCC, node graphs can be thought as a node with children inside, so there it may be permissible to also think of the connection as node="childNG".

I think we should indeed use nodegraph="childNG" for MaterialX. I just wanted to mention it, in case there are some other implications (eg, on scripting, etc).

rafalSFX avatar Mar 09 '23 16:03 rafalSFX

Hi @rafalSFX , Indeed, If we treat a nodegraph as something you can encapsulate as a node we'd only need node connection concept.

The notation of using nodegraph= and node= is historical, (as is interfacename=), so just following what's there now.

We had a proposal to generalize syntax to use 'connect=' (which would also align better with USD) but this pulled in too many other elements at one time. The thought is to perhaps revive this as a separate proposal as a syntactic change and / or an API generalization. (e.g. a getConnection/setConnection() API).

kwokcb avatar Mar 09 '23 18:03 kwokcb