APEX IOFx Programmers Guide

../_images/ParticlesIcon.JPG

Introduction

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 RenderVolumes.

IOFX Semantics and Render Layouts

IOFX can output different render semantics (defined in IofxRenderSemantic enum):

POSITION     - 3D Position of particle
COLOR        - Color of particle
VELOCITY     - 3D Linear velocity of particle
SCALE        - 2D or 3D Scale of particle
LIFE_REMAIN  - Normalized life of particle = 1.0 (spawned) -> 0.0 (dead)
DENSITY      - Particle density
SUBTEXTURE   - Sub-texture index of particle (only Sprites)
ORIENTATION  - Particle screen orientation = angle in radians (CCW in screen plane, only Sprites)
ROTATION     - 3D particle rotation (only Meshes)
USER_DATA    - User data - 32 bits (passed from APEX Emitter)

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; POSITION, ROTATION, SCALE, VELOCITY, LIFE_REMAIN, and DENSITY if the underlying IOS supplies a density value (currently only 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.

Sprite/Mesh render buffer represents an array of structures, like ordinary Vertex buffer in Computer Graphics. Each structure of render buffer is defined by render layout (corresponding to Vertex Declaration in Computer Graphics), consisting of set {semantic, format, offset} and stride.

All available Sprite semantics & format pairs are defined in IofxSpriteRenderLayoutElement enum:

POSITION_FLOAT3
COLOR_RGBA8
COLOR_BGRA8
COLOR_FLOAT4
VELOCITY_FLOAT3
SCALE_FLOAT2
LIFE_REMAIN_FLOAT1
DENSITY_FLOAT1
SUBTEXTURE_FLOAT1
ORIENTATION_FLOAT1
USER_DATA_UINT1

All available Mesh semantics & format pairs are defined in IofxMeshRenderLayoutElement enum:

POSITION_FLOAT3
ROTATION_SCALE_FLOAT3x3
POSE_FLOAT3x4
VELOCITY_LIFE_FLOAT4
DENSITY_FLOAT1
COLOR_RGBA8
COLOR_BGRA8
COLOR_FLOAT4
USER_DATA_UINT1

Render layout for Sprite/Mesh render buffer is defined in IofxSpriteRenderLayout/IofxMeshRenderLayout. It has offsets array indexed by layout element, which encodes render buffer structure: if offset value is -1 the corresponding layout element is not present, otherwise it’s present in the structure at the provided offset. Stride in render layout sets the whole size of the render buffer structure, and should be enough to store all presented layout elements with corresponding offsets.

Sprite render layout also supports render from surfaces. In this case IofxSpriteRenderLayout::surfaceCount value is greater than zero and IofxSpriteRenderLayout::surfaceDescs array describes surfaces and IofxSpriteRenderLayout::surfaceElements array defines layout for each surface.

All available Sprite surface layouts are defined in IofxSpriteRenderLayoutSurfaceElement enum:

POSITION_FLOAT4
SCALE_ORIENT_SUBTEX_FLOAT4
COLOR_RGBA8
COLOR_BGRA8
COLOR_FLOAT4

By default APEX creates Sprite/Mesh render layout based on the current set of semantics, so that all output semantics are presented in the render buffer. User can create its own render layout by implementing IofxRenderCallback and overriding getIofxSpriteRenderLayout and getIofxMeshRenderLayout methods:

class IofxRenderCallbackImpl : public IofxRenderCallback
{
    virtual bool getIofxSpriteRenderLayout(IofxSpriteRenderLayout& spriteRenderLayout, uint32_t spriteCount, uint32_t spriteSemanticsBitmap, RenderInteropFlags::Enum interopFlags)
    {
        //fill spriteRenderLayout here
        return true;
    }
    virtual bool getIofxMeshRenderLayout(IofxMeshRenderLayout& meshRenderLayout, uint32_t meshCount, uint32_t meshSemanticsBitmap, RenderInteropFlags::Enum interopFlags)
    {
        //fill meshRenderLayout here
        return true;
    }
};

Argument spriteCount/meshCount determines the 1D size of render buffer or 2D size of render surfaces. Surface width should be power of two and >= 32. It is a good idea to determine surface width as a square root of spriteCount ceiled to the nearest power of two. Non-zero bits in the spriteSemanticsBitmap/meshSemanticsBitmap argument correspond to the semantics defined in the IofxRenderSemantic enum and describe which semantics are presented in IOFX output. User are free to provide any set of layout elements in the render layout, even if the corresponding semantics are not present in IOFX output, in this case IOFX outputs default values for these semantics. The most common case is to create just one or a few fixed render layout(s) matching user particles shader(s) input.

Renderables

Rendering is implemented using IOFX Renderable object. Each IOFX actor has its own IOFX Renderable which stores all needed rendering information and can live after the original IOFX actor has been released. Before accessing rendering data in IOFX Renderables, it should be updated once for each rendering frame by calling ModuleIofx::prepareRenderables(const Scene&). IOFX module has method ModuleIofx::createIofxRenderableIterator(const Scene&) to create iterator over all IOFX renderables. The following code snipper illustrates how to use it all together:

mIofxModule->prepareRenderables(*mApexScene);
IofxRenderableIterator* iter = mIofxModule->createIofxRenderableIterator(*mApexScene);
for (IofxRenderable* renderable = iter->getFirst(); renderable ; renderable = iter->getNext())
{
    if (visibilityCheck(renderable->getBounds())
    {
        if (const IofxSpriteRenderData* spriteRenderData = renderable->getSpriteRenderData())
        {
            renderSprites(spriteRenderData);
        }
        else
        if (const IofxMeshRenderData* meshRenderData = renderable->getMeshRenderData())
        {
            renderMeshes(meshRenderData);
        }
    }
    renderable->release();
}
iter->release();

There are two kinds of render data in IOFX Renderable, one is unique per Renderable and other is shared across several Renderables. IofxSpriteRenderData has references to shared IofxSpriteSharedRenderData, and IofxMeshRenderData has references to shared IofxMeshSharedRenderData.

Shared render data stores Sprite/Mesh Render Layout and Sprite/Mesh Render Buffer or Sprite Surfaces. Unique render data stores Render Resource information (Sprite material or Mesh from IOFX asset) and a range (defined by startIndex and objectCount) which points to the corresponding shared render buffer.

Note

For convenience IOFX provides a way to associate user data with each renderable object: IofxRenderable interface is derived from ApexInteface, which has userData member, and also IofxRenderCallback has 3 trigger methods onCreatedIofxRenderable, onUpdatedIofxRenderable & onReleasingIofxRenderable, which allows user to monitor renderable life time and updates, so that associated user data can be appropriately managed.

Shared Render Buffers & Surfaces

Shared render buffers for both Sprites & Meshes are implemented using UserRenderBuffer interface, and shared render surfaces for Sprites are implemented using UserRenderSurface interface.

To access render data on CPU for both render buffer & surface user have to use map/unmap methods.

By default APEX creates simple render buffer/surface internally based on given render layout, it just stores render data in CPU memory, and user need to map render buffer/surface, then copy its content to user graphics resource, and finally unmap it. This way is simple but not efficient, so APEX has another way for user to provide its own implementation for render buffer/surface, by overriding createRenderBuffer() & createRenderSurface() methods in IofxRenderCallback.

For CPU access user need to implement map/unmap methods, and can directly map/unmap graphics buffer, eliminating data copy.

When IOS simulation is done on GPU there is still overhead to copy data from GPU (compute) to CPU and back from CPU to GPU (graphics), and to eliminate this overhead IOFX supports compute-graphics interop, when IOFX writes render data directly into user graphics buffer on GPU. In order for interop to work, user have to implement getCUDAgraphicsResource() method in UserRenderBuffer/UserRenderSurface.

Note

IOFX always writes output semantics with exact formats and offsets provided by user-defined or default render layout. So when user graphics buffer is directly mapped or used in interop-mode, its structure have to exactly match IOFX render layout!

Render Volumes

RenderVolumes 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:

RenderVolume *createRenderVolume( const Scene& 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 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.

Render Volumes Use Cases

RenderVolumes provide a lot of flexibility in the way particle render resources are managed. Here are just a few use cases.

Simple

A game may allocate just a single RenderVolume with infinite bounds, zero priority, and allIofx set to true.

Renderable Per Emitter

If your game engine prefers to have one renderable per emitter, you may create an RenderVolume 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.

Lighting Considerations

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.

Special IOFX Renderers

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 rendering this actor.

Explicit Culling

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.

Render Volumes At Runtime

In order to render Render Volume user should lock actor list and iterate over it like this:

if (visibilityCheck(mVolume->getBounds())
{
    uint32_t numIofxActors;
    IofxActor* const* iofxActors = mVolume->lockIofxActorList(numIofxActors);
    for (uint32_t i = 0 ; i < numIofxActors ; i++)
    {
        IofxRenderable* renderable = iofxActors[i]->acquireRenderableReference();
        if (renderable)
        {
            if (visibilityCheck(renderable->getBounds())
            {
                if (const IofxSpriteRenderData* spriteRenderData = renderable->getSpriteRenderData())
                {
                    renderSprites(spriteRenderData);
                }
                else
                if (const IofxMeshRenderData* meshRenderData = renderable->getMeshRenderData())
                {
                    renderMeshes(meshRenderData);
                }
            }
            renderable->release();
        }
    }
    mVolume->unlockIofxActorList();
}

Errors and Warnings

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 RenderMeshAsset 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