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
 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
{
    "BundleData": {
        "version": 1,
        "categories": "tutorials",
        "description": ["This is a tutorial node. It exercises functionality for access of data within",
                        "bundle attributes."
        ],
        "metadata":
        {
           "uiName": "Tutorial Node: Bundle Data"
        },
        "inputs": {
            "bundle": {
                "type": "bundle",
                "description": ["Bundle whose contents are modified for passing to the output"],
                "metadata": {
                    "uiName": "Input Bundle"
                }
             }
        },
        "outputs": {
            "bundle": {
                "type": "bundle",
                "description": ["This is the bundle with values of known types doubled."]
             }
        }
    }
}

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
  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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
// 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 <OgnTutorialBundleDataDatabase.h>

using omni::graph::core::Type;
using omni::graph::core::NameToken;
using omni::graph::core::BaseDataType;


namespace
{
// The function of this node is to double the values for all bundled attributes
// with integral types, including tuples and arrays.
//
// The parameter is a ogn::RuntimeAttribute<kOgnOutput, ogn::kCpu>, which is the type of data returned when iterating
// over an output bundle on the CPU.
// It contains a description of the attribute within the bundle and access to the attribute's data.
// BundledInput is a similar type, which is what you get when iterating over an input bundle. The main difference
// between the two is the ability to modify the attribute or its data.
template <typename POD>
bool doubleSimple(ogn::RuntimeAttribute<ogn::kOgnOutput, ogn::kCpu>& bundledAttribute)
{
    // When an attribute is cast to the wrong type (e.g. an integer attribute is extracted with a float
    // template parameter on the get<,>() method) a nullptr is returned. That can be used to determine
    // the attribute type. You can also use the bundledAttribute.type() method to access the full type
    // information and select a code path using that.
    auto podValue = bundledAttribute.get<POD>();
    if (podValue)
    {
        *podValue *= 2;
        return true;
    }
    return false;
};
// Array and tuple data has iterator capabilities for easy access to individual elements
template <typename CppType>
bool doubleArray(ogn::RuntimeAttribute<ogn::kOgnOutput, ogn::kCpu>& bundledAttribute)
{
    // Strings and paths look like uint8_t[] but are not, so don't process them
    if ((bundledAttribute.type().role == AttributeRole::eText) || (bundledAttribute.type().role == AttributeRole::ePath))
    {
        return false;
    }
    auto arrayValue = bundledAttribute.get<CppType>();
    if (arrayValue)
    {
        for (auto& arrayElement : *arrayValue)
        {
            arrayElement *= 2;
        }
        return true;
    }
    return false;
};
// Tuple arrays must have nested iteration
template <typename CppType>
bool doubleTupleArray(ogn::RuntimeAttribute<ogn::kOgnOutput, ogn::kCpu>& bundledAttribute)
{
    auto tupleSize = bundledAttribute.type().componentCount;
    auto tupleArrayValue = bundledAttribute.get<CppType>();
    if (tupleArrayValue)
    {
        for (auto& arrayElement : *tupleArrayValue)
        {
            for (size_t i=0; i<tupleSize; ++i)
            {
                arrayElement[i] *= 2;
            }
        }
        return true;
    }
    return false;
};
}

