Split and Join#
Goal. Feed one node’s output to several consumers, or several producers into one node.
Before you start. Chain Nodes.
The Shape#
Both are ordinary connections. A port may be the source of more than one edge, and a node may declare more than one input.
Fan out. One node’s output feeds a published AOV and a second node at the same time.
digraph { rankdir=LR bgcolor="transparent" node [shape=box style="rounded,filled" fontname="Helvetica" fontsize=11 penwidth=0] edge [color="#9aa0a6" penwidth=1.2 arrowsize=0.7] "LdrColor" [fillcolor="#4c566a" fontcolor="white"] "Out1" [fillcolor="#4c566a" fontcolor="white"] "Out2" [fillcolor="#4c566a" fontcolor="white"] "a" [fillcolor="#76b900" fontcolor="#1b1b1b"] "b" [fillcolor="#76b900" fontcolor="#1b1b1b"] "LdrColor" -> "a" "a" -> "Out1" "a" -> "b" "b" -> "Out2" }Fan in. One node takes two inputs, here an upstream node’s output and an AOV directly.
digraph { rankdir=LR bgcolor="transparent" node [shape=box style="rounded,filled" fontname="Helvetica" fontsize=11 penwidth=0] edge [color="#9aa0a6" penwidth=1.2 arrowsize=0.7] "LdrColor" [fillcolor="#4c566a" fontcolor="white"] "Out" [fillcolor="#4c566a" fontcolor="white"] "a" [fillcolor="#76b900" fontcolor="#1b1b1b"] "n" [fillcolor="#76b900" fontcolor="#1b1b1b"] "LdrColor" -> "a" "a" -> "n" "LdrColor" -> "n" "n" -> "Out" }Diamond. A split that rejoins, so d runs only after both branches have.
How It Works#
A node’s output may feed more than one consumer. Connect the same outputs: port to each
one. The blur example does this with the first of its two
passes, which feeds the second pass and a RenderVar of its own:
blur_scene.usda, from the runnable blur example#def Scope "Render"
{
def RenderProduct "BlurDemo"
{
uniform int2 resolution = (1280, 720)
rel camera = </World/Camera>
rel orderedVars = [ <LdrColor>, <LdrBlurH>, <LdrBlurred> ]
def RenderVar "LdrColor"
{
uniform string sourceName = "LdrColor"
opaque omni:rtx:aov
}
# BlurH's output goes two places at once: on to BlurV, and to a
# RenderVar of its own so the half-finished blur can be read back. One
# output may be the source of any number of edges.
def RenderVar "LdrBlurH"
{
uniform string sourceName = "LdrBlurH"
opaque omni:rtx:aov.connect = <../BlurH.outputs:Blurred>
}
def RenderVar "LdrBlurred"
{
uniform string sourceName = "LdrBlurred"
opaque omni:rtx:aov.connect = <../BlurV.outputs:Blurred>
}
def Shader "BlurH" (
references = @BlurHorizontal.usda@
)
{
opaque inputs:Image.connect = <../LdrColor.omni:rtx:aov>
}
def Shader "BlurV" (
references = @BlurVertical.usda@
)
{
opaque inputs:Image.connect = <../BlurH.outputs:Blurred>
}
}
}
A published RenderVar may also feed more than one node. Both fan-outs are allowed, and the
choice is not about the graph but about who else may see the intermediate. A direct connection keeps
it inside the graph; a RenderVar exposes it to the host and to other products, and costs you a name
in orderedVars. Refer to Publish, Overwrite and Read Back AOVs.
A node’s inputs may come from different kinds of source. In the fan-in shape, n takes one
input from an upstream node and one straight from an AOV. A node whose inputs are two other nodes’
outputs works the same way, including when one of those is itself a join.
The built-in nodes example joins in the simplest possible way: one AOV feeds both inputs of an Add node, which is how it doubles brightness.
stdlib_scene.usda, from the runnable built-in nodes example#def Scope "Render"
{
def RenderProduct "StdlibDemo"
{
uniform int2 resolution = (1280, 720)
rel camera = </World/Camera>
rel orderedVars = [ <LdrColor>, <Downscaled> ]
def RenderVar "LdrColor"
{
uniform string sourceName = "LdrColor"
opaque omni:rtx:aov
}
def RenderVar "Downscaled"
{
uniform string sourceName = "Downscaled"
opaque omni:rtx:aov.connect = <../ScaleNode.outputs:Output>
}
def Shader "AddNode"
{
uniform token info:implementationSource = "id"
uniform token info:id = "spg:rtx.spg.stdlib/Add"
opaque inputs:A.connect = <../LdrColor.omni:rtx:aov>
opaque inputs:B.connect = <../LdrColor.omni:rtx:aov>
opaque outputs:Result
}
def Shader "ScaleNode"
{
uniform token info:implementationSource = "id"
uniform token info:id = "spg:rtx.spg.stdlib/Scale"
float inputs:scaleX = 0.5
float inputs:scaleY = 0.5
opaque inputs:Input.connect = <../AddNode.outputs:Result>
opaque outputs:Output
}
}
}
Order still comes from the edges. Every branch of a split runs after its producer, and a join runs after all of its producers. Refer to Order comes from connections.
Two graphs with no shared edge are independent. LdrColor -> a -> OutA, LdrColor -> b -> OutB
is two graphs in one product, not one graph.
Verify It Worked#
Give the branches kernels with different, recognisable effects, then read every published AOV back. Each must show its own branch’s transform, and no two may be equal. Two identical outputs mean one branch is unwired and both RenderVars are reading the same producer.
The blur example reads all three of its AOVs and checks that they fall in order: the sharpest edge in the image drops once after the horizontal pass and again after the vertical one. The middle value only exists because the first pass feeds a RenderVar as well as the second node. The check is the ordering, not the three figures, which move with the render.
When It Goes Wrong#
A branch never runs: its output is consumed by nothing and it publishes no RenderVar. Refer to Nothing Appears.
A join reads one input and not the other: the second connection is missing, or its port name does not match the shader definition.