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
  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
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
{
    "SimpleData" : {
        "version": 1,
        "categories": "tutorials",
        "description": [
            "This is a tutorial node. It creates both an input and output attribute of every simple",
            "supported data type. The values are modified in a simple way so that the compute modifies values."
        ],
        "$uiNameMetadata": "The value of the 'uiName' metadata can also be expressed at the top level as well",
        "uiName": "Tutorial Node: Attributes With Simple Data",
        "inputs": {
            "a_bool": {
                "type": "bool",
                "metadata": {
                    "$comment": "Metadata can also be added at the attribute level",
                    "uiName": "Sample Boolean Input"
                },
                "description": ["This is an attribute of type boolean"],
                "default": true
             },
            "a_half": {
                "type": "half",
                "$uiNameMetadata": "Like the node uiName metadata, the attribute uiName metadata also has a shortform",
                "uiName": "Sample Half Precision Input",

                "description": ["This is an attribute of type 16 bit float"],
                "$comment": "0 is used as the decimal portion due to reduced precision of this type",
                "default": 0.0
             },
            "a_int": {
                "type": "int",
                "description": ["This is an attribute of type 32 bit integer"],
                "default": 0
             },
            "a_int64": {
                "type": "int64",
                "description": ["This is an attribute of type 64 bit integer"],
                "default": 0
             },
            "a_float": {
                "type": "float",
                "description": ["This is an attribute of type 32 bit floating point"],
                "default": 0
             },
            "a_double": {
                "type": "double",
                "description": ["This is an attribute of type 64 bit floating point"],
                "default": 0
             },
             "a_token": {
                "type": "token",
                "description": ["This is an attribute of type interned string with fast comparison and hashing"],
                "default": "helloToken"
            },
            "a_path": {
                "type": "path",
                "description": ["This is an attribute of type path"],
                "default": ""
            },
            "a_string": {
                "type": "string",
                "description": ["This is an attribute of type string"],
                "default": "helloString"
            },
            "a_objectId": {
                "type": "objectId",
                "description": ["This is an attribute of type objectId"],
                "default": 0
            },
            "unsigned:a_uchar": {
               "type": "uchar",
               "description": ["This is an attribute of type unsigned 8 bit integer"],
               "default": 0
           },
            "unsigned:a_uint": {
               "type": "uint",
               "description": ["This is an attribute of type unsigned 32 bit integer"],
               "default": 0
            },
            "unsigned:a_uint64": {
                "type": "uint64",
                "description": ["This is an attribute of type unsigned 64 bit integer"],
                "default": 0
             },
             "a_constant_input": {
                 "type": "int",
                 "description": ["This is an input attribute whose value can be set but can only be connected as a source."],
                 "metadata": {
                     "outputOnly": "1"
                 }
             }
        },
        "outputs": {
            "a_bool": {
                "type": "bool",
                "uiName": "Sample Boolean Output",
                "description": ["This is a computed attribute of type boolean"],
                "default": false
             },
            "a_half": {
                "type": "half",
                "uiName": "Sample Half Precision Output",
                "description": ["This is a computed attribute of type 16 bit float"],
                "default": 1.0
             },
            "a_int": {
                "type": "int",
                "description": ["This is a computed attribute of type 32 bit integer"],
                "default": 2
             },
            "a_int64": {
                "type": "int64",
                "description": ["This is a computed attribute of type 64 bit integer"],
                "default": 3
             },
            "a_float": {
                "type": "float",
                "description": ["This is a computed attribute of type 32 bit floating point"],
                "default": 4.0
             },
            "a_double": {
                "type": "double",
                "description": ["This is a computed attribute of type 64 bit floating point"],
                "default": 5.0
            },
            "a_token": {
                "type": "token",
                "description": ["This is a computed attribute of type interned string with fast comparison and hashing"],
                "default": "six"
            },
            "a_path": {
                "type": "path",
                "description": ["This is a computed attribute of type path"],
                "default": "/"
            },
            "a_string": {
                "type": "string",
                "description": ["This is a computed attribute of type string"],
                "default": "seven"
            },
            "a_objectId": {
                "type": "objectId",
                "description": ["This is a computed attribute of type objectId"],
                "default": 8
            },
            "unsigned:a_uchar": {
                "type": "uchar",
                "description": ["This is a computed attribute of type unsigned 8 bit integer"],
                "default": 9
            },
            "unsigned:a_uint": {
                "type": "uint",
                "description": ["This is a computed attribute of type unsigned 32 bit integer"],
                "default": 10
            },
            "unsigned:a_uint64": {
                "type": "uint64",
                "description": ["This is a computed attribute of type unsigned 64 bit integer"],
                "default": 11
            }
        },
        "tests": [
            {
                "$comment": ["Each test has a description of the test and a set of input and output values. ",
                             "The test runs by setting all of the specified inputs on the node to their values, ",
                             "running the compute, then comparing the computed outputs against the values ",
                             "specified in the test. Only the inputs in the list are set; others will use their ",
                             "default values. Only the outputs in the list are checked; others are ignored."],
                "description": "Check that false becomes true",
                "inputs:a_bool": false,
                "outputs:a_bool": true
            },
            {
                "$comment": "This is a more verbose format of test data that provides a different grouping of values",
                "description": "Check that true becomes false",
                "inputs": {
                    "a_bool": true
                },
                "outputs": {
                    "a_bool": false
                }
            },
            {
                "$comment": "Even though these computations are all independent they can be checked in a single test.",
                "description": "Check all attributes against their expected values",
                "inputs:a_bool": false, "outputs:a_bool": true,
                "inputs:a_double": 1.1, "outputs:a_double": 2.1,
                "inputs:a_float": 3.3, "outputs:a_float": 4.3,
                "inputs:a_half": 5.0, "outputs:a_half": 6.0,
                "inputs:a_int": 7, "outputs:a_int": 8,
                "inputs:a_int64": 9, "outputs:a_int64": 10,
                "inputs:a_token": "helloToken", "outputs:a_token": "worldToken",
                "inputs:a_string": "helloString", "outputs:a_string": "worldString",
                "inputs:a_objectId": 5, "outputs:a_objectId": 6,
                "inputs:unsigned:a_uchar": 11, "outputs:unsigned:a_uchar": 12,
                "inputs:unsigned:a_uint": 13, "outputs:unsigned:a_uint": 14,
                "inputs:unsigned:a_uint64": 15, "outputs:unsigned:a_uint64": 16
            },
            {
                "$comment": "Make sure embedded quotes in a string functon correctly",
                "inputs:a_token": "hello'Token", "outputs:a_token": "world'Token",
                "inputs:a_string": "hello\"String", "outputs:a_string": "world\"String"
            },
            {
                "$comment": "Make sure the path append does the right thing",
                "inputs:a_path": "/World/Domination", "outputs:a_path": "/World/Domination/Child"
            },
            {
                "$comment": "Check that strings and tokens get correct defaults",
                "outputs:a_token": "worldToken", "outputs:a_string": "worldString"
            }
        ]
    }
}

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
  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
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// Copyright (c) 2020-2021, NVIDIA CORPORATION. All rights reserved.
//
// NVIDIA CORPORATION and its licensors retain all intellectual property
// and proprietary rights in and to this software, related documentation
// and any modifications thereto.  Any use, reproduction, disclosure or
// distribution of this software and related documentation without an express
// license agreement from NVIDIA CORPORATION is strictly prohibited.
//
#include <OgnTutorialSimpleDataDatabase.h>
#include <string>
// Even though the path is stored as a string this tutorial will use the SdfPath API to manipulate it
#include <pxr/usd/sdf/path.h>

