High Level (Toolkit) API (NvBlastTk)

Table of Contents

Introduction to NvBlastTk

NvBlastTk Class Hierarchy

Linking and Header Files

Creating the TkFramework

Creating a TkAsset

Instancing a TkAsset: Creation of a TkActor and a TkFamily

Groups

Applying Damage to Actors and Families

Joints

Events

Object and Type Identification

Introduction to NvBlastTk

The high-level API, NvBlastTk (Tk stands for “toolkit”), is intended to be a more powerful library and a much more convenient entry point into the use of Blast. Like the low-level library, Tk is physics and graphics-agnostic. Whereas the low-level API is C-style, Tk uses a C++ API. Everything in Tk is in the namespace:

Nv::Blast

(the only exceptions are global-scope functions to create and access a framework singleton, see below). Every object in Tk is prefixed with ‘Tk’. For example, the Tk framework interface is:

Nv::Blast::TkFramework

For the remainder of this page we will be in the Nv::Blast namespace, and will drop the explicit scope Nv::Blast:: from our names.

BlastTk adds:

  • An object class hierarchy (see NvBlastTk Class Hierarchy, below).

  • A global framework, TkFramework (a singleton). This keeps track of TkIdentifiable objects and allows the user to query them based upon either GUID or TkIdentifiable subclass type, and also provides a number of functions to create the various objects in BlastTk.

  • Processing groups with a task interface (see TkGroup).

  • Event dispatching for actor families (see TkFamily).

  • Intra-actor and inter-actor joint management (see TkJoint). Note, these “joints” only hold descriptor data, since physical objects are not handled by BlastTk.

NvBlastTk Class Hierarchy

  • There are two abstract interfaces, one of which deriving from the other: TkObject <- TkIdentifiable. * Lightweight objects are derived from TkObject. * Objects which use a GUID and class identification are derieved from TkIdentifiable.

  • TkAsset derives from TkIdentifiable. This is mostly a wrapper for NvBlastAsset, however it also stores extra data associated with the asset such as internal joint descriptors.

  • TkFamily derives from TkIdentifiable. One of these objects is made when a TkActor is instanced from a TkAsset. All actors that are created by splitting the family’s original actor remain within the same family. Actor and joint events are dispatched from the TkFamily.

  • TkGroup derives from TkIdentifiable. Groups are processing units. The user may create as many groups as they please, and add or remove actors as they please from groups. The group provides a worker (TkGroupWorker) interface which allows the user to process multiple jobs in the group asynchoronously. These jobs, along with a call to TkGroup::endProcess(), perform the tasks of generating fracture commands, applying fracture commands, and actor splitting at the low-level. The user is informed of splitting through listeners given to TkFamily objects.

  • TkActor derives from TkObject. It is mostly a wrapper for NvBlastActor, but it also provides a number of damage functions to the user.

  • TkJoint derives from TkObject. TkAsset descriptors, cause internal TkJoint obejcts to be created within an actor (joining chunks within the same actor). Alternatively, the TkFramework provides a function which allows the user to create an external joint between any two different actors. As actors split, internal joints may become external. The user gets notification whenever joints become external, or when actors joined by joints change or are deleted, through listeners attached to the associated TkFamily objects.

Linking and Header Files

To use the BlastTk library, the application need only inlclude the header NvBlastTk.h, found in the include/toolkit folder, and link against the appropriate version of the NvBlastTk library. Depending on the platform and configuration, various suffixes will be added to the library name. The general naming scheme is

NvBlastTk(config)(arch).(ext)

(config) is DEBUG, CHECKED, OR PROFILE for the corresponding configurations. For a release configuration there is no (config) suffix.

(arch) is _x86 or _x64 for Windows 32- and 64-bit builds, respectively, and empty for non-Windows platforms.

(ext) is .lib for static linking and .dll for dynamic linking on Windows.

Creating the TkFramework

As a reminder, in this document we assume we are in the Nv::Blast namespace:

using namespace Nv::Blast;

In order to use NvBlastTk, one first has to create a TkFramework singleton. This simply requires a call to the global function NvBlastTkFrameworkCreate:

TkFramework* framework = NvBlastTkFrameworkCreate();

The framework may be accessed via:

TkFramework* framework = NvBlastTkFrameworkGet();

