Tutorial 15 - Bundle Manipulation

Attribute bundles are a construct that packages up groups of attributes into a single entity that can be passed around the graph. Some advantages of a bundle are that they greatly simplify graph connections, only requiring a single connection between nodes rather than dozens or even hundreds, and they do not require static definition of the data they contain so it can change as the evaluation of the nodes dictate. The only disadvantage is that the node writer is responsible for analyzing the contents of the bundle and deciding what to do with them.

OgnTutorialBundles.ogn

The ogn file shows the implementation of a node named “omni.graph.tutorials.BundleManipulation”, which has some bundles as inputs and outputs. It’s called “manipulation” as the focus of this tutorial node is on operations applied directly to the bundle itself, as opposed to on the data on the attributes contained within the bundles. See future tutorials for information on how to deal with that.

 1{
 2    "BundleManipulation": {
 3        "version": 1,
 4        "categories": "tutorials",
 5        "description": ["This is a tutorial node. It exercises functionality for the manipulation of bundle",
 6                        "attribute contents."
 7        ],
 8        "uiName": "Tutorial Node: Bundle Manipulation",
 9        "inputs": {
10            "fullBundle": {
11                "type": "bundle",
12                "description": ["Bundle whose contents are passed to the output in their entirety"],
13                "metadata": {
14                    "uiName": "Full Bundle"
15                }
16             },
17             "filteredBundle": {
18                "type": "bundle",
19                "description": ["Bundle whose contents are filtered before being added to the output"],
20                "uiName": "Filtered Bundle"
21             },
22             "filters": {
23                "type": "token[]",
24                "description": [
25                    "List of filter names to be applied to the filteredBundle. Any filter name",
26                    "appearing in this list will be applied to members of that bundle and only those",
27                    "passing all filters will be added to the output bundle. Legal filter values are",
28                    "'big' (arrays of size > 10), 'x' (attributes whose name contains the letter x), ",
29                    "and 'int' (attributes whose base type is integer)."
30                ],
31                "default": []
32             }
33        },
34        "outputs": {
35            "combinedBundle": {
36                "type": "bundle",
37                "description": ["This is the union of fullBundle and filtered members of the filteredBundle."]
38             }
39        }
40    }
41}

OgnTutorialBundles.cpp

The cpp file contains the implementation of the compute method. It exercises each of the available bundle manipulation functions.

  1// SPDX-FileCopyrightText: Copyright (c) 2020-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 <OgnTutorialBundlesDatabase.h>
 11
 12using omni::graph::core::BaseDataType;
 13using omni::graph::core::NameToken;
 14using omni::graph::core::Type;
 15
 16namespace
 17{
 18// Tokens to use for checking filter names
 19static NameToken s_filterBigArrays;
 20static NameToken s_filterNameX;
 21static NameToken s_filterTypeInt;
 22}
 23
 24class OgnTutorialBundles
 25{
 26public:
 27    // Overriding the initialize method allows caching of the name tokens which will avoid string comparisons
 28    // at evaluation time.
 29    static void initialize(const GraphContextObj& contextObj, const NodeObj&)
 30    {
 31        s_filterBigArrays = contextObj.iToken->getHandle("big");
 32        s_filterNameX = contextObj.iToken->getHandle("x");
 33        s_filterTypeInt = contextObj.iToken->getHandle("int");
 34    }
 35
 36    static bool compute(OgnTutorialBundlesDatabase& db)
 37    {
 38        // Bundle attributes are extracted from the database in the same way as any other attribute.
 39        // The only difference is that a different interface is provided, suited to bundle manipulation.
 40        const auto& fullBundle = db.inputs.fullBundle();
 41        const auto& filteredBundle = db.inputs.filteredBundle();
 42        const auto& filters = db.inputs.filters();
 43        auto& outputBundle = db.outputs.combinedBundle();
 44
 45        // The first thing this node does is to copy the contents of the fullBundle to the output bundle.
 46        // operator=() has been defined on bundles to make this a one-step operation. Note that this completely
 47        // replaces any previous bundle contents. If you wish to append another bundle then you would use:
 48        //    outputBundle.insertBundle(fullBundle);
 49        outputBundle = fullBundle;
 50
 51        // Set some booleans that determine which filters to apply
 52        bool filterBigArrays{ false };
 53        bool filterNameX{ false };
 54        bool filterTypeInt{ false };
 55        for (const auto& filterToken : filters)
 56        {
 57            if (filterToken == s_filterBigArrays)
 58            {
 59                filterBigArrays = true;
 60            }
 61            else if (filterToken == s_filterNameX)
 62            {
 63                filterNameX = true;
 64            }
 65            else if (filterToken == s_filterTypeInt)
 66            {
 67                filterTypeInt = true;
 68            }
 69            else
 70            {
 71                db.logWarning("Unrecognized filter name '%s'", db.tokenToString(filterToken));
 72            }
 73        }
 74
 75        // The bundle object has an iterator for looping over the attributes within in
 76        for (const auto& bundledAttribute : filteredBundle)
 77        {
 78            // The two main accessors for the bundled attribute provide the name and type information
 79            NameToken name = bundledAttribute.name();
 80            Type type = bundledAttribute.type();
 81
 82            // Check each of the filters to see which attributes are to be skipped
 83            if (filterTypeInt)
 84            {
 85                if ((type.baseType == BaseDataType::eInt) || (type.baseType == BaseDataType::eUInt) ||
 86                    (type.baseType == BaseDataType::eInt64) || (type.baseType == BaseDataType::eUInt64))
 87                {
 88                    continue;
 89                }
 90            }
 91            if (filterNameX)
 92            {
 93                std::string nameString(db.tokenToString(name));
 94                if (nameString.find('x') != std::string::npos)
 95                {
 96                    continue;
 97                }
 98            }
 99            if (filterBigArrays)
100            {
101                // A simple utility method on the bundled attribute provides access to array size
102                if (bundledAttribute.size() > 10)
103                {
104                    continue;
105                }
106            }
107
108            // All filters have been passed so the attribute is eligible to be copied onto the output.
109            outputBundle.insertAttribute(bundledAttribute);
110        }
111        return true;
112    }
113};
114
115REGISTER_OGN_NODE()

