OGN Code Samples - C++

This files contains a collection of examples for using the .ogn generated code from C++. There is no particular flow to these examples, they are used as reference data for the OGN User Guide.

Minimal C++ Node Implementation

Every C++ node must contain an include of the file containing the generated database definition, and an implementation of the compute method that takes the database as a parameter and returns a boolean indicating if the compute succeeded.

#include <OgnNoOpDatabase.h>

class OgnNoOp:
{
public:
    static bool compute(OgnNoOpDatabase& db)
    {
        // This logs a warning to the console, once
        db.logWarning("This node does nothing");

        // These methods provide direct access to the ABI objects, should you need to drop down to direct ABI calls
        // for any reason. (You should let us know if this is necessary as we've tried to minimize the cases in which
        // it becomes necessary.)
        const auto& contextObj = db.abi_context();
        const auto& nodeObj = db.abi_node();

        if (! contextObj.iAttributeData || ! nodeObj.iNode)
        {
            // This logs an error to the console and should only be used for compute failure
            db.logError("Could not retrieve the ABI interfaces");
            return false;
        }
        return true;
    }
};
REGISTER_OGN_NODE()

[Python Version]

Node Type Metadata Access

When node types have any metadata added to them they can be accessed through the ABI node type interface.

#include <OgnNodeMetadataDatabase.h>
#include <alloca.h>
class OgnNodeMetadata:
{
public:
    static bool compute(OgnNodeMetadataDatabase& db)
    {
        auto nodeTypeObj = db.abi_node().getNodeTypeObj(db.abi_node());
        // Specifically defined metadata can be accessed by name
        std::cout << "The author of this node is " << nodeTypeObj.iNodeType->getMetadata(nodeTypeObj, "author") << std::endl;

        // Some metadata is automatically added; you can see it by iterating over all of the existing metadata
        size_t metadataCount = nodeTypeObj.iNodeType->getMetadataCount(nodeTypeObj);
        const char** metadataKeyBuffer = reinterpret_cast<const char**>(alloca(sizeof(char*) * metadataCount));
        const char** metadataValueBuffer = reinterpret_cast<const char**>(alloca(sizeof(char*) * metadataCount));
        size_t found = nodeTypeObj.iNodeType->getAllMetadata(nodeTypeObj, metadataKeyBuffer, metadataValueBuffer, metadataCount);
        for (size_t i=0; i<found; ++i)
        {
            std:: cout << "Metadata for " << metadataKeyBuffer[i] << " = " << metadataValueBuffer[i] << std::endl;
        }
        return true;
    }
};
REGISTER_OGN_NODE()

[Python Version]

Node Icon Location Access

Specifying the icon location and color information creates consistently named pieces of metadata that the UI can use to present a more customized visual appearance.

#include <OgnNodeWithIconDatabase.h>
class OgnNodeWithIcon:
{
public:
    static bool compute(OgnNodeWithIconDatabase& db)
    {
        auto nodeTypeObj = db.abi_node().getNodeTypeObj(db.abi_node());
        // The icon properties are just special cases of metadata. The metadata name is made available with the node type
        auto path = nodeTypeObj.iNodeType->getMetadata(nodeTypeObj, kOgnMetadataIconPath);
        auto color = nodeTypeObj.iNodeType->getMetadata(nodeTypeObj, kOgnMetadataIconColor);
        auto backgroundColor = nodeTypeObj.iNodeType->getMetadata(nodeTypeObj, kOgnMetadataIconBackgroundColor);
        auto borderColor = nodeTypeObj.iNodeType->getMetadata(nodeTypeObj, kOgnMetadataIconBorderColor);
        if (path)
        {
            std::cout << "Icon found at " << path << std::endl;
            if (color)
            {
                std::cout << "...color override is " << color << std::endl;
            }
            else
            {
                std::cout << "...using default color" << std::endl;
            }
            if (backgroundColor)
            {
                std::cout << "...backgroundColor override is " << backgroundColor << std::endl;
            }
            else
            {
                std::cout << "...using default backgroundColor" << std::endl;
            }
            if (borderColor)
            {
                std::cout << "...borderColor override is " << borderColor << std::endl;
            }
            else
            {
                std::cout << "...using default borderColor" << std::endl;
            }
        }
        return true;
    }
};
REGISTER_OGN_NODE()

[Python Version]

Node Type Scheduling Hints

Specifying scheduling hints makes it easier for the OmniGraph scheduler to optimize the scheduling of node evaluation.

#include <OgnNodeSchedulingHintsDatabase.h>
class OgnNodeSchedulingHints:
{
public:
    static bool compute(OgnNodeSchedulingHintsDatabase& db)
    {
        auto nodeTypeObj = db.abi_node().getNodeTypeObj(db.abi_node());

        // Ordinarily you would not need to access this scheduling information as it is mainly for OmniGraph's use,
        // however it is available through the ABI so you can access it at runtime if you wish.
        auto schedulingHintsObj = nodeTypeObj.getSchedulingHints(nodeTypeObj);
        std::string safety;
        switch (schedulingHintsObj.getThreadSafety(schedulingHintsObj))
        {
            case eThreadSafety::eSafe:
                safety = "Safe";
                break;
            case eThreadSafety::eUnsafe:
                safety = "Unsafe";
                break;
            case eThreadSafety::eUnknown:
            default:
                safety = "Unknown";
                break;
        }
        std::cout << "Is this node threadsafe? " << safety << std::endl;

        return true;
    }
};
REGISTER_OGN_NODE()

[Python Version]

C++ Singleton Node Types

Specifying that a node type is a singleton creates a consistently named piece of metadata that can be checked to see if multiple instances of that node type will be allowed in a graph or its child graphs. Attempting to create more than one of such node types in the same graph or any of its child graphs will result in an error.

#include <OgnNodeSingletonDatabase.h>
class OgnNodeSingleton:
{
public:
    static bool compute(OgnNodeSingletonDatabase& db)
    {
        auto nodeTypeObj = db.abi_node().getNodeTypeObj(db.abi_node());
        // The singleton value is just a special case of metadata. The metadata name is made available with the node type
        auto singletonValue = nodeTypeObj.iNodeType->getMetadata(nodeTypeObj, kOgnMetadataSingleton);
        if (singletonValue && singletonValue[0] != '1')
        {
            std::cout << "I am a singleton" << std::endl;
        }

        return true;
    }
};
REGISTER_OGN_NODE()

[Python Version]

Node Type Tags

Specifying the node tags creates a consistently named piece of metadata that the UI can use to present a more friendly grouping of the node types to the user.

#include <OgnNodeTagsDatabase.h>
class OgnNodeTags:
{
public:
    static bool compute(OgnNodeTagsDatabase& db)
    {
        auto nodeTypeObj = db.abi_node().getNodeTypeObj(db.abi_node());
        // The tags value is just a special case of metadata. The metadata name is made available with the node type
        std::cout << "Tagged as " << nodeTypeObj.iNodeType->getMetadata(nodeTypeObj, kOgnMetadataTags) << std::endl;

        return true;
    }
};
REGISTER_OGN_NODE()