In the sections that follow, it is assumed that a framework has been created, and we have a pointer to it named ‘framework’ within scope.

Finally, to release the framework, use

framework->release();

This will release all assets, families, actors, joints, and groups.

Creating a TkAsset

The TkAsset object is a high-level wrapper for the low-level NvBlastAsset (see Creating an Asset from a Descriptor (Authoring)). The descriptor used to create a TkAsset, a TkAssetDesc, is derived from NvBlastAssetDesc. The base fields should be filled in as described in (Creating an Asset from a Descriptor (Authoring)). The new field is an optional array of flags to be associated with each bond in the base descriptor. Currently the only flag is “BondJointed,” and if set will cause an “internal joint” to be created in actors (TkActor type) created from the asset. See (Joints) for more on joints in BlastTk.

TkAssetDesc desc;

myFunctionToFillInLowLevelAssetFields(desc);    // Fill in the low-level (NvBlastAssetDesc) fields as usual

std::vector<uint8_t*> bondFlags(desc.bondCount, 0); // Clear all flags

// Set BondJointed flags corresponding to joints selected by the user (assumes a myBondIsJointedFunction to make this selection)
for (uint32_t i = 0; i < desc.bondCount; ++i)
{
    if (myBondIsJointedFunction(i)) // User-authored
    {
        bondFlags[i] |= TkAssetDesc::BondJointed;
    }
}

TkAsset* asset = framework->createAsset(desc);  // Create a new TkAsset

The createAsset function used above creates a low-level NvBlastAsset from the base fields of the descriptor, and then adds internal joint descriptors based upon the bonds’ centroids and attached chunks. An alternative method to create a TkAsset allows the user to pass in a pre-existing NvBlastAsset, and a list of joint descriptors. If the TkAsset is to have no internal joints, then the joint descriptors are not necessary and with an NvBlastAsset pointer llAsset, a TkAsset may be created simply by using

TkAsset* asset = framework->createAsset(llAsset);

By default, such a TkAsset will not “own” the llAsset. When the TkAsset is released, the llAsset memory will be untouched. You can pass ownership to the TkAsset using all of the default parameters of the createAsset function:

TkAsset* asset = framework->createAsset(llAsset, nullptr, 0, true);

The last parameter sets ownership. N.B.: in order for the TkAsset to own the underlying llAsset, and therefore release it when the TkAsset is released, the memory for the llAsset must be allocated using the allocator accessed through NvBlastGlobals (see Globals API (NvBlastGlobals)).

If one wants to author internal joints in a TkAsset using this second createAsset method, one must pass in a valid array of joint descriptors of type TkAssetJointDesc. Each joint descriptor takes two positions and two node indices. The positions are the joint’s attachment positions in asset space, and the nodes indices are those of the graph nodes that correspond to support chunks. These indices are not, in general, the same as the chunk indices. An example of initialization of the joint descriptors is given below.

std::vector<TkAssetJointDesc> jointDescs(jointCount);   // Assume jointCount = the number of joints to add
jointDescs[0].nodeIndices[0] = 0;   // Attach node 0 to node 1
jointDescs[0].nodeIndices[1] = 1;
jointDescs[0].attachPoistions[0] = nvidia::NvVec3( 1.0f, 2.0f, 3.0f );  // Attachment positions are often the same within an asset, but they don't have to be
jointDescs[0].attachPoistions[1] = nvidia::NvVec3( 1.0f, 2.0f, 3.0f );
// ... etc.

TkAsset* asset = framework->createAsset(llAsset, jointDescs.data(), jointDescs.size());

The code above assumes you know the support graph nodes to which you’d like to attach joints. Often, the user only knows the corresponding chunk indices. Fortunately it’s easy to map chunk indices to graph node indices. In order to get the map, use the low-level function

const uint32_t map = NvBlastAssetGetChunkToGraphNodeMap(llAsset, logFn);

This map is an array with an entry for every chunk index. To get the graph node index for a chunk indexed chunkIndex, use

uint32_t nodeIndex = map[chunkIndex];

If the chunk indexed by chunkIndex does not correspond to a support chunk, then the mapped value will be UINT32_MAX, the invalid index. Otherwise, the mapped value will be a valid graph node index.

