OmniGraph and USD

USD is used as a source of data for OmniGraph. Both can be described as a set of nodes, attributes, and connections, each with their own set of strengths and restrictions.

USD is declarative - it has no system for procedurally generating attribute values (such as expression binding, keyframing, etc). Timesamples can give different values when requested at different times, however they too are declarative - they are fixed values at fixed times and are unaffected by any configuration of the USD stage outside of composition. OmniGraph adds a procedural engine to Omniverse on top of USD that does generate computed attribute values. OmniGraph is not constrained by the data values in its definition. It can create entirely new values at runtime based on the computation algorithms in its nodes.

A USD stage can contain several layers forming “opinions” on what its declared values should be, which are composed together to form the final result. OmniGraph only operates on the composed USD stage in general, although it is possible for special purpose nodes to access the USD layer data through the USD APIs.

Syncing With Fabric

In order to store and manage data efficiently, OmniGraph uses a data storage component called Fabric, which is a part of Omniverse. After the USD stage is composed, USD prim data is read into Fabric, where it can be efficiently accessed by OmniGraph for its computations. When appropriate, OmniGraph directs Fabric to synchronize with USD and the computed data becomes available to the USD stage.

sequenceDiagram autonumber USD->>Fabric: Populate Node Data loop Computation Fabric->>OmniGraph: Read Data For Computation OmniGraph->>Fabric: Update With Results Of Computation end Fabric-->>USD: Sync Computed Data

The nature of the computation being performed by OmniGraph will determine the interval for syncing computed data back to USD. If, for example, the graph is deforming a mesh for passing on to some external stress analysis software then the resulting mesh must be synced to USD on each playback frame so that each version of the deformed mesh is available for analysis. On the other hand if the graph is performing a large set of Machine Learning calculations for the purposes of training a model then it only needs to send the results back to USD when the training is complete and the resulting model needs to be stored.

Important

While some nodes within OmniGraph access USD data, and most have a direct correspondence with a USD prim, neither of these is required for OmniGraph to work properly. If an OmniGraph node is only required temporarily to perform some intermediate calculation, for example, it might only be known to Fabric and never appear in the USD stage.

Note

To avoid awkward wording the generic term “graph” will be used herein when discussing the component of the OmniGraph subsystem that represents a graph to be evaluated. OmniGraph as a whole comprises the collection of components required to perform runtime evaluation (graphs, evaluators, schedulers) and their interaction.

How The Graph Appears In USD

We’ll start with a simple example of how a graph looks in the USD Ascii format and the progressively fill in the details of exactly what each part of it represents.

The graph will contain two nodes connected together to perform the two-part computation of multiplying a value by four using two nodes whose computation multiplies a value by two.

def OmniGraph "Graph"
{
    token evaluator:type = "push"
    token fabricCacheBacking = "Shared"
    int2 fileFormatVersion = (1, 4)
    token pipelineStage = "pipelineStageOnDemand"

    def OmniGraphNode "times_2_node_1"
    {
        token node:type = "omni.graph.nodes.Times2"
        int node:typeVersion = 1
        custom double inputs:a = 2.5
        custom double outputs:two_a = 5.0
    }

    def OmniGraphNode "times_2_node_2"
    {
        token node:type = "omni.graph.nodes.Times2"
        int node:typeVersion = 1
        custom double inputs:a = 0.0
        custom double inputs:a.connect = </Graph/times_2_node_1.outputs:two_a>
        custom double outputs:two_a = 10.0
    }
}

The Graph

The graph itself is the top level USD Prim of type OmniGraph. It serves to define the parameters of the graph definition in its attribute values, and as a scope for all of the nodes that are contained within the graph.

Important

Nodes must all be contained within the scope of an OmniGraph prim, and no prims that are not of type OmniGraph or OmniGraphNode are allowed within that scope.

def OmniGraph "Graph"
{
    token evaluator:type = "push"
    token fabricCacheBacking = "Shared"
    int2 fileFormatVersion = (1, 4)
    token pipelineStage = "pipelineStageOnDemand"

    def OmniGraphNode "times_2_node_1"
    {
        token node:type = "omni.graph.nodes.Times2"
        int node:typeVersion = 1
        custom double inputs:a = 2.5
        custom double outputs:two_a = 5.0
    }

    def OmniGraphNode "times_2_node_2"
    {
        token node:type = "omni.graph.nodes.Times2"
        int node:typeVersion = 1
        custom double inputs:a = 0.0
        custom double inputs:a.connect = </Graph/times_2_node_1.outputs:two_a>
        custom double outputs:two_a = 10.0
    }
}
classDiagram class Graph Graph: token evaluator.type Graph: token fabricCacheBacking Graph: int2 fileFormatVersion Graph: token pipelineStage

