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])