Publish, Overwrite and Read Back AOVs#

Goal. Decide what a graph exposes to the rest of the pipeline, and get it back on the host.

Before you start. A working node, from Your First Node.

The Shape#

A RenderVar connects a node’s output to a name, and orderedVars lists what the product produces:

rel orderedVars = [ <LdrColor>, <LdrGrayscale> ]

def RenderVar "LdrGrayscale"
{
    uniform string sourceName = "LdrGrayscale"
    opaque omni:rtx:aov.connect = <../GrayscaleKernel.outputs:LdrGrayscale>
}

How It Works#

Ordered as the decision, then each option, then what constrains them.

A node output ends up in one of three states, and you choose which. It stays inside the graph, consumed by another node and readable by nothing else. It is published under a new name, so the host and other products can read it. Or it is published under a name the renderer already produces, so existing consumers pick it up without knowing SPG is involved. The first costs nothing and hides the result. The third changes what everything downstream sees, which is either the whole point or an accident, depending on whether you meant it. An output in none of the three is consumed by nothing and published as nothing, and the node never runs.

Publishing a new AOV needs both parts: a sourceName, which registers the name, and the connection to the node’s output. A RenderVar with only one of them produces nothing.

The grayscale scene publishes one AOV. Highlighted: the sourceName that registers the name, the connection that fills it, and the orderedVars entry that puts it in the product.

grayscale_scene.usda, from the runnable grayscale example#
def Scope "Render"
{
    def RenderProduct "GrayscaleDemo"
    {
        uniform int2 resolution = (1280, 720)
        rel camera = </World/Camera>
        rel orderedVars = [ <LdrColor>, <LdrGrayscale> ]

        def RenderVar "LdrColor"
        {
            uniform string sourceName = "LdrColor"
            opaque omni:rtx:aov
        }

        def RenderVar "LdrGrayscale"
        {
            uniform string sourceName = "LdrGrayscale"
            opaque omni:rtx:aov.connect = <../GrayscaleKernel.outputs:LdrGrayscale>
        }

        def Shader "GrayscaleKernel" (
            references = @GrayscaleKernel.usda@
        )
        {
            opaque inputs:LdrColor.connect = <../LdrColor.omni:rtx:aov>
        }
    }
}

Overwriting an existing AOV is the same wiring with the renderer’s own name. Consumers downstream need to know nothing about SPG; they read the name they always read. The producing node may sit in the same product or in another one:

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] subgraph cluster_A { label="product A" fontname="Helvetica" fontsize=10 fontcolor="#9aa0a6" color="#9aa0a6" style=dashed "A:LdrColor" [fillcolor="#4c566a" fontcolor="white" label="LdrColor"] } subgraph cluster_B { label="product B" fontname="Helvetica" fontsize=10 fontcolor="#9aa0a6" color="#9aa0a6" style=dashed "b" [fillcolor="#76b900" fontcolor="#1b1b1b"] "B:LdrColor" [fillcolor="#4c566a" fontcolor="white" label="LdrColor"] } "A:LdrColor" -> "b" "b" -> "B:LdrColor" }

Four rules constrain those choices.

A node cannot read an AOV and republish it under that same name in the same product. Read one name and publish another, or put the producer in a different product.

The same name in two products is two different AOVs. Each product’s RenderVar receives its own graph’s result, and one may be the source for the other’s.

Names collide silently. If a sourceName matches an AOV the renderer already produces and you did not intend to overwrite it, the built-in output shadows the node’s. Give it a name of its own.

One output, one RenderVar. A node output can be bound to a single RenderVar at a time.

Reading back is not SPG-specific. A published AOV is mapped exactly like a built-in one:

main.py, from the runnable grayscale example#
    # An SPG output AOV is read exactly like any built-in render var.
    OUTPUT_DIR.mkdir(exist_ok=True)
    frame = products[RENDER_PRODUCT].frames[0]
    save_render_var(frame, LDR_COLOR_PATH, OUTPUT_DIR / "input.png")
    save_render_var(frame, OUTPUT_VAR_PATH, OUTPUT_DIR / "grayscale.png")


    del frame, products
    renderer.detach_ovstage()
    stage.destroy()
    renderer.destroy()

Verify It Worked#

Read the published AOV back and check an invariant only your node can produce. For a grayscale node that is R == G == B in every pixel, which the renderer’s own LdrColor will not satisfy. An AOV that is present but fails the invariant means you are reading the built-in output under that name rather than yours.

The grayscale example checks exactly that, and tests the input as well, so a node that never ran cannot pass on a scene that was already grey.

When It Goes Wrong#

Refer to Nothing Appears, which lists the causes in the order worth checking.