Instancing a TkAsset: Creation of a TkActor and a TkFamily
Applying Damage to Actors and Families
Object and Type Identification
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:
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:
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. On XBoxOne it is .lib, and on PS4 it is .a.
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.
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] = physx::PxVec3( 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] = physx::PxVec3( 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();
The TkFamily has a special role in NvBlastTk, holding user-supplied event listeners (TkEventListener). 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 (TkActorDesc) 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.
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 Nv::Blast::ExtGroupTaskManager (in NvBlastExtPxTask.h). This resides in PhysX™ Extensions (NvBlastExtPhysX), because it uses physx::PxTask.
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);
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.
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, ¶ms); // params must be kept around until TkGroup::endProcess is called!
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));
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 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] = physx::PxVec3(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] = physx::PxVec3(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] = physx::PxVec3(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] = physx::PxVec3(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);
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 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.
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:
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().