Tutorial 16 - Bundle Data

Attribute bundles are a construct that packages up groups of attributes into a single entity that can be passed around the graph. These attributes have all of the same properties as a regular attribute, you just have to go through an extra step to access their values. This node illustrates how to break open a bundle to access and modify values in the bundled attributes.

OgnTutorialBundleData.ogn

The ogn file shows the implementation of a node named “omni.graph.tutorials.BundleData”, which has one input bundle and one output bundle.

 1{
 2    "BundleData": {
 3        "version": 1,
 4        "categories": "tutorials",
 5        "description": ["This is a tutorial node. It exercises functionality for access of data within",
 6                        "bundle attributes."
 7        ],
 8        "metadata":
 9        {
10           "uiName": "Tutorial Node: Bundle Data"
11        },
12        "inputs": {
13            "bundle": {
14                "type": "bundle",
15                "description": ["Bundle whose contents are modified for passing to the output"],
16                "metadata": {
17                    "uiName": "Input Bundle"
18                }
19             }
20        },
21        "outputs": {
22            "bundle": {
23                "type": "bundle",
24                "description": ["This is the bundle with values of known types doubled."]
25             }
26        }
27    }
28}

OgnTutorialBundleData.cpp

The cpp file contains the implementation of the compute method. It accesses any attributes in the bundle that have integral base types and doubles the values of those attributes.

  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 <OgnTutorialBundleDataDatabase.h>
 11
 12using omni::graph::core::BaseDataType;
 13using omni::graph::core::NameToken;
 14using omni::graph::core::Type;
 15
 16
 17namespace
 18{
 19// The function of this node is to double the values for all bundled attributes
 20// with integral types, including tuples and arrays.
 21//
 22// The parameter is a ogn::RuntimeAttribute<kOgnOutput, ogn::kCpu>, which is the type of data returned when iterating
 23// over an output bundle on the CPU.
 24// It contains a description of the attribute within the bundle and access to the attribute's data.
 25// BundledInput is a similar type, which is what you get when iterating over an input bundle. The main difference
 26// between the two is the ability to modify the attribute or its data.
 27template <typename POD>
 28bool doubleSimple(ogn::RuntimeAttribute<ogn::kOgnOutput, ogn::kCpu>& bundledAttribute)
 29{
 30    // When an attribute is cast to the wrong type (e.g. an integer attribute is extracted with a float
 31    // template parameter on the get<,>() method) a nullptr is returned. That can be used to determine
 32    // the attribute type. You can also use the bundledAttribute.type() method to access the full type
 33    // information and select a code path using that.
 34    auto podValue = bundledAttribute.get<POD>();
 35    if (podValue)
 36    {
 37        *podValue *= 2;
 38        return true;
 39    }
 40    return false;
 41};
 42// Array and tuple data has iterator capabilities for easy access to individual elements
 43template <typename CppType>
 44bool doubleArray(ogn::RuntimeAttribute<ogn::kOgnOutput, ogn::kCpu>& bundledAttribute)
 45{
 46    // Strings and paths look like uint8_t[] but are not, so don't process them
 47    if ((bundledAttribute.type().role == AttributeRole::eText) || (bundledAttribute.type().role == AttributeRole::ePath))
 48    {
 49        return false;
 50    }
 51    auto arrayValue = bundledAttribute.get<CppType>();
 52    if (arrayValue)
 53    {
 54        for (auto& arrayElement : *arrayValue)
 55        {
 56            arrayElement *= 2;
 57        }
 58        return true;
 59    }
 60    return false;
 61};
 62// Tuple arrays must have nested iteration
 63template <typename CppType, size_t tupleSize>
 64bool doubleTupleArray(ogn::RuntimeAttribute<ogn::kOgnOutput, ogn::kCpu>& bundledAttribute)
 65{
 66    auto tupleArrayValue = bundledAttribute.get<CppType>();
 67    if (tupleArrayValue)
 68    {
 69        for (auto& arrayElement : *tupleArrayValue)
 70        {
 71            for (size_t i = 0; i < tupleSize; ++i)
 72            {
 73                arrayElement[i] *= 2;
 74            }
 75        }
 76        return true;
 77    }
 78    return false;
 79};
 80}
 81
 82class OgnTutorialBundleData
 83{
 84public:
 85    static bool compute(OgnTutorialBundleDataDatabase& db)
 86    {
 87        // Bundle attributes are extracted from the database in the same way as any other attribute.
 88        // The only difference is that a different interface class is provided, suited to bundle manipulation.
 89        const auto& inputBundle = db.inputs.bundle();
 90        auto& outputBundle = db.outputs.bundle();
 91
 92        // This loop processes all integer typed attributes in the bundle
 93        for (const auto& bundledAttribute : inputBundle)
 94        {
 95            auto podValue = bundledAttribute.get<int>();
 96            if (podValue)
 97            {
 98                // Found an integer typed attribute
 99            }
100        }
101
102        // Copying the entire bundle is more efficient than adding one member at a time. As the output bundle
103        // will have the same attributes as the input, even though the values will be different, this is the best
104        // approach. If the attribute lists were different then you would copy or create the individual attributes
105        // as required.
106        outputBundle = inputBundle;
107
108        // Now walk the bundle to look for types to be modified
109        for (auto& bundledAttribute : outputBundle)
110        {
111            // This shows how using a templated function can simplify handling of several different bundled
112            // attribute types. The data types for each of the POD attributes is fully explained in the documentation
113            // page titled "Attribute Data Types". The list of available POD data types is:
114            //
115            //      bool
116            //      double
117            //      float
118            //      pxr::GfHalf
119            //      int
120            //      int64_t
121            //      NameToken
122            //      uint8_t
123            //      uint32_t
124            //      uint64_t
125            //
126            if (doubleSimple<int>(bundledAttribute))
127                continue;
128            if (doubleSimple<int64_t>(bundledAttribute))
129                continue;
130            if (doubleSimple<uint8_t>(bundledAttribute))
131                continue;
132            if (doubleSimple<uint32_t>(bundledAttribute))
133                continue;
134            if (doubleSimple<uint64_t>(bundledAttribute))
135                continue;
136
137            // Plain ints are the only integral types supporting tuples. Double those here
138            if (doubleArray<int[2]>(bundledAttribute))
139                continue;
140            if (doubleArray<int[3]>(bundledAttribute))
141                continue;
142            if (doubleArray<int[4]>(bundledAttribute))
143                continue;
144
145            // Arrays are looped differently than tuples so they are also handled differently
146            if (doubleArray<int[]>(bundledAttribute))
147                continue;
148            if (doubleArray<int64_t[]>(bundledAttribute))
149                continue;
150            if (doubleArray<uint8_t[]>(bundledAttribute))
151                continue;
152            if (doubleArray<uint32_t[]>(bundledAttribute))
153                continue;
154            if (doubleArray<uint64_t[]>(bundledAttribute))
155                continue;
156
157            // Tuple arrays require a two level iteration
158            if (doubleTupleArray<int[][2], 2>(bundledAttribute))
159                continue;
160            if (doubleTupleArray<int[][3], 3>(bundledAttribute))
161                continue;
162            if (doubleTupleArray<int[][4], 4>(bundledAttribute))
163                continue;
164
165            // Any attributes not passing the above tests are not integral and therefore not handled by this node.
166        }
167        return true;
168    }
169};
170
171REGISTER_OGN_NODE()

