Tutorial 2 - Simple Data Node

The simple data node creates one input attribute and one output attribute of each of the simple types, where “simple” refers to data types that have a single component and are not arrays. (e.g. “float” is simple, “float[3]” is not, nor is “float[]”). See also Tutorial 10 - Simple Data Node in Python for a similar example in Python.

OgnTutorialSimpleData.ogn

The ogn file shows the implementation of a node named “omni.graph.tutorials.SimpleData”, which has one input and one output attribute of each simple type.

  1{
  2    "SimpleData" : {
  3        "version": 1,
  4        "categories": "tutorials",
  5        "scheduling": ["threadsafe"],
  6        "description": [
  7            "This is a tutorial node. It creates both an input and output attribute of every simple",
  8            "supported data type. The values are modified in a simple way so that the compute modifies values."
  9        ],
 10        "$uiNameMetadata": "The value of the 'uiName' metadata can also be expressed at the top level as well",
 11        "uiName": "Tutorial Node: Attributes With Simple Data",
 12        "inputs": {
 13            "a_bool": {
 14                "type": "bool",
 15                "metadata": {
 16                    "$comment": "Metadata can also be added at the attribute level",
 17                    "uiName": "Sample Boolean Input"
 18                },
 19                "description": ["This is an attribute of type boolean"],
 20                "default": true
 21             },
 22            "a_half": {
 23                "type": "half",
 24                "$uiNameMetadata": "Like the node uiName metadata, the attribute uiName metadata also has a shortform",
 25                "uiName": "Sample Half Precision Input",
 26
 27                "description": ["This is an attribute of type 16 bit float"],
 28                "$comment": "0 is used as the decimal portion due to reduced precision of this type",
 29                "default": 0.0
 30             },
 31            "a_int": {
 32                "type": "int",
 33                "description": ["This is an attribute of type 32 bit integer"],
 34                "default": 0
 35             },
 36            "a_int64": {
 37                "type": "int64",
 38                "description": ["This is an attribute of type 64 bit integer"],
 39                "default": 0
 40             },
 41            "a_float": {
 42                "type": "float",
 43                "description": ["This is an attribute of type 32 bit floating point"],
 44                "default": 0
 45             },
 46            "a_double": {
 47                "type": "double",
 48                "description": ["This is an attribute of type 64 bit floating point"],
 49                "default": 0
 50             },
 51             "a_token": {
 52                "type": "token",
 53                "description": ["This is an attribute of type interned string with fast comparison and hashing"],
 54                "default": "helloToken"
 55            },
 56            "a_path": {
 57                "type": "path",
 58                "description": ["This is an attribute of type path"],
 59                "default": ""
 60            },
 61            "a_string": {
 62                "type": "string",
 63                "description": ["This is an attribute of type string"],
 64                "default": "helloString"
 65            },
 66            "a_objectId": {
 67                "type": "objectId",
 68                "description": ["This is an attribute of type objectId"],
 69                "default": 0
 70            },
 71            "unsigned:a_uchar": {
 72               "type": "uchar",
 73               "description": ["This is an attribute of type unsigned 8 bit integer"],
 74               "default": 0
 75           },
 76            "unsigned:a_uint": {
 77               "type": "uint",
 78               "description": ["This is an attribute of type unsigned 32 bit integer"],
 79               "default": 0
 80            },
 81            "unsigned:a_uint64": {
 82                "type": "uint64",
 83                "description": ["This is an attribute of type unsigned 64 bit integer"],
 84                "default": 0
 85             },
 86             "a_constant_input": {
 87                 "type": "int",
 88                 "description": ["This is an input attribute whose value can be set but can only be connected as a source."],
 89                 "metadata": {
 90                     "outputOnly": "1"
 91                 }
 92             }
 93        },
 94        "outputs": {
 95            "a_bool": {
 96                "type": "bool",
 97                "uiName": "Sample Boolean Output",
 98                "description": ["This is a computed attribute of type boolean"],
 99                "default": false
100             },
101            "a_half": {
102                "type": "half",
103                "uiName": "Sample Half Precision Output",
104                "description": ["This is a computed attribute of type 16 bit float"],
105                "default": 1.0
106             },
107            "a_int": {
108                "type": "int",
109                "description": ["This is a computed attribute of type 32 bit integer"],
110                "default": 2
111             },
112            "a_int64": {
113                "type": "int64",
114                "description": ["This is a computed attribute of type 64 bit integer"],
115                "default": 3
116             },
117            "a_float": {
118                "type": "float",
119                "description": ["This is a computed attribute of type 32 bit floating point"],
120                "default": 4.0
121             },
122            "a_double": {
123                "type": "double",
124                "description": ["This is a computed attribute of type 64 bit floating point"],
125                "default": 5.0
126            },
127            "a_token": {
128                "type": "token",
129                "description": ["This is a computed attribute of type interned string with fast comparison and hashing"],
130                "default": "six"
131            },
132            "a_path": {
133                "type": "path",
134                "description": ["This is a computed attribute of type path"],
135                "default": "/"
136            },
137            "a_string": {
138                "type": "string",
139                "description": ["This is a computed attribute of type string"],
140                "default": "seven"
141            },
142            "a_objectId": {
143                "type": "objectId",
144                "description": ["This is a computed attribute of type objectId"],
145                "default": 8
146            },
147            "unsigned:a_uchar": {
148                "type": "uchar",
149                "description": ["This is a computed attribute of type unsigned 8 bit integer"],
150                "default": 9
151            },
152            "unsigned:a_uint": {
153                "type": "uint",
154                "description": ["This is a computed attribute of type unsigned 32 bit integer"],
155                "default": 10
156            },
157            "unsigned:a_uint64": {
158                "type": "uint64",
159                "description": ["This is a computed attribute of type unsigned 64 bit integer"],
160                "default": 11
161            }
162        },
163        "tests": [
164            {
165                "$comment": ["Each test has a description of the test and a set of input and output values. ",
166                             "The test runs by setting all of the specified inputs on the node to their values, ",
167                             "running the compute, then comparing the computed outputs against the values ",
168                             "specified in the test. Only the inputs in the list are set; others will use their ",
169                             "default values. Only the outputs in the list are checked; others are ignored."],
170                "description": "Check that false becomes true",
171                "inputs:a_bool": false,
172                "outputs:a_bool": true
173            },
174            {
175                "$comment": "This is a more verbose format of test data that provides a different grouping of values",
176                "description": "Check that true becomes false",
177                "inputs": {
178                    "a_bool": true
179                },
180                "outputs": {
181                    "a_bool": false
182                }
183            },
184            {
185                "$comment": "Even though these computations are all independent they can be checked in a single test.",
186                "description": "Check all attributes against their expected values",
187                "inputs:a_bool": false, "outputs:a_bool": true,
188                "inputs:a_double": 1.1, "outputs:a_double": 2.1,
189                "inputs:a_float": 3.3, "outputs:a_float": 4.3,
190                "inputs:a_half": 5.0, "outputs:a_half": 6.0,
191                "inputs:a_int": 7, "outputs:a_int": 8,
192                "inputs:a_int64": 9, "outputs:a_int64": 10,
193                "inputs:a_token": "helloToken", "outputs:a_token": "worldToken",
194                "inputs:a_string": "helloString", "outputs:a_string": "worldString",
195                "inputs:a_objectId": 5, "outputs:a_objectId": 6,
196                "inputs:unsigned:a_uchar": 11, "outputs:unsigned:a_uchar": 12,
197                "inputs:unsigned:a_uint": 13, "outputs:unsigned:a_uint": 14,
198                "inputs:unsigned:a_uint64": 15, "outputs:unsigned:a_uint64": 16
199            },
200            {
201                "$comment": "Make sure embedded quotes in a string function correctly",
202                "inputs:a_token": "hello'Token", "outputs:a_token": "world'Token",
203                "inputs:a_string": "hello\"String", "outputs:a_string": "world\"String"
204            },
205            {
206                "$comment": "Make sure the path append does the right thing",
207                "inputs:a_path": "/World/Domination", "outputs:a_path": "/World/Domination/Child"
208            },
209            {
210                "$comment": "Check that strings and tokens get correct defaults",
211                "outputs:a_token": "worldToken", "outputs:a_string": "worldString"
212            }
213        ]
214    }
215}

