OmniGraph

Introduction

../_images/ext_omnigraph_header.png

Welcome to Omniverse OmniGraph. Omniverse OmniGraph is a graph system that provides a compute framework for Omniverse through the use of nodes and graphs. In time, this computational framework will form the foundation of many other subsystems in Omniverse. In this first tutorial, we’ll show you how to work with the system, as well as showing you how you can create your own nodes through Python. In the future, other languages such as C++ will be supported for node creation, but for the time being we’ll keep things simple with Python.

Running your first graph

Before we can do anything with Omniverse OmniGraph, we must make sure the appropriate extensions have been loaded. Please open the extension manager:

../_images/ext_omnigraph_tutorial1_0_0.png

Make sure at least the following extensions are loaded. It’s a good idea to turn auto-load for these as well, so we’ll only need to do it once:

This is the omni.graph.core extension - nothing Omniverse OmniGraph related will work without it:

../_images/ext_omnigraph_tutorial1_0_1.png

This is the omni.graph.ui extension - contains all the UI elements for Omniverse OmniGraph:

../_images/ext_omnigraph_tutorial1_0_2.png

This is the Python examples extension, containing some of the Python nodes that we’ll be working with:

../_images/ext_omnigraph_tutorial1_0_3.png

Note in particular the folder icon. The sample files for this tutorial can be found by clicking on the folder icon. Please copy the .usda files in the data folder to a location that is convenient for loading and editing - for example the Documents folder.

To start things off lets run an existing graph already assembled. Please open the python_node_tutorial1_finished.usda file. Select the green ball (Sphere in the stage view) and move it:

../_images/ext_omnigraph_tutorial1_1_1.png

As you do so, you should see the text change color and move, and the plane underneath the text should deform:

../_images/ext_omnigraph_tutorial1_1_2.png

Let’s take a look at how this is working. Bring up the Omniverse OmniGraph Editor like so:

../_images/ext_omnigraph_tutorial1_1_3.png

You can navigate the graph by panning using the middle mouse button, and zooming using the mouse wheel. Zoom out to see the full graph:

../_images/ext_omnigraph_tutorial1_1_4.png

That looks a little complicated. But fear not, it’s not that complicated at all. Let’s zoom in to the left part of the graph. Let’s take a look at the connections from this first prim node. There are essentially two branches of connections - a whole bunch that goes up, and one that goes down. Let’s take a look the connects that go up first.

../_images/ext_omnigraph_tutorial1_1_7.png

If you follow one of the connections, it’ll go to a node that looks like this:

../_images/ext_omnigraph_tutorial1_1_8.png

It’s a node that takes the position information and converts it to color. This is in turn driving the display color of the text.

Let’s now take a look at the connection in the other direction:

../_images/ext_omnigraph_tutorial1_1_5.png

We can immediately see that the first node takes the position information, which is a tuple of 3 floats and breaks it up into its constituent parts. The next node takes the absolute value of the X coordinate, because we want the effect to be symmetrical around y-z plane.

Following the connection further, we see several further nodes:

../_images/ext_omnigraph_tutorial1_1_6.png

The basic idea is this: we subtract the positional X value from a fixed value, so that as we move the ball towards the center, the resultant value goes up, as we want our text to be lifted. The value is then clamped to a lower bound so that the text doesn’t sink below 0 as we drag the sphere far away from the origin. This clamped value is then multiplied by a different amount depending on which letter it is, and the final results is then composed again into a tuple to drive the position on one of the letters.

This concludes the first part of the tutorial. Next, let’s see how you can create your own graphs with existing nodes.

Creating your first graph

Now that we understand the basics of navigating and viewing an existing graph, the next step is naturally to create our own graphs with existing nodes. In this part of the tutorial we’ll hook up the graph for one of the letters, so that it changes color and moves up and down as we move our sphere around.

Note that as the Omniverse OmniGraph is still in its early phases of release, there are some usability glitches that we’ll have to work around for now, which will be fixed in time. In particular, as you place nodes, you may need to zoom out a little bit from the graph to see the newly placed nodes.

To start things off, please open the python_node_tutorial1_start.usda file. We’ll need some data from the sphere, so we need it as a node in the graph. Drag and drop the sphere from the stage view onto the Graph Editor like so:

../_images/ext_omnigraph_tutorial1_2_1.png

This creates a node from the sphere prim that we dragged in, and such a node is what we call a “prim node”. These “prim nodes” have a one to one correspondence with regular prims on the USD stage. They act as either the source of the data for our computations or the destination (sink) of the data when the computation is finished. So the pattern you’ll often see is:

prim node -> compute node -> compute node -> … -> prim node

In this case, the sphere prim we just dragged in is going to act as the source of our computation. These prim nodes are crucial to our system because without them, there’d be no source of data and nowhere to put the data once the computation has completed. As an aside, another way to create prim nodes is by selecting the prim in the stage view, and RMB to do it in the context menu:

../_images/ext_omnigraph_tutorial1_2_2.png

Now that we have our first node in the graph, let’s move onto the second. RMB to bring up the context menu, and from there choose:

Add node > omni.graph.example.python > PositionToColorPython

../_images/ext_omnigraph_tutorial1_2_3.png

Notice currently the available nodes are organized by extension. In order to use nodes from a particular extension, the extension itself must first be loaded, otherwise the nodes will not show up in the context menu.

Connect the xformOp:translate attribute to the inputs:position attribute by dragging from one port to the other:

../_images/ext_omnigraph_tutorial1_2_4.png

Select the PositionToColorPython node to display the attributes in the property window. Set the inputs:scale attribute to 3, as the grid is about 3 units long. Notice the output:color is already being computed.

Next, let’s create another prim node to act as the sink of our computation. Drag the Text_O mesh into the Graph Editor:

../_images/ext_omnigraph_tutorial1_2_5.png

Connect the outputs:color attribute to the primvars:displayColor attribute:

../_images/ext_omnigraph_tutorial1_2_6.png

Voila! We have a first working graph! If you now drag the sphere around, the “O” letter should change color:

../_images/ext_omnigraph_tutorial1_2_7.png

Next, we can start hook up the other branch of the graph that drives the position of the letter. Add the DecomposeDouble3 node to decompose the position attribute into its constituent parts. Hook that up to an AbsDouble node to take the absolute value, and then a SubtractDouble node:

../_images/ext_omnigraph_tutorial1_2_8.png

Select the SubtractDouble node and set its inputs:a attribute to 3, as the grid is about 3 units long in each direction.

Next, put down a ClampDouble node and set its inputs:max to 10 (10 is arbitrary, but we certainly want the max to be greater than 0):

../_images/ext_omnigraph_tutorial1_2_9.png

We’ll want to control how high the text moves in response to the sphere moving, so create a Multiply Double node, wire it up and set the inputs:a attribute to 0.18 so the O letter doesn’t bounce too high:

../_images/ext_omnigraph_tutorial1_2_10.png

Create a ComposeDouble3 node, wire up the output of the MultDouble node to the inputs:z attribute as we want the letter to move up and down and our scene is z-up. Take care to set the inputs:x value to -2.34 though, as otherwise our letter will be stuck near the origin:

../_images/ext_omnigraph_tutorial1_2_11.png

Finally, it’s time to bring all together. Wire up the ComposeDouble3 node to the xformOp:translate attribute of the Text_O prim node that we created earlier:

../_images/ext_omnigraph_tutorial1_2_12.png

That’s it! Now, as you move the sphere around, the letter should bounce up and down as well as changing color.

Next, we’ll look at how you can create your own nodes with its own behavior in Python.

Creating your first nodes

As we now understand how to work with the graph using existing nodes, this section of the tutorials walks through how to create new nodes to augment the existing functionality. OmniGraph uses a framework we call OGN (OmniGraph Nodes) for node creation. OGN is a powerful framework that takes a bare minimum of high level description about the node and generates most of the impplementaion required for your node. In this tutorial, it will be generating Python code, but it is capable of generating code in other languages like C++ as well, though we have not exposed that capability yet in this release. In addition, it’s capable of generating docs and even tests for your node.

In this tutorial, we’ll create two simple nodes, NegateDouble and AddDouble, to replace the functionality of the SubtractDouble node.

Please open the Node Description Editor:

../_images/ext_omnigraph_tutorial1_3_1.png

First, we’ll create the NegateDouble node. Please fill in the first section of the Node Description Editor as seen below. Note that it’s strongly recommended to leave the Extension location field to its default value, as the application only looks for extensions in a fixed number of locations (for the precise list and instructions on how to alter it, please refer to general Omniverse docs). The default location above should be one of these locations. If you choose something else, make sure it’s a location where Omniverse is set to look for extensions, else your nodes will not load.

../_images/ext_omnigraph_tutorial1_3_2.png

Next, please fill out the Inputs and Outputs sections as depicted blow:

../_images/ext_omnigraph_tutorial1_3_3.png

Please pay particular attentiion to the Base Type section - make sure the type is set to double so that it matches the rest of the nodes already created. Otherwise, you will not be able to connect the attributes. Note that automatic type conversion (say between floats, ints, and doubles) is on our roadmap and coming soon, but not yet in place as of this writing.