Bundled Attribute Data Manipulation Methods

These are the methods for accessing the data that the bundled attributes encapsulate. In regular attributes the code generated from the .ogn file provides accessors with predetermined data types. The data types of attributes within bundles are unknown until compute time so it is up to the node writer to explicitly cast to the correct data type.

Extracting Bundled Attribute Data - Simple Types

For reference, simple types, tuple types, array types, tuple array types, and role types are all described in omni.graph.docs.ogn_attribute_types. However, unlike normal attributes the bundled attributes are always accessed as their raw native data types. For example instead of pxr::GfVec3f you will access with float[3], which can always be cast to the explicit types if desired.

Note

One exception to the type casting is tokens. In normal attributes you retrieve tokens as NameToken. Due to certain compiler restrictions the bundled attributes will be retrieved as the helper type OgnToken, which is castable to NameToken for subsequent use.

// As the attribute data types are only known at runtime you must perform a type-specific cast
// to get the data out in its native form.
const auto& inputBundle = db.inputs.bundle();
// Note the "const" here, to ensure we are not inadvertently modifying the input data.
const auto weight = inputBundle.attributeByName(weightToken);
const float* weightValue = weight.value<float>();

// nullptr return means the data is not of the requested type
assert( nullptr == weight.value<int>() );

Extracting Bundled Attribute Data - Tuple Types