OgnTutorialSimpleData.cpp

The cpp file contains the implementation of the compute method, which modifies each of the inputs in a simple way to create outputs that have different values.

  1// SPDX-FileCopyrightText: Copyright (c) 2020-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
  2// SPDX-License-Identifier: LicenseRef-NvidiaProprietary
  3//
  4// NVIDIA CORPORATION, its affiliates and licensors retain all intellectual
  5// property and proprietary rights in and to this material, related
  6// documentation and any modifications thereto. Any use, reproduction,
  7// disclosure or distribution of this material and related documentation
  8// without an express license agreement from NVIDIA CORPORATION or
  9// its affiliates is strictly prohibited.
 10#include <OgnTutorialSimpleDataDatabase.h>
 11#include <string>
 12// Even though the path is stored as a string this tutorial will use the SdfPath API to manipulate it
 13#include <pxr/usd/sdf/path.h>
 14
 15// This class exercises access to the DataModel through the generated database class for all simple data types
 16
 17// It's a good practice to namespace your nodes, so that they are guaranteed to be unique. Using this practice
 18// you can shorten your class names as well. This class could have equally been named "OgnSimpleData", since
 19// the "Tutorial" part of it is just another incarnation of the namespace.
 20namespace omni
 21{
 22namespace graph
 23{
 24namespace core
 25{
 26namespace tutorial
 27{
 28
 29class OgnTutorialSimpleData
 30{
 31public:
 32    static bool compute(OgnTutorialSimpleDataDatabase& db)
 33    {
 34        // Inside the database the contained object "inputs" holds the data references for all input attributes and the
 35        // contained object "outputs" holds the data references for all output attributes.
 36
 37        // Each of the attribute accessors are named for the name of the attribute, with the ":" replaced by "_".
 38        // The colon is used in USD as a convention for creating namespaces so it's safe to replace it without
 39        // modifying the meaning. The "inputs:" and "outputs:" prefixes in the generated attributes are matched
 40        // by the container names.
 41        //
 42        // For example attribute "inputs:translate:x" would be accessible as "db.inputs.translate_x" and attribute
 43        // "outputs:matrix" would be accessible as "db.outputs.matrix".
 44
 45        // The "compute" of this method modifies each attribute in a subtle way so that a test can be written
 46        // to verify the operation of the node. See the .ogn file for a description of tests.
 47        db.outputs.a_bool() = !db.inputs.a_bool();
 48        db.outputs.a_half() = 1.0f + db.inputs.a_half();
 49        db.outputs.a_int() = 1 + db.inputs.a_int();
 50        db.outputs.a_int64() = 1 + db.inputs.a_int64();
 51        db.outputs.a_double() = 1.0 + db.inputs.a_double();
 52        db.outputs.a_float() = 1.0f + db.inputs.a_float();
 53        db.outputs.a_objectId() = 1 + db.inputs.a_objectId();
 54
 55        // The namespace separator ":" has special meaning in C++ so it is replaced by "_" when it appears in names
 56        // Attribute "outputs:unsigned:a_uchar" becomes "outputs.unsigned_a_uchar".
 57        db.outputs.unsigned_a_uchar() = 1 + db.inputs.unsigned_a_uchar();
 58        db.outputs.unsigned_a_uint() = 1 + db.inputs.unsigned_a_uint();
 59        db.outputs.unsigned_a_uint64() = 1 + db.inputs.unsigned_a_uint64();
 60
 61        // Internally the string type is more akin to a std::string_view, not available until C++17.
 62        // The data is a pair of (const char*, size_t), but the interface provided through the accessor is
 63        // castable to a std::string.
 64        //
 65        // This code shows the recommended way to use it, extracting inputs into a std::string for manipulation and
 66        // then assigning outputs from the results. Using the referenced object directly could cause a lot of
 67        // unnecessary fabric allocations. (i.e. avoid auto& outputStringView = db.outputs.a_string())
 68        std::string outputString(db.inputs.a_string());
 69        if (outputString.length() > 0)
 70        {
 71            auto foundStringAt = outputString.find("hello");
 72            if (foundStringAt != std::string::npos)
 73            {
 74                outputString.replace(foundStringAt, 5, "world");
 75            }
 76            db.outputs.a_string() = outputString;
 77        }
 78        else
 79        {
 80            db.outputs.a_string() = "";
 81        }
 82
 83
 84        // The token interface is made available in the database as well, for convenience.
 85        // By calling "db.stringToToken()" you can look up the token ID of a given string.
 86        // There is also a symmetrical "db.tokenToString()" for going the other way.
 87        std::string outputTokenString = db.tokenToString(db.inputs.a_token());
 88        if (outputTokenString.length() > 0)
 89        {
 90            auto foundTokenAt = outputTokenString.find("hello");
 91            if (foundTokenAt != std::string::npos)
 92            {
 93                outputTokenString.replace(foundTokenAt, 5, "world");
 94                db.outputs.a_token() = db.stringToToken(outputTokenString.c_str());
 95            }
 96        }
 97        else
 98        {
 99            db.outputs.a_token() = db.stringToToken("");
100        }
101
102        // Path just gets a new child named "Child". There's not requirement that the path point to anything
103        // that exists in the scene so any string will work here.
104        //
105        std::string outputPath = (std::string)db.inputs.a_path();
106
107        // In the implementation the string is manipulated directly, as it does not care if the SdfPath is valid or
108        // not. If you want to manipulate it using the pxr::SdfPath API this is how you could do it:
109        //
110        //     pxr::SdfPath sdfPath{outputPath};
111        //     pxr::TfToken childToken{asTfToken(db.stringToToken("/Child"))};
112        //     if (sdfPath.IsValid())
113        //     {
114        //         db.outputs.a_path() = sdfPath.AppendChild(childToken).GetString();
115        //     }
116        //
117        outputPath += "/Child";
118        db.outputs.a_path() = outputPath;
119
120        // Drop down to the ABI to find attribute metadata, currently not available through the database
121        auto& nodeObj = db.abi_node();
122        auto attributeObj = nodeObj.iNode->getAttribute(nodeObj, "inputs:a_bool");
123        // The hardcoded metadata keyword is available through the node
124        auto uiName = attributeObj.iAttribute->getMetadata(attributeObj, kOgnMetadataUiName);
125        std::string expectedUiName{ "Sample Boolean Input" };
126        if (not uiName or (expectedUiName != uiName))
127        {
128            db.logError("Found unexpected UI name");
129            return false;
130        }
131
132        // Confirm that the piece of metadata that differentiates objectId from regular uint64 is in place
133        auto objectIdAttributeObj = nodeObj.iNode->getAttribute(nodeObj, "inputs:a_objectId");
134        auto objectIdMetadata = attributeObj.iAttribute->getMetadata(objectIdAttributeObj, kOgnMetadataObjectId);
135        if (not objectIdMetadata)
136        {
137            db.logError("Found unexpected object ID");
138            return false;
139        }
140
141        return true;
142    }
143};
144
145// namespaces are closed after the registration macro, to ensure the correct class is registered
146REGISTER_OGN_NODE()
147
148} // namespace tutorial
149} // namespace core
150} // namespace graph
151} // namespace omni