// This class exercises access to the DataModel through the generated database class for all simple data types

// It's a good practice to namespace your nodes, so that they are guaranteed to be unique. Using this practice
// you can shorten your class names as well. This class could have equally been named "OgnSimpleData", since
// the "Tutorial" part of it is just another incarnation of the namespace.
namespace omni
{
namespace graph
{
namespace core
{
namespace tutorial
{

class OgnTutorialSimpleData
{
public:
    static bool compute(OgnTutorialSimpleDataDatabase& db)
    {
        // Inside the database the contained object "inputs" holds the data references for all input attributes and the
        // contained object "outputs" holds the data references for all output attributes.

        // Each of the attribute accessors are named for the name of the attribute, with the ":" replaced by "_".
        // The colon is used in USD as a convention for creating namespaces so it's safe to replace it without
        // modifying the meaning. The "inputs:" and "outputs:" prefixes in the generated attributes are matched
        // by the container names.
        //
        // For example attribute "inputs:translate:x" would be accessible as "db.inputs.translate_x" and attribute
        // "outputs:matrix" would be accessible as "db.outputs.matrix".

        // The "compute" of this method modifies each attribute in a subtle way so that a test can be written
        // to verify the operation of the node. See the .ogn file for a description of tests.
        db.outputs.a_bool() = !db.inputs.a_bool();
        db.outputs.a_half() = 1.0f + db.inputs.a_half();
        db.outputs.a_int() = 1 + db.inputs.a_int();
        db.outputs.a_int64() = 1 + db.inputs.a_int64();
        db.outputs.a_double() = 1.0 + db.inputs.a_double();
        db.outputs.a_float() = 1.0f + db.inputs.a_float();
        db.outputs.a_objectId() = 1 + db.inputs.a_objectId();

        // The namespace separator ":" has special meaning in C++ so it is replaced by "_" when it appears in names
        // Attribute "outputs:unsigned:a_uchar" becomes "outputs.unsigned_a_uchar".
        db.outputs.unsigned_a_uchar() = 1 + db.inputs.unsigned_a_uchar();
        db.outputs.unsigned_a_uint() = 1 + db.inputs.unsigned_a_uint();
        db.outputs.unsigned_a_uint64() = 1 + db.inputs.unsigned_a_uint64();

        // Internally the string type is more akin to a std::string_view, not available until C++17.
        // The data is a pair of (const char*, size_t), but the interface provided through the accessor is
        // castable to a std::string.
        //
        // This code shows the recommended way to use it, extracting inputs into a std::string for manipulation and
        // then assigning outputs from the results. Using the referenced object directly could cause a lot of
        // unnecessary flatcache allocations. (i.e. avoid auto& outputStringView = db.outputs.a_string())
        std::string outputString(db.inputs.a_string());
        if (outputString.length() > 0)
        {
            auto foundStringAt = outputString.find("hello");
            if (foundStringAt != std::string::npos)
            {
                outputString.replace(foundStringAt, 5, "world");
            }
            db.outputs.a_string() = outputString;
        }
        else
        {
            db.outputs.a_string() = "";
        }


        // The token interface is made available in the database as well, for convenience.
        // By calling "db.stringToToken()" you can look up the token ID of a given string.
        // There is also a symmetrical "db.tokenToString()" for going the other way.
        std::string outputTokenString = db.tokenToString(db.inputs.a_token());
        if (outputTokenString.length() > 0)
        {
            auto foundTokenAt = outputTokenString.find("hello");
            if (foundTokenAt != std::string::npos)
            {
                outputTokenString.replace(foundTokenAt, 5, "world");
                db.outputs.a_token() = db.stringToToken(outputTokenString.c_str());
            }
        }
        else
        {
            db.outputs.a_token() = db.stringToToken("");
        }

        // Path just gets a new child named "Child". There's not requirement that the path point to anything
        // that exists in the scene so any string will work here.
        //
        std::string outputPath = (std::string)db.inputs.a_path();

        // In the implementation the string is manipulated directly, as it does not care if the SdfPath is valid or
        // not. If you want to manipulate it using the pxr::SdfPath API this is how you could do it:
        //
        //     pxr::SdfPath sdfPath{outputPath};
        //     pxr::TfToken childToken{asTfToken(db.stringToToken("/Child"))};
        //     if (sdfPath.IsValid())
        //     {
        //         db.outputs.a_path() = sdfPath.AppendChild(childToken).GetString();
        //     }
        //
        outputPath += "/Child";
        db.outputs.a_path() = outputPath;

        // Drop down to the ABI to find attribute metadata, currently not available through the database
        auto& nodeObj = db.abi_node();
        auto attributeObj = nodeObj.iNode->getAttribute(nodeObj, "inputs:a_bool");
        // The hardcoded metadata keyword is available through the node
        auto uiName = attributeObj.iAttribute->getMetadata(attributeObj, kOgnMetadataUiName);
        std::string expectedUiName{ "Sample Boolean Input" };
        CARB_ASSERT(uiName && (expectedUiName == uiName));

        // Confirm that the piece of metadata that differentiates objectId from regular uint64 is in place
        auto objectIdAttributeObj = nodeObj.iNode->getAttribute(nodeObj, "inputs:a_objectId");
        auto objectIdMetadata = attributeObj.iAttribute->getMetadata(objectIdAttributeObj, kOgnMetadataObjectId);
        CARB_ASSERT(objectIdMetadata);

        return true;
    }
};

// namespaces are closed after the registration macro, to ensure the correct class is registered
REGISTER_OGN_NODE()

} // namespace tutorial
} // namespace core
} // namespace graph
} // 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 FlatCache, 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 consisting of a description and attribute values, both inputs and outputs, that will be used for the test.

The test runs by setting all of the named input attributes to their values, running the compute, 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, these two test values could be specified:

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.

For simple attribute lists an abbreviated version of the syntax can be used, where the inputs and outputs get their fully namespaced names so that there is no need for the “inputs” and “outputs” objects.