Finally, to release a TkAsset, as with any TkObject-derived object, use the release() method:

asset->release();

Instancing a TkAsset: Creation of a TkActor and a TkFamily

Whereas with the Blast low-level (Low Level API (NvBlast)), one must explicitly create a family (NvBlastFamily) from an asset (NvBlastAsset) before creating the first actor (NvBlastActor) in the family, NvBlastTk creates a TkFamily automatically when an unfractured TkActor is instanced from a TkAsset using the framework’s createActor function. This family is accessible through the actor and any actor that is created from splitting it. The family is not released automatically when all actors within it have been released. The user must use the TkFamily’s release() method (see TkObject base API) to do so. (Or wait until the framework is released.) If a family is released that contains actors, the actors within will be released as well.

The TkFamily has a special role in NvBlastTk, holding user-supplied event listeners (Events). All internal actor creation and destruction events are broadcast to listeners through split events (TkSplitEvent). These signal when a fracturing operation has destroyed an actor and created child actors from it. TkActor creation or release that occurs from an explicit API call do not produce events. For example when creating a first unfractured instance of an asset using createAsset, or when calling the release() method on a TkActor. TkJoint events are similarly broadcast to receivers (TkJointEvent). These signal when the actors which are joined by the joints change, so that the user may update a corresponding physical joint. They also signal when a joint no longer attaches actors and is therefore unreferenced. The user may invalidate or release the joint using the TkObject release() method when this occurs (more on joint ownership in Joints).

To create an unfractured TkActor instance from a TkAsset, one first fills in a descriptor (Instancing a TkAsset: Creation of a TkActor and a TkFamily) and passes it to the framework’s createActor function. As with the TkAssetDesc, the TkActorDesc is derived from its low-level counterpart, the NvBlastActorDesc. In addition the TkActorDesc holds a pointer to the TkAsset being instanced. An example of TkActor creation is given below, given a TkAsset pointer asset.

TkActorDesc desc;   // The TkActorDesc constructor sets sane default values for the base (NvBlastActorDesc) fields, giving uniform chunk and bond healths of 1.0.
desc.asset = asset; // This field of TkActorDesc must be set to a valid asset pointer.

TkActor* actor = framework->createActor(desc);

The TkFamily created with the actor above may be accessed through the actor’s getFamily field:

TkFamily& family = actor->getFamily();

The returned value is a reference since a TkActor’s family can never be NULL. Actors resulting from the split of a “parent” actor will always belong to the parent’s family.

For most applications, the user will need to create a listener object to pass to every family created, in order to keep their physics and graphics representations in sync with the splitting of the TkActor. For more on this, see Events.

Groups

One important feature of NvBlastTk is the ability to multitask damage processing. The mechanism by which the toolkit does this is the group object, TkGroup. Groups are created at the request of the user; the user may create as many groups as he or she likes. Actors may be added or removed from groups in any way the user wishes, with the only constraint being that a given actor may belong to no more than one group. A group is a processing object, much like a scene in a physics simulation. Indeed, a natural pattern would be to associate one group per physics scene, and synchronize the group processing with scene simulation. Another pattern would be to subdivide the world into neighborhoods, and associate each neighborhood with a group. A distributed game could take advantage of this structure to similarly distribute computation.

Group processing is performed by workers, which have a TkGroupWorker API exposed to the user. The number of workers may be set by the user, with the idea being that this should correspond to the number of threads available for group processing. Processing starts with a call to TkGroup::startProcess(). This creates a number of jobs which the user may assign to workers as they like, each worker potentially on its own thread. The jobs calculate the effects of all damage taken by the group’s actors. After all jobs have been run, the user must call TkGroup::endProcess(). This will result in all events being fired off to listeners associated with families with actors in the group.

A convenience function, TkGroup::process(), is provided which uses one worker to perform all jobs sequentially on the calling thread. This is useful shortcut to get BlastTk up and running quickly. A multithreaded group processing implementation is given by TkGroupTaskManager (in NvBlastTkGroupTaskManager.h).

Actors resulting from the split of a “parent” actor will be placed automatically into the group that the parent belonged to. This is similar to the assigment of families from a split, except that unlike families, the user then has the option to move the new actors to other groups, or no group at all.

