OmniGraph Core Concepts¶
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.
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).
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++ an AttributeObj, which references a specific NodeObj, uniquely identifies a location in the graph. Once we have the attribute, we can ask the graph context for the value of that attribute.
In order to register a new type of node with the system, we’ll need to fill the exposed
INodeType interface with our
own custom functions in order to register our 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
INodeType interface. This can be used for both C++ and Python node type implementations.
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 to the USD data types (so can be cast to it), but can be defined independently. Also, our types capture some usefule metadata, such as the role of the data. For example, a float3 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. Currently our data type strucuture captures the following:
Base Data Type: can be bool, uchar, int, uint, int64, uint64, half, float, double, token, relationship, asset, prim, connection, tag
Component Count: 1 for raw base types, 2 for things like vector2f, 3 for point3d, normal3f etc
Array Depth: 0 for single value, 1 for array, 2 for array of array (not yet supported)
Role: can be none, vector, normal, position, color, texcoord, quaternion, frame, time code, text, applied schema, prim type name, execution, matrix, object id, or unknown
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 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.
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.
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, the Fabric handles data synchronization between CPU data, GPU data, and USD data, offloading that task from OmniGraph.