Example Setup - Creating An OGN-enabled Extension

This document assumes familiarity with the extension 2.0 implementation described in Extensions. Implementation details that relate to the extension system but not to .ogn files in particular will not be described in detail. Refer to the above document for more information.

For the purposes of illustration an extension names omni.my.extension will be created, with a single node OgnMyNode.

premake5.lua

The .lua file containing an extension 2.0 implementation of an extension. The OGN-specific additions are commented. (Other lines would normally be commented as well in good build code; omitted here for clarity.)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
local ext = get_current_extension_info()

-- ----------------------------------------------------------------------
-- Initialize a helper variable containing standard configuration information for projects containing OGN files
local ogn = get_ogn_project_information(ext, "omni/my/extension")

-- This group is just for keeping things organized in the generated build files.
ext.group = "simulation"
project_ext( ext )


-- ----------------------------------------------------------------------
-- The main project is for building your plugin, mainly C++ nodes and the plugin support code.
project_ext_plugin( ext, ogn.plugin_project )
    -- ----------------------------------------------------------------------
    -- OGN files can be grouped together. By default "ogn.nodes_path" is just "nodes", as per the directory structure
    add_files("ogn/", ogn.nodes_path)
    add_files("impl", ogn.plugin_path)
    add_files("config", "config")
    add_files("docs", ogn.docs_path)

    -- ----------------------------------------------------------------------
    -- This sets up the OGN project dependencies, including the include path, library path, and extension dependencies
    add_ogn_dependencies(ogn)

    links {"carb", "vt", "gf", "sdf", "arch", "usd", "tf", "usdUtils", "usdGeom", "usdSkel"}


-- ----------------------------------------------------------------------
-- This creates a separate project tasked with processing the .ogn files to create the generated code.
-- By making it a separate project the build is guaranteed to always process the files before trying to compile.
-- The dependency on this project is auto-generated by the add_ogn_dependencies() function above.
project_ext_ogn( ext, ogn )


-- ----------------------------------------------------------------------
-- Note how the ogn object is used to populate the Python binding and script information
project_ext_bindings {
    ext = ext,
    project_name = ogn.python_project,
    module = ogn.bindings_module,
    src = ogn.bindings_path,
    target_subdir = ogn.bindings_target_path
}

    add_files("bindings", "bindings")
    add_files("python", "python/*.py")
    add_files("python/_impl", "python/_impl")
    add_files("python/tests", "python/tests")

    -- ----------------------------------------------------------------------
    -- If you do not have Python node implementations this line may be safely omitted.
    -- The second parameter is a list of directories containing Python node files, which enables OmniGraph to
    -- hot-load the Python node files when they change, so that you can modify their behaviour live.
    add_ogn_dependencies(ogn, {"python/_impl/nodes"})

    -- ----------------------------------------------------------------------
    -- Copy the init script directly into the build tree to avoid reload conflicts.
    repo_build.prebuild_copy {
        { "python/__init__.py", ogn.python_target_path },
    }
    -- Linking directories allows them to hot reload when files are modified in the source tree
    repo_build.prebuild_link {
        { "python/_impl", ogn.python_target_path.."/_impl" },
        { "python/tests", ogn.python_tests_target_path },
    }

Here is that same file with appropriate comments that can be used as a starting template.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
-- ----------------------------------------------------------------------
-- Set up standard extension information
local ext = get_current_extension_info()

-- ----------------------------------------------------------------------
-- Set up standard OGN information
local ogn = get_ogn_project_information(ext, "omni/my/extension")

-- Put the extension into a top-level group and define the main project
ext.group = "simulation"
project_ext( ext )

-- ----------------------------------------------------------------------
-- Define the plugin project, which contains all of the C++ code
project_ext_plugin( ext, ogn.plugin_project )

    -- ----------------------------------------------------------------------
    -- Add the files to the project, reorganizing to make them easier to find in the Visual Studio project
    add_files("ogn/", ogn.nodes_path)
    add_files("impl", ogn.plugin_path)
    add_files("config", "config")
    add_files("docs", ogn.docs_path)

    -- ----------------------------------------------------------------------
    -- Set up all build dependencies required by the OGN generated code
    add_ogn_dependencies(ogn)

    -- ----------------------------------------------------------------------
    -- Add dependent libraries to the extension link phase
    links {"carb", "vt", "gf", "sdf", "arch", "usd", "tf", "usdUtils", "usdGeom", "usdSkel"}