Note how the attribute values are available through the OgnTutorialSimpleDataDatabase class. The generated interface creates access methods for every attribute, named for the attribute itself. Inputs will be returned as const references, outputs will be returned as non-const references.

Attribute Data

Two types of attribute data are created, which help with ease of access and of use - the attribute name lookup information, and the attribute type definition.

Attribute data is accessed via a name-based lookup. This is not particularly efficient, so to facilitate this process the attribute name is translated into a fast access token. In addition, the information about the attribute’s type and default value is constant for all nodes of the same type so that is stored as well, in static data.

Normally you would use an auto declaration for attribute types. Sometimes you want to pass around attribute data so it is helpful to have access to the attribute’s data type. In the generated code a using namespace is set up to provide a very simple syntax for accessing the attribute’s metadata from within the node:

std::cout << "Attribute name is " << inputs::a_bool.m_name << std::endl;
std::cout << "Attribute type is " << inputs::a_bool.m_dataType << std::endl;

extern "C" void processAttribute(inputs::a_bool_t& value);
// Equivalent to extern "C" void processAttribute(bool& value);

Attribute Data Access

The attributes are automatically namespaced with inputs and outputs. In the USD file the attribute names will appear as inputs:XXX or outputs:XXX. In the C++ interface the colon is illegal so a contained struct is used to make use of the period equivalent, as inputs.XXX or outputs.XXX.

