The Op API

The Op API offers a powerful C++ plug-in interface for manipulating the scene graph and modifying attributes. All of Katana's shipped Ops are written with the Op API. This API allows you to create plug-ins that can arbitrarily create and manipulate scene data. An Op can be given any number of scene graph inputs, inspect the attribute data at any location from those inputs, and can create, delete, and modify the attributes at the current location. Ops can also create and delete child locations, or even delete themselves.

In other words, anything that you can do with any Katana node, you can do with an Op. Examples of the things you can do with Ops include:

Using context-aware generators and importers,

Advanced custom merge operations,

Instancing of hierarchies,

Building network materials out of fragment parts, and

Processing to generate geometry for crowds.

Note:  Though the Op API is meant to take the place of the Scene Graph Generator and Attribute Modifier Plug-in APIs, they can still be used in post-2.0v1 version of Katana.

Op API Basics

Geolib3 is a library for efficiently loading and processing scene graph data. The Geolib3 scene graph is defined as a hierarchy of named scene graph locations, with each location having a set of named attributes. Scene graph locations are generated and processed on demand to support large data sets.

Example scene graph locations:

/root

/root/world/geo/mesh

Example attributes:

StringAttr("hello world")

FloatAttr([1.0 2.0 3.0 … ])

Operators (Ops) are the core processing unit of Geolib3, called upon to compute the scene graph’s locations and attributes. Ops can both generate new scene graph locations - the equivalent to Scene Graph Generators - and can also process incoming attributes - the equivalent to Attribute Modifiers. In fact, Geolib3 Ops are a super-set of both APIs and, in practice, no distinction is made between scene graph generation and modification. The code you need to write for the Op API is also much simpler.

Example Op (Pseudocode):

attr = getAttribute("taco")

setAttribute("cheese", value)

createChild("world")

The OpTree

The tree of connected Operators (the OpTree), is both persistent and mutable. The persistent OpTree allows Katana to inform Geolib3 of only the changes to the OpTree’s topology and arguments, rather than having to describe the complete set of Ops again from scratch. This persistent OpTree is efficient by not only allowing simpler update mechanisms when only a sub-set of Ops have changed, but is also more efficient from a computational standpoint, as the underlying engine can potentially reuse previously computed (and cached) results.

Katana cannot directly query from arbitrary Ops in the OpTree. Instead, Clients are created and pointed at an Op. Locations and attributes, which represent the cumulative result of the upstream OpTree, can then be computed upon request.

The Runtime is the underlying computational engine responsible for maintaining the persistent representation of the OpTree, scheduling Op execution, and delivering results to Clients. The runtime can be used either in a synchronous or asynchronous manner. The synchronous interaction model is common at render time while the asynchronous model is common during UI interaction.

Core Concepts with Geolib3

There are three core concepts in Geolib3 that pertains to the Op API: the Runtime, Ops, and Clients. In the sections below, we'll address the host of the system - the Runtime - and the services it provides before looking closely at Ops and the concept of the Client, and its use.

Geolib3: Into the Details

Geolib3 is Katana’s new deferred scene graph processing library. Geolib3 works at Katana’s core, processing and generating scene graph locations on demand, to support large data sets. Geolib3 supports an asynchronous processing model allowing the UI to remain responsive while scene graph data is being processed.

Operators (Ops) are the core processing unit of Geolib3. Ops can both generate new scene graph locations (equivalent to Geolib2 Scene Graph Generators) and process incoming attributes (equivalent to Geolib2 Attribute Modifiers).

Katana uses Clients to query attributes on specific locations when requested by the UI, for example to show attribute values in the Attributes tab, or during rendering, when the scene graph is traversed and processed to deliver data to the selected renderer.

Differences Between Geolib2 and Geolib3

Geolib2 did not have a persistent scene graph data model. Conceptually, the entire scene graph is reconstructed on every edit. Conversely, Geolib3’s OpTree is persistent, allowing for inter-cook scene data re-use.