-- ----------------------------------------------------------------------
-- Create a project for processing the .ogn files to create the generated code.
project_ext_ogn( ext, ogn )


-- ----------------------------------------------------------------------
-- Create a project for providing the Python interface to the extension
project_ext_bindings {
    ext = ext,
    project_name = ogn.python_project,
    module = ogn.bindings_module,
    src = ogn.bindings_path,
    target_subdir = ogn.bindings_target_path
}

    -- ----------------------------------------------------------------------
    -- Add the files to the project, reorganizing to make them easier to find in the Visual Studio project
    add_files("bindings", "bindings")
    add_files("python", "python/*.py")
    add_files("python/_impl", "python/_impl")
    add_files("python/tests", "python/tests")

    -- ----------------------------------------------------------------------
    -- Python bindings have OGN dependencies so set them up here.
    add_ogn_dependencies(ogn, {"python/_impl/nodes"})

    -- ----------------------------------------------------------------------
    -- Copy the init script directly into the build tree to avoid reload conflicts.
    repo_build.prebuild_copy {
        { "python/__init__.py", ogn.python_target_path },
    }
    -- Linking directories allows them to hot reload when files are modified in the source tree
    repo_build.prebuild_link {
        { "python/_impl", ogn.python_target_path.."/_impl" },
        { "python/tests", ogn.python_tests_target_path },
    }

Changing Type Definitions

By default the code generator uses certain C++, CUDA, and Python data types for the code it produces. Sometimes you may wish to use alternative types, e.g. your favourite math library’s types. This information can be modified through a type definition file. To modify the type definitions for an entire project you add an extra parameter to the call to get_ogn_project_information() in your premake5.lua file, with details described in Type Definition Overrides.

bindings/BindingsPython.cpp

This file is set up as per the regular extension set up you can find in Extensions with no OGN-specific changes required.

config/extension.toml

In addition to the other required dependencies, the OmniGraph dependencies need to be added for the extension loading to work properly. These are the OmniGraph core, and the Kit async engine used for running tests.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
[package]
title = "My Feature"

# Main module for the Python interface
[[python.module]]
name = "omni.my.feature"

# Watch the .ogn files for hot reloading (only works for Python files)
[fswatcher.patterns]
include = ["*.ogn", "*.py"]
exclude = ["Ogn*Database.py"]

# Other extensions that need to load before this one
[dependencies]
"omni.graph" = {}
"omni.graph.tools" = {}

[[native.plugin]]
path = "bin/${platform}/${config}/*.plugin"
recursive = false

data/preview.png

This file is an image that will be displayed in the extension manager. Nothing extra needs to be done to make that happen; just create the file and ensure that the data/ directory is installed into the extension’s destination directory.

docs/README.md

Markdown format documentation for your extension that will appear in the extension manager. It should be a short description of what the extension does. Here is a minimal version of a readme.

# My Extension [omni.my.extension]

My extension can cook your breakfast, mow your lawn, and fix the clog in your kitchen sink.

docs/CHANGELOG.md

Markdown format documentation for your extension changes that will appear in the extension manager. Here is a simple template that uses a standard approach to maintaining change logs.

# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.0.0] - 2021-03-01
### Initial Version

docs/index.rst

ReStructuredText format documentation for your extension. The node’s documentation will be generated automatically by the OGN processor.

nodes/OgnMyNode.cpp,OgnMyNode.ogn

These files contain the description and implementation of your node. See the detailed examples in omni.graph.tutorials: OmniGraph Walkthrough Tutorials for information on what can go into these files.

The naming of the files is important. They must both have the same base name (OgnMyNode in this case). It is also a good idea to have the common prefix Ogn as that makes nodes easier to locate.

plugins/PluginInterface.cpp

This file sets up your plugin. The OGN processing requires a couple of small additions to enable the automatic registration and deregistration of nodes when your plugin starts up and shuts down.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include "IOmniMyFeature.h"

#include <carb/Framework.h>
#include <carb/PluginUtils.h>

#include <omni/graph/core/iComputeGraph.h>
#include <omni/graph/core/ogn/Registration.h>
#include <omni/kit/IEditor.h>
#include <omni/kit/IMinimal.h>

const struct carb::PluginImplDesc pluginDesc = { "omni.my.feature.plugin", "My Feature", "NVIDIA",
                                                carb::PluginHotReload::eEnabled, "dev" };

