Tutorial 19 - Extended Attribute Types¶
Extended attribute types are so-named because they extend the types of data an attribute can accept from one type to several types. Extended attributes come in two flavours. The _any_ type is the most flexible. It allows a connection with any other attribute type:
"inputs": {
"myAnyAttribute": {
"description": "Accepts an incoming connection from any type of attribute",
"type": "any",
}
}
The union type, represented as an array of type names, allows a connection from a limited subset of attribute types. Here’s one that can connect to attributes of type _float[3]_ and _double[3]_:
"inputs": {
"myUnionAttribute": {
"description": "Accepts an incoming connection from attributes with a vector of a 3-tuple of numbers",
"type": ["float[3]", "double[3]"],
}
}
Note
“union” is not an actual type name, as the type names are specified by a list. It is just the nomenclature used for the set of all attributes that can be specified in this way. More details about union types can be found in Attribute Data Types.
As you will see in the code examples, the value extracted from the database for such attributes has to be checked for the actual resolved data type. Until an extended attribute is connected its data type will be unresolved and it will not have a value. For this reason _”default”_ values are not allowed on extended attributes.
OgnTutorialExtendedTypes.ogn¶
The ogn file shows the implementation of a node named “omni.graph.tutorials.ExtendedTypes”, which has inputs and outputs with the extended attribute types.
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 | { "ExtendedTypes": { "version": 1, "categories": "tutorials", "description": ["This is a tutorial node. It exercises functionality for the manipulation of the extended", "attribute types." ], "uiName": "Tutorial Node: Extended Attribute Types", "inputs": { "floatOrToken": { "$comment": [ "Support for a union of types is noted by putting a list into the attribute type.", "Each element of the list must be a legal attribute type from the supported type list." ], "type": ["float", "token"], "description": "Attribute that can either be a float value or a token value", "uiName": "Float Or Token", "unvalidated": true }, "toNegate": { "$comment": "An example showing that array and tuple types are also legal members of a union.", "type": ["bool[]", "float[]"], "description": "Attribute that can either be an array of booleans or an array of floats", "uiName": "To Negate", "unvalidated": true }, "tuple": { "$comment": "Tuple types are also allowed, implemented as 'any' to show similarities", "type": "any", "description": "Variable size/type tuple values", "uiName": "Tuple Values", "unvalidated": true }, "flexible": { "$comment": "You don't even have to have the same shape of data in a union", "type": ["float[3][]", "token"], "description": "Flexible data type input", "uiName": "Flexible Values", "unvalidated": true } }, "outputs": { "doubledResult": { "type": "any", "description": ["If the input 'simpleInput' is a float this is 2x the value.", "If it is a token this contains the input token repeated twice." ], "uiName": "Doubled Input Value", "unvalidated": true }, "negatedResult": { "type": ["bool[]", "float[]"], "description": "Result of negating the data from the 'toNegate' input", "uiName": "Negated Array Values", "unvalidated": true }, "tuple": { "type": "any", "description": "Negated values of the tuple input", "uiName": "Negative Tuple Values", "unvalidated": true }, "flexible": { "type": ["float[3][]", "token"], "description": "Flexible data type output", "uiName": "Inverted Flexible Values", "unvalidated": true } } } } |
OgnTutorialExtendedTypes.cpp¶
The cpp file contains the implementation of the compute method. It illustrates how to determine and set the data types on extended attribute types.
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 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 | // Copyright (c) 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 <OgnTutorialExtendedTypesDatabase.h> #include <algorithm> // // Attributes whose data types resolve at runtime ("any" or "union" types) are resolved by having connections made // to them of a resolved type. Say you have a chain of A->B->C where B has inputs and outputs of these types. The // connection from A->B will determine the type of data at B's input and the connection B->C will determine the type // of data at B's output (assuming A's outputs and C's inputs are well-defined types). // // For this reason it is the node's responsibility to verify the type resolution of the attributes as part of the // compute method. Any unresolved types (db.Xputs.attrName().resolved() == false) that are required by the compute // should result in a warning and compute failure. Any attributes resolved to incompatible types, for example an input // that resolves to a string where a number is needed, should also result in a warning and compute failure. // // It is up to the node to decide how flexible the resolution requirements are to be. In the string/number case above // the node may choose to parse the string as a number instead of failing, or using the length of the string as the // input number. The only requirement from OmniGraph is that the node handle all of the resolution types it has // claimed it will handle in the .ogn file. "any" attributes must handle all data types, even if some types result in // warnings or errors. "union" attributes must handle all types specified in the union. // class OgnTutorialExtendedTypes { public: static bool compute(OgnTutorialExtendedTypesDatabase& db) { bool computedOne = false; auto typeWarning = [&](const char* message, const Type& type1, const Type& type2) { db.logWarning("%s (%s -> %s)", message, type1.getOgnTypeName().c_str(), type2.getOgnTypeName().c_str()); }; auto typeError = [&](const char* message, const Type& type1, const Type& type2) { db.logError("%s (%s -> %s)", message, type1.getOgnTypeName().c_str(), type2.getOgnTypeName().c_str()); }; auto computeSimpleValues = [&]() { // ==================================================================================================== // Compute for the union types that resolve to simple values. // Accepted value types are floats and tokens. As these were the only types specified in the union definition // the node does not have to worry about other numeric types, such as int or double. // The node can decide what the meaning of an attempt to compute with unresolved types is. // For this particular node they are treated as silent success. const auto& floatOrToken = db.inputs.floatOrToken(); auto& doubledResult = db.outputs.doubledResult(); if (floatOrToken.resolved() && doubledResult.resolved()) { // Check for an exact type match for the input and output if (floatOrToken.type() != doubledResult.type()) { // Mismatched types are possible, and result in no compute typeWarning("Simple resolved types do not match", floatOrToken.type(), doubledResult.type()); return false; } // When extracting extended types the templated get<> method returns an object that contains the cast data. // It can be cast to a boolean for quick checks for matching types. // // Note: The single "=" in these if statements is intentional. It facilitates one-line set-and-test of the // typed values. // if (auto floatValue = floatOrToken.get<float>()) { // Once the existence of the cast type is verified it can be dereferenced to get at the raw data, // whose types are described in the tutorial on bundled data. if (auto doubledValue = doubledResult.get<float>()) { *doubledValue = *floatValue * 2.0f; } else { // This could be an assert because it should never happen. The types were confirmed above to match, // so they should have cast to the same types without incident. typeError("Simple types were matched as bool then failed to cast properly", floatOrToken.type(), doubledResult.type()); return false; } } else if (auto tokenValue = floatOrToken.get<OgnToken>()) { if (auto doubledValue = doubledResult.get<OgnToken>()) { std::string inputString{ db.tokenToString(*tokenValue) }; inputString += inputString; *doubledValue = db.stringToToken(inputString.c_str()); } else { // This could be an assert because it should never happen. The types were confirmed above to match, // so they should have cast to the same types without incident. typeError("Simple types were matched as token then failed to cast properly", floatOrToken.type(), doubledResult.type()); return false; } } else { // As Union types are supposed to restrict the data types being passed in to the declared types // any unrecognized types are an error, not a warning. typeError("Simple types resolved to unknown types", floatOrToken.type(), doubledResult.type()); return false; } } else { // Unresolved types are reasonable, resulting in no compute return true; } return true; }; auto computeArrayValues = [&]() { // ==================================================================================================== // Compute for the union types that resolve to arrays. // Accepted value types are arrays of bool or arrays of float, which are extracted as interfaces to // those values so that resizing can happen transparently through the flatcache. // // These interfaces are similar to what you've seen in regular array attributes - they support resize(), // operator[], and range-based for loops. // const auto& toNegate = db.inputs.toNegate(); auto& negatedResult = db.outputs.negatedResult(); if (toNegate.resolved() && negatedResult.resolved()) { // Check for an exact type match for the input and output if (toNegate.type() != negatedResult.type()) { // Mismatched types are possible, and result in no compute typeWarning("Array resolved types do not match", toNegate.type(), negatedResult.type()); return false; } // Extended types can be any legal attribute type. Here the types in the extended attribute can be either // an array of booleans or an array of integers. if (auto boolArray = toNegate.get<bool[]>()) { auto valueAsBoolArray = negatedResult.get<bool[]>(); if (valueAsBoolArray) { valueAsBoolArray.resize( boolArray->size() ); size_t index{ 0 }; for (auto& value : *boolArray) { (*valueAsBoolArray)[index++] = ! value; } } else { // This could be an assert because it should never happen. The types were confirmed above to match, // so they should have cast to the same types without incident. typeError("Array types were matched as bool[] then failed to cast properly", toNegate.type(), negatedResult.type()); return false; } } else if (auto floatArray = toNegate.get<float[]>()) { auto valueAsFloatArray = negatedResult.get<float[]>(); if (valueAsFloatArray) { valueAsFloatArray.resize( floatArray->size() ); size_t index{ 0 }; for (auto& value : *floatArray) { (*valueAsFloatArray)[index++] = - value; } } else { // This could be an assert because it should never happen. The types were confirmed above to match, // so they should have cast to the same types without incident. typeError("Array types were matched as float[] then failed to cast properly", toNegate.type(), negatedResult.type()); return false; } } else { // As Union types are supposed to restrict the data types being passed in to the declared types // any unrecognized types are an error, not a warning. typeError("Array type not recognized", toNegate.type(), negatedResult.type()); return false; } } else { // Unresolved types are reasonable, resulting in no compute return true; } return true; }; auto computeTupleValues = [&]() { // ==================================================================================================== // Compute for the "any" types that only handle tuple values. In practice you'd only use "any" when the // type of data you handle is unrestricted. This is more an illustration to show how in practical use the // two types of attribute are accessed exactly the same way, the only difference is restrictions that the // OmniGraph system will put on potential connections. // // For simplicity this node will treat unrecognized type as a warning with success. // Full commentary and error checking is elided as it will be the same as for the above examples. // The algorithm for tuple values is a component-wise negation. const auto& tupleInput = db.inputs.tuple(); auto& tupleOutput = db.outputs.tuple(); if (tupleInput.resolved() && tupleOutput.resolved()) { if (tupleInput.type() != tupleOutput.type()) { typeWarning("Tuple resolved types do not match", tupleInput.type(), tupleOutput.type()); return false; } // This node will only recognize the float[3] and int[2] cases, to illustrate that tuple count and // base type are both flexible. if (auto float3Input = tupleInput.get<float[3]>()) { if (auto float3Output = tupleOutput.get<float[3]>()) { (*float3Output)[0] = -(*float3Input)[0]; (*float3Output)[1] = -(*float3Input)[1]; (*float3Output)[2] = -(*float3Input)[2]; } } else if (auto int2Input = tupleInput.get<int[2]>()) { if (auto int2Output = tupleOutput.get<int[2]>()) { (*int2Output)[0] = -(*int2Input)[0]; (*int2Output)[1] = -(*int2Input)[1]; } } else { // As "any" types are not restricted in their data types but this node is only handling two of // them an unrecognized type is just unimplemented code. typeWarning("Unimplemented type combination", tupleInput.type(), tupleOutput.type()); return true; } } else { // Unresolved types are reasonable, resulting in no compute return true; } return true; }; auto computeFlexibleValues = [&]() { // ==================================================================================================== // Complex union type that handles both simple values and an array of tuples. It illustrates how the // data types in a union do not have to be related in any way. // // Full commentary and error checking is elided as it will be the same as for the above examples. // The algorithm for tuple array values is to negate everything in the float3 array values, and to reverse // the string for string values. const auto& flexibleInput = db.inputs.flexible(); auto& flexibleOutput = db.outputs.flexible(); if (flexibleInput.resolved() && flexibleOutput.resolved()) { if (flexibleInput.type() != flexibleOutput.type()) { typeWarning("Flexible resolved types do not match", flexibleInput.type(), flexibleOutput.type()); return false; } // Arrays of tuples are handled with the same interface as with normal attributes. if (auto float3ArrayInput = flexibleInput.get<float[][3]>()) { if (auto float3ArrayOutput = flexibleOutput.get<float[][3]>()) { float3ArrayOutput.resize( float3ArrayInput.size() ); size_t index{ 0 }; for (auto& value : *float3ArrayInput) { (*float3ArrayOutput)[index][0] = - value[0]; (*float3ArrayOutput)[index][1] = - value[1]; (*float3ArrayOutput)[index][2] = - value[2]; index++; } } } else if (auto tokenInput = flexibleInput.get<OgnToken>()) { if (auto tokenOutput = flexibleOutput.get<OgnToken>()) { std::string toReverse{ db.tokenToString(*tokenInput) }; std::reverse( toReverse.begin(), toReverse.end() ); *tokenOutput = db.stringToToken(toReverse.c_str()); } } else { typeError("Unrecognized type combination", flexibleInput.type(), flexibleOutput.type()); return false; } } else { // Unresolved types are reasonable, resulting in no compute return true; } return true; }; // This approach lets either section fail while still computing the other. computedOne = computeSimpleValues(); computedOne = computeArrayValues() || computedOne; computedOne = computeTupleValues() || computedOne; computedOne = computeFlexibleValues() || computedOne; if (! computedOne) { db.logWarning("None of the inputs had resolved type, resulting in no compute"); } return ! computedOne; } static void onConnectionTypeResolve(const NodeObj& nodeObj) { // The attribute types resolve in pairs AttributeObj pairs[][2] { { nodeObj.iNode->getAttributeByToken(nodeObj, inputs::floatOrToken.token()), nodeObj.iNode->getAttributeByToken(nodeObj, outputs::doubledResult.token()) }, { nodeObj.iNode->getAttributeByToken(nodeObj, inputs::toNegate.token()), nodeObj.iNode->getAttributeByToken(nodeObj, outputs::negatedResult.token()) }, { nodeObj.iNode->getAttributeByToken(nodeObj, inputs::tuple.token()), nodeObj.iNode->getAttributeByToken(nodeObj, outputs::tuple.token()) }, { nodeObj.iNode->getAttributeByToken(nodeObj, inputs::flexible.token()), nodeObj.iNode->getAttributeByToken(nodeObj, outputs::flexible.token()) } }; for (auto& pair : pairs) { nodeObj.iNode->resolveCoupledAttributes(nodeObj, &pair[0], 2); } } }; REGISTER_OGN_NODE() |
Information on the raw types extracted from the extended type values can be seen in Tutorial 16 - Bundle Data.
OgnTutorialExtendedTypesPy.py¶
This is a Python version of the above C++ node with exactly the same set of attributes and the same algorithm. It
shows the parallels between manipulating extended attribute types in both languages. (The .ogn file is omitted for
brevity, being identical to the previous one save for the addition of a "language": "python"
property.
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 | """ Implementation of the Python node accessing attributes whose type is determined at runtime. This class exercises access to the DataModel through the generated database class for all simple data types. """ import omni.graph.core as og # Hardcode each of the expected types for easy comparison FLOAT_TYPE = og.Type(og.BaseDataType.FLOAT) TOKEN_TYPE = og.Type(og.BaseDataType.TOKEN) BOOL_ARRAY_TYPE = og.Type(og.BaseDataType.BOOL, array_depth=1) FLOAT_ARRAY_TYPE = og.Type(og.BaseDataType.FLOAT, array_depth=1) FLOAT3_TYPE = og.Type(og.BaseDataType.FLOAT, tuple_count=3) INT2_TYPE = og.Type(og.BaseDataType.INT, tuple_count=2) FLOAT3_ARRAY_TYPE = og.Type(og.BaseDataType.FLOAT, tuple_count=3, array_depth=1) class OgnTutorialExtendedTypesPy: """Exercise the runtime data types through a Python OmniGraph node""" @staticmethod def compute(db) -> bool: """Implements the same algorithm as the C++ node OgnTutorialExtendedTypes.cpp. It follows the same code pattern for easier comparison, though in practice you would probably code Python nodes differently from C++ nodes to take advantage of the strengths of each language. """ def __compare_resolved_types(input_attribute, output_attribute) -> og.Type: """Returns the resolved type if they are the same, outputs a warning and returns None otherwise""" resolved_input_type = input_attribute.type resolved_output_type = output_attribute.type if resolved_input_type != resolved_output_type: db.log_warn(f"Resolved types do not match {resolved_input_type} -> {resolved_output_type}") return None return resolved_input_type if resolved_input_type.base_type != og.BaseDataType.UNKNOWN else None # --------------------------------------------------------------------------------------------------- def _compute_simple_values(): """Perform the first algorithm on the simple input data types""" # Unlike C++ code the Python types are flexible so you must check the data types to do the right thing. # This works out better when the operation is the same as you don't even have to check the data type. In # this case the "doubling" operation is slightly different for floats and tokens. resolved_type = __compare_resolved_types(db.inputs.floatOrToken, db.outputs.doubledResult) if resolved_type == FLOAT_TYPE: db.outputs.doubledResult.value = db.inputs.floatOrToken.value * 2.0 elif resolved_type == TOKEN_TYPE: db.outputs.doubledResult.value = db.inputs.floatOrToken.value + db.inputs.floatOrToken.value # A Pythonic way to do the same thing by just applying an operation and checking for compatibility is: # try: # db.outputs.doubledResult = db.inputs.floatOrToken * 2.0 # except TypeError: # # Gets in here for token types since multiplying string by float is not legal # db.outputs.doubledResult = db.inputs.floatOrToken + db.inputs.floatOrToken return True # --------------------------------------------------------------------------------------------------- def _compute_array_values(): """Perform the second algorithm on the array input data types""" resolved_type = __compare_resolved_types(db.inputs.toNegate, db.outputs.negatedResult) if resolved_type == BOOL_ARRAY_TYPE: db.outputs.negatedResult.value = [not value for value in db.inputs.toNegate.value] elif resolved_type == FLOAT_ARRAY_TYPE: db.outputs.negatedResult.value = [-value for value in db.inputs.toNegate.value] return True # --------------------------------------------------------------------------------------------------- def _compute_tuple_values(): """Perform the third algorithm on the 'any' data types""" resolved_type = __compare_resolved_types(db.inputs.tuple, db.outputs.tuple) # Notice how, since the operation is applied the same for both recognized types, the # same code can handle both of them. if resolved_type in (FLOAT3_TYPE, INT2_TYPE): db.outputs.tuple.value = tuple(-x for x in db.inputs.tuple.value) # An unresolved type is a temporary state and okay, resolving to unsupported types means the graph is in # an unsupported configuration that needs to be corrected. elif resolved_type is not None: type_name = resolved_type.get_type_name() db.log_error(f"Only float[3] and int[2] types are supported by this node, not {type_name}") return False return True # --------------------------------------------------------------------------------------------------- def _compute_flexible_values(): """Perform the fourth algorithm on the multi-shape data types""" resolved_type = __compare_resolved_types(db.inputs.flexible, db.outputs.flexible) if resolved_type == FLOAT3_ARRAY_TYPE: db.outputs.flexible.value = [(-x, -y, -z) for (x, y, z) in db.inputs.flexible.value] elif resolved_type == TOKEN_TYPE: db.outputs.flexible.value = db.inputs.flexible.value[::-1] return True # --------------------------------------------------------------------------------------------------- compute_success = _compute_simple_values() compute_success = _compute_array_values() and compute_success compute_success = _compute_tuple_values() and compute_success compute_success = _compute_flexible_values() and compute_success # --------------------------------------------------------------------------------------------------- # As Python has a much more flexible typing system it can do things in a few lines that require a lot # more in C++. One such example is the ability to add two arbitrary data types. Here is an example of # how, using "any" type inputs "a", and "b", with an "any" type output "result" you can generically # add two elements without explicitly checking the type, failing only when Python cannot support # the operation. # # try: # db.outputs.result = db.inputs.a + db.inputs.b # return True # except TypeError: # a_type = inputs.a.type().get_type_name() # b_type = inputs.b.type().get_type_name() # db.log_error(f"Cannot add attributes of type {a_type} and {b_type}") # return False return True @staticmethod def on_connection_type_resolve(node) -> None: # There are 4 sets of type-coupled attributes in this node, meaning that the base_type of the attributes # must be the same for the node to function as designed. # 1. floatOrToken <-> doubledResult # 2. toNegate <-> negatedResult # 3. tuple <-> tuple # 4. flexible <-> flexible # # The following code uses a helper function to resolve the attribute types of the coupled pairs. Note that # without this logic a chain of extended-attribute connections may result in a non-functional graph, due to # the requirement that types be resolved before graph evaluation, and the ambiguity of the graph without knowing # how the types are related. og.resolve_fully_coupled( [node.get_attribute("inputs:floatOrToken"), node.get_attribute("outputs:doubledResult")] ) og.resolve_fully_coupled([node.get_attribute("inputs:toNegate"), node.get_attribute("outputs:negatedResult")]) og.resolve_fully_coupled([node.get_attribute("inputs:tuple"), node.get_attribute("outputs:tuple")]) og.resolve_fully_coupled([node.get_attribute("inputs:flexible"), node.get_attribute("outputs:flexible")]) |