Geolib2’s scene graph was traversed using an implicit index mechanism, for example getFirstChild(), getNextSibling(), with scene graph location names determined by the name attribute. In Geolib3, children are natively indexed by name. Thus, in Geolib3 you can selectively cook a location, by name, without cooking any peers. Consequently, the name attribute is meaningless. However, this also implies that locations cannot rename themselves (you can rename children, however).

Geolib2 was not amenable to either asynchronous or concurrent evaluation. Geolib3 supports both of these features.

The Runtime

The Runtime is responsible for coordinating Op execution, and provides a few key services:

A means of configuring and modifying the persistent OpTree. This includes creating instances of Ops, connecting Ops to each other, and providing them with arguments to determine their behavior. Within Katana, artists interact with nodes rather than the OpTree directly. There is roughly a 1:1 correspondence between nodes in the node graph and Ops in the OpTree.

The ability to register your interest in specific scene graph locations and their attributes that are produced as a result of evaluating the OpTree.

Internally, the Runtime has a number of other responsibilities including:

Managing the scheduling and evaluation of Ops.

Observing dependencies between Ops to ensure correct scene graph generation.

Caching of location and attribute data for retrieval.

Distribution of location and attribute data to clients.

The Runtime is able to use all the information it gathers from your interactions with it to efficiently manage resources. For example, if you don't attach any Clients to the OpTree then it does not need to evaluate any Ops or, if no dependencies exist between two Ops, it can concurrently schedule their evaluation to make best use of multicore systems.

From a technical perspective, you can interact with the Runtime through a C++ or Python interface, which provides a great deal of flexibility in how you configure your OpTree and listen to scene graph updates.

Interface

Languages Available

Ops C++ and Lua (with OpScript)
Client Configuration C++ and Python
OpTree Configuration C++ and Python

Ops

A Katana Geolib3 Op is the lowest level scene graph processing “unit”, responsible for building or processing scene graph data on demand. All scene graph loading/processing functionality internal to Katana is implemented using the same Op API available for your own custom development. Conceptually, Ops are a super-set of the old geometry APIs in Katana, including Scene Graph Generators and Attribute Modifiers.

Examples of what Ops can do include:

Setting attributes

Creating child scene graph locations

Deleting child scene graph locations

Getting attributes from the incoming scene graph

Getting the available Op arguments.

Rules for Ops include:

The OpTree defines the connectivity for what an Op sees as its input.

Each Op is responsible for registering a stateless function, which is called on demand at all locations on the input scene graph. This function is also referred to, later on, as the Cook function.

For Ops that do not have an input, the function is called at the root location giving them the opportunity to construct a more complex scene graph.

From the perspective of an Op implementer, the incoming scene is immutable. Only the output scene can be modified.

Although an Op can run on many different locations, it's called separately for each location. Each time an Op is called, the result is a single scene graph location - the 'output location'.

Ops are expected in their implementation to do the minimum amount of work necessary to produce the specified scene graph location, in order to be a “good citizen” in a deferred processing system.

Roughly speaking, when a downstream client is evaluated, all upstream Ops in the OpTree are run over all scene graph locations that exist (and are expanded) in the incoming tree. While there are more sophisticated API calls to change which Op runs at child locations, substituting out your OpArgs, the Optype, or even calling into another Op entirely, these can be ignored during your initial exposure to Op writing.

An Op is evaluated from a starting location. This is usually the familiar /root, however, Geolib3 provides mechanisms that allow you to redefine an Op’s starting location. The ability to change an Op’s starting location is extremely powerful and allows you to write Ops than can work either relative to your start location or in a more absolute manner.

Ops have two types of input:

1.   Op arguments - these are provided by the user to govern how the Op behaves when evaluated at a particular scene graph location. When you instantiate an Op you provide a set of root location arguments, which are the arguments the Op receives when run at its starting location. For instance, parameter values from nodes and system args, such as the current frame time, are passed to Ops using Op Arguments.
2.   Scene graph input(s) - locations and attributes that have been produced by other upstream Ops in the OpTree, which are connected to the Op currently being evaluated, are available as input and query-able in a read-only state.

The Runtime evaluates your Op at, potentially many, scene graph locations and it is up to you, the Op Writer, to determine the action taken at any particular location. As an Op Writer, you have access to a rich API, whose functionality can be broken down into three areas:

Functions to interrogate the scene graph location the Op is currently being evaluated at.

Functions to interrogate the state of the connected incoming scene graph.

Functions to modify the output scene graph as a result of evaluating the Op at a given location. In addition to changing the output scene graph, it is also possible for an Op to change, at evaluation time, what Op and corresponding arguments are evaluated at child locations. It's also possible for an Op to arbitrarily execute other Ops during its evaluation.

Clients

In order to view the scene graph locations and attributes generated as a result of evaluating Ops, such as to walk the scene graph to declare data to a renderer, or to inspect the values in the Attributes tab, we use Clients. A Client is connected to a specific Op in the OpTree and, in this context, we refer to it as a Terminal Op. We can control the scene graph locations we are interested in receiving updates for using the Client API.

To ensure the Runtime re-computes scene graph locations for every commit, set these locations as active. To ensure the Runtime re-computes a scene graph location’s children whenever it is cooked, set the location as open. As an extension to the open state, you can set a location to recursive open, which also sets the child locations produced as a result of evaluating that location to open in a recursive manner. This provides behavior the same as the existing forceExpand option. To conduct a one-shot computation of a scene graph location, you can instruct the Runtime to ready it.

Once you have created a Client, connected to a specific Op, you can then declare locations in the scene graph that you are interested in getting data from. The client then receives events from the Geolib3 Runtime when the requested data is ready.

Examples of Clients include:

The Attributes tab - sets a single location in the Scene Graph tab as active. When anything at that location is changed the Attributes tab is notified of the updates.

The Scene Graph tab - sets /root as active. As you open locations in the UI, these locations are set to open on the Client. On subsequent updates to the OpTree, open portions of the scene graph are automatically recomputed.

Renderers - typically make repeated calls to the Client to ready a location, read the required attributes, declare them to the renderer’s API, then immediately discard the data.

Client Configuration

You can point a Client at a particular Op, configure it to listen to particular locations, and interrogate the scene graph locations and attributes that are returned by the Runtime. With client configuration, you are also able to use transactions, which are objects used to batch together operations that are submitted to the Runtime at one time.

The first step in Client configuration is the setup of the client with the Runtime. To do this:

# Create the Runtime and a transaction to batch our work for the Runtime into runtime = FnGeolib.GetRegisteredRuntimeInstance() transaction = runtime.createTransaction()   # Create the Client and point it a terminalOp client = transaction.createClient() transaction.setClientOp(client, terminalOp)   # Push these changes to the Runtime runtime.commit([transaction,])   # Set a location we're interested in as active client.setLocationsActive(('/root/world',))

We can then ask the Client for information about the locations we’ve registered interest in, at any point:

# Get the list of changed locations locationDataChangeEvents = client.getLocationEvents() if not locationDataChangeEvents:     return   # Iterate over each location we've been informed about and interrogate it for event in locationDataChangeEvents:     location = event.getLocationPath()     locationData = event.getLocationData()     locationAttrs = locationData.getAttrs()     if not isinstance(locationAttrs, FnAttribute.GroupAttribute):         continue   # Only look at locations of type 'light' typeAttr = locationAttrs.getChildByName('type') if (not isinstance(typeAttr, FnAttribute.StringAttribute)     or typeAttr.getValue('', False) != 'light'):     continue   # Only want to look at xform or material updates for attrName in ('xform.matrix', 'material',):     attr = locationAttrs.getChildByName(attrName)   # Do something with attr

