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 "DynamicAttributes": {
3 "version": 1,
4 "categories": "tutorials",
5 "scheduling": ["threadsafe"],
6 "description": [
7 "This is a C++ node that exercises the ability to add and remove database attribute",
8 "accessors for dynamic attributes. When the dynamic attribute is added the property will exist",
9 "and be able to get/set the attribute values. When it does not the property will not exist.",
10 "The dynamic attribute names are found in the tokens below. If neither exist then the input",
11 "value is copied to the output directly. If 'firstBit' exists then the 'firstBit'th bit of the input",
12 "is x-ored for the copy. If 'secondBit' exists then the 'secondBit'th bit of the input is x-ored",
13 "for the copy. (Recall bitwise match xor(0,0)=0, xor(0,1)=1, xor(1,0)=1, and xor(1,1)=0.)",
14 "For example, if 'firstBit' is present and set to 1 then the bitmask will be b0010, where bit 1 is set.",
15 "If the input is 7, or b0111, then the xor operation will flip bit 1, yielding b0101, or 5 as the result.",
16 "If on the next run 'secondBit' is also present and set to 2 then its bitmask will be b0100, where bit",
17 "2 is set. The input of 7 (b0111) flips bit 1 because firstBit=1 and flips bit 2 because",
18 "secondBit=2, yielding a final result of 1 (b0001)."
19 ],
20 "uiName": "Tutorial Node: Dynamic Attributes",
21 "tokens": {"firstBit": "inputs:firstBit", "secondBit": "inputs:secondBit", "invert": "inputs:invert"},
22 "inputs": {
23 "value": {
24 "type": "uint",
25 "description": "Original value to be modified."
26 }
27 },
28 "outputs": {
29 "result": {
30 "type": "uint",
31 "description": "Modified value"
32 }
33 }
34 }
35}
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// SPDX-FileCopyrightText: Copyright (c) 2021-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 <omni/graph/core/CppWrappers.h>
11
12#include <OgnTutorialDynamicAttributesDatabase.h>
13
14namespace omni
15{
16namespace graph
17{
18
19using core::AttributeRole;
20
21namespace tutorials
22{
23
24class OgnTutorialDynamicAttributes
25{
26public:
27 static bool compute(OgnTutorialDynamicAttributesDatabase& db)
28 {
29 auto iNode = db.abi_node().iNode;
30
31 // Get a copy of the input so that it can be modified in place
32 uint32_t rawOutput = db.inputs.value();
33
34 // Only run this section of code if the dynamic attribute is present
35 if (iNode->getAttributeExists(db.abi_node(), db.tokenToString(db.tokens.firstBit)))
36 {
37 AttributeObj firstBit = iNode->getAttributeByToken(db.abi_node(), db.tokens.firstBit);
38 // firstBit will invert the bit with its number, if present.
39 const auto firstBitPtr = getDataR<uint32_t>(
40 db.abi_context(), firstBit.iAttribute->getConstAttributeDataHandle(firstBit, db.getInstanceIndex()));
41 if (firstBitPtr)
42 {
43 if (0 <= *firstBitPtr && *firstBitPtr <= 31)
44 {
45 rawOutput ^= 1 << *firstBitPtr;
46 }
47 else
48 {
49 db.logWarning("Could not xor bit %ud. Must be in [0, 31]", *firstBitPtr);
50 }
51 }
52 else
53 {
54 db.logError("Could not retrieve the data for firstBit");
55 }
56 }
57
58 if (iNode->getAttributeExists(db.abi_node(), db.tokenToString(db.tokens.secondBit)))
59 {
60 AttributeObj secondBit = iNode->getAttributeByToken(db.abi_node(), db.tokens.secondBit);
61 // secondBit will invert the bit with its number, if present
62 const auto secondBitPtr = getDataR<uint32_t>(
63 db.abi_context(), secondBit.iAttribute->getConstAttributeDataHandle(secondBit, db.getInstanceIndex()));
64 if (secondBitPtr)
65 {
66 if (0 <= *secondBitPtr && *secondBitPtr <= 31)
67 {
68 rawOutput ^= 1 << *secondBitPtr;
69 }
70 else
71 {
72 db.logWarning("Could not xor bit %ud. Must be in [0, 31]", *secondBitPtr);
73 }
74 }
75 else
76 {
77 db.logError("Could not retrieve the data for secondBit");
78 }
79 }
80
81 if (iNode->getAttributeExists(db.abi_node(), db.tokenToString(db.tokens.invert)))
82 {
83 AttributeObj invert = iNode->getAttributeByToken(db.abi_node(), db.tokens.invert);
84 // invert will invert the bits, if the role is set and the attribute access is correct
85 const auto invertPtr = getDataR<double>(
86 db.abi_context(), invert.iAttribute->getConstAttributeDataHandle(invert, db.getInstanceIndex()));
87 if (invertPtr)
88 {
89 // Verify that the invert attribute has the (random) correct role before applying it
90 if (invert.iAttribute->getResolvedType(invert).role == AttributeRole::eTimeCode)
91 {
92 rawOutput ^= 0xffffffff;
93 }
94 }
95 else
96 {
97 db.logError("Could not retrieve the data for invert");
98 }
99 }
100
101 // Set the modified result onto the output as usual
102 db.outputs.result() = rawOutput;
103
104 return true;
105 }
106};
107
108REGISTER_OGN_NODE()
109
110} // namespace tutorials
111} // namespace graph
112} // namespace omni
OgnTutorialDynamicAttributesPy.py
The py file contains the same algorithm as the C++ node, with only the implementation language being different.
1"""Implementation of the node OgnTutorialDynamicAttributesPy.ogn"""
2
3from contextlib import suppress
4from operator import xor
5
6import omni.graph.core as og
7
8
9class OgnTutorialDynamicAttributesPy:
10 @staticmethod
11 def compute(db) -> bool:
12 """Compute the output based on the input and the presence or absence of dynamic attributes"""
13 raw_output = db.inputs.value
14
15 # The suppression of the AttributeError will just skip this section of code if the dynamic attribute
16 # is not present
17 with suppress(AttributeError):
18 # firstBit will invert the bit with its number, if present.
19 if 0 <= db.inputs.firstBit <= 31:
20 raw_output = xor(raw_output, 2**db.inputs.firstBit)
21 else:
22 db.log_error(f"Could not xor bit {db.inputs.firstBit}. Must be in [0, 31]")
23
24 with suppress(AttributeError):
25 # secondBit will invert the bit with its number, if present
26 if 0 <= db.inputs.secondBit <= 31:
27 raw_output = xor(raw_output, 2**db.inputs.secondBit)
28 else:
29 db.log_error(f"Could not xor bit {db.inputs.secondBit}. Must be in [0, 31]")
30
31 with suppress(AttributeError):
32 # invert will invert the bits, if the role is set and the attribute access is correct
33 _ = db.inputs.invert
34 if (
35 db.role.inputs.invert == og.AttributeRole.TIMECODE
36 and db.attributes.inputs.invert == db.abi_node.get_attribute(db.tokens.invert)
37 ):
38 raw_output = xor(raw_output, 0xFFFFFFFF)
39
40 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:
@classmethod
def create_attribute(obj, *args, **kwargs) -> Optional[og.Attribute]: # noqa: N804, PLE0202, PLC0202
"""Create a new dynamic attribute on the node
This function can be called either from the class or using an instantiated object. The first argument is
positional, being either the class or object. All others are by keyword and optional, defaulting to the value
set in the constructor in the object context and the function defaults in the class context.
Args:
obj: Either cls or self depending on how the function was called
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.REGULAR. If the extended type is
og.ExtendedAttributeType.UNION then this parameter will be a
2-tuple with the second element being a list or comma-separated string of union types
undoable: If True the operation is added to the undo queue, else it is done immediately and forgotten
Returns:
omni.graph.core.Attribute: 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:
@classmethod
def remove_attribute(obj, *args, **kwargs) -> bool: # noqa: N804,PLC0202,PLE0202
"""Removes an existing dynamic attribute from a node.
This function can be called either from the class or using an instantiated object. The first argument is
positional, being either the class or object. All others are by keyword and optional, defaulting to the value
set in the constructor in the object context and the function defaults in the class context.
Args:
obj: Either cls or self depending on how the function was called
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
undoable: If True the operation is added to the undo queue, else it is done immediately and forgotten
Raises:
OmniGraphError: if the attribute was not found or could not be removed
"""
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])