The predefined attributes in the OmniGraph prim come from the OmniGraph prim schema, generated using the standard USD Schema Definition API. This is the generated schema definition.

class OmniGraph "OmniGraph" (
    doc = "Base class for OmniGraph primitives containing the graph settings."
)
{
    token evaluationMode = "Automatic" (
        allowedTokens = ["Automatic", "Standalone", "Instanced"]
        doc = """Token that describes under what circumstances the graph should be evaluated. \r
                Automatic mode will evaluate the graph in Standalone mode when \r
                no Prims having a relationship to it exist. Otherwise it will be \r
                evaluated in Instanced mode.\r
                Standalone mode evaluates the graph with itself as the graph\r
                target, and ignores any Prims having relationships to the graph Prim. \r
                Use this mode with graphs that are expected to run only once per frame\r
                and use explicit paths to Prims on the stage. \r
                Instanced graphs will only evaluate the graph when it is part of a \r
                relationship from an OmniGraphAPI interface. Each Prim with a  \r
                relationship to the graph will cause a unique evaluation with the \r
                graph target set to the path of the Prim having the relationship. \r
                Use this mode when the graph represents an asset or template, and \r
                parameterizes Prim paths through the use of the GraphTarget node,\r
                variables, or similar mechanisms. """
    )
    token evaluator:type = "push" (
        allowedTokens = ["dirty_push", "push", "execution"]
        doc = "Type name for the evaluator used by the graph."
    )
    token fabricCacheBacking = "Shared" (
        allowedTokens = ["Shared", "StageWithHistory", "StageWithoutHistory"]
        doc = "Token that identifies the type of FabricCache backing used by the graph."
    )
    int2 fileFormatVersion (
        doc = """A pair of integers consisting of the major and minor versions of the\r
                file format under which the graph was saved."""
    )
    token pipelineStage = "pipelineStageSimulation" (
        allowedTokens = ["pipelineStageSimulation", "pipelineStagePreRender", "pipelineStagePostRender", "pipelineStageOnDemand"]
        doc = """Optional token which is used to indicate the role of a vector\r
                valued field. This can drive the data type in which fields\r
                are made available in a renderer or whether the vector values\r
                are to be transformed."""
    )
}

The Nodes

A graph is more than just a graph prim on its own. Within its scope are an interconnected set of nodes with attribute values that are a visual representation of the computation algorithm of the graph. Each of these nodes are represented in USD as prims with the schema type OmniGraphNode.

def OmniGraph "Graph"
{
    token evaluator:type = "push"
    token fabricCacheBacking = "Shared"
    int2 fileFormatVersion = (1, 4)
    token pipelineStage = "pipelineStageOnDemand"

    def OmniGraphNode "times_2_node_1"
    {
        token node:type = "omni.graph.nodes.Times2"
        int node:typeVersion = 1
        custom double inputs:a = 2.5
        custom double outputs:two_a = 5.0
    }

    def OmniGraphNode "times_2_node_2"
    {
        token node:type = "omni.graph.nodes.Times2"
        int node:typeVersion = 1
        custom double inputs:a = 0.0
        custom double inputs:a.connect = </Graph/times_2_node_1.outputs:two_a>
        custom double outputs:two_a = 10.0
    }
}
flowchart LR subgraph Graph times_2_node_1 --> times_2_node_2 end

The predefined attributes in the OmniGraphNode prim come from the OmniGraphNode prim schema, also defined through the USD Schema Definition API. This is the generated schema for the node’s prim type.

class OmniGraphNode "OmniGraphNode" (
    doc = "Base class for OmniGraph nodes."
)
{
    token node:type (
        doc = "Unique identifier for the name of the registered type for this node."
    )
    int node:typeVersion (
        doc = """Each node type is versioned for enabling backward compatibility. This value indicates which version of\r
                the node type definition was used to create this node. By convention the version numbers start at 1."""
    )
}

Connections

Although OmniGraph connections between attributes are implemented and handled directly within OmniGraph they are published within USD as UsdAttribute connections. Imagine a “Times2” node with one input and one output whose value is computed as the input multiplied by two. Then you can connect two of these in a row to multiple the original input by four. In USD this would look like:

def OmniGraph "Graph"
{
    token evaluator:type = "push"
    token fabricCacheBacking = "Shared"
    int2 fileFormatVersion = (1, 4)
    token pipelineStage = "pipelineStageOnDemand"

    def OmniGraphNode "times_2_node_1"
    {
        token node:type = "omni.graph.nodes.Times2"
        int node:typeVersion = 1
        custom double inputs:a = 2.5
        custom double outputs:two_a = 5.0
    }

    def OmniGraphNode "times_2_node_2"
    {
        token node:type = "omni.graph.nodes.Times2"
        int node:typeVersion = 1
        custom double inputs:a = 0.0
        custom double inputs:a.connect = </Graph/times_2_node_1.outputs:two_a>
        custom double outputs:two_a = 10.0
    }
}
flowchart LR subgraph Graph times_2_node_1 --> times_2_node_2 end

One thing worth mentioning here - in the second node /Graph/times_2_node_2 the value at inputs:a defines a value of 0.0, which is used in the event that a connection is not authored, or the connection is not evaluated (for example: outside an OmniGraph context), or an invalid connection is authored for the attribute. Where a valid connection is authored, it will take precedence during OmniGraph evaluation. In this example this is seen by the computation using the connected value of 5.0 as the multiplicand, not the authored value of 0.0.

Graph Extras

The graph evaluation type is a hint to OmniGraph on how a set of nodes should be evaluated. One of the more common variations is the type execution, which is usually referred to as an Action Graph.

def OmniGraph "ActionGraph"
{
    token evaluator:type = "execution"
    token fabricCacheBacking = "Shared"
    int2 fileFormatVersion = (1, 4)
    token pipelineStage = "pipelineStageOnDemand"

    def OmniGraphNode "DoNothingNode" (
        prepend apiSchemas = ["NodeGraphNodeAPI"]
    )
    {
        token node:type = "omni.graph.nodes.Noop"
        int node:typeVersion = 1
        uint inputs:execIn = 0
        uniform token ui:nodegraph:node:expansionState = "open"
        uniform float2 ui:nodegraph:node:pos = (-196.9863, 332.03223)
    }
}
flowchart LR subgraph ActionGraph DoNothingNode end

The key differentiator of an Action Graph is its use of execution attributes to trigger evaluation, rather than some external event such as the update of a USD stage.

The additional two ui:nodegraph attributes shown in this example illustrate how extra information can be added to the node and be stored with the prim definition. In this case the Action Graph editor has added the information indicating the node’s position and expansion state so that when the file is reloaded and the editor is opened the node will appear in the same location it was last edited. The node graph editor has added the API schema NodeGraphNodeAPI to help it in interpreting the extra data it has added. This is a standard Pixar USD schema that you can read more about in the USD documentation.

Custom Attributes

Every node type can define its own set of custom attributes, specified using the .ogn format. These custom attributes are what an OmniGraph node uses for its computation. Although the attributes are represented in USD, the actual values OmniGraph nodes use are stored in Fabric. These are the values that need to be synchronized by OmniGraph as mentioned above.

For the most part, every USD attribute type has a corresponding .ogn attribute type, though some may have some different interpretations. The full type correspondence shows how the types relate.

Here is how the definition of our “multiply by two” node might look in .ogn format:

{
    "Times2": {
        "version": 1,
        "description": "Multiplies a double value by 2.0",
        "inputs": {
            "a": {
                "type": "double",
                "description": "The number to be doubled"
            }
        },
        "outputs": {
            "two_a": {
                "type": "double",
                "description": "The input multiplied by 2.0"
            }
        }
    }
}

When the graph creates an instance of this node type then it will publish it in the equivalent USD format. The data that can be recreated from the node type definition (i.e. the descriptions in this case) do not get published into USD.

def OmniGraph "Graph"
{
    token evaluator:type = "push"
    token fabricCacheBacking = "Shared"
    int2 fileFormatVersion = (1, 4)
    token pipelineStage = "pipelineStageOnDemand"

    def OmniGraphNode "times_2_node_1"
    {
        token node:type = "omni.graph.nodes.Times2"
        int node:typeVersion = 1
        custom double inputs:a = 2.5
        custom double outputs:two_a = 5.0
    }

    def OmniGraphNode "times_2_node_2"
    {
        token node:type = "omni.graph.nodes.Times2"
        int node:typeVersion = 1
        custom double inputs:a = 0.0
        custom double inputs:a.connect = </Graph/times_2_node_1.outputs:two_a>
        custom double outputs:two_a = 10.0
    }
}

