Tutorial 25 - Dynamic Attributes¶
A dynamic attribute is like any other attribute on a node, except that it is added at runtime rather than being part
of the .ogn specification. These are added through the ABI function INode::createAttribute
and removed from the
node through the ABI function INode::removeAttribute
.
Once a dynamic attribute is added it can be accessed through the same ABI and script functions as regular attributes.
Warning
While the Python node database is able to handle the dynamic attributes through the same interface as regular
attributes (e.g. db.inputs.dynAttr
), the C++ node database is not yet similarly flexible and access to
dynamic attribute values must be done directly through the ABI calls.
OgnTutorialDynamicAttributes.ogn¶
The ogn file shows the implementation of a node named “omni.graph.tutorials.DynamicAttributes”, which has a simple float input and output.
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 | { "DynamicAttributes": { "version": 1, "categories": "tutorials", "description": [ "This is a Python node that exercises the ability to add and remove database attribute", "accessors for dynamic attributes. When the dynamic attribute is added the property will exist", "and be able to get/set the attribute values. When it does not the property will not exist.", "The dynamic attribute names are found in the tokens below. If neither exist then the input", "value is copied to the output directly. If 'firstBit' exists then the 'firstBit'th bit of the input", "is x-ored for the copy. If 'secondBit' exists then the 'secondBit'th bit of the input is x-ored", "for the copy. (Recall bitwise match xor(0,0)=0, xor(0,1)=1, xor(1,0)=1, and xor(1,1)=0.)", "For example, if 'firstBit' is present and set to 1 then the bitmask will be b0010, where bit 1 is set.", "If the input is 7, or b0111, then the xor operation will flip bit 1, yielding b0101, or 5 as the result.", "If on the next run 'secondBit' is also present and set to 2 then its bitmask will be b0100, where bit", "2 is set. The input of 7 (b0111) flips bit 1 because firstBit=1 and flips bit 2 because", "secondBit=2, yielding a final result of 1 (b0001)." ], "uiName": "Tutorial Node: Dynamic Attributes", "tokens": {"firstBit": "inputs:firstBit", "secondBit": "inputs:secondBit", "invert": "inputs:invert"}, "inputs": { "value": { "type": "uint", "description": "Original value to be modified." } }, "outputs": { "result": { "type": "uint", "description": "Modified value" } } } } |
OgnTutorialDynamicAttributes.cpp¶
The cpp file contains the implementation of the compute method. It passes the input directly to the output unless it finds a dynamic attribute named “multiplier”, in which case it multiplies by that amount instead.
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 | // 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 <OgnTutorialDynamicAttributesDatabase.h> #include <omni/graph/core/CppWrappers.h> namespace omni { namespace graph { using core::AttributeRole; namespace tutorials { class OgnTutorialDynamicAttributes { public: static bool compute(OgnTutorialDynamicAttributesDatabase& db) { auto iNode = db.abi_node().iNode; // Get a copy of the input so that it can be modified in place uint32_t rawOutput = db.inputs.value(); // Only run this section of code if the dynamic attribute is present if (iNode->getAttributeExists(db.abi_node(), db.tokenToString(db.tokens.firstBit))) { AttributeObj firstBit = iNode->getAttributeByToken(db.abi_node(), db.tokens.firstBit); // firstBit will invert the bit with its number, if present. const auto firstBitPtr = getDataR<uint32_t>(db.abi_context(), firstBit.iAttribute->getConstAttributeDataHandle(firstBit)); if (firstBitPtr) { if( 0 <= *firstBitPtr && *firstBitPtr <= 31) { rawOutput ^= 1 << *firstBitPtr; } else { db.logWarning("Could not xor bit %ud. Must be in [0, 31]", *firstBitPtr); } } else { db.logError("Could not retrieve the data for firstBit"); } } if (iNode->getAttributeExists(db.abi_node(), db.tokenToString(db.tokens.secondBit))) { AttributeObj secondBit = iNode->getAttributeByToken(db.abi_node(), db.tokens.secondBit); // secondBit will invert the bit with its number, if present const auto secondBitPtr = getDataR<uint32_t>(db.abi_context(), secondBit.iAttribute->getConstAttributeDataHandle(secondBit)); if (secondBitPtr) { if( 0 <= *secondBitPtr && *secondBitPtr <= 31) { rawOutput ^= 1 << *secondBitPtr; } else { db.logWarning("Could not xor bit %ud. Must be in [0, 31]", *secondBitPtr); } } else { db.logError("Could not retrieve the data for secondBit"); } } if (iNode->getAttributeExists(db.abi_node(), db.tokenToString(db.tokens.invert))) { AttributeObj invert = iNode->getAttributeByToken(db.abi_node(), db.tokens.invert); // invert will invert the bits, if the role is set and the attribute access is correct const auto invertPtr = getDataR<double>(db.abi_context(), invert.iAttribute->getConstAttributeDataHandle(invert)); if (invertPtr) { // Verify that the invert attribute has the (random) correct role before applying it if (invert.iAttribute->getResolvedType(invert).role == AttributeRole::eTimeCode) { rawOutput ^= 0xffffffff; } } else { db.logError("Could not retrieve the data for invert"); } } // Set the modified result onto the output as usual db.outputs.result() = rawOutput; return true; } }; REGISTER_OGN_NODE() } // namespace tutorials } // namespace graph } // namespace omni |
OgnTutorialDynamicAttributesPy.py¶
The py file contains the same algorithm as the C++ node, with only the implementation language being different.
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 | """Implementation of the node OgnTutorialDynamicAttributesPy.ogn""" from contextlib import suppress from operator import xor import omni.graph.core as og class OgnTutorialDynamicAttributesPy: @staticmethod def compute(db) -> bool: """Compute the output based on the input and the presence or absence of dynamic attributes""" raw_output = db.inputs.value # The suppression of the AttributeError will just skip this section of code if the dynamic attribute # is not present with suppress(AttributeError): # firstBit will invert the bit with its number, if present. if 0 <= db.inputs.firstBit <= 31: raw_output = xor(raw_output, 2**db.inputs.firstBit) else: db.log_error(f"Could not xor bit {db.inputs.firstBit}. Must be in [0, 31]") with suppress(AttributeError): # secondBit will invert the bit with its number, if present if 0 <= db.inputs.secondBit <= 31: raw_output = xor(raw_output, 2**db.inputs.secondBit) else: db.log_error(f"Could not xor bit {db.inputs.secondBit}. Must be in [0, 31]") with suppress(AttributeError): # invert will invert the bits, if the role is set and the attribute access is correct _ = db.inputs.invert if ( db.role.inputs.invert == og.AttributeRole.TIMECODE and db.attributes.inputs.invert == db.abi_node.get_attribute(db.tokens.invert) ): raw_output = xor(raw_output, 0xFFFFFFFF) db.outputs.result = raw_output |
Adding And Removing Dynamic Attributes¶
In addition to the above ABI functions the Python og.Controller
class provides the ability to add and remove
dynamic attributes from a script.
To create a dynamic attribute you would use this function:
@staticmethod
def create_attribute(
node: Node_t,
attr_name: str,
attr_type: AttributeType_t,
attr_port: Optional[og.AttributePortType] = og.AttributePortType.ATTRIBUTE_PORT_TYPE_INPUT,
attr_default: Optional[Any] = None,
attr_extended_type: Optional[ExtendedAttribute_t] = og.ExtendedAttributeType.EXTENDED_ATTR_TYPE_REGULAR,
) -> Optional[og.Attribute]:
"""Create a new dynamic attribute on the node
Args:
node: Node on which to create the attribute (path or og.Node)
attr_name: Name of the new attribute, either with or without the port namespace
attr_type: Type of the new attribute, as an OGN type string or og.Type
attr_port: Port type of the new attribute, default is og.AttributePortType.ATTRIBUTE_PORT_TYPE_INPUT
attr_default: The initial value to set on the attribute, default is None which means use the type's default
attr_extended_type: The extended type of the attribute, default is
og.ExtendedAttributeType.EXTENDED_ATTR_TYPE_REGULAR. If the extended type is
og.ExtendedAttributeType.EXTENDED_ATTR_TYPE_UNION then this parameter will be a
2-tuple with the second element being a list or comma-separated string of union types
Returns:
The newly created attribute, None if there was a problem creating it
"""
For example this is the code to create a float[3] input and a bundle output on an existing node:
import omni.graph.core as og
new_input = og.Controller.create_attribute("/World/MyNode", "newInput", "float[3]")
new_output = og.Controller.create_attribute("/World/MyNode", "newOutput", "bundle", og.AttributePortType.ATTRIBUTE_PORT_TYPE_OUTPUT)
# The proper namespace will be added to the attribute, though you can also be explicit about it
other_input = og.Controller.create_attribute("/World/MyNode", "inputs:otherInput", "float[3]")
When the node is deleted the dynamic attribute will also be deleted, and the attribute will be stored in the USD file. If you want to remove the attribute from the node at any time you would use this function:
@staticmethod
def remove_attribute(attribute: Attribute_t, node: Optional[Node_t] = None) -> bool:
"""Removes an existing dynamic attribute from a node.
Args:
attribute: Reference to the attribute to be removed
node: If the attribute reference is a string the node is used to find the attribute to be removed
Returns:
True if the attribute was successfully removed, else False (including when the attribute wasn't found)
"""
The second optional parameter is only needed when the attribute is passed as a string. When passing an og.Attribute the node is already known, being part of the attribute.
import omni.graph.core as og
new_attr = og.Controller.create_attribute("/World/MyNode", "newInput", "float[3]")
# When passing the attribute the node is not necessary
og.Controller.remove_attribute(new_attr)
# However if you don't have the attribute available you can still use the name, noting that the
# namespace must be present.
# og.Controller.remove_attribute("inputs:newInput", "/World/MyNode")
Adding More Information¶
While the attribute name and type are sufficient to unambiguously create it there is other information you can add that would normally be present in the .ogn file. It’s a good idea to add some of the basic metadata for the UI.
import omni.graph.core as og
new_attr = og.Controller.create_attribute("/World/MyNode", "newInput", "vectorf[3]")
new_attr.set_metadata(og.MetadataKeys.DESCRIPTION, "This is a new input with a vector in it")
new_attr.set_metadata(og.MetadataKeys.UI_NAME, "Input Vector")
While dynamic attributes don’t have default values you can do the equivalent by setting a value as soon as you create the attribute:
import omni.graph.core as og
new_attr = og.Controller.create_attribute("/World/MyNode", "newInput", "vectorf[3]")
og.Controller.set(new_attr, [1.0, 2.0, 3.0])
This default value can also be changed at any time (even when the attribute is already connected):
new_attr.set_default([1.0, 0.0, 0.0])