// The tuple data types can be accessed in exactly the same way as simple data types, with the proper cast.
const auto& inputBundle = db.inputs.bundle();
const auto weight3 = inputBundle.attributeByName(weight3Token);
const auto weight3Value = weight3.value<float[3]>();
// type of weight3Value == const float[3]*

// If you have a preferred library for manipulating complex types you can cast to them if they are compatible.
static_assert( std::is_convertible(pxr::GfVec3f, float[3]) );
const pxr::GfVec3f* usdWeight = reinterpret_cast<const pxr::GfVec3f*>(weight3Value);

Extracting Bundled Attribute Data - Array Types

// As with tuple types, the array types are extracted directly with the native array cast
const auto& inputBundle = db.inputs.bundle();
const auto weights = inputBundle.attributeByName(weightsToken);
const auto weightsValue = weights.value<float[]>();
// type == const float[]*

auto& outputBundle = db.outputs.bundle();
// As this is an output, the const is omitted so that the data can be modified
auto nWeights = outputBundle.attributeByName(nWeightsToken);
// As with regular attributes, bundled array outputs must be resized to allocate space before filling them.
// These array types also have the normal array capabilities, with a size() method and range-based for loops.
nWeights.resize( weights.size() );
size_t index = 0;
for (const auto& weightValue : *weightsValue)
{
    nWeights[index++] = weightValue / 256.0f;
}

Extracting Bundled Attribute Data - Tuple Array Types

// Tuple-arrays behave as you would expect, using the native tuple-array as the cast type
const auto& inputBundle = db.inputs.bundle();
const auto weights3 = inputBundle.attributeByName(weights3Token);
const auto weights3Value = weights.value<float[][3]>();
// type == const float[][3]*

OgnTutorialBundleDataPy.py

This is a Python version of the above C++ node with exactly the same set of attributes and a similar algorithm. The main difference is that for the Python version the type definitions are much more flexible so the algorithm can be applied to every type of bundled attribute with minimal code. (The .ogn file is omitted for brevity, being identical to the previous one save for the addition of a "language": "python" property.)

 1"""
 2Implementation of the Python node accessing attributes through the bundle in which they are contained.
 3"""
 4
 5import numpy as np
 6import omni.graph.core as og
 7
 8# Types recognized by the integer filter
 9_INTEGER_TYPES = [og.BaseDataType.INT, og.BaseDataType.UINT, og.BaseDataType.INT64, og.BaseDataType.UINT64]
10
11
12class OgnTutorialBundleDataPy:
13    """Exercise the bundled data types through a Python OmniGraph node"""
14
15    @staticmethod
16    def compute(db) -> bool:
17        """Implements the same algorithm as the C++ node OgnTutorialBundleData.cpp
18        As Python is so much more flexible it doubles values on any attribute type that can handle it, unlike
19        the C++ node which only operates on integer types
20        """
21
22        input_bundle = db.inputs.bundle
23        output_bundle = db.outputs.bundle
24
25        # This does a copy of the full bundle contents from the input bundle to the output bundle so that the
26        # output data can be modified directly.
27        output_bundle.bundle = input_bundle
28
29        # The "attributes" member is a list that can be iterated. The members of the list do not contain real
30        # og.Attribute objects, which must always exist, they are wrappers on og.AttributeData objects, which can
31        # come and go at runtime.
32        for bundled_attribute in output_bundle.attributes:
33
34            attribute_type = bundled_attribute.type
35            # Only integer types are recognized for this node's operation (doubling all integral values).
36            # It does operate on tuples and arrays though so that part does not need to be set.
37            # if attribute_type.base_type not in _INTEGER_TYPES:
38            #     continue
39
40            # This operation does the right thing on all compatible types, unlike the C++ equivalent where it
41            # requires special handling for each variation of the data types it can handle.
42            if attribute_type.base_type == og.BaseDataType.TOKEN:
43                if attribute_type.array_depth > 0:
44                    bundled_attribute.value = [f"{element}{element}" for element in bundled_attribute.value]
45                else:
46                    bundled_attribute.value = f"{bundled_attribute.value}{bundled_attribute.value}"
47            elif attribute_type.role in [og.AttributeRole.TEXT, og.AttributeRole.PATH]:
48                bundled_attribute.value = f"{bundled_attribute.value}{bundled_attribute.value}"
49            else:
50                try:
51                    bundled_attribute.value = np.multiply(bundled_attribute.value, 2)
52                except TypeError:
53                    db.log_error(f"This node does not handle data of type {attribute_type.get_type_name()}")
54
55        return True