OgnTutorialBundlesPy.py

The py file duplicates the functionality in the cpp file, except that it is implemented in Python.

 1"""
 2Implementation of the Python node accessing attributes through the bundle in which they are contained.
 3"""
 4
 5import omni.graph.core as og
 6
 7# Types recognized by the integer filter
 8_INTEGER_TYPES = [og.BaseDataType.INT, og.BaseDataType.UINT, og.BaseDataType.INT64, og.BaseDataType.UINT64]
 9
10
11class OgnTutorialBundlesPy:
12    """Exercise the bundled data types through a Python OmniGraph node"""
13
14    @staticmethod
15    def compute(db) -> bool:
16        """Implements the same algorithm as the C++ node OgnTutorialBundles.cpp"""
17
18        full_bundle = db.inputs.fullBundle
19        filtered_bundle = db.inputs.filteredBundle
20        filters = db.inputs.filters
21        output_bundle = db.outputs.combinedBundle
22
23        # This does a copy of the full bundle contents from the input bundle to the output bundle
24        output_bundle.bundle = full_bundle
25
26        # Extract the filter flags from the contents of the filters array
27        filter_big_arrays = "big" in filters
28        filter_type_int = "int" in filters
29        filter_name_x = "x" in filters
30
31        # The "attributes" member is a list that can be iterated. The members of the list do not contain real
32        # og.Attribute objects, which must always exist, they are wrappers on og.AttributeData objects, which can
33        # come and go at runtime.
34        for bundled_attribute in filtered_bundle.attributes:
35
36            # The two main accessors for the bundled attribute provide the name and type information
37            name = bundled_attribute.name
38            attribute_type = bundled_attribute.type
39
40            # Check each of the filters to see which attributes are to be skipped
41            if filter_type_int and attribute_type.base_type in _INTEGER_TYPES:
42                continue
43
44            if filter_name_x and name.find("x") >= 0:
45                continue
46
47            # A method on the bundled attribute provides access to array size (non-arrays are size 1)
48            if filter_big_arrays and bundled_attribute.size > 10:
49                continue
50
51            # All filters have been passed so the attribute is eligible to be copied onto the output.
52            output_bundle.insert(bundled_attribute)
53
54        return True

Bundle Notes

Bundles are implemented in USD as “virtual primitives”. That is, while regular attributes appear in a USD file as attributes on a primitive, a bundle appears as a nested primitive with no members.

Naming Convention

Attributes can and do contain namespaces to make them easier to work with. For example, outputs:operations is the namespaced name for the output attribute operations. However as USD does not allow colons in the names of the primitives used for implementing attribute bundles they will be replaced by underscores, vis outputs_operations.

Bundled Attribute Manipulation Methods

There are a few methods for manipulating the bundle contents, independent of the actual data inside. The actual implementation of these methods may change over time however the usage should remain the same.

The Bundle As A Whole

// The bundle attribute is extracted from the database in exactly the same way as any other attribute.
const auto& inputBundle = db.inputs.myBundle();

// Output and state bundles are the same, except not const
auto& outputBundle = db.outputs.myBundle();

// The size of a bundle is the number of attributes it contains
auto bundleAttributeCount = inputBundle.size();

// Full bundles can be copied using the assignment operator
outputBundle = inputBundle;

Accessing Attributes By Name

// The attribute names should be cached somewhere as a token for fast access.
static const NameToken normalsName = db.stringToToken("normals");

// Then it's a call into the bundle to find an attribute with matching name.
// Names are unique so there is at most one match, and bundled attributes do not have the usual attribute
// namespace prefixes "inputs:", "outputs:", or "state:"
const auto& inputBundle = db.inputs.myBundle();
auto normals = inputBundle.attributeByName(normalsName);
if (normals.isValid())
{
    // If the attribute is not found in the bundle then isValid() will return false.
}

Putting An Attribute Into A Bundle

// Once an attribute has been extracted from a bundle a copy of it can be added to a writable bundle.
const auto& inputBundle = db.inputs.myBundle();
auto& outputBundle = db.outputs.myBundle();
auto normals = inputBundle.attributeByName(normalsToken);
if (normals.isValid())
{
    // Clear the contents of stale data first since it will not be reused here.
    outputBundle.clear();
    // The attribute wrapper knows how to insert a copy into a bundle
    outputBundle.insertAttribute(normals);
}

Iterating Over Attributes

// The range-based for loop provides a method for iterating over the bundle contents.
const auto& inputBundle = db.inputs.myBundle();
for (const auto& bundledAttribute : inputBundle)
{
    // Type information is available from a bundled attribute, consisting of a structure defined in
    // include/omni/graph/core/Type.h
    auto type = bundledAttribute.type();

    // The type has four pieces, the first is the basic data type...
    assert( type.baseType == BaseDataType::eFloat );

    // .. the second is the role, if any
    assert( type.role == AttributeRole::eNormal );

    // .. the third is the number of tuple components (e.g. 3 for float[3] types)
    assert( type.componentCount == 3 );

    // .. the last is the array depth, either 0 or 1
    assert( type.arrayDepth == 0 );
}