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
 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
{
    "BundleManipulation": {
        "version": 1,
        "categories": "tutorials",
        "description": ["This is a tutorial node. It exercises functionality for the manipulation of bundle",
                        "attribute contents."
        ],
        "uiName": "Tutorial Node: Bundle Manipulation",
        "inputs": {
            "fullBundle": {
                "type": "bundle",
                "description": ["Bundle whose contents are passed to the output in their entirety"],
                "metadata": {
                    "uiName": "Full Bundle"
                }
             },
             "filteredBundle": {
                "type": "bundle",
                "description": ["Bundle whose contents are filtered before being added to the output"],
                "uiName": "Filtered Bundle"
             },
             "filters": {
                "type": "token[]",
                "description": [
                    "List of filter names to be applied to the filteredBundle. Any filter name",
                    "appearing in this list will be applied to members of that bundle and only those",
                    "passing all filters will be added to the output bundle. Legal filter values are",
                    "'big' (arrays of size > 10), 'x' (attributes whose name contains the letter x), ",
                    "and 'int' (attributes whose base type is integer)."
                ],
                "default": []
             }
        },
        "outputs": {
            "combinedBundle": {
                "type": "bundle",
                "description": ["This is the union of fullBundle and filtered members of the filteredBundle."]
             }
        }
    }
}

OgnTutorialBundles.cpp

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

  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
// Copyright (c) 2020-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 <OgnTutorialBundlesDatabase.h>

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

namespace
{
// Tokens to use for checking filter names
static NameToken s_filterBigArrays;
static NameToken s_filterNameX;
static NameToken s_filterTypeInt;
}

class OgnTutorialBundles
{
public:
    // Overriding the initialize method allows caching of the name tokens which will avoid string comparisons
    // at evaluation time.
    static void initialize(const GraphContextObj& contextObj, const NodeObj&)
    {
        s_filterBigArrays = contextObj.iToken->getHandle("big");
        s_filterNameX = contextObj.iToken->getHandle("x");
        s_filterTypeInt = contextObj.iToken->getHandle("int");
    }

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

        // The first thing this node does is to copy the contents of the fullBundle to the output bundle.
        // operator=() has been defined on bundles to make this a one-step operation. Note that this completely
        // replaces any previous bundle contents. If you wish to append another bundle then you would use:
        //    outputBundle.insertBundle(fullBundle);
        outputBundle = fullBundle;

        // Set some booleans that determine which filters to apply
        bool filterBigArrays{ false };
        bool filterNameX{ false };
        bool filterTypeInt{ false };
        for (const auto& filterToken : filters)
        {
            if (filterToken == s_filterBigArrays)
            {
                filterBigArrays = true;
            }
            else if (filterToken == s_filterNameX)
            {
                filterNameX = true;
            }
            else if (filterToken == s_filterTypeInt)
            {
                filterTypeInt = true;
            }
            else
            {
                db.logWarning("Unrecognized filter name '%s'", db.tokenToString(filterToken));
            }
        }

        // The bundle object has an iterator for looping over the attributes within in
        for (const auto& bundledAttribute : filteredBundle)
        {
            // The two main accessors for the bundled attribute provide the name and type information
            NameToken name = bundledAttribute.name();
            Type type = bundledAttribute.type();

            // Check each of the filters to see which attributes are to be skipped
            if (filterTypeInt)
            {
                if ((type.baseType == BaseDataType::eInt) || (type.baseType == BaseDataType::eUInt) ||
                    (type.baseType == BaseDataType::eInt64) || (type.baseType == BaseDataType::eUInt64))
                {
                    continue;
                }
            }
            if (filterNameX)
            {
                std::string nameString(db.tokenToString(name));
                if (nameString.find('x') != std::string::npos)
                {
                    continue;
                }
            }
            if (filterBigArrays)
            {
                // A simple utility method on the bundled attribute provides access to array size
                if (bundledAttribute.size() > 10)
                {
                    continue;
                }
            }

            // All filters have been passed so the attribute is eligible to be copied onto the output.
            outputBundle.insertAttribute(bundledAttribute);
        }
        return true;
    }
};

REGISTER_OGN_NODE()

OgnTutorialBundlesPy.py

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

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

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


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

    @staticmethod
    def compute(db) -> bool:
        """Implements the same algorithm as the C++ node OgnTutorialBundles.cpp"""

        full_bundle = db.inputs.fullBundle
        filtered_bundle = db.inputs.filteredBundle
        filters = db.inputs.filters
        output_bundle = db.outputs.combinedBundle

        # This does a copy of the full bundle contents from the input bundle to the output bundle
        output_bundle.bundle = full_bundle

        # Extract the filter flags from the contents of the filters array
        filter_big_arrays = "big" in filters
        filter_type_int = "int" in filters
        filter_name_x = "x" in filters

        # 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 filtered_bundle.attributes:

            # The two main accessors for the bundled attribute provide the name and type information
            name = bundled_attribute.name
            attribute_type = bundled_attribute.type

            # Check each of the filters to see which attributes are to be skipped
            if filter_type_int and attribute_type.base_type in _INTEGER_TYPES:
                continue

            if filter_name_x and name.find("x") >= 0:
                continue

            # A method on the bundled attribute provides access to array size (non-arrays are size 1)
            if filter_big_arrays and bundled_attribute.size > 10:
                continue

            # All filters have been passed so the attribute is eligible to be copied onto the output.
            output_bundle.insert(bundled_attribute)

        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 attriute 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 );
}