This example introduces some helper constants which contain strings set to the key values of special internal metadata. These are metadata elements managed by the code generator. Using these name accessors ensures consistent access.

kOgnMetadataAllowedTokens       # On attributes of type token, a CSV formatted comma-separated list of potential legal values for the UI
kOgnMetadataDescription         # On attributes and node types, contains their description from the .ogn file
kOgnMetadataExtension           # On node types, contains the extension that owns this node type
kOgnMetadataHidden              # On attributes and node types, indicating to the UI that they should not be shown
kOgnMetadataIconPath            # On node types, contains the file path to the node's icon representation in the editor
kOgnMetadataIconBackgroundColor # On node types, overrides the background color of the node's icon
kOgnMetadataIconBorderColor     # On node types, overrides the border color of the node's icon
kOgnMetadataSingleton           # On node types its presence indicates that only one of the node type may be created in a graph
kOgnMetadataTags                # On node types, a comma-separated list of tag categories for the type
kOgnMetadataUiName              # On attributes and node types, user-friendly name specified in the .ogn file

[Python Version]

Token Access

There are two accelerators for dealing with tokens. The first is the tokens that are predefined in the generated code. The second include a couple of methods on the database that facilitate translation between strings and tokens.

#include <OgnTokensDatabase.h>
class OgnTokens:
{
public:
    static bool compute(OgnTokensDatabase& db)
    {
        // Tokens are members of the database, by name. When the dictionary-style definition is used in the .ogn
        // file the names are the dictionary keys. These members were created, of type omni::graph::core::NameToken:
        //   db.tokens.red
        //   db.tokens.green
        //   db.tokens.blue

        // As tokens are just IDs in order to print their values you have to use the utility method "tokenToString"
        // on the database to convert them to strings
        std::cout << "The name for red is " << db.tokenToString(db.tokens.red) << std::endl;
        std::cout << "The name for green is " << db.tokenToString(db.tokens.green) << std::endl;
        std::cout << "The name for blue is " << db.tokenToString(db.tokens.blue) << std::endl;

        // This confirms that tokens are uniquely assigned and the same for all types.
        auto redToken = db.stringToToken("red");
        if (redToken != db.tokens.red)
        {
            std::cout << "ERROR: red token is not consistent" << std::endl;
            return false;
        }

        return true;
    }
};
REGISTER_OGN_NODE()

[Python Version]

Node Type UI Name Access

Specifying the node UI name creates a consistently named piece of metadata that the UI can use to present a more friendly name of the node type to the user.

#include <OgnNodeUiNameDatabase.h>
class OgnNodeUiName:
{
public:
    static bool compute(OgnNodeUiNameDatabase& db)
    {
        // The uiName value is just a special case of metadata. The special metadata name is available from the ABI
        auto nodeTypeObj = db.abi_node().getNodeTypeObj(db.abi_node());
        std::cout << "Call me " << nodeTypeObj.iNodeType->getMetadata(nodeTypeObj, kOgnMetadataUiName) << std::endl;

        return true;
    }
};
REGISTER_OGN_NODE()

[Python Version]

Simple Attribute Data Type