The OP API Explained

This section covers the following elements of the Op API:

The cook interface, what it is, and how it fits into Geolib3.

Op arguments and modifying arguments that are passed down to children.

Scene graph creation and hierarchy topology management, including how to create and delete scene graph locations, and controlling where an Op is executed.

Reading scene graph input from potentially many inputs, and the associated issues.

CEL and other utility functions that are available to you, as an Op writer, to accomplish common tasks.

Integrating your Op with the node graph.

You can find concrete examples of the above concepts in the $KATANA_HOME/plugins/Src/Ops directory where the source code for a number of core Ops is kept. Below is a brief overview of some of these Ops, and examples of where they are currently used:

AttributeCopy - provides the implementation for the AttributeCopy node, which copies attributes at locations from one branch of a scene to another.

AttributeSet - the back-end to the AttributeSet node, it allows you to set, change, and delete attributes at arbitrary locations in the incoming scene graph.

HierarchyCopy - like the AttributeSet Op, it's the back-end to the HierarchyCopy node, allowing you to copy arbitrary portions of scene graph hierarchy to other parts of the scene graph.

Prune - removes any locations that match the CEL expression you provide from the scene.

StaticSceneCreate - produces a static hierarchy based on a set of arguments you provide. This Op is the core of HierarchyCreate, and is used extensively by other Ops and nodes to produce the hierarchies of locations and attributes that they need. For example, the CameraCreate node uses a StaticSceneCreate Op to produce the required hierarchy for a camera location.

The Cook Interface

The cook interface is the interface Geolib3 provides to implement your Op’s functionality. You are passed a valid instance of this interface when your Op’s cook() method is called. As discussed above, this interface provides methods that allow you to interrogate arguments, create or modify scene graph topology, and read scene graph input. You can find a full list of the available methods on the cook interface in $KATANA_HOME/plugin_apis/include/FnGeolib/op/FnGeolibCookInterface.h.

Op Arguments

As discussed previously, Ops are provided with two forms of input: scene graph input created by upstream Ops and Op arguments, which are passed to the Op to configure how it should run. Examples of user arguments include CEL statements describing the locations where the Op should run, a file path pointing to a geometry cache that should be loaded, or a list of child locations the Op should create.

We’ll first look at the simple case of interrogating arguments and then look at a common pattern of recursively passing arguments down to child locations.

Reading Arguments

Arguments are passed to your Op as instances of the FnAttribute class. The cook interface has the following function call to retrieve Op arguments:

FnAttribute::Attribute getOpArg(     const std::string& specificArgName = std::string()) const;

For example, the StaticSceneCreate Op accepts a GroupAttribute called a that contains a list of attributes, which contain values to set at a given location. This appears as:

StaticSceneCreate handles the a argument as follows:

FnAttribute::GroupAttribute a = interface.getOpArg("a"); if (a.isValid()) {     for (int i = 0; i < a.getNumberOfChildren(); ++i)     {         interface.setAttr(a.getChildName(i), a.getChildByIndex(i));     } }

Note:  It's important to check the validity of an attribute after retrieving it using the isValid() call. You should check an attribute’s validity every time you are returned an attribute from the cook interface.

Passing Arguments to Child Locations

There is a common recursive approach to passing arguments down to child locations on which an Op runs. The StaticSceneCreate Op exemplifies this pattern quite nicely.

StaticSceneCreate sets attributes and creates a hierarchy of child locations based on the value of one of the arguments passed to it. This argument is a GroupAttribute that for each location describes:

a - attributes values

c - the names of any child locations

x - whether an additional Op needs to be evaluated at that location

To pass arguments to the children it creates, it peels off the lower layers of the c argument and passes them to its children. Conceptually, you can consider it as follows (details of a and x are omitted for brevity):

The Op running at its current location reads a, c, and x. For each child GroupAttribute of c it creates a new child location with the GroupAttribute’s name, for example, child1/child2, and pass the GroupAttribute as that location’s arguments.