The minimum information provided by these wrapper classes is a reference to the underlying data, accessed by operator(). For this class, these are the types it provides:

Database Function

Returned Type

inputs.a_bool()

const bool&

inputs.a_half()

const pxr::GfHalf&

inputs.a_int()

const int&

inputs.a_int64()

const int64_t&

inputs.a_float()

const float&

inputs.a_double()

const double&

inputs.a_path()

const std::string&

inputs.a_string()

const std::string&

inputs.a_token()

const NameToken&

outputs.a_bool()

bool&

outputs.a_half()

pxr::GfHalf&

outputs.a_int()

int&

outputs.a_int64()

int64_t&

outputs.a_float()

float&

outputs.a_double()

double&

outputs.a_string()

std::string&

outputs.a_token()

NameToken&

The data returned are all references to the real data in the Fabric, our managed memory store, pointed to the correct location at evaluation time.

Note how input attributes return const data while output attributes do not. This reinforces the restriction that input data should never be written to, as it would cause graph synchronization problems.

The type pxr::GfHalf is an implementation of a 16-bit floating point value, though any other may also be used with a runtime cast of the value. omni::graph::core::NameToken is a simple token through which a unique string can be looked up at runtime.

Helpers

A few helpers are provided in the database class definition to help make coding with it more natural.

