Core Concepts
Graph
The graph comprises two conceptual pieces - the Authoring Graph and the Execution Graph. The term is often used to refer to one or both of these graphs, though among users and casual developers it most commonly refers the the Authoring Graph.
One type of graph you might hear a lot of is the Action Graph where you can build up behaviors that are triggered on some event. See this Action Graph for an introduction to how it can be used.
Execution Graph
The execution graph is what the system uses when it is evaluating the computation the authoring graph has described. It is free to take the description in the authoring graph and use it as-is by calling functions in the same graph structure, or to perform any optimization it sees fit to transform the authoring graph into a more efficient representation for execution. For example it might take two successive nodes that double a number and change it into a single node that quadruples a number.
Graph Context
The graph context contains things such as the current evaluation time and other information relevant to the current context of evaluation (and hence the name). Thus, we can ask it for the values on a particular attribute (as it will need to take things such as time into consideration when determining the value).
In C++ the graph is accessed through the omni::graph::core::IGraphContext
interface. In Python the bindings
are available in omni.graph.core.GraphContext
.
Graph Type
The graph type, sometimes referred to as the evaluation type, indicates how the graph is to be executed. This includes the process of graph transformation that takes an Authoring Graph and creates from it an Execution Graph that follows the desired type of evaluation pattern. Examples of graph types include the Action Graph, Push Graph, and Lazy Graph.
Graph Registry
This is where we register new node types (and unregister, when our plugin is unloaded). The code generated through the descriptive .ogn format automatically handles interaction with the registry.
Interaction with the registry is handled through the C++ ABI interface omni::graph::core::IGraphRegistry
and in Python through the binding omni.graph.core.GraphRegistry
.
Node
The heart of any node graph system is of course, the node. The most important exposed method on the node is one to get
an attribute, which houses all of the data the node uses for computing. The most import method you implement is the
compute
method, which performs the node’s computation algorithm.
In C++ the graph is accessed through the omni::graph::core::INode
interface. In Python the bindings
are available in omni.graph.core.Node
.
Node Type
In order to register a new type of node with the system, you will need to fill the exposed
omni::graph::core::INodeType
interface with
your own custom functions in order to register your new node type with the system. To simplify this process a
descriptive format (.ogn files) has been created which is described in OGN User Guide. Each node type has a
unique implementation of the omni::graph::core::INodeType
interface. This can be used for both C++ and
Python node type implementations. The Python nodes use bindings and function forwarding to interface with the
C++ ABI in omni.graph.core.NodeType
.
Attribute
An Attribute has a name and contains some data. This should be no surprise to anyone who has worked with graphs before. Attributes can be connected to other attributes on other nodes to form an evaluation network.
In C++ the graph is accessed through the omni::graph::core::IAttribute
interface. In Python the bindings
are available in omni.graph.core.Attribute
.
Attribute Data
While the Attribute defines the connection points on a node and its Attribute Type defines the type(s) of data the attribute will take on, the Attribute Data is the actual data of the attribute, usually stored in Fabric.
In C++ the graph is accessed through the omni::graph::core::IAttributeData
interface. In Python the bindings
are available in omni.graph.core.AttributeData
.
Attribute Type
OmniGraph mostly relies on Fabric for data, and Fabric was designed to mostly just mirror the data in USD, but in more compute friendly form. That said, we did not want to literally use the USD data types, as that creates unnecessary dependencies. Instead, we create data types that are binary compatible with the USD data types (so that in C++ they can be cast directly), but can be defined independently.
Also, our types capture some useful metadata, such as the role of the data. For example, a float[3] can be used both to describe a position as well as a normal. However, the way the code would want to deal with the data is very different depending on which of the two roles it plays. Our types have a role field to capture this sort of meta-data.
See more detailed documentation in Attribute Type Definition.
Connections
If the Node is thought of as the vertex in the Graph then the Connections are the edges. They are a representation of a directed dependency between two specific Attributes on Nodes in the graph.
Bundles
To address the limitations of regular attributes, we introduced the notion of the Bundle. As the name suggests, this is a flexible bundle of data, similar to a USD Prim. One can dynamically create any number of attributes inside the bundle and transport that data down the graph.
This serves two important purposes. First, the system becomes more flexible - we are no longer limited to pre-declared data. Second, the system becomes more usable. Instead of many connections in the graph, we have just a single connection, with all the necessary data that needs to be transported.
A brief introduction to bundles can be seen Bundles and Bundles User Guide.
Fabric
The data model for OmniGraph is based on the Fabric data manager. Fabric is used as a common location for all data within the nodes in OmniGraph. This common data location allows for many efficiencies. Further, Fabric handles data synchronization between CPU data, GPU data, and USD data, offloading that task from OmniGraph.
Fabric is a cache of USD-compatible data in vectorized, compute friendly form. OmniGraph leverages Fabric’s data vectorization feature to help optimize its performance. Fabric also facilitates the movement of data between CPU and GPU, allowing data to migrate between the two in a transparent manner.
There are currently two kinds of Fabric caches. There is a single SimStageWithHistory cache in the whole system, and any number of StageWithoutHistory caches.
When a graph is created, it must specify what kind of Fabric cache “backs” it. All the data for the graph will be stored in that cache. Full documentation on what Fabric is and how it works can be found online
USD
Like the rest of Omniverse, OmniGraph interacts with and is highly compatible with USD. We use USD for persistence of the graph. Moreover, since Fabric is essentially USD in compute efficient form, any transient compute data can be “hardened” to USD if we so choose. That said, while it is possible to do so, we recommend that node writers refrain from accessing USD data directly from the node, as USD is essentially global data, and accessing it from the node would prevent it from being parallel scheduled and thus get in the way of scaling and distribution of the overall system.
Instancing
Instancing is a templating technique that allows to create a graph once, and to apply it to multiple prims. It brings several benefits:
for authoring: The graph is created once but can be used multiple time without additional effort.
for maintenance: Any change made to this shared “template” is automatically applied to all “instances”
for runtime:
Vectorized data: All the runtime data for “instances” of a given graph is allocated in contiguous arrays per attributes. This compact data organization provides data locality which reduces cache misses, thus improving performance.
Compute “transposition”: Instead of iterating over each graph and execute all its node, the framework can iterate over the graph nodes and execute all its “instances”. This brings a huge benefit when there are a lot of “instances”.
Compute “factorization”: All “instances” can be provided to the compute at once, so the framework don’t even have to iterate the “instances” anymore
Vectorized compute: A node can decide to implement a vectorized compute for further optimizations: this allows to use SIMD instruction sets for example, or apply similar types of optimizations. Note that this feature is currently only available for Push Graph.
Auto-instancing
Auto-instancing is a framework feature that automatically factorizes similar graphs as instances, in order to benefit from the runtime performance gains explained above. This feature will set up compute for all of those graphs, at once, through the same execution pipline than regular instances. This feature does not require any user action, and is meant to bring the same level of performance as regular instancing. It is particularly helpful when replicating/referencing the same “Smart Asset” (an asset embedding its own logic/behavior as a graph) many times in a stage.
Graph Variables
Variables are values associated with an instance of a graph that can be read or changed by the graph during its execution. Variables can be used to keep track of state within the graph between execution frames, or to supply individualized values for different instances of the same graph. The initial value of a variable is read from USD, and the runtime value is maintained in Fabric. Variable values are never written back to USD, so any changes to their value during execution are lost when the application completes.
In C++ variable properties can be queried through the omni::graph::core::IVariable
interface, with methods
to create, remove and find variables available on omni::graph::core::IGraph
. In python, the bindings are
available on the omni.graph.core.IVariable
and omni.graph.core.Graph
classes, respectively.
Reading and writing variable runtime values in Fabric can be accomplished by accessing their corresponding Attribute Data available through a Graph Context in a similar manner to working with an Attribute. Reading and writing initial, a.k.a default, values in python can be accomplished with methods provided with the Controller Class.
Compounds
Compounds are a representation used to group related nodes together into a sub-network. This grouping of nodes can be done for organization purposes or to create a single definition to be reused multiple times, either within a single OmniGraph or across OmniGraphs. OmniGraph currently supports Compound Subgraphs, which are a collection of nodes that are represented by a single Compound Node in the parent graph. Compound Subgraphs share state, including variables, with the OmniGraph of the Compound Node.