Also similar to families, groups are not automatically released when the last actor is removed from it. Unlike families, when a group is released, the actors which belong to the group are not released. They will, however, be removed from the group before the release is complete.

A typical usage is outlined below. See Applying Damage to Actors and Families for methods of applying damage to actors.

// Create actors from descriptors desc1, desc2, ... etc., and attach a listener to each new family created
TkActor* actor1 = framework->createActor(desc1);
actor1->getFamily().addListener(gMyReceiver);       // gMyReceiver is a TkEventListener-derived object.  More on events in a subsequent section.
TkActor* actor2 = framework->createActor(desc2);
actor2->getFamily().addListener(gMyReceiver);
TkActor* actor3 = framework->createActor(desc3);
actor3->getFamily().addListener(gMyReceiver);
// etc...

// Let's create two groups.  First, create a group descriptor.  This may be used to create both groups.
TkGroupDesc groupDesc;
groupDesc.workerCount = 1;  // this example processes groups on the calling thread only

// Now create the groups
TkGroup* group1 = framework->createGroup(groupDesc);
TkGroup* group2 = framework->createGroup(groupDesc);

// Add actor1 and actor2 to group1, and actor2 to group3...
group1->addActor(actor1);
group1->addActor(actor2);
group2->addActor(actor3);
// etc...

// Now apply damage to all actors - *NOTE* damage is described in detail in the next section.
// For now we will just assume a "myDamageFunction" to apply the damage.
myDamageFunction(actor1);
myDamageFunction(actor2);
myDamageFunction(actor3);
// etc...

// Calling the groups' process functions will (synchronously) run all jobs to process damage taken by the contained actors.
group1->process();
group2->process();

// When the groups are no longer needed, they may be released with the usual release method.
group1->release();
group2->release();

Multithreaded processing

When distributing the jobs as mentioned above, every job must be processed exactly once (over all user tasks).

The number of jobs processed per worker can range from a single job (resulting in a user task per job) to all jobs (like Nv::Blast::TkGroup::process() does).

At any point in time, no more than the set workerCount amount of workers may have been acquired. Return the worker at the end of each task.

Nv::Blast::TkGroupWorker* worker = group->acquireWorker();
// process some jobs
group->returnWorker(worker);

Applying Damage to Actors and Families

Damage in NvBlastTk uses the same damage program scheme as the low-level SDK (see Damage and Fracturing). One passes the program (NvBlastDamageProgram), damage descriptor (program-dependent), and material (also program-dependent) to a TkActor::damage function. Ultimately, the damage descriptor and material data are all parameters used by the damage program. The distinction is that the damage descriptor should describe properties of the thing doing the damage, while the material should describe properties of the actor (the thing being damaged). The interpretation of this data is entirely up to the program’s functions, however.

For convenience, the user may set a default material in the actor’s family. This assumes, of course, that the material parameters for this default are compatible with the program being used to damage the family’s actors.

Examples of the three TkActor damage methods are given below.

Multiple Damage Descriptors using NvBlastProgramParams

N.B. - with this method of damage, the lifetime of the NvBlastProgramParams *must* extend at least until the TkGroup::endProcess call for the actor.

NvBlastDamageProgram program =
{
    myGraphShaderFunction,      // A function with the NvBlastGraphShaderFunction signature
    mySubgraphShaderFunction    // A function with the NvBlastSubgraphShaderFunction signature
};

// The example struct "RadialDamageDesc" is modeled after NvBlastExtRadialDamageDesc in the NvBlastExtShaders extension
RadialDamageDesc damageDescs[2];

damageDescs[0].compressive = 10.0f;
damageDescs[0].position[0] = 1.0f;
damageDescs[0].position[1] = 2.0f;
damageDescs[0].position[2] = 3.0f;
damageDescs[0].minRadius = 0.0f;
damageDescs[0].maxRadius = 1.0f;

damageDescs[1].compressive = 100.0f;
damageDescs[1].position[0] = 3.0f;
damageDescs[1].position[1] = 4.0f;
damageDescs[1].position[2] = 5.0f;
damageDescs[1].minRadius = 0.0f;
damageDescs[1].maxRadius = 5.0f;

// The example material "Material" is modeled after NvBlastExtMaterial in the NvBlastExtShaders extension
Material material;