Creating a child in code makes use of the following key function call:

void createChild(const std::string& name,                  const std::string& optype = "",                  const FnAttribute::Attribute& args = FnAttribute::Attribute(),                  ResetRoot resetRoot = ResetRootAuto,                  void* privateData = 0x0,                  void (*deletePrivateData)(void* data) = 0x0);

The createChild() function creates a child of the location where the Op is being evaluated at. The function also instructs the Runtime the type of Op that should run there (by default, the same type of Op as the Op that called createChild()) and the arguments that should be passed to it. In StaticSceneCreate this looks as follows:

for (int childindex = 0; childindex < c.getNumberOfChildren(); ++childindex) {     std::string childName = c.getChildName(childindex);     FnAttribute::GroupAttribute childArgs = c.getChildByIndex(childindex);     interface.createChild(childName, "", childArgs); }

Scene Graph Creation

One of the main tasks of an Op is to produce scene graph locations and attributes. The Op API offers a rich set of functionality in order to do this. There are five key functions that can be used to modify scene graph topology and control Op execution, which we'll explain below.

Note:  It is important to remember the distinction between the set of functions described here and those described in Reading Scene Graph Input. All functions described here operate on the output of an Op at a given Scene Graph location. The functions described in Reading Scene Graph Input relate only to reading the scene graph data on the input of an Op at a given scene graph location, which is immutable.

The setAttr() Function

void setAttr(const std::string& attrName,              const FnAttribute::Attribute& value,              const bool groupInherit = true);

The setAttr() function allows you to set an attribute value at the location at which your Op is currently being evaluated. For example, to set a StringAttribute at your Op’s root location you can do the following:

if (interface.atRoot()) {     interface.setAttr("myAttr", FnAttribute::StringAttribute("Val")); }

It is not possible to set attributes at locations other than those where your Op is currently being evaluated. If you call setAttr() for a given attribute name multiple times on the same location, the last one called is the one that is used. The groupInherit parameter is used to determine if the attribute should be inherited by its children.

Note:  Since setAttr() sets values on the Op’s output, while getAttr() is reading immutable values on a given input, if a call to setAttr() is followed immediately by getAttr(), the result is still just the value from the relevant input, rather than returning the value set by the setAttr().

The createChild() Function

void createChild(const std::string& name,                  const std::string& optype = "",                  const FnAttribute::Attribute& args = FnAttribute::Attribute(),                  ResetRoot resetRoot = ResetRootAuto,                  void* privateData = 0x0,                  void (*deletePrivateData)(void* data) = 0x0);

The createChild() function allows you to create children under the scene graph location at which your Op is being evaluated. In the simplest case it requires the name of the location to create, and arguments that should be passed to the Op that is evaluated at that location. For example:

interface.createChild(childName, "", childArgs);

If you specify optype as an empty string, the same Op that called create child is evaluated at the child location. However, you can specify any other optype and that is run instead.

Note:  Multiple calls to createChild() for the same named child location causes the last specified optype to be used, that is to say, successive calls to createChild() mask prior calls.

The resetRoot parameter takes one of three values:

ResetRootTrue - the root location of the Op evaluated at the new location is reset to the new location path.

ResetRootAuto (the default) - the root location is reset only if optype is different to the Op calling createChild().

ResetRootFalse - the root location of the Op evaluated at the new location is inherited from the Op that called createChild().

This parameter controls what is used as the rootLocation for the Op when it is run at the child location.

The execOp() Function

void execOp(const std::string& opType,             const FnAttribute::GroupAttribute& args);

By the time the Geolib3 Runtime comes to evaluating the OpTree, it is static and fixed. The cook interface provides a number of functions, which allow you to request that Ops that were not declared when the OpTree was constructed, be executed during evaluation time of the OpTree.