initializeType

Function signature static void initializeType(const NodeTypeObj& nodeTypeObj) is an implementation of the ABI function that is called once for each node type, initializing such things as its mandatory attributes and their default values.

validate

Function signature bool validate(). If any of the mandatory attributes do not have values then the generated code will exit early with an error message and not actually call the node’s compute method.

token

Function signature NameToken token(const char* tokenName).

Provides a simple conversion from a string to the unique token representing that string, for fast comparison of strings and for use with the attributes whose data types are token.

Compute Status Logging

Two helper functions are providing in the database class to help provide more information when the compute method of a node has failed. Two methods are provided, both taking printf-like variable sets of parameters.

void logError(Args...) is used when the compute has run into some inconsistent or unexpected data, such as two input arrays that are supposed to have the same size but do not, like the normals and vertexes on a mesh.

void logWarning(Args...) can be used when the compute has hit an unusual case but can still provide a consistent output for it, for example the deformation of an empty mesh would result in an empty mesh and a warning since that is not a typical use for the node.

typedefs

Although not part of the database class per se, a typedef alias is created for every attribute so that you can use its type directly without knowing the detailed type; a midway point between exact types and auto. The main use for such types might be passing attribute data between functions.

Here are the corresponding typedef names for each of the attributes:

Typedef Alias

Actual Type

inputs.a_bool_t

const bool&

inputs.a_half_t

const pxr::GfHalf&

inputs.a_int_t

const int&

inputs.a_int64_t

const int64_t&

inputs.a_float_t

const float&

inputs.a_double_t

const double&

inputs.a_token_t

const NameToken&

outputs.a_bool_t

bool&

outputs.a_half_t

pxr::GfHalf&

outputs.a_int_t

int&

outputs.a_int64_t

int64_t&

outputs.a_float_t

float&

outputs.a_double_t

double&

outputs.a_token_t

NameToken&

Notice the similarity between this table and the one above. The typedef name is formed by adding the extension _t to the attribute accessor name, similar to C++ standard type naming conventions. The typedef should always correspond to the return value of the attribute’s operator().

Direct ABI Access

All of the generated database classes provide access to the underlying INodeType ABI for those rare situations where you want to access the ABI directly. There are two methods provided, which correspond to the objects passed in to the ABI compute method.

Context function signature const GraphContextObj& abi_context() const, for accessing the underlying OmniGraph evaluation context and its interface.

Node function signature const NodeObj& nodeObj abi_node() const, for accessing the underlying OmniGraph node object and its interface.

In addition, the attribute ABI objects are extracted into a shared structure so that they can be accessed in a manner similar to the attribute data. For example db.attributes.inputs.a_bool() returns the AttributeObj that refers to the input attribute named a_bool. It can be used to directly call ABI functions when required, though again it should be emphasized that this will be a rare occurrence - all of the common operations can be performed more easily using the database interfaces.

Node Computation Tests

The “tests” section of the .ogn file contains a list of tests that take on two general forms. The first consists of a description and attribute values, both inputs and outputs, that will be used for the test, while the second contains the name of an external test scene to use along with various paths to nodes within the scene that are coupled with output attribute values that need to be checked.

The test runs by either setting all of the named input attributes to their values or loading the specified test scene, running the compute, and then comparing the resulting output attribute values against those specified by the test.

For example, to test the computation of the boolean attribute (whose output is the negation of the input) one could either specify two test values to use, or point to a test scene containing the node (along with any other necessary machinery that sets up the test) plus the expected output value.

The “description” field is optional, though highly recommended to aid in debugging which tests are failing. Any unspecified inputs take their default value, and any unspecified outputs do not get checked after the compute.

Note that the file attribute can be specified either as an absolute path or as a relative path in respect to the .ogn file. In the above example, “FileWithPredefinedTestSetup.usda” should be located relative to the .ogn, which implies that both reside in the same directory.

Various abbreviated syntaxes exist for writing out test objects, which mostly rely around in-lining node attribute names with their attribute namespaces (e.g., “inputs” and “outputs”) and node paths, thus reducing the number of required objects for defining tests.