Since the Particle emitter spawns the IOS and the IOFx actors, most programmer interaction with this module focuses on the rendering side of the IOFX actors and NxApexRenderVolumes.
The APEX Particles feature will allocate one sprite or mesh instance buffer per-IOS actor per-output instance.
By default each IOS actor requires two output instances for simple double buffering: rendering the current frame while we are simulating the next. However, When CUDA interop is in use the IOS allocates three output instances to ensure there is always a mapped graphical buffer available to CUDA when the simulation is ready to start. If the game engine can ensure that the scene’s prepareRenderResourceContexts() method will always be called between each simulation step’s fetchResults() and simulate() calls, this triple buffering can be reduced back to double buffering. See the interop documentation in the framework programmers guide for details.
In addition to the these per-IOS particle buffer instances, the APEX Particles feature will allocate per-output instance NxUserRenderResources per IOFX actor. In general, APEX can create one IOFX actor per IOS asset per IOFX asset per render volume in the scene. Fortunately, APEX defers these actor creations until there are particles for that actor to draw.
Note
IOS actors are not renderables, so they are not capable of allocating sprite or mesh instance buffers. As a result, their IOFX actors will allocate those buffers on their behalf.
Note
Similarly, if all of the IOFX actors in an IOS are culled (not updated by the render thread), no writeBuffer() call will be performed for the IOS.
Each IOS actor operates in either mesh or sprite mode, meaning all of the IOFX assets used with a single IOS actor will either all be mesh IOFX or sprite IOFX. This detail is not exposed to the artist. APEX simply creates two IOS actors if the one IOS asset is used with both sprite and mesh IOFX assets.
The IOS actor will create sprite buffers if it is operating in sprite mode, or mesh instance buffers if it is operating in mesh mode. Mesh instance buffers will have fixed semantics; pose, velocity, life, and density if the underlying IOS supplies a density value (currently only the 2.8.4 NxFluid and 3.x Particle IOSes with SPH enabled, or BasicIOS or 3.x ParticleIOS when grid density is enabled).
Sprite buffers will have semantics for the sum of all IOFX assets currently in use with an IOS actor. This means the semantics output by an IOS actor may change as emitters are added to or removed from a scene. The next time the IOFX actors of that IOS are updated by the render thread, the old sprite buffers will be released and new sprite buffers will be allocated with the new semantics. No rendering data is lost for any frames because the simulation output buffer always has the correct semantics.
There are two ways to update render buffers - either using output to texture or to a vertex buffer (sprite buffer). To set this up the following method needs to be implemented:
virtual bool NxUserRenderResourceManager::getSpriteLayoutData(physx::PxU32 spriteCount, physx::PxU32 spriteSemanticsBitmap, NxUserRenderSpriteBufferDesc* textureDescArray)
There is no default implementation, so the application should at least implement it and return false, in which case the sprite buffer will be set to use the default sprite buffer layout, which uses all present semantics with a maximum possible sprite count. However in case default behavior is not needed this function should return true. Making this function set up texture descriptions and returning their count causes APEX to use writeTexture() method instead of writeBuffer(). This way the sprite buffer will be updated with only the needed semantics (e.g. pose only), enumerated in the getSpriteTextureData method. The following semantic layouts are supported:
POSITION_FLOAT4
SCALE_ORIENT_SUBTEX_FLOAT4
COLOR_FLOAT4
COLOR_BGRA8
Argument spriteCount determines the sprite count and allows the application to determine texture dimensions. Texture width should be power of two and >= 32. It is a good idea to determine texture width as a square root of spriteCount ceiled to the nearest power of two. Non-zero bits in the spriteSemanticsBitmap argument correspond to the semantics defined in the NxRenderSpriteSemantic and used in the APEX asset for which sprite texture layouts are defined. Argument textureDescArray is an array allocated by the APEX. Its size is determined by NxUserRenderSpriteBufferDesc::MAX_SPRITE_TEXTURES, hence access beyond this index is not valid. An implementation of this function should set textureDescArray array with texture descriptions (layout, width, height, pitch) and return a number of valid texture descriptions set in textureDescArray array.
If the use prefers to use the writeBuffer() method, the textureCount field in the sprite buffer description should be set to 0 and semanticOffsets array should be filled with the offsets of needed semantics in the target sprite buffer using following semantic names as index in this array:
POSITION_FLOAT3
COLOR_RGBA8
COLOR_BGRA8
COLOR_FLOAT4
VELOCITY_FLOAT3
SCALE_FLOAT2
LIFE_REMAIN_FLOAT1
DENSITY_FLOAT1
SUBTEXTURE_FLOAT1
ORIENTATION_FLOAT1
USER_DATA_UINT1
User should also specify the stride in the sprite buffer layout.
In case user specifies invalid sprite buffer description, APEX falls back to a default sprite buffer layout.
This situation is slightly more complicated when CUDA interop is enabled. The simulation will realize that the current semantics do not match those that are currently allocated in the mapped render buffer, so it will refuse to write into it. Instead it allocates a temporary output buffer with the correct semantics to use for just this one simulation step. When it is time to render those simulation results, the IOFX actor will allocate a new sprite buffer with the proper semantics then issue a writeBuffer() call from the temporary output buffer to the new sprite buffer. The temporary output buffer is freed once sprite buffers with the proper semantics are mapped and available for simulation.
A similar situation is caused whenever the graphics device is reset for any reason, causing the graphic buffers to be re-allocated. When the simulation thread cannot get a valid mapped buffer for any reason, it falls back to the temporary output CUDA buffer and writeBuffer() approach.
Note
When CUDA interop is not in use, the game engine has flexibility in the manner in which it lays out the graphics buffers it allocates for particle data. In the writeBuffer() call it may re-order the fields as it likes. However, once CUDA interop is enabled this flexibility disappears. The CUDA IOFX kernels expect the graphics buffer to have the exact size and layout specified in the buffer descriptor.
NxApexRenderVolumes are the primary renderables of the APEX Particles pipeline. A volume “owns” a portion of world space, though ownership volumes are explictly allowed to overlap. Volumes are created via an IOFx module method:
NxApexRenderVolume *createRenderVolume( const NxApexScene& apexScene, const PxBounds3& b, PxU32 priority, bool allIofx );
The bounds argument defines the volume’s ownership bounds. The priority is used to break ties when particles are within multiple volumes. If a particle is within multiple volumes of the same priority, it will prefer its previous volume when possible. The allIofx flag determines whether a volume will take ownership of all particles within its bounds, or only those particles emitted to specific IOFX assets.
The game may create or release render volumes at any time, their insertions and deletions are queued to make them thread-safe. As volumes take ownership of particles, IOFX actors are instantiated on demand to manage the render resources required to draw them. This deferred IOFX actor creation may be disabled via an IOFx module method: disableDeferredRenderableAllocation().
Calling getBounds() on the render volume will return the bounds of all the particles in that render volume that frame. It is usually much smaller than the volume’s ownership bounds and should be used for culling purposes. Culling a render volume (by not calling its update|dispatch methods) implicitly culls all of the IOFX actors it holds.
If a particle is not owned by any volume at the end of a simulation step, it is considered “homeless” and is not rendered. It will be deleted on the next simulation step.
NxApexRenderVolumes provide a lot of flexibility in the way particle render resources are managed. Here are just a few use cases.
A game may allocate just a single NxApexRenderVolume with infinite bounds, zero priority, and allIofx set to true. Calling update|dispatchRenderResources() on this volume will draw all particles in the scene.
If your game engine prefers to have one renderable per emitter, you may create an NxApexRenderVolume for each APEX emitter and set the volume as the emitter’s preferred volume:
PxBounds3 b; b.setInfinite();
vol = IofxModule->createRenderVolume( scene, infBounds, 0, true );
emitterActor->setPreferredRenderVolume( vol );
Since particles stay within the volume they were injected until they encounter a higher priority volume, each emitter’s volume will hold all the particles emitted by this emitter. If the game wished, they could limit the size of the bounds and update its position to follow the emitter, though this is not strictly necessary. In general we prefer you allow our LOD system to delete particles that are no longer “interesting”, so only restrict the bounding volume if you truly require all of the particles drawn by that volume to be spatially coherent.
If your game engine incurs a serious cost penalty for every light that can see any particle in a draw call, you can use volumes per light to collect together all the particles a particular light can see. When used in this way, you will typically have one “world” volume with zero priority and priority one volumes for each light.
Beware that APEX volume migration was optimized for only a few volumes being active at a time (less than a dozen). So the game should try to create and delete volumes as game play progresses to keep the total count low.
If your game engine has special rendering code for a small number of IOFX assets, you can create a render volume with non-zero priority and allIofx = false. You then explicitly set the IOFX assets you do want the render volume to affect. That volume will then only take ownership of (and create IOFX actors for) particles created for those IOFX assets.
Note that you could achieve the same effect by querying the IOFX asset of each IOFX actor before calling its update|dispatchRenderResources methods.
If your game has live “cut scenes” where the camera path is fixed and you want to maximize the number of particles in front of the camera, you can create a single render volume that only covers the visible world and allow all non-visible particles to become homeless and culled. Be aware that finite bounds volumes can create havoc with APEX emitters that try to keep an area of the world covered with particles, aka the Air/Ground emitter. It is best not to combine the two.
Also note there is a PhysX SDK solution for culling particles that reach areas you would prefer them not to reach. They are called “particle drains”. These are PhysX shapes that have a flag set on them that cause any particle which collides with the shape to be deleted. Obviously this technique only works if you are using an NxFluid IOS.
Since render volumes are collections of IOFX assets and actors, and most of its methods iterate over those collections, it is important to have the volume’s render data lock acquired while you use them:
mVolume->lockRenderResources();
if(visibilityCheck(mVolume->getBounds())
{
mVolume->updateRenderResources( NULL );
mVolume->dispatchRenderResources( myRenderer );
}
mVolume->unlockRenderResources();
If you prefer to further check each IOFX actor for visibility, it should look like this:
mVolume->lockRenderResources();
if(visibilityCheck(mVolume->getBounds())
{
physx::PxU32 numActors;
physx::PxU32 drawnParticles = 0;
NxIofxActor* const *actors = mVolume->getIofxActorList( numActors );
for( physx::PxU32 i = 0 ; i < numActors ; i++ )
{
actors[i]->lockRenderResources();
if(visibilityCheck(actors[i]->getBounds())
{
actors[i]->updateRenderResources( NULL );
actors[i]->dispatchRenderResources( myRenderer );
drawnParticles += actors[i]->getObjectCount();
}
actors[i]->unlockRenderResources();
}
}
mVolume->unlockRenderResources();
APEX IOFx outputs the following error and warning messages using the standard APEX error stream. All are caused by authoring problems.
ERROR CODE | MESSAGE | Explanation |
---|---|---|
APEX_INVALID_PARAMETER | NxApexRenderMeshAsset with name “%s” not found. | The specified mesh asset could not be found. |
APEX_INVALID_OPERATION | Unable to set Sprite Material Name (%s). Systems can be either Mesh or Sprite, but not both. This system is already a Mesh. | Authoring problem. The asset is marked as both a sprite and a mesh. |
APEX_INVALID_OPERATION | Unable to set Mesh Material Name (%s). Systems can be either Mesh or Sprite, but not both. This system is already a Sprite. | Authoring problem. The asset is marked as both a sprite and a mesh. |
APEX_INVALID_OPERATION | Unable to set Mesh Material Weight (%d). Systems can be either Mesh or Sprite, but not both. This system is already a Sprite. | Authoring problem. The asset is marked as both a sprite and a mesh. |
APEX_INVALID_OPERATION | Specifying modifiers before specifying the mesh or sprite asset is invalid | Authoring problem. A mesh or sprite must be specified to which the modifier is applied. |
APEX_INVALID_OPERATION | The specified modifier doesn’t work on that system type (e.g. Sprite Modifier on a Mesh System or vice-versa). | Authoring problem. Only mesh modifiers can be used on meshes and only sprite modifiers can be applied to sprites. |
APEX_INVALID_OPERATION | The specified modifier doesn’t work in that stage. | Authoring problem. |
APEX_INVALID_OPERATION | position %d is greater than modifier stack size: %d | APEX internal error |