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