The Apex Destruction module deals primarily with three kinds of objects:
After loading an DestructibleAsset, you may use it to instance DestructibleActors. The DestructibleAsset carries with it a template of fracture parameters, the DestructibleParameter structure. These parameters are used by default with an DestructibleActor instance, and you may customize these parameters per-instance. These parameters allow you to specify how much damage it takes to fracture a chunk, whether chunks take damage via impact, whether or not they can deform before breaking, and more.
DestructibleActors may start off life static or dynamic. If static, you may define certain pieces (chunks) to be “support” pieces in various ways. This emulates being “held” by the environment. For example, a destructible wall fixture might have support chunks where it touches the wall. When you break off pieces of the fixture, as long as there is a connected path from a given chunk to a support chunk, then it will remain fixed. Otherwise, the disconnected chunks will break free and become dynamic. A powerful feature of this module is “inter-actor support.” This means that if a static destructible with support touches another static destructible, the support may be extend across the destructibles. In this way you may effectively build large destructible structures out of smaller destructible “building blocks.”
(Note: “static” destructible actors actually create kinematic PhysX actors, instead of truly static PhysX actors. In this way the actors may be easily made dynamic when a fracture event knocks chunks loose.)
The DestructibleActor comes with a damage API, allowing the application to apply damage directly or through impacts (collisions). Scene query functions are also supplied (ray casts and obb sweeps).
The DestructibleActorJoints are intended to act just as you’d expect a PhysX joint to act, when attached to a PhysX actor. They hide the fact that the PhysX actors which compose an DestructibleActor may be changing “under the hood” while fracturing is occuring.
A callback mechanism is provided which notifies the user when chunks are fractured or destroyed, so that sound and graphical effects may be played.
An APEX particle system (see the documentation for the particle system module) may be used by the DestructibleActor to emulate debris when the smallest chunks are broken.
Finally, LOD parameters are supplied to scale the physics computation for a given scene. These parameters are:
Through the various parameters and interfaces described above, the destruction module provides a rich variety of physical behaviors to enhance game play experience.
A good place to start is the SimpleDestruction sample application in APEX/samples. This demonstrates asset loading, instancing, fluid debris specification, and very importantly has a sample user renderer implementation (see the APEX Framework documentation).
This section highlights the main points of the SimpleDestruction sample.
An Apex application needs to first initialize PhysX. In what follows, it will be assumed that a PhysX SDK has been created (see the PhysX documentation):
PxPhysics* m_physxSDK;
PxFoundation* m_foundation;
m_foundation = PxCreateFoundation(PX_PHYSICS_VERSION, ...);
m_physxSDK = PxCreatePhysics( ... );
PxInitExtensions(*m_physxSDK);
... and that we have a cooking library:
PxCookingParams params;
PxCooking* m_cooking;
m_cooking = PxCreateCooking(PX_PHYSICS_VERSION, m_physicsSDK->getFoundation(), params);
After initializing the PhysX SDK, an APEX application needs to initialize the Apex SDK. See the APEX Framework documentation.
This is done as follows:
/* Fill out the Apex SDK descriptor */
ApexSDKDesc apexDesc;
/* Apex needs an allocator and error stream. By default it uses those of the PhysX SDK. */
/* Let Apex know about our PhysX SDK and cooking library */
apexDesc.physXSDK = m_physxSDK;
apexDesc.cooking = m_physxCooking;
apexDesc.outputStream = pxErrorStream;
/* Our custom render resource manager */
apexDesc.renderResourceManager = m_renderResourceManager;
/* Our custom named resource handler */
apexDesc.resourceCallback = m_resourceCallback;
/* Some debug renderer materials */
apexDesc.wireframeMaterial = "materials/simple_unlit.xml";
apexDesc.solidShadedMaterial = "materials/simple_lit_color.xml";
/* Finally, create the Apex SDK */
m_apexSDK = CreateApexSDK(apexDesc);
PX_ASSERT(m_apexSDK);
In the block above we assume we have a named resource provider m_renderResourceManager. SimpleDestruction is built on top of SampleFramework, which provides a class SampleApexRenderResourceManager for this purpose. It provides a name-to-object look-up for various resources, including destructible assets, apex render mesh assets, and material IDs.
Also, the render resource manager m_resourceCallback is used. This is also provided in SampleFramework. This class allows the user to customize the creation of render resources (such as vertex buffers). If you are using apex in an application that does not render (such as a commandline conversion utility), you may use the APEX-provided class NullRenderResourceManager in shared/external/include/NullRenderer.h.
You must also create an Scene, to hold APEX actors and renderables. This is initialized using the PhysX scene. The PhysX scene is created in the usual manner:
PxScene* m_physxScene;
...
/* Create PhysX scene */
physx::PxSceneDesc sceneDesc(scale);
sceneDesc.gravity = physx::PxVec3(0,-9.8f,0);
...
m_physxScene = m_physxSDK->createScene(sceneDesc);
... and the APEX scene:
/* Create an APEX scene from a PhysX scene */
Scene* m_apexScene;
...
SceneDesc sceneDesc;
sceneDesc.scene = m_physxScene;
m_apexScene = gApexSDK->createScene( sceneDesc );
An Apex application then needs to create the Apex modules it will use. These modules will populate the APEX scene with representations of their objects, and also populate the PhysX scene with corresponding physics objects.
This link describles all of the Destruction Module Parameters.
Create the destruction module (class ModuleDestructible) using
ModuleDestructible* m_apexDestructibleModule;
...
m_apexDestructibleModule = static_cast<ModuleDestructible*>(m_apexSDK->createModule("Destructible"));
and then initialize it using its default parameters:
NvParameterized::Interface* params = m_apexDestructibleModule->getDefaultModuleDesc();
m_apexDestructibleModule->init(*params);
Note, if you wish to set some parameters to non-default values, you may do so using the NvParameterized interface.
Destructible actors (DestructibleActor) may use the APEX Particles for particle system debris. This is initialized using the particle, iofx, and emitter modules:
m_apexParticlesModule = static_cast<ModuleParticles*>(m_apexSDK->createModule("BasicIOS"));
NvParameterized::Interface* params = m_apexParticlesModule->getDefaultModuleDesc();
m_apexParticlesModule->init(*params);
m_apexIofxModule = static_cast<ModuleIofx*>(m_apexSDK->createModule("IOFX"));
NvParameterized::Interface* params = m_apexIofxModule->getDefaultModuleDesc();
m_apexIofxModule->init(*params);
m_apexEmitterModule = static_cast<ModuleEmitter*>(m_apexSDK->createModule("Emitter"));
NvParameterized::Interface* params = m_apexEmitterModule->getDefaultModuleDesc();
m_apexEmitterModule->init(*params);
With that, the Apex Destruction is ready to use.
This link describles all of the Destructible asset parameters.
To create a destructible, first a destructible asset (DestructibleAsset) needs to be created or loaded. PhysXLab provides an easy way to create DestructibleAssets. These can then be saved in either a binary or an ASCII (XML) format. The asset type may be determined by peeking into the file stream, so the file extension does not matter. The current file name convention is that the .apb extension is used for binary format, and .apx for the ASCII format.
In this example we will load an DestructibleAsset from the file “Wall.apb”. To load APEX assets, create an PxFileBuf using the ApexSDK::createStream method. Assuming we have our working directory set up so that “fullpath” is the path to our asset directory,
physx::PxFileBuf* stream = m_apexSDK->createStream( "fullpath/Wall.apb", physx::PxFileBuf::OPEN_READ_ONLY );
... creates the necessary stream. This function returns NULL if the stream cannot be created. To determine the format from the stream, peek into it using:
char peekData[32];
stream->peek(peekData, 32);
NvParameterized::Serializer::SerializeType iSerType = m_apexSDK->getSerializeType(peekData, 32);
Then create a serializer for the serialization type iSerType:
NvParameterized::Serializer* ser = m_apexSDK->createSerializer(iSerType);
And load the asset using the deserialize method:
NvParameterized::Serializer::DeserializedData data;
NvParameterized::Serializer::ErrorType serError = ser->deserialize(*stream, data);
The data structure now contains pointers to one or more APEX assets, depending on how many were in the stream. Assuming there is only one in the stream, use
NvParameterized::Interface *params = data[0];
Asset* asset = m_apexSDK->createAsset( params, "Asset Name" );
The second parameter a name you can used to refer to the asset later. To use the destructible asset api for this asset, cast the pointer to an DestructibleAsset:
DestructibleAsset* destructibleAsset = static_cast<DestructibleAsset*>( asset );
This link describles all of the Destructible actor parameters.
Creating a destructible actor from a destructible asset is very simple if you use the default parameters (class DestructibleParameters) that are stored with the destructible asset.
One piece of additional information that you’re almost certainly going to want to change is the initial global pose for the destructible, which we’ll assume we have in the PxMat44 pose.
First, get the default actor description parameters from the asset:
NvParameterized::Interface* descParams = destructibleAsset->getDefaultActorDesc();
To modify the global pose, use
NvParameterized::setParamMat44( *descParams, "globalPose", pose );
Then simply use the descriptor, along with the APEX scene, to create an DestructibleActor instance of the DestructibleAsset:
Actor* actor = asset->createApexActor( *descParams, apexScene );
Again, to use the DestructibleActor API, typecast actor:
DestructibleActor* destructibleActor = static_cast<DestructibleActor*>( actor );
In general, you’ll need to set more fields of the Destructible actor parameters. Notably,
Finally, there are many parameters you may set in destructibleParameters. These set how much damage it takes to fracture a destructible, whether or not damage can come from impact, and many more things. A full description is given in the PhysXLab documentation.
At times it may be desirable to save and/or load the state of a destructible actor; saving a fracture sequence for subsequent replay and synchronizing fractured actor states between different PCs are two such scenarios. Serialization and deserialization provide a convenient approach to this end.
Each destructible actor exposes a parameterized interface
const NvParameterized::Interface* stateParams = actor->getNvParameterized( DestructibleParameterizedType::State );
with which we can easily serialize and store the actor’s state:
NvParameterized::Serializer* ser = m_apexSDK->createSerializer( NvParameterized::SerializeType::NST_BINARY );
...
physx::PxFileBuf* outStream = m_apexSDK->createStream( "fullpath/ActorSavedState.apb", physx::PxFileBuf::OPEN_WRITE_ONLY );
...
NvParameterized::Serializer::ErrorType serError = ser->serialize( *outStream, stateParams, 1 );
For deserialization, there are several options. Rather than creating the actor with a default set of parameters, we can pass the deserialized actor’s state handle to the creation method. We first construct our serializer and an input stream from the serialized actor’s state:
NvParameterized::Serializer* ser = m_apexSDK->createSerializer( NvParameterized::SerializeType::NST_BINARY );
...
physx::PxFileBuf* inStream = m_apexSDK->createStream( "fullpath/ActorSavedState.apb", physx::PxFileBuf::OPEN_READ_ONLY );
We then deserialize the input stream and provide the parameterized handle to the actor creation method:
NvParameterized::Serializer::DeserializedData data;
NvParameterized::Serializer::ErrorType serError = ser->deserialize( *inStream, data );
...
NvParameterized::Interface* stateParams = data[0];
Actor* actor = asset->createApexActor( *stateParams, apexScene );
...
stateParams->destroy();
With deserialization, a provided optimization allows the created destructible actor to take ownership of the provided parameterized interface. An additional method has been provided that creates the actor while consuming the specified handle:
Actor* actor = asset->createDestructibleActor( stateParams, apexScene );
The alternative to creating a new destructible actor from state is to explicitly set the state an an existing actor:
actor->setNvParameterized( stateParams );
Note that the actor takes ownership of the provided state handle.
Several guidelines should help the user make the most of these features:
Once the DestructibleActor is in the APEX scene, it will generate PhysX actors and shapes in the PhysX scene as it sees fit. However, the user may want some control over some of the parameters of the generated actors and shapes. For this purpose, fields which are used in the construction of PhysX actors and shapes are represented in the destructible actor descriptor.
Let’s say we wish to specify the shapes’ groupsMask, and the actors’ density. An example of this is:
/* Set the shapes' groupsMask */
NvParameterized::setParamU32( *descParams, "p3ShapeDescTemplate.simulationFilterData.word0", 2 );
NvParameterized::setParamU32( *descParams, "p3ShapeDescTemplate.simulationFilterData.word0", ~0 );
/* Set the shapes' density */
NvParameterized::setParamF32( *descParams, "p3BodyDescTemplate.density", 10.0f );
For the purpose of allowing synchronization of the destructed state of actors across different processes in real-time, APEX Destruction presents a scaled-down alternative to full actor state serialization and deserialization. This method exposes only enough data to synchronize between actors at a customizable combination of detail.
At the module level, the application must implement the type-parameterized interface class provided: class UserDestructibleSyncHandler The type argument would be of the data type that the application wants to synchronize between. There are 3 synchronizable types.
The application should then instantiate and set these callback(s) for APEX via
module->setSyncParams( userDamageEventHandler, userFractureEventHandler, userChunkMotionHandler );
Typically, the application should only use one of either userDamageEventHandler or userFractureEventHandler, plus userChunkMotionHandler optionally. This is the main way by which APEX passes synchronization data back to the application. Note that any shared resource used by the callback classes must be thread-safe.
For write operations (passing data to the application), APEX will call the following 2 methods whenever such data becomes available. This is a request for a writable memory buffer which APEX will use, followed by a notification when APEX is done writing:
callbackFoo->onWriteBegin( bufferStart, bufferSize );
callbackFoo->onWriteDone( headerCount );
APEX does not cache this information - they are computed, used, and then discarded. This information is only available to the application during this window.
For read operations (passing data to APEX), APEX will call the following 4 methods whenever such data is about to be processed. 2 of the methods are used for preparing the memory buffer into a state usable by APEX:
callbackFoo->onPreProcessReadBegin( bufferStart, bufferSize, continuePointerSwizzling );
callbackFoo->onPreProcessReadDone( headerCount );
These methods serve 2 functions. Firstly, to update the pointers into valid values for this memory space, and secondly, to provide a convenient outlet for the application to chain together multiple buffers.
Following that, the actual read request is made through the following 2 methods. This is a request for a readable memory buffer which APEX will use, followed by a notification when APEX is done reading:
callbackFoo->onReadBegin( bufferStart );
callbackFoo->onReadDone( debugMessage );
APEX will only reference this information during this window.
These callbacks contain data for all actors that are marked for synchronization. Marking an actor for synchronization is done at the actor level.
At the actor level, the application needs to mark them as participating synchronizable actors:
actor->setSyncParams( userActorID, actorSyncFlags, actorSyncState, chunkSyncState)
The userActorID is used to match actors across processes to synchronize data between. It must be a non-zero value. For every matched userActorID, the client must set a readFoo flag for every copyFoo flag set by the source, to be passed in as actorSyncFlags. Flag options are presented in the struct DestructibleActorSyncFlags. Using actorSyncState and chunkSyncState will allow for even more fine-grained control over the data to synchronize between.
Additionally, at the per-actor level, APEX also provides an even lower-cost way to track and use chunks that were ever impacted. This is especially useful in situations where detail mattered the least - such as when an actor is far away or when an actor is required to be quickly synchronized to the current environment. Minimizing of computation time and bandwidth used is the goal here. This method also represents a departure from how the other data are synchronized - this data is cached by APEX and is fetchable via an API call, thus relieving the application from the burden of having to save this information until they actually need to use it. There are 4 API calls relating to this feature:
actor->setHitChunkTrackingParams( flushHistory, startTracking, trackingDepth );
actor->getHitChunkHistory( hitChunkContainer, hitChunkCount );
actor->forceChunkHits( hitChunkContainer, hitChunkCount , removeChunks );
actor->setDeleteFracturedChunks( inDeleteChunkMode );
The actor-level parameters can be safely tweaked anytime during runtime. This is in contrast to the callback methods, where memory must be “locked” during the short FooBegin() - FooDone() window.
Here are some sample use-cases for actor synchronization. Please refer to the sections under Module-level semantics and Actor-level semantics for the how-tos in setting up actors for synchronization. This section only serves to identify and reconcile the tweaking of the parameters responsible for the different use-cases.
Client joining late - updating the client via hit chunk history list.
1.1) On the source-side, be sure to have the actor track its chunk history:
actor->setHitChunkTrackingParams( flushHistory, startTracking, trackingDepth );
Here, startTracking must be set to true. The trackingDepth should be set at a level less than or equal to the maximum chunk depth for the asset.
1.2) Just before the actor on the destination-side is to be deleted, retrieve the chunk history list for the actor on the source-side:
actor->getHitChunkHistory( hitChunkContainer, hitChunkCount );
The application should then use the returned arguments to supply the destination-side.
1.3) On the destination-side, apply the chunk history list.
actor->forceChunkHits( hitChunkContainer, hitChunkCount , removeChunks );
Here, removeChunks must be set to true.
Actor out-of-sight - Turning on / off actor delete mode in runtime.
2.1) On the local-side, simply set the actor to delete-mode to have it delete its chunks instead of simulating them when hit:
actor->setDeleteFracturedChunks( inDeleteChunkMode );
Here, inDeleteChunkMode must be set to true. Use this same API to have it return back to the normal simulating state.
Level-of-detail - Changing the filter depth for damage/fracture events in runtime.
3.1) On the source-side, be sure to set the optional parameters for the actor:
actor->setSyncParams( userActorID, actorSyncFlags, actorSyncState, chunkSyncState);
Here, be sure that the requisite flag is set in actorSyncFlags for the corresponding actorSyncState and/or chunkSyncState parameter. Define and set values for actorSyncState and/or chunkSyncState. For example, if we are filtering the depth of damage events, be sure to have the CopyDamageEvents flag included (OR-ed) as well.
actorSyncFlags |= CopyDamageEvents;
actorSyncState = new DestructibleActorSyncState;
actorSyncState->damageEventFilterDepth = 2;
The optional parameters actorSyncState and chunkSyncState can be tweaked without having to re-call setSyncParams after the first time it is set.
3.2) On the destination-side, be sure to have the corresponding readFoo flag set.
actor->setSyncParams( userActorID, actorSyncFlags, actorSyncState, chunkSyncState);
Here, be sure that the corresponding read flag is set in actorSyncFlags. actorSyncState and chunkSyncState are inconsequential here, thus can be left as NULL. For example, include (OR) the flag ReadDamageEvents if CopyDamageEvents was used in step 3.1).
actorSyncFlags |= ReadDamageEvents;
In this section we discuss some of the potential quirkiness associated with destructible synchronization.
Filtering is a source-side (write-side / send-side / server) decision. The destination-side (read-side / receive-side / client) actor has no control over filtering.
Actor delete-mode is a local decision. The source-side has no control over deletion on the destination-side.
Overruled filter level / graphical LOD due to simulation LOD - filter level mismatches
Overruled filter level:
- For example,
sending-side actor LOD = 2, filter = 3,
Only up till and including level 2 chunks will go into the outgoing synchronization buffer.
Overruled graphical LOD:
- For example,
receiving-side actor LOD = 2, sending-side actor LOD = 3, sending-side filter level = 3
The level 3 chunks on the receiving-side actor will break off too.
A simple guideline is to have the filter levels set to the lowest actor LOD level across machines.
Ignored chunk deletion during runtime
If an actor had already been fractured into smaller pieces (as can happen midway in a game), the smaller chunks will not be deleted when the deletion list is applied on the actor. This is because only chunks at the tracking depth are tracked as a “hit chunk” in the “hit chunk history” of an actor. This is for cost-savings purposes, consistent with the spirit of minimizing computation costs and bandwidth usage for such usage.
Due to the non-deterministic nature of physX generating poses (orientation and position), dynamic chunks’ poses will not match on different machines, given the same damage event. As a consequence, due to the way damage events are applied (chunk poses are a factor), this may cause some dynamic chunks to ignore the damage events coming in from synchronization buffer. Similar challenges apply for fracture events. However, they are much less pronounced.
A simplistic guideline is to use fracture events instead of damage events to get a better match across machines. This will come at a higher bandwidth cost though.
Fractures initiated by the application locally always takes precedent over synchronized fractures. For any given tick, fractures could be postponed over to the next frame if the computation limit for fracturing is breached. In that event, fractures initiated remotely through synchronization will take a backseat. They will still be processed eventually, but only after all locally-initiated fractures are processed. APEX will make a copy of the outstanding remotely-initiated fracture events (They are usually only being referenced if they can all be processed this frame). This behaviour is by design, so as to allow for better responsiveness to locally-initiated events.
In APEX 1.3 and later, the destruction module includes an option for runtime fracturing. That is, fracturing is performed on the fly, with no precalculation of the fractured pieces. To enable runtime fracturing, set both the CRUMBLE_SMALLEST_CHUNKS and CRUMBLE_VIA_RUNTIME_FRACTURE flags in the destructible parameters. With these flags set, the deepest-level prefractured chunks (those with no children) will continue to be fractured using the runtime system.
If you want the entire destructible to runtime fractured, simple create a destructible asset from a mesh, with no prefracturing, and set the aforementioned flags. One thing to note, however, is that the runtime fracture system is activated exactly when crumbles would be, that is, only when a deepest-level chunk takes enough damage to be fractured further. When there is only one chunk, this means the chunk would need to first take enough damage to break free (if it’s static), then enough to fracture further to crumbles or runtime fracture. This essentially doubles the damage it needs to take to fracture. To prevent this, you may set the minimumFractureDepth (in the destructible parameters) to one more than the deepest depth, meaning fracturing will begin with crumbles or runtime fracture. So, if you import a single-chunk destructible for runtime fracture, set minimumFractureDepth to 1 (since there is only a depth-0 chunk).
This fracture mode also comes in two types: full and partial. Since a fracture pattern is a decomposition of all of space, when applied to a mesh it will cut the entire mesh into pieces. Partial fracture means that beyond a given radius, the pieces are sewn back together, so that the only fragments seen are the ones within the given radius. We have merged this into the prefracture system by using the radius obtained from applyRadiusDamage. This radius is used for the partial fracture mode in realtime fracturing of chunks.
Scene 9 in SimpleDestruction demonstrates runtime fracture on a thin sheet, with a glass fracture pattern.
Runtime fracturing is based upon precomputed fracture patterns that may be applied anywhere (and with any orientation or scale) within a mesh. We plan to support a variety of fracture patterns in subsequent releases of APEX. In the current release we only have one pattern available, a glass fracture pattern.
In addition to the flags which enable this feature, mentioned above, there are a number of parameters that control its behavior. these are listed here.
Parameter name | Type | Default value | Description |
---|---|---|---|
destructibleParameters.runtimeFracture.sheetFracture | bool | true | If true, align fracture pattern to largest face. If false, the fracture pattern will be aligned to the hit normal with each fracture. |
destructibleParameters.runtimeFracture.depthLimit | unsigned | 2 | How many times pieces can be recursively fractured using the runtime fracture system. |
destructibleParameters.runtimeFracture.destroyIfAtDepthLimit | bool | false | If true, destroy chunks when they hit their depth limit. |
destructibleParameters.runtimeFracture.minConvexSize | float | 0.02 | Minimum size of convex piece produced by a fracture. |
destructibleParameters.runtimeFracture.impulseScale | float | 1.0 | Scales impulse applied by a fracture. |
destructibleParameters.runtimeFracture.glass | FractureGlass | Glass fracture pattern settings. See the table below. | |
destructibleParameters.runtimeFracture.attachment | FractureAttachment | Attachment Settings. Allows the sides of a runtime fracture chunk to be kinematic rather than dynamic. See the description below. |
The FractureGlass parameters are described in the following table. The pattern is described by a number of ‘sectors’ and ‘segments’. Sectors are the radial slices, segments the cross-slices within the sectors.
Parameter name | Type | Default value | Description |
---|---|---|---|
numSectors | unsigned | 10 | Number of angular slices in the glass fracture pattern. |
sectorRand | float | 0.3 | Creates variance in the angle of slices. A value of zero results in all angular slices having the same angle. |
firstSegmentSize | float | 0.06 | The minimum shard size. Shards below this size will not be created and thus not visible. |
segmentScale | float | 1.4 | Scales the radial spacing in the glass fracture pattern. A larger value results in radially longer shards. |
segmentRand | float | 0.3 | Creates variance in the radial size of shards. A value of zero results in a low noise circular pattern. |
The FractureAttachment parameters are simply a set of six bools, describing which sides of the destructible are attached to the world. This description is in the local space of the destructible. The fields are posX, negX, posY, etc. In this way, when a local fracture (using radius damage) is done, pieces outside of the damage radius will remain attached if they are joined to an attachment side.
Sometimes it is useful to have information about chunks which are fractured loose, or destroyed completely. For example the user may wish to generate sound or particle effects associated with the fracture event. APEX Destruction facilitates this with the UserChunkReport. The user must derive their own class from this virtual base class and implement the onDamageNotify function. After doing this, pass an instance of the derived class to the destruction module using the ModuleDestructible method
virtual void setChunkReport(UserChunkReport* chunkReport) = 0;
Once a frame, if fractures occurred since the last frame, then the onDamageNotify function will be called with information stored in an DamageEventReportData structure. (See DamageEventReportData.) This gives general information about the chunks which broke free or were destroyed, as well as a detailed list of chunks if the chunks’ hierarchical depth is at or below a maximum depth set by the ModuleDestructible method
virtual void setChunkReportMaxFractureEventDepth(physx::PxU32 chunkReportMaxFractureEventDepth) = 0;
Chunks contribute to the DamageEventReportData for various reasons, enumerated in the ChunkFlag::Enum enum. This not only allows the user to respond to the fracture event based upon the reason (the ChunkFlag is in the ChunkData for each explicitly-listed chunk), but it also allows for filtering of the chunks which contribute to the report. By setting a flag mask using the ModuleDestructible method
virtual void setChunkReportSendChunkStateEvents(bool chunkReportSendChunkStateEvents) = 0;
If enabled, once a frame during simulate, the onStateChangeNotify callback is called with a list of chunks that have changed their visibility state, including their current state. This allows for event based updates of the chunk representation in the application.
virtual void setChunkReportBitMask(physx::PxU32 chunkReportBitMask) = 0;
only fracture events with ChunkFlag flags that overlap (boolean ‘and’) the given mask will contribute.
See the APEX Framework documentation section “APEX Automatic LOD” for the general introduction to APEX LOD.
The destruction module uses two factors to determine an DestructibleActor’s benefit: angular size, and age. Unlike the “solid angle” importance function described in the LOD section of the framework documentation, the angular size calculation only takes into account a linear arc size subtended by the actor from the point of view of the camera location. In this way the importance falls off linearly with distance, not with the square of the distance as with the solid angle, and therefore the benefit does not fall off as quickly.
The age benefit is calculated using the linear ramp-down described in the framework LOD section.
The LOD parameters are set through the DestructibleActor function
virtual void setLODWeights(PxF32 maxDistance, PxF32 distanceWeight, PxF32 maxAge, PxF32 ageWeight, PxF32 bias)
The parameters are described below.
The total benefit for an actor is calculated using:
B(r,t) = distanceWeight*D(r) + ageWeight*A(t) + bias.
The weights should total 1, and the bias should be kept in the unit [0,1] range. This keeps the benefit function B(r,t) “normalized,” so that it’s on the order of one or less.
The DestructibleActor derives from the APEX rendering API (see Renderable), so that you may simply render the destructible using this API. However, many applications use a separate rendering thread that runs concurrently with a simulation thread that ticks the APEX scene. It’s possible that during this tick, a destructible may be deleted, but a reference to it or its rendering data could still be waiting in the rendering thread. To accomodate this usage, the destructible actor uses a render proxy object to hold all of its rendering data. This object exists separately from the DestructibleActor, and so the actor may be deleted without deleting the render proxy. This way, the rendering thread can hold a reference to the render proxy instead of the actor, and if the actor is deleted the render thread is unaffected.
To acquire a handle to the render proxy, use
virtual DestructibleRenderable* acquireRenderableReference()
The object DestructibleRenderable is also derived from Renderable, so you may use it to render in exactly the same way as you would have rendered the DestructibleActor. This object is reference counted, and the DestructibleActor holds a reference to it while it exists. Once you are through with the proxy, you may use the DestructibleRenderable::release() method to decrement the reference count. If the reference count goes to zero, the proxy is actually deleted. This way the proxy will be properly deleted if the DestructibleActor is deleted before the render thread is through with the proxy.
If there are any external references to the proxy, the DestructibleActor’s render API will no longer function. You must then use the proxy to render the destructible. Once you release all external references, the DestructibleActor’s render API will operate normally.
When we use skinning method to render the destructible chunks, each chunk is associated with one bone. Traditional skinning method limits the number of bones per draw call. Therefore the rendering performance is CPU bounded when the number of chunks increase. The Vertex Texture Fetch, known as VTF, rendering method deposits a destructible’s entire rendering bone buffer in a texture whose width is 4 and height is the maximum number of bones of the actor. Thus each row of the texture represents a 4x4 tranform matrix. The vertex shader reads the bones from a texture instead of the constant buffer. In this way, all dynamic chunks require only one draw call per rendering pass and greatly reduce the CPU cycles. Note that this technique only applies to GPUs with shader model 3.0 or later versions. For earlier GPUs that don’t support this feature, the traditional skinning technique is still supported in the sample and can be toggled with the following code in SampleDestructible.cpp
switch(renderer->getDriverType())
{
case SampleRenderer::Renderer::DRIVER_DIRECT3D9:
case SampleRenderer::Renderer::DRIVER_DIRECT3D11:
renderer->setEnableVTF(true);
}
All APEX modules come with debug visualization rendering, to help understand what is happening in an APEX scene. When you hit the V key in SimpleDestruction, default APEX visualization rendering is turned on, as well as default PhysX SDK visualization. In addition to the default settings, APEX Destruction LOD benefits and support visualization are shown.
Click here for a list of the Destructible debug visualization parameters
For general information on how to use debug visualization within APEX, please see Debug Visualization.
Starting SimpleDestruction and repeatedly hitting ‘V’ (for visualization) and ‘G’ (to hide meshes) will show a subset of the debug visualization options for APEX render meshes and destructible actors. The sample may look slightly different in its current form, but the debug visualization walk-through offered here is still valuable.
Here you see the PhysX collision representation of the scene, as well as the destructible’s support structure. The white circles on the ground represent the collision plane, while the orange box represents the collision volume of the wall. It is a single volume since the wall is not yet damaged. The blue boxes on the ground outline the support chunks in the wall. The support depth for this asset is set at depth = 2, and the outlined chunks are the depth-2 chunks that touch a static actor (the ground plane) in the PhysX scene. Since “World overlap” support is selected in the asset, these chunks become support chunks. All chunks touching them, and chunks touching those chunks, and so on, are supported. This is represented by the network of blue lines, drawn between the centers of each chunk that gains support from a supported neighbor, and that supported neighbor’s center.
Once we apply some damage to the wall, freeing chunks at depth 2 or deeper, those chunks cease to be supported, and this modifies the support network:
Once enough chunks have been removed from the support network to form an island of chunks that don’t contain a supported chunk, that island becomes free, and breaks off whole:
Level of Detail (LOD) benefit and cost are also displayed. The green squares represent the benefit of a chunk island. The benefit is proportional to the area. If the square is large enough, the benefit value is shown in the square. Braking off some chunks you can see the benefits changing as the chunk islands age, and as they fill different areas of the screen:
Chunk island benefit is a function of both the age of chunk and the screen area percentage it fills. Both have adjustable prefactors, as well as an exponential decay factor for the age. Therefore each new chunk island that forms starts with a certain benefit which decays over time, settling on a benefit that only depends on screen area percentage.
In LOD calculations the benefit of each chunk is weighed against its cost. The cost is simply the calculated as the number of chunks times a factor which can be set by the user. This cost is compared with a total budget given to the destruction module. That budget and the total cost of the chunks in the scene are plotted in the bar on the bottom. The bar is scaled so that half the width of the screen represents the total budget. Chunk costs are summed up (stacked) on that bar. The cost of all “essential” chunks is shown as a blue bar. These are the chunks that are not deeper than the “essential depth” set for the destructible asset. Essential chunks will never be removed from the scene from LOD considerations. For example, game play logic or experience may require this.
On top of the essential chunk cost is a yellow bar representing the non-essential chunk cost. If the total cost is less than the budget, the remainder of the budget bar is plotted in white. If the total cost exceeds the budget, the overrun is plotted in red and will extend to the right-half of the screen. Budget overrun can occur if the essential chunk cost exceeds the budget. Also, miscalculations (due to approximations in budget prediction) may cause a temporary budget overrun, which will be corrected in subsequent frames.
If the budget is exceeded by non-essential chunks, or is predicted to be (due to a new fracture event), then the destruction LOD system goes to work and either reduces the detail of fracturing from the fracture event, or removes low-benefit chunks. This is done in order of increasing benefit/cost, until the budget is satisfied or all chunks are essential.
When a chunks is removed to satisfy resource budget, a red square with an “X” across it is displayed temporarily in its place, to show where the removal occurred.
Other visualization options are available. For example, you can view the chunk island poses (local coordinate systems), if you set the VISUALIZE_DESTRUCTIBLE_FRAGMENT_POSE visualizer flag. An example of this is shown here:
APEX Destruction outputs the following error and warning messages using the standard APEX error stream.
ERROR CODE | MESSAGE | Explanation |
---|---|---|
APEX_INVALID_PARAMETER | The NvParameterized::Interface object is the wrong type | The parameterized descriptor passed into ModuleDestructible::init(...) was not a destructible module descriptor. |
APEX_INVALID_OPERATION | DestructibleActor does not support this operation | An LOD operation (getLodRange, getActiveLod, or forceLod) has been called which APEX destruction does not support. |
APEX_INTERNAL_ERROR | Destructible actors need EmitterExplicitGeom emitters. | A crumble or dust emitter was assigned to a destructible actor which was not an EmitterExplicitGeom emitter. |
APEX_INTERNAL_ERROR | DestructibleAssetAuthoring::cookChunks: cookingDesc invalid. | Destructible authoring: DestructibleAssetAuthoring::cookChunks(...) was called with an invalid descriptor. |
APEX_INTERNAL_ERROR | Destructible asset render mesh uninitialized or wrong part count. | Destructible authoring: DestructibleAssetAuthoring::cookChunks(...) was called without assigning an RenderMeshAssetAuthoring, or it does not have the correct number of parts. |
APEX_DEBUG_WARNING | APEX Destruction physx::PxFileBuf (de)serialization is obsolete. You must use NvParameterized (de)serialization. | A call was made to destructible asset serialize() or deserialize(). This should no longer occur. |
APEX_DEBUG_WARNING | Deprecated interface, use ApexSDK::{create,release}Asset or ApexSDK::{create,release}AssetAuthoring. | A call was made to a deprecated asset create or release method. Asset creation and release is handled through the APEX SDK now. |