Notice that as you’re filling out the information, the editor is showing you the raw .ogn file it is generating:

../_images/ext_omnigraph_tutorial1_3_4.png

The .ogn file is what really matters here. It is a json file that describes what the node looks like from a high level. It is treated as a source file, which is parsed by our OGN framework, that then generates source files in Python (and in time, C++), as well as documentation and tests. These generated source files provide functionality like accessing data, checking validity, and generally reduces boilerplate code to simplify the task for programmers to work with the system. The Node Description Editor UI you are using merely simplifies the task of writing the .ogn file, as new users may not be familiar with its syntax.

After filling out the information, click first on the “Populate Extension” button, followed by “Generate Blank Implementation”. The first button ensures that the extension is setup correctly and also genreates the .ogn file within the extension. The second button generates a sample implementation file.

../_images/ext_omnigraph_tutorial1_3_5.png

If all went well, you should already be able to place our new node into the system, even though at this time, with only the default implementation, it won’t do anything useful yet. Place a NegateDouble node into the graph and wire it up like so:

../_images/ext_omnigraph_tutorial1_3_6.png

Let’s now navigate to the generated extension directory to take a look at the generated implementation file. If things were left at its default, this directory would be something like:

Documents/Kit/shared/exts/omni.my.extension/omni/my/extension/nodes

This is the source directory. Notice also the

Documents/Kit/shared/exts/omni.my.extension/omni/my/extension/ogn

directory. This is the generated code / documentation by the OGN framework. This is considered a build directory and not source.

Open up the OgnNegateDouble.py file. It should look like the following:

"""
This is the implementation of the OGN node defined in OgnNegateDouble.ogn
"""

# Array or tuple values are accessed as numpy arrays so you probably need this import
import numpy


class OgnNegateDouble:
    """
        takes a double as input and negates it
    """
    @staticmethod
    def compute(db) -> bool:
        """Compute the outputs from the current input"""

        # ======================================================================
        # Use these methods to access the input values
        # ======================================================================

        # input_value = db.inputs.in_double

        # ======================================================================
        # Use these methods to set the output values
        # ======================================================================

        # db.outputs.out_double = new_output_value

        # If anything causes your compute to fail report the error and return False
        # if errors_found:
        #     db.log_error(errors_found)
        #     return False

        # Even if inputs were edge cases like empty arrays, correct outputs mean success
        return True

Notice the OGN framework has generated sample access / mutate methods, put in comments for reference. To complete our NegateDouble node, we only need to change a line or two:

"""
This is the implementation of the OGN node defined in OgnNegateDouble.ogn
"""

# Array or tuple values are accessed as numpy arrays so you probably need this import
import numpy


class OgnNegateDouble:
    """
        takes a double as input and negates it
    """
    @staticmethod
    def compute(db) -> bool:
        """Compute the outputs from the current input"""

        # ======================================================================
        # Use these methods to access the input values
        # ======================================================================

        input_value = db.inputs.in_double

        # ======================================================================
        # Use these methods to set the output values
        # ======================================================================

        db.outputs.out_double = -1.0*input_value

        # If anything causes your compute to fail report the error and return False
        # if errors_found:
        #     db.log_error(errors_found)
        #     return False

        # Even if inputs were edge cases like empty arrays, correct outputs mean success
        return True

Once you save the file, notice that your node magically starts functioning:

../_images/ext_omnigraph_tutorial1_3_8.png

This demonstrates the idea of “hot reloading”, which is very handy for Python nodes, and allows programmers to iterate rapidly.

Now that our NegateDouble node is working, let’s move on to our next node, AddDouble. In the Node Description Editor, please click on File -> New Node to start the creation process for a new node:

../_images/ext_omnigraph_tutorial1_3_9.png

Fill in the node details as follows:

../_images/ext_omnigraph_tutorial1_3_10.png ../_images/ext_omnigraph_tutorial1_3_11.png ../_images/ext_omnigraph_tutorial1_3_12.png

As before, click on the “Populate Extension” button, followed by “Generate Blank Implementation”.

../_images/ext_omnigraph_tutorial1_3_13.png

Navigate to the folder containing the source as before, and alter the implementation of the AddDouble node as follows:

"""
This is the implementation of the OGN node defined in OgnAddDouble.ogn
"""

# Array or tuple values are accessed as numpy arrays so you probably need this import
import numpy