Accessors are created on the generated database class that return a reference to the underlying attribute data, which lives in Fabric. You can use auto&` and ``const auto& type declarations to provide local names to clarify your code, or you can specify the exact types that are referenced, so long as they are compatible. e.g. if you have a local typedef Byte that is a uint8_t then you can use that type to get a reference to the data. This will be more useful with more complex data types later.

Tip

For convenience in debugging the actual typedefs for the data are included in the database. They can be found in the same location as the attributes, but with a _t suffix. For example the attribute db.outputs.length will have a corresponding typedef of db.outputs.length_t, which resolves to int64_t.

References to Fabric are actually done through lightweight accessor classes, which provide extra functionality such as the ability to check if an attribute value is valid or not - compute() will not be called if any of the required attributes are invalid. For example with the simple data types the accessor implements operator() to directly return a reference to the Fabric memory, as seen in the example.

#include <OgnTokenStringLengthDatabase.h>
class OgnTokenStringLength:
{
public:
    static bool compute(OgnTokenStringLengthDatabase& db)
    {
        // Access pattern is "db", the database, "inputs", the attribute's access type, and "token", the name the
        // attribute was given in the .ogn file
        // Inputs should always be const-references, outputs and state access types can be just plain references.
        const auto& tokenToMeasure = db.inputs.token();
        // The database has typedefs for all attributes, which can be used if you prefer explicit types
        db.outputs.length_t& stringLength = db.outputs.length();

        // Use the utility to convert the token to a string first
        std::string tokenString{ db.tokenToString(tokenToMeasure) };

        // Simple assignment to the output attribute's accessor is all you need to do to set the value, as it is
        // pointing directly to that data.
        stringLength = tokenString.length();

        return true;
    }
};
REGISTER_OGN_NODE()

Tip

As these accessor types may be subject to change it’s best to avoid exposing their types (i.e. the data type of db.inputs.token). If you must pass around attribute data, use the direct references returned from operator().

[Python Version]

Tuple Attribute Data Type

The wrappers for tuple-types are largely the same as for simple types. The main difference is the underlying data type that they are wrapping. By default, the pxr::GfVec types are the types returned from operator() on tuple types. Their memory layout is the same as their equivalent POD types, e.g. pxr::GfVec3f -> float[3] so you can cast them to any other equivalent types from your favourite math library.

#include <OgnVectorMultiplyDatabase.h>
class OgnVectorMultiply:
{
public:
    static bool compute(OgnVectorMultiplyDatabase& db)
    {
        const auto& vector1 = db.inputs.vector1();
        const auto& vector2 = db.inputs.vector2();
        auto& product = db.outputs.product();
        // Use the functionality in the GfVec classes to perform the multiplication.
        // You can use your own library functions by casting to appropriate types first.
        for (int row=0; row<4; ++row)
        {
            product.SetRow(row, vector1 * vector2[row]);
        }

        return true;
    }
};
REGISTER_OGN_NODE()

[Python Version]

Role Attribute Data Type

The wrappers for role-types are identical to their underlying tuple types. The only distinction between the types is the value that will be returned from the wrapper’s role() method.

#include <OgnPointsToVectorDatabase.h>
// Being explicit about the namespaced types you are using is a good balance between namespace pollution and long names
using omni::graph::core::AttributeRole;
class OgnPointsToVector:
{
public:
    static bool compute(OgnPointsToVectorDatabase& db)
    {
        // Validate the roles for the computation. They aren't used in the actual math, though they could be used
        // to do something useful (e.g. cast data with a "color" role to a class that handles color spaces)
        if (db.inputs.point1.role() != AttributeRole::ePosition)
        {
            db.logError("The attribute point1 does not have the point role");
            return false;
        }
        if (db.inputs.point2.role() != AttributeRole::ePosition)
        {
            db.logError("The attribute point2 does not have the point role");
            return false;
        }
        if (db.outputs.vector.role() != AttributeRole::eVector)
        {
            db.logError("The attribute vector does not have the vector role");
            return false;
        }

        // The GfVec3f type supports simple subtraction for vector calculation
        db.outputs.vector() = db.inputs.point2() - db.inputs.point2();

        return true;
    }
};
REGISTER_OGN_NODE()

[Python Version]

Array Attribute Data Type

Array attributes are stored in a wrapper that mirrors the functionality of std::span. The memory being managed belongs to Fabric, with the wrapper providing the functionality to seamlessly manage the memory when the array size changes (on writable attributes only).

#include <OgnPartialSumsDatabase.h>
#include <numeric>
class OgnPartialSums:
{
public:
    static bool compute(OgnPartialSumsDatabase& db)
    {
        const auto& inputArray = db.inputs.array();
        auto& outputArray = db.outputs.partialSums();

        // This is a critical step, setting the size of the output array. Without this the array has no memory in
        // which to write.
        //
        // For convenience the size() and resize() methods are available at the database level and the wrapper level,
        // with the latter method only available for writable attributes. This makes initializing an output with the
        // same size as an input a one-liner in either of these two forms:
        //       db.outputs.partialSum.resize( db.inputs.array.size() );
        outputArray.resize(inputArray.size());

        // The wrapper to arrays behaves like a std::span<>, where the external memory they manage comes from Fabric.
        // The wrapper handles the synchronization, and is set up to behave mostly like
        // a normal std::array.
        //
        //     for (const auto& inputValue : inputArray)
        //     inputArray[index]
        //     inputArray.at(index)
        //     inputArray.empty()
        //     rawData = inputArray.data()

        // Since the standard range-based for-loop and general iteration is supported you can make use of the wide
        // variety of STL algorithms as well.
        //
        std::partial_sum(inputArray.begin(), inputArray.end(), outputArray.begin());

        return true;
    }
};
REGISTER_OGN_NODE()

[Python Version]

Tuple-Array Attribute Data Type

The wrappers for tuple-array types are largely the same as for simple array types. The main difference is the underlying data type that they are wrapping. See the Tuple Attribute Data Type section for details on accessing tuple data.

#include <OgnCrossProductDatabase.h>
#include <algorithm>
class OgnCrossProduct:
{
public:
    static bool compute(OgnCrossProductDatabase& db)
    {
        // It usually keeps your code cleaner if you put your attribute wrappers into local variables, avoiding
        // the constant use of the "db.inputs" or "db.outputs" namespaces.
        const auto& a = db.inputs.a();
        const auto& b = db.inputs.b();
        auto& crossProduct = db.outputs.crossProduct();

        // This node chooses to make mismatched array lengths an error. You could also make it a warning, or just
        // simply calculate the result for the minimum number of available values.
        if (a.size() != b.size())
        {
            db.logError("Input array lengths do not match - '%zu' vs. '%zu'", a.size(), b.size());
            return false;
        }

        // As with simple arrays, the size of the output tuple-array must be set first to allocate Fabric memory
        crossProduct.resize(a.size());

        // Edge case is easily handled
        if (a.size() == 0)
        {
            return true;
        }

        // Simple cross product - your math library may have a built-in one you can use
        auto crossProduct = [](const GfVec3d& a, const GfVec3d& b) -> GfVec3d {
            GfVec3d result;
            result[0] = a[1] * b[2] - a[2] * b[1];
            result[1] = -(a[0] * b[2] - a[2] * b[0]);
            result[2] = a[0] * b[1] - a[1] * b[0];
            return result;
        };

        // STL support makes walking the parallel arrays and computing a breeze
        std::transform(a.begin(), a.end(), b.begin(), crossProduct.begin(), crossProduct);

        return true;
    }
};
REGISTER_OGN_NODE()

[Python Version]

String Attribute Data Type

As the normal operations applied to strings involve size changes, and Fabric requires new allocations every time an array’s size changes, it is best to use a local variable for string manipulations rather than modifying the string directly. Doing so also gives you the ability to access the wealth of string manipulation library functions.

#include <OgnReverseStringDatabase.h>
class OgnReverseString:
{
public:
    static bool compute(OgnReverseStringDatabase& db)
    {
        // The attribute wrapper for string types provides a conversion to a std::string with pre-allocated memory.
        // Constructing a new string based on that gives you a copy that can be modified locally and assigned after.
        std::string result(db.inputs.original());

        // Other functions provided by the string wrapper include:
        //    size() - how many characters in the string?
        //    empty() - is the string empty?
        //    data() - the raw char* pointer - note that it is not necessarily null-terminated
        //    comparison operators for sorting
        //    iterators
        // For writable strings (output and state) there are also modification functions:
        //    resize() - required before writing, unless you use an assigment operator
        //    clear() - empty out the string
        //    assignment operators for const char*, std::string, and string wrappers

        // The local copy can be reversed in place.
        std::reverse(std::begin(result), std::end(result));
        // The assignment operator handles resizing the string as well
        db.outputs.result() = result;

        // Since in this case the length of the output string is known up front the extra copy can be avoided by
        // making use of the iterator feature.
        //    auto& resultString = db.outputs.result();
        //    resultString.resize(db.inputs.original.size());
        //    std::reverse(std::begin(resultString), std::end(resultString));

        return true;
    }
};
REGISTER_OGN_NODE()

[Python Version]

Extended Attribute Data Type - Any

Extended attributes, of type “any” and union, have data types that are resolved at runtime. This requires extra information that identifies the resolved types and provides methods for extracting the actual data from the wrapper around the extended attribute.

In C++ this is accomplished with prudent use of templated methods and operator overloads. The wrapper around the extended attribute types uses this to provide access to a layered wrapper for the resolved data type, which in turn provides wrapped access to the underlying Fabric functionality.

This is the documentation for the interface of the extended attribute wrapper class, taken directly from the implementation file:

// A runtime attribute can either be an attribute defined as one of the extended types ("union" or "any") or an
// attribute that is a member of a bundle. As you might guess, the defining feature of such attributes is the fact
// that the type of data they store is not known until runtime. And further, that type can change from one evaluation
// to the next. For that reason the runtime attribute accessors have different methods of acquiring their data than
// regular attributes.
//
// The two ways of acquiring the accessor for a runtime attribute are directly, for extended types
const auto& anyType = db.inputs.anyTypeAttribute();
// and as a member, for bundled attributes
const auto memberAttribute = db.inputs.myBundle().attributeByName(db.tokens.memberName);

// Runtime attributes can be copied, which copies both the type and the data of the attribute (unlike regular
// attributes, which would just copy the data)
auto newAttribute = anyType;

// There is also another method that will just copy the data, though it is up to you to ensure that the data
// types of the two attributes are the same.
newAttribute.copyData(anyType);

// As with regular attributes you can check if their data is valid...
const bool anyIsValid = anyType.isValid();
// ...get the number of elements in the array, if they are an array type...
const size_t howManyAnysDoIHave = anyType.size();
// ...and drop down to the ABI to get a handle for direct ABI calls (although for runtime attributes the handle
//    is AttributeDataHandle/ConstAttributeDataHandle, not AttributeHandle as it is for regular attributes since
//    the ABI has different capabilities for them)
const auto& abiHandle = anyType.abi_handle();

// They also have a method to determine if the actual type of the attribute has been resolved. Until the type is
// resolved the attribute's data cannot be accessed
const bool isTheTypeKnown = anyType.resolved();

// For bundled attributes the name is not known until runtime either so a method to access that is provided,
// returning the hardcoded name for extended attributes
const memberName&  = memberAttribute.name();

// And the resolved type information is also available. Checking for an unknown type is another way to determine
// if the attribute type has not yet been resolved.
const auto& anyAttributesType = anyType.type();

// Finally there are the data access methods. The primary one is a templated function through which you can access
// the attribute's data in its raw form. The value returned isn't the data itself, it is a thin wrapper around the
// data that has a few functions of its own.
//
// This is the function to call for the majority of attributes, whose memory space is fixed either to CPU or GPU.
// It returns an object that can be used to access information about the attribute's value, including its memory location.
const auto dataAsFloatObj = anyType.get<float>();

// The types allowed in the template are the set of all allowed attribute types, expressed as regular C++ types
// without the attribute roles. For example float, float[], float[3], float[][3], etc. In most cases trying to access
// the data with an unsupported type will result in a compile error (the exceptions being types that are aliases for
// a supported type, e.g. "using float3 = float[3]"). In fact, since the "NameToken" supported type is an alias for
// another supported type it must be retrieved with a special type set up for that purpose
const auto dataAsToken = anyType.get<OgnToken>();

// The wrapper has a boolean cast operator, which checks to see if the requested type matches the actual resolved
// data type of the attribute. This allows you to make a cascading check for types of attributes you are supporting
if (const auto dataAsFloatObj = anyType.get<float>())
{
    processAsFloat(*dataAsFloatObj);
}
else if (const auto dataAsDoubleObj = anyType.get<double>())
{
    processAsDouble(*dataAsDoubleObj);
}

// In addition to the simple boolean validity test, the wrapper returned will have a few different methods,
// depending on the template data type.
// The dereference operators return references to the actual underlying attribute data (on the CPU - if your
// attribute lives on the GPU you'll get a reference to a pointer to the underlying attribute data, which lives in
// the GPU memory space and cannot be dereferenced on the CPU). Note that the default memory location of a bundled
// attribute is whatever was specified for the bundle itself.
const auto dataAsFloatObj = anyType.get<float>();
float const& floatValueDeref = *dataAsFloatObj;
float const& floatValueFn = dataAsFloatObj();
float const* floatValuePtr = dataAsFloatObj.operator->();

// The same dereference operators work for tuple types as well
const auto dataAsFloat3Obj = anyType.get<float[3]();
float const (&float3ValueDeref)[3] = *dataAsFloat3Obj;

// The tuple values also give you access to the tuple count and element-wise access
float x = dataAsFloat3Obj[0];
assert( dataAsFloat3Obj.tupleSize() == 3);

// Array data type wrappers dereference to the same array wrappers you get from regular attributes
const auto dataAsFloatArrayObj = anyType.get<float[]>();
for (const auto& floatValue : *dataAsFloatArrayObj)
{ /* ... */ }
size_t arrayElements = dataAsFloatArrayObj->size();

// For GPU attributes, which do not have the ability to dereference their array memory location, the wrapper instead
// returns a raw pointer to the underlying GPU memory location of the array.
const auto gpuFloatArrayObj = anyType.get<float[]>();
float const ***ptrToRawGpuData = *gpuFloatArrayObj;

// When the node is configured to extract CUDA pointers on the CPU there is one fewer level of indirection for
// arrays as the pointer returned is on the CPU.
const auto gpuFloatArrayObj = anyType.get<float[]>();
float const ***ptrToGpuDataOnCpu = *gpuFloatArrayObj;
float const **ptrToRawGpuData = *ptrToGpuDataOnCpu;

// As with regular array attributes, before writing to elements of an output array attribute you must first resize
// it to have the desired number of elements.
auto outputFloatArrayObj = data.outputs.results().get<float[]>();
outputFloatArrayObj.resize( howManyDoINeed );

// For attributes whose memory space is determined at runtime, or when you want to access attribute data in a different
// memory space than they were originally defined, you can force the retrieved data to be either on the CPU or GPU.
const auto gpuVersionObj = anyType.getGpu<float>();
const auto cpuVersionObj = anyType.getCpu<float>();

// On rare occasions you may need to resolve the attribute's type at runtime, inside a node's compute() function. In
// those cases the runtime attribute data can get out of sync so you need to notify it that a change has been made.
AttributeObj out = db.abi_node().iNode->getAttributeByToken(db.abi_node(), outputs::anyOutput.m_token);
out.iAttribute->setResolvedType(out, someNewType);
anyOutput.reset(db.abi_context(), out.iAttribute->getAttributeDataHandle(out));

The first extended type, any, is allowed to resolve to any other attribute type.

#include <OgnMultiplyNumbersDatabase.h>
class OgnMultiplyNumbers:
{
public:
    static bool compute(OgnMultiplyNumbersDatabase& db)
    {
        // Full details on handling extended types can be seen in the example for the "any" type. This example
        // shows only the necessary parts to handle the two types accepted for this union type (float and double).
        // The underlying code is all the same, the main difference is in the fact that the graph only allows
        // resolving to types explicitly mentioned in the union, rather than any type at all.

        const auto& a = db.inputs.a();
        const auto& b = db.inputs.b();
        auto& product = db.outputs.product();

        bool handledType{ false };
        if (auto aValue = a.get<float>())
        {
            if (auto bValue = b.get<float>())
            {
                if (auto productValue = product.get<float>())
                {
                    handledType = true;
                    *productValue = *aValue * *bValue;
                }
            }
        }
        else if (auto aValue = a.get<double>())
        {
            if (auto bValue = b.get<double>())
            {
                if (auto productValue = product.get<double>())
                {
                    handledType = true;
                    *productValue = *aValue * *bValue;
                }
            }
        }

        if (! handledType)
        {
            db.logError("Types were not resolved ('%s'*'%s'='%s')",
                        a.typeName().c_str(), b.typeName().c_str(), product.typeName().c_str());
            return false;
        }

        return true;
    }
};
REGISTER_OGN_NODE()

[Python Version]

Extended Attribute Data Type - Union

The access pattern for the union attribute types is exactly the same as for the any type. There is just a tacit agreement that the resolved types will always be one of the ones listed in the union type description.

#include <OgnMultiplyNumbersDatabase.h>
class OgnMultiplyNumbers:
{
public:
    static bool compute(OgnMultiplyNumbersDatabase& db)
    {
        // Full details on handling extended types can be seen in the example for the "any" type. This example
        // shows only the necessary parts to handle the two types accepted for this union type (float and double).
        // The underlying code is all the same, the main difference is in the fact that the graph only allows
        // resolving to types explicitly mentioned in the union, rather than any type at all.

        const auto& a = db.inputs.a();
        const auto& b = db.inputs.b();
        auto& product = db.outputs.product();

        bool handledType{ false };
        if (auto aValue = a.get<float>())
        {
            if (auto bValue = b.get<float>())
            {
                if (auto productValue = product.get<float>())
                {
                    handledType = true;
                    *productValue = *aValue * *bValue;
                }
            }
        }
        else if (auto aValue = a.get<double>())
        {
            if (auto bValue = b.get<double>())
            {
                if (auto productValue = product.get<double>())
                {
                    handledType = true;
                    *productValue = *aValue * *bValue;
                }
            }
        }

        if (! handledType)
        {
            db.logError("Types were not resolved ('%s'*'%s'='%s')",
                        a.typeName().c_str(), b.typeName().c_str(), product.typeName().c_str());
            return false;
        }

        return true;
    }
};
REGISTER_OGN_NODE()

[Python Version]

Extended Attribute Type Resolution using ogn::compute

As you might see, resolving attributes manually in c++ results in a lot of repeated code that is difficult to read. the ogn::compute api, defined in <omni/graph/core/ogn/ComputeHelpers.h> aims to make this easier for developers.

/*
 * In python, working with extended unions types is easy - dynamic type resolution is incredibly simple, and NumPy
 * handles array broadcasting, where a vector with smaller dimensions will be "repeated" so the inputs are compatible
 * the compute helpers API hopes to provide utilities that make working with C++ union types significantly easier
 */

// The compute helpers API primarily relies on using generic lambdas for operations.
// The compiler will resolve the input types, allowing us to use the add function for a variety
// of different types (eg: double, float, int)
auto add = [](auto const& a, auto const& b, auto& result) { result = a + b; };

// We can use the ogn::compute::tryCompute function to attempt to apply add() to a variety of different types
// tryCompute will return true if the types are resolved properly, and false if they aren't. an ogn::compute::InputError
// will be thrown if the types are resolved but the computation is impossible (due to different array sizes, for example)
if (ogn::compute::tryCompute(db.inputs.a().get<int>(), db.inputs.b().get<int>(), db.inputs.result().get<int>(), add))
    return true;
else if (ogn::compute::tryCompute(db.inputs.a().get<float>(), db.inputs.b().get<float>(), db.inputs.result().get<float>(), add))
    return true;
// For arrays, add() will be called with each input in parallel, ie: add(a[i], b[i], result[i]).
else if (ogn::compute::tryCompute(db.inputs.a().get<double[]>(), db.inputs.b().get<double[]>(), db.inputs.result().get<double[]>(), add))
    return true;
// For a mixture of arrays and singulars, the singular will be broadcast: add(a[i], b, result[i])
else if (ogn::compute::tryCompute(db.inputs.a().get<double[]>(), db.inputs.b().get<double>(), db.inputs.result().get<double[]>(), add))
    return true;
else
{
    db.logWarning("Failed to resolve input types");
    return false;
}

/*
 * Sometimes we want to support a mix of arrays and singular values, using broadcasting to match the singular values
 * to the dimensions of the array. For this, we can use ogn::compute::tryComputeWithArrayBroadcasting().
 */

// Assuming a, b, and result all have base type int, tryComputeUsingArrrayBroadcasting will attempt to resolve each
// input to int or int[]. Then, perform broadcasting as necessary.
if (ogn::compute::tryComputeUsingArrrayBroadcasting<int>(db.inputs.a(), db.inputs.b(), db.inputs.sum(), add))
    return true;
// Assumes a has base type int, b has base type float, and result has base type float
else if (ogn::compute::tryComputeUsingArrrayBroadcasting<int, float, float>(db.inputs.a(), db.inputs.b(), db.inputs.sum(), add))
    return true;
// Also supports 3 arguments and a result
else if (ogn::compute::tryComputeUsingArrrayBroadcasting<int>(db.inputs.a(), db.inputs.b(), db.inputs.c(), db.inputs.sum(), add3))
    return true;
else if (ogn::compute::tryComputeUsingArrrayBroadcasting<float, int, int, float>(db.inputs.a(), db.inputs.b(), db.inputs.c(), db.inputs.sum(), add3))
    return true;

/*
 * For tuple types, you'll have to change your lambda function to work with c++ fixed size arrays T[N]
 * Your lambda function will need to be specialized for each different N.
 * For this, I recommend using a helper function in your node implementation
 */

// Empty namespace to avoid multiple declarations at linking
namespace {
// compute helper assuming a scalar base type
template<typename T>
bool tryComputeAssumingType(db_type& db)
{
    auto functor = [](auto const& a, auto const& b, auto& result)
    {
        result = a + b;
    };
    return ogn::compute::tryComputeWithArrayBroadcasting<T>(db.inputs.a(), db.inputs.b(), db.outputs.sum(), functor);
}
// compute helper assuming a tuple base type
template<typename T, size_t N>
bool tryComputeAssumingType(db_type& db)
{
    auto functor = [](auto const& a, auto const& b, auto& result)
    {
        for(size_t i = 0; i < N; i++)
        {
            result[i] = a[i] + b[i];
        }
    };
    return ogn::compute::tryComputeWithArrayBroadcasting<T[N]>(db.inputs.a(), db.inputs.b(), db.outputs.sum(), functor);
}
} // namespace

// ...

if (tryComputeAssumingType<int>(db)) return true;  // Calls the scalar helper
else if (tryComputeAssumingType<float>(db)) return true;
else if (tryComputeAssumingType<int, 3>(db)) return true;  // Calls the tuple helper
else if (tryComputeAssumingType<float, 3>(db)) return true;

/*
 * You may also want to support adding tuples to scalars. The above code unfortunately won't support that.
 *  For Tuple broadcasting, you can use ogn::compute::tryComputeWithTupleBroadcasting. This function will
 *  resolve each input to type T, T[], T[N] or T[N][], performing broadcasting as necessary.
 */

template<typename T, size_t N>
bool tryComputeAssumingType(db_type& db)
{
    auto functor = [](auto const& a, auto const& b, auto& result)
    {
        result = a + b;
    };
    // Perform computation with tuple AND array broadcasting
    return ogn::compute::tryComputeWithTupleBroadcasting<T, N>(db.inputs.a(), db.inputs.b(), db.outputs.sum(), functor);
}

For a complete code sample, see the implementation of the OgnAdd node:

#include <OgnAddDatabase.h>
#include <omni/graph/core/ogn/ComputeHelpers.h>
#include <carb/logging/Log.h>

namespace omni {
namespace graph {
namespace nodes {
// unnamed namespace to avoid multiple declaration when linking
namespace {
template<typename T>
bool tryComputeAssumingType(OgnAddDatabase& db)
{
    auto functor = [](auto const& a, auto const& b, auto& result)
    {
        result = a + b;
    };
    return ogn::compute::tryComputeWithArrayBroadcasting<T>(db.inputs.a(), db.inputs.b(), db.outputs.sum(), functor);
}
template<typename T, size_t N>
bool tryComputeAssumingType(OgnAddDatabase& db)
{
    auto functor = [](auto const& a, auto const& b, auto& result)
    {
        result[i] = a[i] + b[i];
    };
    return ogn::compute::tryComputeWithTupleBroadcasting<T, N>(db.inputs.a(), db.inputs.b(), db.outputs.sum(), functor);
}
} // namespace

class OgnAdd
{
public:
    static bool compute(OgnAddDatabase& db)
    {
        try
        {
            // All possible types excluding ogn::string and bool
            // scalers
            else if (tryComputeAssumingType<double>(db)) return true;
            else if (tryComputeAssumingType<float>(db)) return true;
            else if (tryComputeAssumingType<pxr::GfHalf>(db)) return true;
            else if (tryComputeAssumingType<int32_t>(db)) return true;
            else if (tryComputeAssumingType<int64_t>(db)) return true;
            else if (tryComputeAssumingType<unsigned char>(db)) return true;
            else if (tryComputeAssumingType<uint32_t>(db)) return true;
            else if (tryComputeAssumingType<uint64_t>(db)) return true;

            // tuples
            else if (tryComputeAssumingType<int32_t, 2>(db)) return true;
            else if (tryComputeAssumingType<int32_t, 3>(db)) return true;
            else if (tryComputeAssumingType<int32_t, 4>(db)) return true;
            else if (tryComputeAssumingType<double, 2>(db)) return true;
            else if (tryComputeAssumingType<double, 3>(db)) return true;
            else if (tryComputeAssumingType<double, 4>(db)) return true;       // quaternion (IJKR), RGBA, etc
            else if (tryComputeAssumingType<double, 9>(db)) return true;       // Matrix3f type
            else if (tryComputeAssumingType<double, 16>(db)) return true;      // Matrix4f type
            else if (tryComputeAssumingType<float, 2>(db)) return true;
            else if (tryComputeAssumingType<float, 3>(db)) return true;
            else if (tryComputeAssumingType<float, 4>(db)) return true;        // quaternion (IJKR), RGBA, etc
            else if (tryComputeAssumingType<pxr::GfHalf, 2>(db)) return true;
            else if (tryComputeAssumingType<pxr::GfHalf, 3>(db)) return true;
            else if (tryComputeAssumingType<pxr::GfHalf, 4>(db)) return true;  // quaternion (IJKR), RGBA, etc
        }
        catch (ogn::compute::InputError &error)
        {
            db.logWarning(error.what());
        }
        return false;
    }

    static void onConnectionTypeResolve(const NodeObj& node){
        auto a = node.iNode->getAttributeByToken(node, inputs::a.token())
        auto b = node.iNode->getAttributeByToken(node, inputs::b.token())
        auto sum = node.iNode->getAttributeByToken(node, outputs::sum.token())

        auto aType = a.iAttribute->getResolvedType(a);
        auto bType = b.iAttribute->getResolvedType(b);

        // Require inputs to be resolved before determining sum's type
        if (aType.baseType != BaseDataType::eUnknown && bType.baseType != BaseDataType::eUnknown)
        {
            std::array<AttributeObj, 3> attrs { a, b, sum };
            // a, b, and sum should all have the same tuple count
            std::array<uint8_t, 3> tupleCounts {
                aType.componentCount,
                bType.componentCount,
                std::max(aType.componentCount, bType.componentCount)
            };
            std::array<uint8_t, 3> arrayDepths {
                aType.arrayDepth,
                bType.arrayDepth,
                // Allow for a mix of singular and array inputs. If any input is an array, the output must be an array
                std::max(aType.arrayDepth, bType.arrayDepth)
            };
            std::array<AttributeRole, 3> rolesBuf {
                aType.role,
                bType.role,
                // Copy the attribute role from the resolved type to the output type
                AttributeRole::eUnknown
            };
            node.iNode->resolvePartiallyCoupledAttributes(node, attrs.data(), tupleCounts.data(),
                                                        arrayDepths.data(), rolesBuf.data(), attrs.size());
        }
    }
};

REGISTER_OGN_NODE()

} // namespace nodes
} // namespace graph
} // namespace omni

Bundle Attribute Data Type

Bundle attribute information is accessed the same way as information for any other attribute type. As an aggregate, the bundle can be treated as a container for attributes, without any data itself.

#include <OgnMergeBundlesDatabase.h>
class OgnMergeBundle:
{
public:
    static bool compute(OgnMergeBundlesDatabase& db)
    {
        const auto& bundleA = db.inputs.bundleA();
        const auto& bundleB = db.inputs.bundleB();
        auto& mergedBundle = db.outputs.bundle();

        // Bundle assignment means "assign all of the members of the RHS bundle to the LHS bundle". It doesn't
        // do a deep copy of the bundle members.
        mergedBundle = bundleA;

        // Bundle insertion adds the contents of a bundle to an existing bundle. The bundles may not have members
        // with the same names
        mergedBundle.insertBundle( bundleB );

        return true;
    }
};
REGISTER_OGN_NODE()

[Python Version]

When you want to get at the actual data, you use the bundle API to extract the runtime attribute accessors from the bundle for those attributes you wish to process.

#include <OgnCalculateBrightnessDatabase.h>
class OgnCalculateBrightness:
{
public:
    // The actual algorithm to run using a well-defined conversion
    static float brightnessFromRGB(float r, float g, float b)
    {
        return (r * (299.f) + (g * 587.f) + (b * 114.f)) / 256.f;
    }
    static bool compute(OgnCalculateBrightnessDatabase& db)
    {
        // Retrieve the bundle accessor
        const auto& color = db.inputs.color();
        // Using the bundle accessor, try to retrieve the RGB color members. In this case the types have to be
        // float, though in a more general purpose node you might also allow for double, half, and int types.
        const auto r = color.attributeByName(db.tokens.r).get<float>();
        const auto g = color.attributeByName(db.tokens.g).get<float>();
        const auto b = color.attributeByName(db.tokens.b).get<float>();
        // Validity of a member is a boolean
        if (r && g && b)
        {
            db.outputs.brightness() = brightnessFromRGB(r, g, b);
            return true;
        }
        // Having failed to extract RGB members, do the same check for CMYK members
        const auto c = color.attributeByName(db.tokens.c).get<float>();
        const auto m = color.attributeByName(db.tokens.m).get<float>();
        const auto y = color.attributeByName(db.tokens.y).get<float>();
        const auto k = color.attributeByName(db.tokens.k).get<float>();
        if (c && m && y && k)
        {
            db.outputs.brightness() = brightnessFromRGB(
                (1.f - c/100.f) * (1.f - k/100.f),
                (1.f - m/100.f) * (1.f - k/100.f),
                (1.f - y/100.f) * (1.f - k/100.f) );
            return true;
        }

        // You could be more verbose about the reason for the problem as there are a few different scenarios:
        //    - some but not all of r,g,b or c,m,y,k were in the bundle
        //    - none of the color components were in the bundle
        //    - some or all of the color components were found but were of the wrong data type
        db.logError("Neither the groups (r, g, b) nor (c, m, y, k) are in the color bundle. Cannot compute brightness");
        return false;
    }
};
REGISTER_OGN_NODE()

Tip

Although you access them in completely different ways the attributes that are bundle members use the same accessors as the extended attribute types. See further information in Extended Attribute Data Type - Any

This documentation for bundle access is pulled directly from the code. It removes the extra complication in the accessors required to provide proper typing information for bundle members and shows the appropriate calls in the bundle attribute API.

// A bundle can be described as an opaque collection of attributes that travel together through the graph, whose
// contents and types can be introspected in order to determine how to deal with them. This section describes how
// the typical node will interface with the bundle content access. Use of the attributes within the bundles is the
// same as for the extended type attributes, described with their access methods.
//
// An important note regarding GPU bundles is that the bundle itself always lives on the CPU, specifying a memory
// space of "GPU/CUDA" for the bundle actually means that the default location of the attributes it contains will
// be on the GPU.
//
// The main bundle is extracted the same as any other attribute, by referencing its generated database location.
// For this example the bundle will be called "color" and it will have members that could either be the set
// ("r", "g", "b", "a") or the set ("c", "m", "y", "k") with the obvious implications of implied color space.
//
// The bundle itself has a path to which it refers; normally unnecessary to use but helpful for debugging
std::cout << "The color bundle came from " << db.inputs.color.path() << std::endl;

// As with other attribute types you can get an accessor to the bundle:
const auto& colorBundle = db.inputs.color();

// The accessor can determine if it points to valid data
const bool validColor = colorBundle.isValid();

// It can be queried for the number of attributes it holds
auto bundleAttributeCount = colorBundle.size();

// It can have its contents iterated over
for (const auto& bundledAttribute : colorBundle)
{ /* ... */ }

// It can be queried for an attribute in it with a specific name
auto bundledAttribute = colorBundle.attributeByName(db.tokens.red);

// And on the rare occasion when it is necessary, it can access the low level ABI handle of the bundle's data
// to make direct ABI calls on it. (This is discouraged as it may bypass some important state updates.)
const auto& bundleHandle = colorBundle.abi_primHandle();

// *** The rest of these methods are for output bundles only, as they change the makeup of the bundle

// It can be assigned to an output bundle, which merely transfers ownership of the bundle.
// As in all C++ it's important to make the distinction between assignment and merely obtaining a reference
auto& computedColorBundle = db.outputs.computedColorBundle();  // No copy, just assignment of a reference object
computedColorBundle = colorBundle; // Copy the input bundle to the output bundle

// It can have its contents (i.e. attribute membership) cleared
computedColorBundle.clear();

// It can insert a new bundle, without replacing its current contents (with the caveat that all attribute names
// in the current and inserted bundle must be unique)
computedColorBundle.insertBundle(colorBundle);

// It can have a single attribute from another bundle inserted into its current list, like if you don't want
// the transparency value in your output color
computedColorBundle.clear();
computedColorBundle.insertAttribute(colorBundle.attributeByName(db.tokens.red));
computedColorBundle.insertAttribute(colorBundle.attributeByName(db.tokens.green));
computedColorBundle.insertAttribute(colorBundle.attributeByName(db.tokens.blue));

// It can add a brand new attribute with a specific type and name
namespace og = omni::graph::core;
og::Type floatType(og::BaseDataType::eFLOAT);
computedColorBundle.addAttribute(db.tokens.opacity, floatType);
// If you are adding an array attribute you can set its initial element count with the same call
og::Type boolArrayType(og::BaseDataType::eBOOLEAN, 1, 1);
computedColorBundle.addAttribute(db.tokens.bits, boolArrayType, 32);

// If you want to remove an attribute from a bundle you only need its name
computedColorBundle.removeAttribute(db.tokens.bits);

[Python Version]

Attribute Memory Location

In C++ nodes the GPU calculations will typically be done by CUDA code. The important thing to remember for this code is that the CPU, where the node compute() method lives, cannot dereference pointers in the GPU memory space. For this reason, all of the APIs that provide access to attribute data values return pointers to the actual data when it lives on the GPU.

For example, if an attribute has a float value

#include <OgnMemoryTypeDatabase.h>
class OgnMemoryType:
{
public:
    static bool compute(OgnMemoryTypeDatabase& db)
    {
        // The operation specifies moving the points data onto the GPU for further computation if the size of
        // the input data reaches a threshold where that will make the computation more efficient.
        // (This particular node just moves data; in practice you would perform an expensive calculation on it.)
        if (db.inputs.points.size() > db.inputs.sizeThreshold())
        {
            // The gpu() methods force the data onto the GPU. They may or may not perform CPU->GPU copies under the
            // covers. Fabric handles all of those details so that you don't have to.
            db.outputs.points.gpu() = db.inputs.points.gpu();
        }
        else
        {
            // The gpu() methods force the data onto the CPU. They may or may not perform GPU->CPU copies under the
            // covers. Fabric handles all of those details so that you don't have to.
            db.outputs.points.cpu() = db.inputs.points.cpu();
        }
        return true;
    }
};
REGISTER_OGN_NODE()

[Python Version]

Attribute CPU Pointers to GPU Data

Note

Although this value takes effect at the attribute level the keyword is only valid at the node level. All attributes in a node will use the same type of CUDA array pointer referencing.

#include <OgnCudaPointersDatabase.h>
extern "C" callCudaFunction(inputs::cudaPoints_t, outputs::cudaPoints_t);
class OgnCudaPointers:
{
public:
    static bool compute(OgnCudaPointersDatabase& db)
    {
        // When the *cudaPointers* keyword is set to *cpu* this wrapped array will contain a CPU pointer that
        // references the GPU array data. If not, it would have contained a GPU pointer that references the GPU
        // array data and not been able to be dereferenced on the CPU side.
        callCudaFunction(db.inputs.cudaPoints(), db.outputs.cudaPoints());
        return true;
    }
};
REGISTER_OGN_NODE()

[Python Version]

Attribute Metadata Access

When attributes have metadata added to them they can be accessed through the ABI attribute interface. This works basically the same as with metadata on node types, just with different accessors.

#include <OgnStarWarsCharactersDatabase.h>
#include <alloca.h>
class OgnStarWarsCharacters:
{
public:
    static bool compute(OgnStarWarsCharactersDatabase& db)
    {
        auto nodeTypeObj = db.abi_node().getNodeTypeObj(db.abi_node());
        auto anakinObj = db.abi_node().getAttribute(db.abi_node(), inputs::anakin.token());
        // Specifically defined metadata can be accessed by name
        std::cout << "Anakin's secret is " << anakinObj->getMetadata(anakinObj, "secret") << std::endl;

        // Some metadata is automatically added; you can see it by iterating over all of the existing metadata
        size_t metadataCount = anakinObj->getMetadataCount(anakinObj);
        const char** metadataKeyBuffer = reinterpret_cast<const char**>(alloca(sizeof(char*) * metadataCount));
        const char** metadataValueBuffer = reinterpret_cast<const char**>(alloca(sizeof(char*) * metadataCount));
        size_t found = anakinObj->getAllMetadata(anakinObj, metadataKeyBuffer, metadataValueBuffer, metadataCount);
        for (size_t i=0; i<found; ++i)
        {
            std:: cout << "Metadata for " << metadataKeyBuffer[i] << " = " << metadataValueBuffer[i] << std::endl;
        }
        return true;
    }
};
REGISTER_OGN_NODE()

[Python Version]

Required vs. Optional Attributes

For most attributes the generated code will check to see if the attribute is valid before it calls the compute() function. Optional attributes will not have this check made. If you end up using their value then you must make the call to the isValid() method yourself first and react appropriately if invalid values are found. Further, the existence of these attributes within the compute method is not guaranteed.

#include <OgnShoesDatabase.h>
#include <string>
#include <stdlib.h>
class OgnShoes:
{
public:
    static bool compute(OgnShoesDatabase& db)
    {
        static std::string[] _shoeTypes{ "Runners", "Slippers", "Oxfords" };
        auto shoeIndex = rand() % 3;
        std::string shoeTypeName{ _shoeTypes[shoeIndex] };

        // If the shoe is a type that has laces then append the lace type name
        if (shoeIndex != 1)
        {
            // As this is an optional value it may or may not be valid at this point.
            const auto& shoelaceStyle = db.inputs.shoelaceStyle();
            // This happens automatically with required attributes. With optional ones it has to be done when used.
            if (db.inputs.shoeLaceStyle.isValid())
            {
                shoeTypeName += " with ";
                shoeTypeName += db.tokenToString(shoelaceStyle);
                shoeTypeName += " laces";
            }
        }
        db.outputs.shoeType() = shoeTypeName;
        return true;
    }
};
REGISTER_OGN_NODE()

[Python Version]

Caution

Optional attributes may not even appear on the node. This puts the onus on the node writer to completely validate the attributes, and provide their own ABI-based access to the attribute data. Use of such attributes should be rare.

Attribute UI Name Access

Specifying the attribute uiName creates a consistently named piece of metadata that the UI can use to present a more friendly version of the attribute name to the user. It can be accessed through the regular metadata ABI, with some constants provided for easier access.

#include <OgnAttributeUiNameDatabase.h>
class OgnAttributeUiName:
{
public:
    static bool compute(OgnAttributeUiNameDatabase& db)
    {
        // The uiName value is just a special case of metadata.
        // Note the use of the namespace-enabled "inputs" value that provides access to an attribute's static name.
        auto attributeObj = db.abi_node().iNode->getAttribute(db.abi_node(), inputs::x.token());
        std::cout << "Call me " << attributeObj.iAttribute->getMetadata(attributeObj, kOgnMetadataUiName) << std::endl;

        return true;
    }
};
REGISTER_OGN_NODE()

Another access point is shown here. As the attribute patterns defined in the .ogn are unchanging the generated code provides access to the name of the attribute through a data member so that you don’t have to replicate the string. The access is through local namespaces inputs::, outputs::, and state::, mirroring the database structure.

inputs::x.token() // The name of the input attribute "x" as a token
inputs::x.name()  // The name of the input attribute "x" as a static string

[Python Version]

Unvalidated Attributes

For most attributes the generated code will check to see if the attribute is valid before it calls the compute() function. unvalidated attributes will not have this check made. If you end up using their value then you must make the call to the isValid() method yourself first and react appropriately if invalid values are found. Further, for attributes with extended types you must verify that they have successfully resolved to a legal type.

#include <OgnShoesDatabase.h>
#include <string>
#include <stdlib.h>
class OgnABTest:
{
public:
    static bool compute(OgnABTestDatabase& db)
    {
        auto choice = db.outputs.choice();
        auto outType = choice.type();
        // Check to see which input is selected and verify that its data type matches the output resolved type
        if (db.inputs.selectA())
        {
            const auto inputA = db.inputs.a();
            if (! inputA.isValid() || (inputA.type() != outType))
            {
                db.logError("Mismatched types at input a - '%s' versus '%s'", inputA.type().getOgnTypeName(), outType.getOgnTypeName());
                return false;
            }
            choice = inputA;
        }
        else
        {
            const auto inputF = db.inputs.b();
            if (! inputB.isValid() || (inputB.type() != outType))
            {
                db.logError("Mismatched types at input b - '%s' versus '%s'", inputB.type().getOgnTypeName(), outType.getOgnTypeName());
                return false;
            }
            choice = inputB;
        }
        return true;
    }
};
REGISTER_OGN_NODE()

[Python Version]

Nodes With Internal State

The easiest way to add internal state information is by adding data members to your node class. The presence of any member variables automatically marks the node as having internal state information, allocating and destroying it when the node itself is initialized and released. Once the internal state has been constructed it is not modified by OmniGraph until the node is released, it is entirely up to the node how and when to modify the data.

The data can be easily retrieved with the templated method internalState<> on the database class.

#include <OgnCounterDatabase.h>
class OgnCounter:
{
    // Simple state information that counts how many times this node has been evaluated
    int m_evaluationCount{ 0 };
public:
    static bool compute(OgnCounterDatabase& db)
    {
        // The state information is on the node so it is the template parameter
        auto& state = db.internalState<OgnCounter>();
        // This prints the message and updates the state information
        std::cout << "This node has been evaluated " << state.m_evaluationCount++ << " times" << std::endl;
        return true;
    }
};
REGISTER_OGN_NODE()

Note

As access to internal state data is templated you can also store your state data in some external structure, however when you do so you must also be sure to override the setHasState() ABI method to always return true. Unless absolutely necessary it’s always easier to make the state a member variable of your node.

[Python Version]

Nodes With Version Upgrades

To provide code to upgrade a node from a previous version to the current version you must override the ABI function updateNodeVersion(). The current context and node to be upgraded are passed in, as well as the old version at which the node was created and the new version to which it should be upgraded. Passing both values allows you to upgrade nodes at multiple versions in the same code.

This example shows how a new attribute is added using the INode ABI interface.

#include <OgnMultiplyDatabase.h>
class OgnMultiply:
{
public:
    static bool compute(OgnMultiplyDatabase& db)
    {
        db.outputs.result() = db.inputs.a() * db.inputs.b() + db.inputs.offset();
        return true;
    }
    // Simply declaring the function is enough to register it as an override to the normal ABI function
    static bool updateNodeVersion(const GraphContextObj&, const NodeObj& nodeObj, int oldVersion, int newVersion)
    {
        if ((oldVersion == 1) && (newVersion == 2))
        {
            // Version upgrade manually adds the new attribute to the node.
            constexpr float zero = 0.0f;
            nodeObj.iNode->createAttribute(nodeObj, "inputs:offset", Type(BaseDataType::eFloat), &zero, nullptr, kAttributePortType_Input, kExtendedAttributeType_Regular, nullptr);
            return true;
        }
        // Always good practice to flag unknown version changes so that they are not forgotten
        db.logError("Do not know how to upgrade Multiply from version %d to %d", oldVersion, newVersion);
        return false;
    }
};
REGISTER_OGN_NODE()

[Python Version]