We have already seen how createChild() allows you to do this by allowing you to specify which Op is run at the child location. The execOp() function allows an Op to directly call the execution of another Op, providing another mechanism to evaluate Ops, which are not directly declared in the original OpTree. This differs from the createChild() behavior, where we declare a different Op to run at child locations in a number of ways, including that:

It should be thought of as a one-shot execution of another Op, and

The Op specified in the execOp() call is evaluated as if it were being run at the same location with the same root location as the caller.

You can see execOp() in action in the StaticSceneCreate Op, where Op types are specified in the x argument:

// Exec some ops? FnAttribute::GroupAttribute opGroups = interface.getOpArg("x"); if (opGroups.isValid()) {     for (int childindex = 0; childindex < opGroups.getNumberOfChildren();          ++childindex)     {          ...          if (!opType.isValid() || !opArgs.isValid())          {              continue;          }          interface.execOp(opType.getValue("", false), opArgs);      } }

The deleteSelf() Function

void deleteSelf();

Thus far, we have only seen mechanisms to add data to the scene graph, but the deleteSelf() function and the associated function deleteChild() allow you to remove locations from the scene graph. Their behavior is self-explanatory but their side effects are less intuitive and are explained fully in Reading Scene Graph Input. For now, however, an example for what a Prune Op may look like by using the deleteSelf() function call is shown below:

// Use CEL Utility function to evaluate CEL expression FnAttribute::StringAttribute celAttr = interface.getOpArg("CEL"); if (!celAttr.isValid())     return;   Foundry::Katana::MatchesCELInfo info; Foundry::Katana::MatchesCEL(info, interface, celAttr);   if (!info.matches)     return; // Otherwise, delete myself interface.deleteSelf();   return;

The stopChildTraversal() Function

void stopChildTraversal();

The stopChildTraversal() function is one of the functions that allows you to control on which locations your Op is run. It stops the execution of this Op at any child of the current location being evaluated. It is best explained by way of example.

Say we have an input scene:

/root

    /world

         /light

Say what we want is:

/root

    /world

         /geo

             /taco

         /light

So we use a StaticSceneCreate Op to create this additional hierarchy at the starting location /root/world:

/geo

    /taco

However, if we don’t call stopChildTraversal() when the StaticSceneCreate Op is at /root/world then this Op is run at both /root/world and /root/world/light, resulting in:

/root

    /world

        /geo

            /taco

        /light

            /geo

                /taco

To summarize, stopChildTraversal() stops your Op from being automatically evaluated at any of the child locations that exist on its input. The most common use of stopChildTraversal() is for efficiency. If we can determine, for example, by looking at a CEL expression, that this Op has no effect at any locations deeper in the hierarchy than the current one, it's good practice to call stopChildTraversal() so that we don’t even call this Op on any child locations.

Reading Scene Graph Input

There are a range of functions that read the input scene graph produced by upstream Ops. All these functions allow only read functionality; the input scene is immutable.

The getNumInputs() function

int getNumInputs() const;

An Op can have the output from multiple other Ops as its input. Obvious use cases for this are instances where you wish to merge multiple scene graphs produced by different OpTrees into a single scene graph, comparing attribute values in two scene graph states, or copying one scene graph into another one. The getNumInputs() function allows you to determine how many Ops you have as inputs, which is a precursor to interrogating different branches of the OpTree for scene graph data.

Warning:  It is worth noting that, given the deferred processing model of Geolib3, the “get” functions, such as getAttr(), getPotentialChildren(), doesInputExist(), may ask for scene graph information that has not yet been computed.

In such this instance, your Op’s execution is aborted (using an exception) and re-scheduled when the requested location is ready. Thus, Op writers should not attempt to blindly catch all exceptions with “(...)” and, furthermore, should attempt to write exception-safe code.

If a user Op does accidentally catch one of these exceptions, the runtime detects this and considers the results invalid, generating an error in the scene graph.