material.health = 10.0f;
material.minDamageThreshold = 0.1f;
material.maxDamageThreshold = 0.8f;

// Set the damage params struct
NvBlastProgramParams params = { damageDescs, 2, &material };

// Apply damage
actor->damage(program, &params);    // params must be kept around until TkGroup::endProcess is called!

Single Damage Descriptor with Default TkFamily Material

This method of damage copies the damage descriptor into a buffer, so the user need not hold onto a copy after the damage function call. Only one damage descriptor may be passed in at once.

To use this method, the user must first set a default material in the actor’s family. For example:

// The example material "Material" is modeled after NvBlastExtMaterial in the NvBlastExtShaders extension
Material material;

material.health = 10.0f;
material.minDamageThreshold = 0.1f;
material.maxDamageThreshold = 0.8f;

// Set the default material used by the material-less TkActor::damage call
actor->getFamily().setMaterial(&material);

N.B. the lifetime of the material set *must* extend at least until the TkGroup::endProcess call for the actor.

Then to apply damage, use:

NvBlastDamageProgram program =
{
    myGraphShaderFunction,      // A function with the NvBlastGraphShaderFunction signature
    mySubgraphShaderFunction    // A function with the NvBlastSubgraphShaderFunction signature
};

// The example struct "RadialDamageDesc" is modeled after NvBlastExtRadialDamageDesc in the NvBlastExtShaders extension
RadialDamageDesc damageDesc;

damageDesc.compressive = 10.0f;
damageDesc.position[0] = 1.0f;
damageDesc.position[1] = 2.0f;
damageDesc.position[2] = 3.0f;
damageDesc.minRadius = 0.0f;
damageDesc.maxRadius = 1.0f;

// Apply damage
actor->damage(program, &damageDesc, (uint32_t)sizeof(RadialDamageDesc));

Single Damage Descriptor with Specified Material

This method is just like the one above, except that the user has the opportunity to override the material used during damage.

N.B. - the lifetime of the material passed in *must* extend at least until the TkGroup::endProcess call for the actor.

This call is just like the one above with an extra material parameter:

actor->damage(program, &damageDesc, (uint32_t)sizeof(RadialDamageDesc), &material);

Joints

Joints in NvBlastTk are abstract representations of physical joints. When joints become active, change the actors they join, or become unreferenced (the actors they join disappear), the user will receive notification via a TkJointUpdateEvent (see Events).

Joints may be defined as a part of a TkAsset, in which case they are consisdered “internal” joints. (See Creating a TkAsset.) Since the first instance of a TkAsset is a single TkActor, internal joints are defined between chunks within the same actor. Therefore they are not active (there is no point in joining two locations in a single rigid body). Upon splitting into multiple actors, however, an internal joint’s chunks may now belong to two different TkActors. When this happens, the user will receive a TkJointUpdateEvent of subtype TkJointUpdateEvent::External. The event contains a pointer to the TkJoint, and from that the user has access to the information needed to create a physical joint between the rigid bodies that correspond to the joined TkActors.

Joints may also be created externally at runtime, using the TkFramework::createJoint function. A joint created this way must be between two different TkActors. Because of this, the joint is immediately considered active, and so no TkJointUpdateEvent is generated from its creation. The user should create a physical joint to correspond to the joint returned by createJoint. An externally created joint of this type has another distinguishing characteristic: it may join an actor to “the world,” or “Newtonial Reference Frame” (NRF). To do this, one TkFamily pointer in the joint descriptor is set to NULL. Examples are given below.

TkJointDesc desc;
desc.families[0] = &actor0->getFamily();    // Assume we have a valid actor0 pointer
desc.chunkIndices[0] = 1;   // This chunk *must* be a support chunk in the asset that created desc.families[0]
desc.attachPositions[0] = nvidia::NvVec3(1.0f, 2.0f; 3.0f); // The attach position is in asset space
desc.families[1] = &actor1->getFamily();    // Assume we have a valid actor1 pointer... note, actor0 and actor1 could have the same family
desc.chunkIndices[1] = 10;  // This chunk *must* be a support chunk in the asset that created desc.families[1]
desc.attachPositions[1] = nvidia::NvVec3(4.0f, 5.0f; 6.0f); // The attach position is in asset space

