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.

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;

Given the concurrent nature of Geolib3, it's entirely possible that an attribute or location being requested on the input may not yet have been computed, in which case your Op is rescheduled and re-evaluated at a later point. The prefetch function can be called from within your Op’s cook() function to instruct the Runtime that you require a given location soon. Essentially, you can think of it as an explicit statement to the Runtime of your dependency on another location.

Tip:  You can use prefetch() to maintain good code practice by using it as early as possible in the code for your Op's cook function. For instance, if your Op depends on data from locations other than the current output location, from any of the inputs, this would be an ideal time to use prefetch().

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.

When using the CEL library you are required to link against libCEL.so, which you can find in $KATANA_HOME/bin/Geolib3/internal/CEL.

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