If your Op is only reading from its default input location (and index) or its parents, “recooks” are unlikely to occur. However, for scattered access queries, either on the input location path or on the input index, "recooks" are likely. If an Op needs to do scattered access queries from a multitude of locations, which would otherwise have unfortunate performance characteristics, an API call - prefetch() - is available and is discussed in further detail later on.

The getAttr() Function

FnAttribute::Attribute getAttr(     const std::string& attrName,     const std::string& inputLocationPath = std::string(),     int inputIndex = kFnKatGeolibDefaultInput) const;

It is often necessary to perform some action or compute a value based on the result stored in another attribute. The getAttr() function allows you to interrogate any part of the incoming scene graph by providing the attribute name and a scene graph location path (either absolute or relative). Additionally, you can specify a particular input index to obtain the attribute value from, which must be smaller than the result of getNumInputs(). It is important to note that getAttr always returns the value as seen at the input to the Op. If you wish to consider any setAttrs already made, either by yourself or another Op invoked with execOp, you must use getOutputAttr.

The following diagram illustrates some of the subtleties of this and, most importantly, that getAttr in an execOp Op, only sees the results of the calling Op when the query location is the current location, otherwise you see the input to the calling Op’s ‘slot’ in the Op graph.

The getPotentialChildren() Function

FnAttribute::StringAttribute getPotentialChildren(     const std::string& inputLocationPath = std::string(),     int inputIndex = kFnKatGeolibDefaultInput) const;

In Scene Graph Creation the function deleteSelf() was introduced, noting that the consequence of such a call is more subtle than it may have first appeared. When an upstream Op is evaluated and creates children, if downstream Ops have the ability to delete them, the upstream Op can only go so far as to state that the children it creates may potentially exist after a downstream Op has been evaluated at those child locations. This is because the Op has no knowledge of what a downstream Op may do when evaluated at such a location. To that extent, getPotentialChildren() returns a list of all the children of a given location on the input of an Op.

The prefetch() Function

void prefetch(const std::string& inputLocationPath = std::string(),               int inputIndex = kFnKatGeolibDefaultInput) const;

Maintain good code practice by using prefetch() as early as possible in the code for your Op's cook function.

The prefetch() function can be used to indicate dependencies between scene graph locations. For example:

/root/world/geo/a may call prefetch() for /root/world/geo/b because during the processing of /root/world/geo/a, attributes present at ./b will be required.

CEL and Utilities

There are a number of tasks that Ops are frequently required to complete, such as:

Creating a hierarchy of locations,

Determining whether an Op should run based on a CEL expression argument,

Reporting errors to the user through the scene graph, and

Obtaining well-known attribute values in an easy to use format, for example, bounding boxes.

Currently you can find headers for these utilities in:

$KATANA_HOME/plugin_apis/include/FnGeolib/op/FnGeolibCookInterface.h

$KATANA_HOME/plugin_apis/include/FnGeolib/util/*

The utility implementations live in:

$KATANA_HOME/plugin_apis/src/FnGeolib/op/FnGeolibCookInterfaceUtils.cpp

$KATANA_HOME/plugin_apis/src/FnGeolib/util/*

Many of these utilities are self-documenting and follow similar patterns. The following example demonstrates using the CEL matching utilities:

// Should we run here? If not, return. FnAttribute::StringAttribute celAttr = interface.getOpArg("CEL"); if (!celAttr.isValid())     return; Foundry::Katana::MatchesCELInfo info; Foundry::Katana::MatchesCEL(info, interface, celAttr); if (!info.canMatchChildren) {     interface.stopChildTraversal(); } if (!info.matches)     return;

In the example above, a couple of things are achieved:

1.   We determine whether the CEL expression could potentially match any of the children, and if not, we direct the Runtime to not evaluate this Op at child locations.
2.   We determine whether we should run at this location, and return early if not.

Feel free to explore the range of utility functions available, as it can increase your productivity when writing Ops.