// Create the external joint from the descriptor, which joins actor0 and actor1
TkJoint* joint = framework->createJoint(desc);

// Now join actor0 to the NRF
// desc.families[0] already contains actor0's family
desc.chunkIndices[0] = 2;   // Again, this chunk must be a support chunk in the asset that created desc.families[0]
desc.attachPositions[0] = nvidia::NvVec3(0.0f, 0.0f; 0.0f); // The attach position is in asset space
desc.families[1] = nullptr; // Setting the family to NULL designates the world (NRF)
// The value of desc.chunkIndices[1] is not used, since desc.families[1] is NULL
desc.attachPositions[1] = nvidia::NvVec3(0.0f, 0.0f, 10.0f);    // Attach position in the world

// Create the external joint which joins actor0 to the world
TkJoint* jointNRF = framework->createJoint(desc);

Releasing Joints

TkJoints are not released by Blast, except when the TkFramework is released. Otherwise, the user is responsible for releasing TkJoints after they become unreferenced. This is facilitated by the Unreferenced subtype of the TkJointUpdateEvent. After receiving this event for joint, the user may choose to release, using the typical TkObject::release() method.

joint->release();

Note, this method can be called at any time, even before the joint is unreferenced. When called, it will remove its references to its attached actors first, causing the joint to then become unreferenced. For example, if the user wishes to break a physical joint in their simulation, they can then release the corresponding TkJoint.

It should be mentioned, however, that joints created with an asset are allocated differently from external joints created using TkFramework::createJoint. Internal joints created from the joint descriptors in a TkAsset are block allocated with every TkFamily that instances the asset. Calling the release() method on those joints will remove any remaining references to them (as mentioned above), but will not perform any deallocation. Only when the TkFamily itself is released will the internal joint memory for that family be released. This is true even if the internal joints become “external” from actor splitting. Joints that become external are still associated with a single family and their memory still resides with that family.

On the other hand, joints that start out life external by way of the TkFramework::createJoint function have a separate allocation, and do not have memory tied to any TkFamily (even if both actors joined are in the same family). Releasing a family holding one of the actors in such a “purely external” joint will trigger a TkJointUpdateEvent of subtype Unreferenced, however, signalling that the joint is ready for user release.

Events

NvBlastTk uses events to communicate the results of actor splitting, joint updates from actor splitting, and fracture event buffers that can be used to synchronize fracturing between multiple clients.

Events are broadcast to listeners which implement the TkEventListener interface. Listeners are held by TkFamily objects. During a TkGroup::endProcess call (see Groups), relevant events are broadcast to the listeners in the families associated with the actors in the group.

A typical user’s receiver implementation might take on the form shown below.

class MyActorAndJointListener : public TkEventListener
{
    // TkEventListener interface
    void receive(const TkEvent* events, uint32_t eventCount) override
    {
        // Events are batched into an event buffer.  Loop over all events:
        for (uint32_t i = 0; i < eventCount; ++i)
        {
            const TkEvent& event = events[i];

            // See TkEvent documentation for event types
            switch (event.type)
            {
            case TkSplitEvent::EVENT_TYPE:  // A TkActor has split into smaller actors
            {
                const TkSplitEvent* splitEvent = event.getPayload<TkSplitEvent>();  // Split event payload

                // The parent actor may no longer be valid.  Instead, we receive the information it held
                // which we need to update our app's representation (e.g. removal of the corresponding physics actor)
                myRemoveActorFunction(splitEvent->parentData.family, splitEvent->parentData.index, splitEvent->parentData.userData);

                // The split event contains an array of "child" actors that came from the parent.  These are valid
                // TkActor pointers and may be used to create physics and graphics representations in our application
                for (uint32_t j = 0; j < splitEvent->numChildren; ++j)
                {
                    myCreateActorFunction(splitEvent->children[j]);
                }
            }
            break;

            case TkJointUpdateEvent::EVENT_TYPE:
            {
                const TkJointUpdateEvent* jointEvent = event.getPayload<TkJointUpdateEvent>();  // Joint update event payload

                // Joint events have three subtypes, see which one we have
                switch (jointEvent->subtype)
                {
                case TkJointUpdateEvent::External:
                    myCreateJointFunction(jointEvent->joint);   // An internal joint has been "exposed" (now joins two different actors).  Create a physics joint.
                    break;
                case TkJointUpdateEvent::Changed:
                    myUpdatejointFunction(jointEvent->joint);   // A joint's actors have changed, so we need to update its corresponding physics joint.
                    break;
                case TkJointUpdateEvent::Unreferenced:
                    myDestroyJointFunction(jointEvent->joint);  // This joint is no longer referenced, so we may delete the corresponding physics joint.
                    break;
                }
            }

            // Unhandled:
            case TkFractureCommands::EVENT_TYPE:
            case TkFractureEvents::EVENT_TYPE:
            default:
            break;
            }
        }
    }
};