CARB_PLUGIN_IMPL(pluginDesc, omni::my::feature::IOmniMyFeature)
CARB_PLUGIN_IMPL_DEPS(omni::kit::IEditor,
                    omni::graph::core::IGraphRegistry,
                    carb::flatcache::IToken)
DECLARE_OGN_NODES()

// carbonite interface for this plugin (may contain multiple compute nodes)
void fillInterface(omni::my::feature::IOmniMyFeature& iface)
{
    iface = {};
}

CARB_EXPORT void carbOnPluginStartup()
{
    INITIALIZE_OGN_NODES()
}

CARB_EXPORT void carbOnPluginShutdown()
{
    RELEASE_OGN_NODES()
}

plugins/IMyExtension.h

This file is set up as per the extension description with no OGN-specific changes required.

python/__init__.py

This file initializes the extension’s Python module as per the extension model. This will import your intended Python API for general use.

from ._impl.utilities import useful_utility
from ._impl.utilities import other_utility

from ._impl.commands import cmds

With this setup the extension user can access your Python API through the main module (similarly to how popular public packages such as numpy and pandas are structured).

import omni.my.feature as mf
mf.useful_utility(["Bart", "Lisa", "Maggie", "Marge", "Homer"])

python/tests/__init__.py

This file may contain setup code for your manually written test scripts that appear in this directory. It should contain the snippet below, which will enable automatic registration of your tests at runtime.

"""
Presence of this file allows the tests directory to be imported as a module so that all of its contents
can be scanned to automatically add tests that are placed into this directory.
"""
scan_for_test_modules = True

Test files should all have a PEP8-compliant name of the form test_my_good_test_file.py. The test prefix ensures that the files will be automatically recognized as containing Python unit tests.

Sample Skeleton Test

Here’s an example of a simple test that does nothing more than run a graph evaluation on a graph consisting of two nodes with a connection between them, verifying the output of a node. While .ogn files have the ability to specify their own automated test configurations, it is very limited and you’ll want to use something like this for more complex testing.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
"""Tests for my simple math nodes"""
# To make code more concise use a shortform in the import, much as you would for numpy -> np
import omni.graph.core as og

# This module contains some useful testing utilities
import omni.graph.core.tests as ogts


# By using the standard test case base class you get scene setUp() and tearDown(), and the current set of default
# OmniGraph settings. If you wish to use any non-standard settings, leave the scene in place after the test completes,
# or add your own scoped setting variables see the set of classes that derive from ogts.OmniGraphTestCase and look at
# the test configuration factory function ogts.test_case_class().
class TestMySimpleNodes(ogts.OmniGraphTestCase):
    """Class containing tests for my simple nodes. The base class allows the tests to be run asynchronously"""

    # ----------------------------------------------------------------------
    async def test_math(self):
        """Run a node network with the math operation (A + 6) * 10 = B
        Exercises the math nodes for addition (my.node.add) and multiplication (my.node.multiply)
        """
        # The Controller class is useful for manipulating the OmniGraph. See the Python help on it for details.
        # Using this shortcut keeps the graph configuration specification more readable.
        keys = og.Controller.Keys

        # This sets up the graph in the configuration required for the math operation to work
        (graph, (plus_node, times_node), _, _) = og.Controller.edit("/mathGraph", {
            keys.CREATE_NODES: [("plus", "my.extension.addInts"), ("times", "my.extension.multiplyInts")],
            keys.CONNECT: [("plus.outputs:result", "times.inputs:a")],
            keys.SET_VALUES: [("plus.inputs:a", 6), ("times.inputs:b", 10)]
        })

        # This contains pairs of (A, B) values that should satisfy the math equation.
        test_data = [(1, 70), (0, 60), (-6, 0)]

        # Creating specific controllers tied to the attributes that will be accessed multiple times in the loop
        # makes the access a little bit faster.
        in_controller = og.Controller(og.Controller.attribute("inputs:b", plus_node))
        out_controller = og.Controller(og.Controller.attribute("outputs:result", times_node))

        # Loop through the operation to set the input and test the output
        for (A, B) in test_data:
            # Set the test input on the node
            in_controller.set(A)

            # This has to be done to ensure the nodes do their computation before testing the results
            await og.Controller.evaluate(graph)

            # Compare the expected value from the test data against the computed value from the node
            self.assertEqual(B, out_controller.get())