A few points of interest in this output:

  • OmniGraph attributes are published as custom attributes

  • The value of node:typeVersion must match the “version” value specified in the .ogn file

  • The value of node:type is an extended version of the name key specified in the .ogn file, with the extension prepended

  • Input attributes are put into the namespace inputs: and output attributes are put into the namespace outputs:

Special Attribute Types

There are some .ogn attribute types that do not have a direct equivalent in USD. They are implemented using existing USD concepts, relying on OmniGraph to interpret them properly.

Execution Attributes

Attributes of type execution are used by Action Graphs to trigger evaluation based on events. They are stored in USD as unsigned integer values, with some custom data to indicate to OmniGraph that they are an execution type, not a plain old value. Here is an example of the builtin OnTick node which has one such input.

def OmniGraphNode "on_tick"
{
    custom uint inputs:framePeriod = 0
    custom bool inputs:onlyPlayback = 1
    custom token node:type = "omni.graph.action.OnTick"
    custom int node:typeVersion = 1
    custom double outputs:absoluteSimTime = 0
    custom double outputs:deltaSeconds = 0
    custom double outputs:frame = 0
    custom bool outputs:isPlaying = 0
    custom uint outputs:tick = 0 (
        customData = {
            bool isExecution = 1
        }
    )
    custom double outputs:time = 0
    custom double outputs:timeSinceStart = 0
}

Note

With execution attributes only the outputs require the customData values.

Bundle Attributes

A bundle attribute in OmniGraph is simply a collection of other attributes.

Bundles cannot be specified directly; they take their attribute members from an input connection. For this reason an input, output and state bundle attribute is represented as a USD rel type.

Here is an example of a graph with two nodes, with an import of a cube prim to use for the extraction.

 1def Xform "World"
 2{
 3    def Mesh "Cube"
 4    {
 5        double3 xformOp:rotateXYZ = (0, 0, 0)
 6        double3 xformOp:scale = (1, 1, 1)
 7        double3 xformOp:translate = (0, 0, 0)
 8        string comment = "The rest of the cube data omitted for brevity"
 9    }
10
11    def OmniGraph "PushGraph"
12    {
13        token evaluator:type = "push"
14        token fabricCacheBacking = "Shared"
15        int2 fileFormatVersion = (1, 8)
16        token pipelineStage = "pipelineStageSimulation"
17
18        def OmniGraphNode "read_prim_into_bundle"
19        {
20            custom token inputs:attrNamesToImport = "*"
21            custom bool inputs:computeBoundingBox = 0
22            custom rel inputs:prim = </World/Cube> (
23                customData = {
24                    dictionary omni = {
25                        dictionary graph = {
26                            string relType = "target"
27                        }
28                    }
29                }
30            )
31            custom token inputs:primPath = ""
32            custom timecode inputs:usdTimecode = nan
33            custom bool inputs:usePath = 0
34            token node:type = "omni.graph.nodes.ReadPrimBundle"
35            int node:typeVersion = 7
36            custom rel outputs_primBundle (
37                customData = {
38                    dictionary omni = {
39                        dictionary graph = {
40                            string relType = "bundle"
41                        }
42                    }
43                }
44            )
45            custom uint64 state:attrNamesToImport
46            custom bool state:computeBoundingBox
47            custom uint64 state:primPath
48            custom uint64 state:target = 0
49            custom timecode state:usdTimecode = -1
50            custom bool state:usePath
51        }
52
53        def OmniGraphNode "extract_bundle"
54        {
55            custom rel inputs:bundle = </World/PushGraph/read_prim_into_bundle.outputs_primBundle> (
56                customData = {
57                    dictionary omni = {
58                        dictionary graph = {
59                            string relType = "bundle"
60                        }
61                    }
62                }
63            )
64            token node:type = "omni.graph.nodes.ExtractBundle"
65            int node:typeVersion = 3
66            custom string outputs:comment
67            custom int[] outputs:faceVertexCounts
68            custom int[] outputs:faceVertexIndices
69            custom normal3f[] outputs:normals
70            custom rel outputs:passThrough (
71                customData = {
72                    dictionary omni = {
73                        dictionary graph = {
74                            string relType = "bundle"
75                        }
76                    }
77                }
78            )
79            custom point3f[] outputs:points
80            custom float2[] outputs:primvars:st
81            custom token outputs:sourcePrimPath = ""
82            custom token outputs:sourcePrimType
83            custom token outputs:subdivisionScheme = ""
84            custom matrix4d outputs:worldMatrix = ( (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0) )
85            custom double3 outputs:xformOp:rotateXYZ = (0, 0, 0)
86            custom double3 outputs:xformOp:scale = (0, 0, 0)
87            custom double3 outputs:xformOp:translate = (0, 0, 0)
88            custom token[] outputs:xformOpOrder
89        }
90    }
91}
flowchart LR /World/Cube subgraph /World/PushGraph read_prim_into_bundle --> extract_bundle end /World/Cube -.-> read_prim_into_bundle