Whenever a new TkActor is created by the user (via TkFramework::createActor, see Instancing a TkAsset: Creation of a TkActor and a TkFamily), its newly-made family should be given whatever listeners the user wishes to attach. For example,

TkActor* actor = framework->createActor(actorDesc);

actor->getFamily().addListener(myListener); //  myListener is an object which implements TkEventListener (see MyActorAndJointListener above, for example)

Listeners may also be removed from families at any time.

Object and Type Identification

NvBlastTk objects that are derived from TkIdentifiable (TkAsset, TkFamily, and TkGroup) support an object and class (type) identification system. The TkIdentifiable interfaces setID and getID allow the user to set and access an NvBlastID for each object. The NvBlastID is a 128-bit identifier. TkIdentifiable objects are tracked by the TkFramework, which may be used to look up an object by its NvBlastID.

Upon creation, TkIdentifiable objects are given a GUID, a unique NvBlastID. The user is welcome to change the object’s guid at any time, with the restriction that the GUID cannot be all zero bytes.

With an object’s GUID, one may look up the object using the TkFramework function findObjectByID:

TkIdentifiable* object = framework->findObjectByID(id); // id = an NvBlastID GUID

If the object is found, a non-NULL pointer will be returned.

TkIdentifiable-derived classes also have a class identification system, the TkType interface. From an individual object one may use the TkIdentifiable interface getType to access the class’s TkType interface. Alternatively, one may use the TkFramework getType function with TkTypeIndex::Enum argument. For example, to get the TkType interface for the TkAsset class, use

const TkType* assetType = framework->getType(TkTypeIndex::Asset);

The type interface may be used:

  • to access class-specific object lists in the framework,

  • identify the class of a TkIdentifiable obtained through ID lookup or deserialization, or

  • to obtain the class’s name and format version number.

For example, to access a list of all families:

// Get the TkFamily type interface
const TkType* familyType = framework->getType(TkTypeIndex::Family);

// Get the family count to allocate a buffer
const uint32_t familyCount = framework->getObjectCount(familyType);
std::vector<TkIdentifiable*> families(familyCount);

// Write the families to the buffer
const uint32_t familiesFound = framework->getObjects(families.data(), familyCount, familyType);

In the above code, the values of familyCount and familiesFound should be equal. An alternative usage of TkFramework::getObjects allows the user to write to a (potentially) smaller buffer, iteratively. For example:

uint32_t familiesFound;
uint32_t totalFamilyCount = 0;
do
{
    // Write to a fixed-size buffer
    TkIdentifiable* familyBuffer[16];
    familiesFound = framework->getObjects(familyBuffer, 16, familyType, totalFamilyCount);
    totalFamilyCount += familiesFound;

    // Process the families found so far
    myProcessFamiliesFunction(familyBuffer, familiesFound);
} while (familiesFound == 16);

To use the type interface to identify a class, perhaps after serialization or lookup by ID, one may do something like:

\\ Assume we have a TkIdentifiable pointer called "object"

// Get the type interfaces of interest
const TkType* assetType = framework->getType(TkTypeIndex::Asset);
const TkType* familyType = framework->getType(TkTypeIndex::Family);

if (object->getType() == *assetType)
{
    TkAsset* asset = static_cast<TkAsset*>(object);

    // Process the object as a TkAsset
}
if (object->getType() == *familyType)
else
{
    TkFamily* family = static_cast<TkFamily*>(object);

    // Process the object as a TkFamily
}

A TkIdentifiable-derived class may be queried for its name using the TkType interface, using TkType::getName(). This function returns a const char pointer to a string.

Finally, one may query the class for its current format version number using TkType::getVersion().