class OgnTutorialBundleData
{
public:
    static bool compute(OgnTutorialBundleDataDatabase& db)
    {
        // Bundle attributes are extracted from the database in the same way as any other attribute.
        // The only difference is that a different interface class is provided, suited to bundle manipulation.
        const auto& inputBundle = db.inputs.bundle();
        auto& outputBundle = db.outputs.bundle();

        for (const auto& bundledAttribute : inputBundle)
        {
            auto podValue = bundledAttribute.get<int>();
        }

        // Copying the entire bundle is more efficient than adding one member at a time. As the output bundle
        // will have the same attributes as the input, even though the values will be different, this is the best
        // approach. If the attribute lists were different then you would copy or create the individual attributes
        // as required.
        outputBundle = inputBundle;

        // Now walk the bundle to look for types to be modified
        for (auto& bundledAttribute : outputBundle)
        {
            // This shows how using a templated function can simplify handling of several different bundled
            // attribute types. The data types for each of the POD attributes is fully explained in the documentation
            // page titled "Attribute Data Types". The list of available POD data types is:
            //
            //      bool
            //      double
            //      float
            //      pxr::GfHalf
            //      int
            //      int64_t
            //      NameToken
            //      uint8_t
            //      uint32_t
            //      uint64_t
            //
            if (doubleSimple<int>(bundledAttribute)) continue;
            if (doubleSimple<int64_t>(bundledAttribute)) continue;
            if (doubleSimple<uint8_t>(bundledAttribute)) continue;
            if (doubleSimple<uint32_t>(bundledAttribute)) continue;
            if (doubleSimple<uint64_t>(bundledAttribute)) continue;

            // Plain ints are the only integral types supporting tuples. Double those here
            if (doubleArray<int[2]>(bundledAttribute)) continue;
            if (doubleArray<int[3]>(bundledAttribute)) continue;
            if (doubleArray<int[4]>(bundledAttribute)) continue;

            // Arrays are looped differently than tupls so they are also handled differently
            if (doubleArray<int[]>(bundledAttribute)) continue;
            if (doubleArray<int64_t[]>(bundledAttribute)) continue;
            if (doubleArray<uint8_t[]>(bundledAttribute)) continue;
            if (doubleArray<uint32_t[]>(bundledAttribute)) continue;
            if (doubleArray<uint64_t[]>(bundledAttribute)) continue;

            // Tuple arrays require a two level iteration
            if (doubleTupleArray<int[][2]>(bundledAttribute)) continue;
            if (doubleTupleArray<int[][3]>(bundledAttribute)) continue;
            if (doubleTupleArray<int[][4]>(bundledAttribute)) continue;

            // Any attributes not passing the above tests are not integral and therefore not handled by this node.
        }
        return true;
    }
};

REGISTER_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 Attribute Data 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
asssert( 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
 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
"""
Implementation of the Python node accessing attributes through the bundle in which they are contained.
"""
import omni.graph.core as og

import numpy as np


# Types recognized by the integer filter
_INTEGER_TYPES = [og.BaseDataType.INT, og.BaseDataType.UINT, og.BaseDataType.INT64, og.BaseDataType.UINT64]


class OgnTutorialBundleDataPy:
    """Exercise the bundled data types through a Python OmniGraph node"""

    @staticmethod
    def compute(db) -> bool:
        """Implements the same algorithm as the C++ node OgnTutorialBundleData.cpp
        As Python is so much more flexible it doubles values on any attribute type that can handle it, unlike
        the C++ node which only operates on integer types
        """

        input_bundle = db.inputs.bundle
        output_bundle = db.outputs.bundle

        # This does a copy of the full bundle contents from the input bundle to the output bundle so that the
        # output data can be modified directly.
        output_bundle.bundle = input_bundle

        # The "attributes" member is a list that can be iterated. The members of the list do not contain real
        # og.Attribute objects, which must always exist, they are wrappers on og.AttributeData objects, which can
        # come and go at runtime.
        for bundled_attribute in output_bundle.attributes:

            type = bundled_attribute.type
            # Only integer types are recognized for this node's operation (doubling all integral values).
            # It does operate on tuples and arrays though so that part does not need to be set.
            # if type.base_type not in _INTEGER_TYPES:
            #     continue

            # This operation does the right thing on all compatible types, unlike the C++ equivalent where it
            # requires special handling for each variation of the data types it can handle.
            if type.base_type == og.BaseDataType.TOKEN:
                if type.array_depth > 0:
                    bundled_attribute.value = [f"{element}{element}" for element in bundled_attribute.value]
                else:
                    bundled_attribute.value = f"{bundled_attribute.value}{bundled_attribute.value}"
            elif type.role in [og.AttributeRole.TEXT, og.AttributeRole.PATH]:
                bundled_attribute.value = f"{bundled_attribute.value}{bundled_attribute.value}"
            else:
                try:
                    bundled_attribute.value = np.multiply(bundled_attribute.value, 2)
                except Exception:
                    db.log_error(f"This node does not handle data of type {type.get_type_name()}")

        return True