class OgnAddDouble:
    """

    """
    @staticmethod
    def compute(db) -> bool:
        """Compute the outputs from the current input"""

        # ======================================================================
        # Use these methods to access the input values
        # ======================================================================

        # input_value = db.inputs.input_a

        # input_value = db.inputs.input_b

        # ======================================================================
        # Use these methods to set the output values
        # ======================================================================

        db.outputs.sum = db.inputs.input_a + db.inputs.input_b

        # If anything causes your compute to fail report the error and return False
        # if errors_found:
        #     db.log_error(errors_found)
        #     return False

        # Even if inputs were edge cases like empty arrays, correct outputs mean success
        return True

Place the AddDouble node into the graph, wire it up and set its input value like so:

../_images/ext_omnigraph_tutorial1_3_15.png

Notice the output value should already be computed.

Go ahead and disconnect the SubtractDouble node from the ClampDouble node, by right clicking on the connection:

../_images/ext_omnigraph_tutorial1_3_16.png

Wire the AddDouble node to the ClampDouble node as follows:

../_images/ext_omnigraph_tutorial1_3_17.png

That’s it - as you move the sphere around, the graph should function as before. As a cleanup, consider deleting the dangling SubtractDouble node.

Note that to successfully run these nodes in a future session, you need to now enable your new extension (omni.my.extension) by loading it from the extension manager like the others. You may want to enable auto-load on the extension for convenience.

In the next chapter, we’ll cover how to create a more complex node - a deformer.

Create more complex nodes

Now that we have gone through the exercise of creating some simple nodes, it’s time to create a more complex node. To this end a deformer would be a good example, as it’s a fairly common use case.

In the Node Description Editor, please click on File -> New Node to start the creation process for a new node:

../_images/ext_omnigraph_tutorial1_3_9.png

Fill in the node details as follows:

../_images/ext_omnigraph_tutorial1_4_1.png ../_images/ext_omnigraph_tutorial1_4_2.png ../_images/ext_omnigraph_tutorial1_4_3.png

Note that key difference is that for the in_points attribute we’re tagging it with a tuple size of 3, and making it an array (it’s also of type float, as opposed to double, as that tends to be the default data for point arrays in the USD sample we have). Also, make sure you set the default value to the empty array: []

From here generate the .ogn file and sample implementation file as we have done for the other nodes in the previous tutorial.

Open the OgnTestDeformer.py implementaion file and fill in its contents like so:

"""
This is the implementation of the OGN node defined in OgnTestDeformer.ogn
"""

# Array or tuple values are accessed as numpy arrays so you probably need this import
import numpy


class OgnTestDeformer:
    """

    """
    @staticmethod
    def compute(db) -> bool:
        """Compute the outputs from the current input"""

        # ======================================================================
        # Use these methods to access the input values
        # ======================================================================

        multiplier = db.inputs.multiplier

        points = db.inputs.in_points

        # ======================================================================
        # Use these methods to set the output values
        # ======================================================================

        wavelength = 2
        offset = 3.14

        db.outputs.out_points_size = points.shape[0]
        # Nothing to evaluate if there are no input points
        if db.outputs.out_points_size == 0:
            return True

        pt = points.copy()  # we still need to do a copy here because we do not want to modify points
        tx = pt[:, 0]
        offset_tx = tx + offset
        ty = pt[:, 2]

        disp = (numpy.sin(offset_tx/wavelength) )
        pt[:, 1] += disp * multiplier

        db.outputs.out_points[:] = pt[:]  # we modify output_points in memory so we do not need to set the value again
        return True

Notice that arrays are bound to numpy - this allows us to have our internal buffers bound to Python without having to copy it, and also allows us to use numpy syntax and functionality for manipulating these arrays.

Now that the node is ready, let’s wire it up in the graph. Start with creating two prim nodes, by dragging and dropping PolyMeshPlane_in and PolyMeshPlane_out into the Graph Editor:

../_images/ext_omnigraph_tutorial1_4_5.png

Proceed to place one of our TestDeformer nodes and wire it up like so:

../_images/ext_omnigraph_tutorial1_4_6.png

Now, as you move the sphere around, the plane should deform, like so:

../_images/ext_omnigraph_tutorial1_4_7.png

Finally, a note about the red “Clean Extension” button that you may have noticed. As the ogn directory is a generated directory, it’s something akin to a build directory. It’s possible that as we add and remove nodes, extra stuff might accumulate in there due to a bug we haven’t caught yet. The button will scan the nodes directory and make sure what generated in the ogn directory matches up with it. You might try to use this button to do a “reset” if you find that ogn is in a bad state - first make sure the stuff in the nodes directory is good and update to date, then click this button to “reset” and clean things up.