The interpretation of the five highlighted sections is:

  1. The import node is given a relationship to the prim it will be importing

  2. The bundle it is creating is given a placeholder child prim of type “Output”

  3. The generated bundle is passed along to the extract_bundle node via a connection

  4. The worldMatrix value was extracted from the bundle containing the cube’s definition

  5. The bundle being extracted is also presented as an output via the passThrough placeholder

Extended Attributes

An extended attribute is one that can take on any number of different types when it is part of an OmniGraph. For example it might be a double, float, or int value. The exact type will be determined by OmniGraph, either when a connection is made or a value is set.

The actual attributes, however, are represented as token types in USD and it is only OmniGraph’s interpretation that provides them with their “real” type. Here is an example of a node having its inputs:floatOrToken attribute type resolve to a float value at runtime since it has an incoming connection from a float type attribute.

def Xform "World"
{
    def OmniGraph "ActionGraph"
    {
        token evaluator:type = "execution"
        token fabricCacheBacking = "Shared"
        int2 fileFormatVersion = (1, 4)
        token pipelineStage = "pipelineStageSimulation"

        def OmniGraphNode "tutorial_node_extended_attribute_types"
        {
            custom token inputs:flexible
            custom token inputs:floatOrToken
            custom token inputs:toNegate
            custom token inputs:tuple
            token node:type = "omni.graph.tutorials.ExtendedTypes"
            int node:typeVersion = 1
            prepend token inputs:floatOrToken.connect = </World/ActionGraph/constant_float.inputs:value>
            custom token outputs:doubledResult
            custom token outputs:flexible
            custom token outputs:negatedResult
            custom token outputs:tuple
        }

        def OmniGraphNode "constant_float"
        {
            custom float inputs:value = 0
            token node:type = "omni.graph.nodes.ConstantFloat"
            int node:typeVersion = 1
        }
    }
}
flowchart LR subgraph /World/PushGraph constant_float --> tutorial_node_extended_attribute_types end

Path Attributes

The special OmniGraph attribute type path is used to represent USD paths, represented as token-type attributes in USD. To OmniGraph they are equivalent to an Sdf.Path that points to a prim or attribute that may or may not exist on the USD stage. When paths are modified, OmniGraph listens to the USD notices and attempts to fix up any path attributes that may have changed locations. To USD though they are simply strings and no updates are attempted when OmniGraph is not enabled and listening to the changes. See the attribute inputs:primPath in the example above for the representation of a path attribute in USD.

Accessing USD Prims From Nodes

OmniGraph accesses USD scene data through special import and export nodes. Examples of each of these are the ReadPrimBundle node for importing a USD prim into an OmniGraph bundle attribute and a corresponding WritePrim node for exporting a computed bundle of attributes back onto a USD prim.

Both of these node types use the path attribute type to specify the location of the prim on the USD stage that they will access.

Once the nodes are connected to the prim they are importing or exporting, usually via specifying the SdfPath to that prim on the USD stage, they can be used the same as any other OmniGraph node, passing the extracted USD data through the graph for computation. See above for a detailed example.

The USD API

OmniGraph listens the USD change notices, so it is able to update values when the USD API is used to modify them, or by proxy when UI elements that use the USD API modify them. However this is not an efficient way to set values as the notification process can be slow so it is best to avoid this if possible.

import omni.usd
import omni.graph.core as og
from pxr import Usd

# Slow Method
prim_node = omni.usd.get_context().get_stage().GetPrimAtPath("/World/ActionGraph/constant_float")
attribute = prim_node.GetAttribute("inputs:value")
attribute.Set(5.0)

# Fast Method
og.Controller("/World/ActionGraph/constant_float.inputs:value").set(5.0)