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.