/*
 * Particle System plugin
 */

/**
 * \ingroup rpprtsys
 * \page rpprtsysoverview RpPrtSys Plugin Overview
 *
 * This plugin provides facilities for creating and using particle systems.
 * 
 * The particle system can either be created on the fly or loaded from a dff
 * file created with the particle editor.
 *
 * A ParticlesAtomic behaves like a regular atomic and is created with a frame
 * attached. It can be used to change the orientation and position of the 
 * particles system.
 *
 * The particles are created within the surface of the emitter (set by 
 * \ref RpParticlesAtomicSetEmitterSize) and will remain in the cone
 * defined by \ref RpParticlesAtomicSetEmitterAngle.
 * 
 * The position of the particles are calculated every time the 
 * \ref RpParticlesAtomicUpdate function is called. 
 * A particle will be active
 * in the system for the length of time defined by 
 * \ref RpParticlesAtomicSetFlightTime
 * (expressed in seconds) and it will then be recycled.
 * 
 */

/****************************************************************************
 *                                                                          *
 *  Module  :   rpprtsys.c                                                  *
 *                                                                          *
 *  Purpose :                                                               *
 *                                                                          *
 ****************************************************************************/

/****************************************************************************
 Includes
 */

#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <string.h>

/* RW includes */
#include "rpplugin.h"
#include <rpdbgerr.h>
#include <rwcore.h>
#include <rpworld.h>
#include <rprandom.h>

#include "prvprtcl.h"
#include "rpprtsys.h"
#include "rpplugin.h"

#if (defined(SKY_DRVMODEL_H))
#include "ppprtclssky.h"
#elif (defined(XBOX_DRVMODEL_H))
#include "ppprtclsxbox.h"
#else
#include "ppprtclswin.h"
#endif

#if (!defined(DOXYGEN))
static const char rcsid[] __RWUNUSED__ =
    "@@@@(#)$Id: rpprtsys.c,v 1.23 2001/09/25 15:34:52 iestynb Exp $";
#endif /* (!defined(DOXYGEN)) */


/****************************************************************************
 Local Types
 */

/****************************************************************************
 Local (Static) Prototypes
 */

/****************************************************************************
 Local Defines
 */

#define ParticleColorToRwRGBAMacro(_b, _c)              \
MACRO_START                                             \
{                                                       \
    (_b)->red   = (RwUInt8)((_c)->red + ((RwReal)0.5) / COLSCALE);   \
    (_b)->green = (RwUInt8)((_c)->green + ((RwReal)0.5) / COLSCALE); \
    (_b)->blue  = (RwUInt8)((_c)->blue + ((RwReal)0.5) / COLSCALE);  \
    (_b)->alpha = (RwUInt8)((_c)->alpha + ((RwReal)0.5) / COLSCALE); \
}                                                       \
MACRO_STOP


/****************************************************************************
 Globals (across program)
 */

/* this should be an engine global */
RxPipeline         *GParticlesObjectPipe;
RxPipeline         *GParticlesMaterialPipe;
RwInt32             GParticlesAtomicDataOffset;

/****************************************************************************
 Local (static) Globals
 */

/****************************************************************************
 Functions
 */

static              RwBool
ParticlesDataDestroy(RpParticlesData * particlesData)
{
    RwBool              result =
        ((RpParticlesData *) NULL != particlesData);

    RWFUNCTION(RWSTRING("ParticlesDataDestroy"));

    if (result)
    {
        RwFree(particlesData);
        particlesData = (RpParticlesData *) NULL;
    }

    RWRETURN(result);
}

static RpParticlesData *
ParticlesDataCreate(void)
{
    RpParticlesData    *particlesData = (RpParticlesData *) NULL;

    RWFUNCTION(RWSTRING("ParticlesDataCreate"));

    particlesData =
        (RpParticlesData *) RwMalloc(sizeof(RpParticlesData));

    if (particlesData)
    {

        particlesData->flightTime = ((RwReal) 0);
        particlesData->time = ((RwReal) 0);
        particlesData->particleSize.x = ((RwReal) 0);
        particlesData->particleSize.y = ((RwReal) 0);

        particlesData->randomSeed = ((RwReal) 0.123456789);

        particlesData->flags = ((RpParticlesFlag)
                                (rpPARTICLESDIRTY |
                                 (RwUInt32) particlesData->flags));
        particlesData->flags = ((RpParticlesFlag)
                                (rpPARTICLESSETBSPHERE |
                                 (RwUInt32) particlesData->flags));
    }

    RWRETURN(particlesData);
}

static void        *
ParticlesOpen(void *instance,
              RwInt32 __RWUNUSED__ offset, RwInt32 __RWUNUSED__ size)
{
    RWFUNCTION(RWSTRING("ParticlesOpen"));

    RWASSERT(instance);

    if (!_rpParticlesSetupPipes())
    {
        instance = (void *) NULL;
    }

    RWRETURN(instance);
}

static void        *
ParticlesClose(void *instance,
               RwInt32 __RWUNUSED__ offset, RwInt32 __RWUNUSED__ size)
{
    RWFUNCTION(RWSTRING("ParticlesClose"));

    RWASSERT(instance);

    _rpParticlesDestroyPipes();

    RWRETURN(instance);
}

static void        *
ParticlesAtomicConstructor(void *object,
                           RwInt32 __RWUNUSED__ offset,
                           RwInt32 __RWUNUSED__ size)
{
    RWFUNCTION(RWSTRING("ParticlesAtomicConstructor"));

    RWASSERT(object);

    *PARTICLESATOMICGETDATA(object) = (RpParticlesData *) NULL;

    RWRETURN(object);
}

static void        *
ParticlesAtomicDestructor(void *object,
                          RwInt32 __RWUNUSED__ offset,
                          RwInt32 __RWUNUSED__ size)
{

    RpParticlesData    *particlesData;

    RWFUNCTION(RWSTRING("ParticlesAtomicDestructor"));

    RWASSERT(object);

    particlesData = *PARTICLESATOMICGETDATA(object);

    if (particlesData)
    {
        *PARTICLESATOMICGETDATA(object) = (RpParticlesData *) NULL;

        ParticlesDataDestroy(particlesData);
    }

    RWRETURN(object);
}

static void        *
ParticlesAtomicCopy(void *dstObject,
                    const void *srcObject,
                    RwInt32 __RWUNUSED__ offset,
                    RwInt32 __RWUNUSED__ size)
{

    const RpAtomic     *srcAtomic;
    RpAtomic           *dstAtomic;
    RpMaterial         *material = (RpMaterial *) NULL;
    const RpParticlesData *srcParticlesData;
    RpParticlesData    *dstParticlesData;

    RWFUNCTION(RWSTRING("ParticlesAtomicCopy"));

    srcAtomic = (const RpAtomic *) srcObject;
    srcParticlesData = *PARTICLESATOMICGETCONSTDATA(srcAtomic);
    if (srcParticlesData)
    {

        dstAtomic = (RpAtomic *) dstObject;
        dstParticlesData = *PARTICLESATOMICGETDATA(dstAtomic);
        if (!dstParticlesData)
        {
            dstParticlesData = ParticlesDataCreate();
            if (!dstParticlesData)
            {
                RWRETURN(NULL);
            }

            *PARTICLESATOMICGETDATA(dstAtomic) = dstParticlesData;
        }

        *dstParticlesData = *srcParticlesData;

        {
            RpGeometry         *geometry;
            RpTriangle         *triangle;

            geometry = RpAtomicGetGeometry(dstAtomic);
            if (geometry)
            {
                triangle = RpGeometryGetTriangles(geometry);
                material =
                    RpGeometryTriangleGetMaterial(geometry, triangle);
            }
        }

        RpAtomicSetPipeline(dstAtomic, GParticlesObjectPipe);
        RpMaterialSetPipeline(material, GParticlesMaterialPipe);
    }
    else
    {

        dstObject = (void *) NULL;
    }

    RWRETURN(dstObject);
}

static RwStream    *
ParticlesAtomicChunkReadCallBack(RwStream * stream,
                                 RwInt32 __RWUNUSEDRELEASE__
                                 binaryLength, void *object,
                                 RwInt32 __RWUNUSED__ offsetInObject,
                                 RwInt32 __RWUNUSED__ sizeInObject)
{

    RpAtomic           *atomic;
    RpMaterial         *material = (RpMaterial *) NULL;
    RpParticlesData    *particlesData;
    RwBool              particlesDefined;
    RwInt32             size, sizeTotal;

    RWFUNCTION(RWSTRING("ParticlesAtomicChunkReadCallBack"));

    sizeTotal = 0;
    atomic = (RpAtomic *) object;

    size = sizeof(RwInt32);
    if (!RwStreamReadInt(stream, &particlesDefined, size))
    {
        RWRETURN((RwStream *) NULL);
    }
    sizeTotal += size;

    if (!particlesDefined)
    {
        /* nothing more to read */
        RWRETURN(stream);
    }

    particlesData = ParticlesDataCreate();
    if (!particlesData)
    {
        RWRETURN((RwStream *) NULL);
    }
    *PARTICLESATOMICGETDATA(atomic) = particlesData;

    /* particle data */
    size = sizeof(RpParticlesData);
    if (!RwStreamRead(stream, particlesData, size))
    {
        RWRETURN((RwStream *) NULL);
    }
    sizeTotal += size;

    {
        RpGeometry         *geometry;
        RpTriangle         *triangle;

        geometry = RpAtomicGetGeometry(atomic);
        if (geometry)
        {
            triangle = RpGeometryGetTriangles(geometry);
            material =
                RpGeometryTriangleGetMaterial(geometry, triangle);
        }
    }

    RpAtomicSetPipeline(atomic, GParticlesObjectPipe);
    RpMaterialSetPipeline(material, GParticlesMaterialPipe);

    RWASSERT(sizeTotal == binaryLength);

    _rpParticleAddGeomData(atomic, particlesData->numParticles);

    particlesData->flags = (RpParticlesFlag)
        (rpPARTICLESDIRTY | (RwUInt32) particlesData->flags);
    particlesData->flags = (RpParticlesFlag)
        (rpPARTICLESSETBSPHERE | (RwUInt32) particlesData->flags);

    RWRETURN(stream);
}

static RwStream    *
ParticlesAtomicChunkWriteCallBack(RwStream * stream,
                                  RwInt32 __RWUNUSEDRELEASE__
                                  binaryLength, const void *object,
                                  RwInt32 __RWUNUSED__ offsetInObject,
                                  RwInt32 __RWUNUSED__ sizeInObject)
{

    const RpAtomic     *atomic;
    const RpParticlesData *particlesData;
    RwBool              particlesDefined;
    RwInt32             size, sizeTotal;

    RWFUNCTION(RWSTRING("ParticlesAtomicChunkWriteCallBack"));

    sizeTotal = 0;

    atomic = (const RpAtomic *) object;
    particlesData = *PARTICLESATOMICGETCONSTDATA(atomic);
    particlesDefined = particlesData ? 1 : 0;

    size = sizeof(RwInt32);
    if (!RwStreamWriteInt(stream, &particlesDefined, size))
    {
        RWRETURN((RwStream *) NULL);
    }
    sizeTotal += size;

    if (!particlesDefined)
    {
        RWASSERT(binaryLength == sizeTotal);

        /* nothing more to write */
        RWRETURN(stream);
    }

    /* flight time */
    size = sizeof(RpParticlesData);
    if (!RwStreamWrite(stream, particlesData, size))
    {
        RWRETURN((RwStream *) NULL);
    }
    sizeTotal += size;

    RWASSERT(sizeTotal == binaryLength);

    RWRETURN(stream);
}

static              RwInt32
ParticlesAtomicChunkGetSizeCallBack(const void *object,
                                    RwInt32 __RWUNUSED__ offsetInObject,
                                    RwInt32 __RWUNUSED__ sizeInObject)
{

    const RpAtomic     *atomic;
    const RpParticlesData *particlesData;
    RwInt32             size;

    RWFUNCTION(RWSTRING("ParticlesAtomicChunkGetSizeCallBack"));

    size = 0;

    atomic = (const RpAtomic *) object;
    particlesData = *PARTICLESATOMICGETCONSTDATA(atomic);

    /* particles present flag */
    size += sizeof(RwInt32);

    if (particlesData)
    {
        size += sizeof(RpParticlesData);
    }

    /* particle data */

    RWRETURN(size);
}

#if (defined(EMITTERGETNEARESTVECTOR))
static RwV3d       *
emitterGetNearestVector(RwV3d * nearestVec, RwV3d * axis,
                        RwV3d * emitterVec, RwV3d * minVec,
                        RwV3d * maxVec, RwReal cosAngle)
{
    RwReal              dotProduct, dotMinVec, dotMaxVec;

    RWFUNCTION(RWSTRING("emitterGetNearestVector"));

    /* is the axis within the emitter cone */
    dotProduct = RwV3dDotProduct(axis, emitterVec);

    if (dotProduct <= cosAngle)
    {
        /* the axis is outside the cone, so find nearest match */
        dotMinVec = RwV3dDotProduct(axis, minVec);
        dotMaxVec = RwV3dDotProduct(axis, maxVec);

        if (dotMinVec > dotMaxVec)
        {
            /* minVec is nearest */
            *nearestVec = *minVec;
        }
        else
        {
            /* maxVec is nearest */
            *nearestVec = *maxVec;
        }
    }
    else
    {
        *nearestVec = *axis;
    }

    RWRETURN(nearestVec);
}
#endif /* (defined(EMITTERGETNEARESTVECTOR))*/

/*
 * Compute 3D particle vertex position given
 * starting position on the emitter ranging from 0..1 in X and Y,
 * initial velocities (ranging from 0..1), randZ probably just 0 and 1 are useful
 * texture coordinates (determines vertex coordinate accordingly)
 * and time ranging from 0 to 1.
 * (This is *supposed* to be the same algorithm implemented in
 * the generic/VU/Vertex shader versions, just with the randomly chosen variables replaced,
 * hence sampling start positions, velocities and times at the extremes
 * should give a reasonable bound sphere.)
 */

static RwV3d 
ComputeParticlePos( RpParticlesData *particlesData,
                    RwReal startX, RwReal startY,
                    RwReal randX, RwReal randY, RwReal randZ,
                    RwReal texU, RwReal texV,
                    RwReal time )
{
    /* for "initiation" */
    RwV3d               initpos;
    RwV3d               initvel;
    RwReal              angleRangeX, angleRangeY;
    RwReal              speed;

    /* for "animation" */
    RwReal              invFlightTime;
    RwReal              prtlcTime, tNorm, t2;
    RwReal              growth;
    RwV2d               size;
    RwV3d               pos;
    RwReal              uScale, vScale;

    RwV3d               up = {0,1,0},
                        right = {0, 0, 1};

    /* aliasing of variables for "convienience" */
    RwReal              width, length;
    RwReal              angle, speedInit, speedVar, dampening;
    RwReal              duration;
    RWFUNCTION(RWSTRING("ComputeParticlePos"));

    width = particlesData->emitterSize.x;
    length = particlesData->emitterSize.y;

    angle = particlesData->angle;
    speedInit = particlesData->speed;
    speedVar = particlesData->speedVariation;
    dampening = particlesData->dampening;
    
    duration = particlesData->duration;

    /* simulate initiation ---------------------------------------------- */

    /* set the initial positions in YX plane */
    initpos.x = (startX - 0.5f) * width;

    initpos.y = (startY - 0.5f) * length;

    initpos.z = ((RwReal) 0);

    /* set the initial velocities */
    angleRangeX = ((randX * ((RwReal) 2)) - ((RwReal) 1));
    angleRangeY = ((randY * ((RwReal) 2)) - ((RwReal) 1));

    initvel.x = angle * angleRangeX;
    initvel.y = angle * angleRangeY;
    initvel.z = ( ((RwReal) 1) - 
                  ((initvel.x * initvel.x) + (initvel.y * initvel.y)) );

    speed = ((speedInit - (speedVar / ((RwReal) 2))) +
             (speedVar * randZ));
    speed *= (((RwReal) 1) -
              (RwRealMin2((angleRangeX * angleRangeX) +
                          (angleRangeY * angleRangeY),
                          ((RwReal) 1)) * dampening * ((RwReal) -
                                                       1)));

    RwV3dScale(&initvel, &initvel, speed);

    /* simulate animation ----------------------------------------------- */

    invFlightTime = ((RwReal) 1) / particlesData->flightTime;

    /* particle time */
    prtlcTime = time * particlesData->flightTime;
    tNorm = prtlcTime * invFlightTime;

    /* particle position */
    /* s = u*t + 0.5*a*(t^2) + p */
    t2 = prtlcTime * prtlcTime;
    pos.x = ( (initvel.x * prtlcTime) + 
              (particlesData->force.x * t2) + 
              initpos.x );
    pos.y = ( (initvel.y * prtlcTime) + 
              (particlesData->force.y * t2) + 
              initpos.y );
    pos.z = ( (initvel.z * prtlcTime) + 
              (particlesData->force.z * t2) + 
              initpos.z );

    /* particle size */
    growth = 0.5f * (((particlesData->growth - 1.0f) * tNorm) + 1.0f);
    RwV2dScale(&size, &particlesData->particleSize, growth);

    /* figure out quad vertex coordinates from position, size & texture u v */
    uScale = size.x * (1.0f - 2.0f * texU);
    vScale = size.y * (1.0f - 2.0f * texV);

    pos.x = pos.x + vScale * up.x + uScale * right.x;
    pos.y = pos.y + vScale * up.y + uScale * right.y;
    pos.z = pos.z + vScale * up.z + uScale * right.z;

    RWRETURN(pos);
}

static void
CalcBoundingSphere( RwV3d *verts, RwInt32 numVerts, RwSphere * boundingSphere )
{
    RwV3d              *vert;
    RwSphere            sphere;
    RwReal              sphere_radius = (RwReal) (0);
    RwBBox              boundBox;

    RWFUNCTION(RWSTRING("CalcBoundingSphere"));

    /* Find the median (sort of) by doing a bounding box,
     * then using the center of the box.
     */
    RwBBoxCalculate(&boundBox, verts, numVerts);
    RwV3dAdd(&sphere.center, &boundBox.inf, &boundBox.sup);
    RwV3dScale(&sphere.center, &sphere.center, (RwReal) (0.5));

    vert = verts;

    /* Find the radius (we do this in square space */
    while (numVerts--)
    {
        RwReal              nDist;
        RwV3d               vTmp;

        RwV3dSub(&vTmp, vert, &sphere.center);
        nDist = RwV3dDotProduct(&vTmp, &vTmp);

        if (nDist > sphere_radius)
        {
            sphere_radius = nDist;
        }

        vert++;
    }

    /* Now do the root */
    if (sphere_radius > (RwReal) (0))
    {
        rwSqrtMacro(sphere_radius, sphere_radius);
    }

    /* Add 0.1% for numerical inaccuracy */
    sphere.radius = sphere_radius * ((RwReal) (1.001));

    /* Save off result */
    (*boundingSphere) = sphere;

    RWRETURNVOID();
}

static RpAtomic           *
ParticlesAtomicSetBoundingSphere(RpAtomic * atomic)
{
    RpParticlesData    *particlesData = (RpParticlesData *)NULL;

    RWFUNCTION(RWSTRING("ParticlesAtomicSetBoundingSphere"));

    RWASSERT(atomic);

    particlesData = *PARTICLESATOMICGETDATA(atomic);

    if (particlesData)
    {
        RwSphere            boundingSphere;
        RpGeometry         *geometry;
        RpMorphTarget      *morph;

        /*
        Simulate the particles animation, replacing randomness with samples at the extreme values
        to produce a set of extreme particle positions and build a bound sphere from that.
        */

        const RwInt32   startXSamples = 2,  /* just the extremes are enough */
                        startYSamples = 2,
                        randXSamples = 3,   /* make sure we get ends & middle of angle param */
                        randYSamples = 3,
                        randZSamples = 2,   /* just the extremes are enough */
                        texUSamples = 2,
                        texVSamples = 2,
                        timeSamples = 3;    /* make sure we get ends and saddle of parabola */

        RwReal  startX, startXStep = 1.0f / (RwReal)(startXSamples - 1),
                startY, startYStep = 1.0f / (RwReal)(startYSamples - 1),
                randX, randXStep = 1.0f / (RwReal)(randXSamples - 1),
                randY, randYStep = 1.0f / (RwReal)(randYSamples - 1),
                randZ, randZStep = 1.0f / (RwReal)(randZSamples - 1),
                texU, texUStep = 1.0f / (RwReal)(texUSamples - 1),
                texV, texVStep = 1.0f / (RwReal)(texVSamples - 1),
                time, timeStep = 1.0f / (RwReal)(timeSamples - 1);

        RwInt32 numSamples =
            startXSamples * startYSamples
          * randXSamples * randYSamples * randZSamples
          * texUSamples * texVSamples
          * timeSamples,
          sampleIndex;

        RwV3d *samples = (RwV3d *)RwMalloc( numSamples * sizeof( RwV3d ) );
        RWASSERT( samples );

        /*
        Really, this isn't as bad as it looks,
        cause we only sample a few points on each parameter!
        */

        /* fudge it a bit to make sure 1.0f is included in the sampling */
        #define SAMPLE_END 1.01f
        
        sampleIndex = 0;

        for (startX = 0.0f; startX < SAMPLE_END; startX += startXStep)
        {
            for (startY = 0.0f; startY < SAMPLE_END; startY += startYStep)
            {
                for (randX = 0.0f; randX < SAMPLE_END; randX += randXStep)
                {
                    for (randY = 0.0f; randY < SAMPLE_END; randY += randYStep)
                    {
                        for (randZ = 0.0f; randZ < SAMPLE_END; randZ += randZStep)
                        {
                            for (texU = 0.0f; texU < SAMPLE_END; texU += texUStep)
                            {
                                for (texV = 0.0f; texV < SAMPLE_END; texV += texVStep)
                                {
                                    for (time = 0.0f; time < SAMPLE_END; time += timeStep)
                                    {
                                        samples[ sampleIndex++ ] =
                                            ComputeParticlePos( particlesData,
                                                startX, startY,
                                                randX, randY, randZ,
                                                texU, texV,
                                                time );
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        RWASSERT( sampleIndex == numSamples );

        CalcBoundingSphere( samples, numSamples, &boundingSphere );

        RwFree( samples );

        geometry = RpAtomicGetGeometry(atomic);
        morph = RpGeometryGetMorphTarget(geometry, 0);
        RpMorphTargetSetBoundingSphere(morph, &boundingSphere);
        _rpAtomicResyncInterpolatedSphere(atomic);
    }
    else
    {
        atomic = (RpAtomic *) NULL;
    }

    RWRETURN(atomic);
}

/*************************************************************************/

/* API */

/**
 * \ingroup rpprtsys
 * \ref RpParticlesAtomicPluginAttach attaches the particles
 * plugin to the RenderWare system to enable the generation of fast
 * parametric particle systems.  
 * The particle plugin must be attached
 * between initializing the system with \ref RwEngineInit and opening it
 * with \ref RwEngineOpen.
 *
 * \warning The include files rpprtsys.h, rprandom.h and rpworld.h are
 * also required and must be included by an application wishing to
 * generate particles.
 *
 * \warning The particle plugin requires
 * \li the particle plugin library rpprtsys.lib
 * \li the random plugin rprandom.lib
 * \li the world plugin rpworld.lib
 * to be linked into the application and attached at run time
 *
 * \return TRUE if successful or FALSE if there is an error.
 *
 * \see RwEngineInit
 * \see RwEngineOpen
 * \see RwEngineStart
 * \see RpWorldPluginAttach
 * \see RpRandomPluginAttach
 *
 */
RwBool
RpParticlesAtomicPluginAttach(void)
{

    RwInt32             offset;

    RWAPIFUNCTION(RWSTRING("RpParticlesAtomicPluginAttach"));

    /* Register the plugIn */
    offset =
        RwEngineRegisterPlugin(0,
                               rwID_PARTICLESPLUGIN,
                               ParticlesOpen, ParticlesClose);
    if (offset < 0)
    {
        RWRETURN(FALSE);
    }

    /* Extend atomic to hold userdata */
    GParticlesAtomicDataOffset =
        RpAtomicRegisterPlugin(sizeof(RpParticlesData *),
                               rwID_PARTICLESPLUGIN,
                               ParticlesAtomicConstructor,
                               ParticlesAtomicDestructor,
                               ParticlesAtomicCopy);
    if (GParticlesAtomicDataOffset < 0)
    {
        RWRETURN(FALSE);
    }

    offset = RpAtomicRegisterPluginStream(rwID_PARTICLESPLUGIN,
                                          ParticlesAtomicChunkReadCallBack,
                                          ParticlesAtomicChunkWriteCallBack,
                                          ParticlesAtomicChunkGetSizeCallBack);
    if (offset < 0)
    {
        RWRETURN(FALSE);
    }

    if (!_rpParticleExtendGeom())
    {
        RWRETURN(FALSE);
    }

    RWRETURN(TRUE);
}

/**
 * \ingroup rpprtsys
 * \ref RpParticlesAtomicDestroyAuxiliaryData is used to destroy a
 * particles atomic associated with the particles.
 *
 * \param atomic
 * The RpAtomic * whose auxiliary data is to be destroyed
 *
 * \return The source RpAtomic *
 */
RpAtomic           *
RpParticlesAtomicDestroyAuxiliaryData(RpAtomic * atomic,
                                      void *__RWUNUSED__ data)
{
    RWAPIFUNCTION(RWSTRING("RpParticlesAtomicDestroyAuxiliaryData"));

    if (atomic)
    {
        RpParticlesData    *particlesData =
            *PARTICLESATOMICGETDATA(atomic);

        if (particlesData)
        {
            ParticlesDataDestroy(particlesData);
            *PARTICLESATOMICGETDATA(atomic) = (RpParticlesData *) NULL;
        }

    }

    RWRETURN(atomic);
}

/**
 * \ingroup rpprtsys
 * \ref RpParticlesAtomicCreate is used to create a
 * new particles atomic according to the specified number of particles
 * and the material to be associated with the particles.
 *
 * The atomic is created with a frame attached.
 *
 * \param numParticles  A RwInt32 value equal to
 *                      the number of particles (maximum allowed 65535).
 * \param materials     A pointer to an RpMaterial hosting the
 *                      particle system.
 *
 * \return Returns a pointer to the new atomic if successful or NULL if
 * there is an error.
 *
 */
RpAtomic           *
RpParticlesAtomicCreate(RwInt32 numParticles, RpMaterial * material)
{

    RpAtomic           *atomic;
    RpGeometry         *geometry;
    RwInt32             flags;
    RpParticlesData    *particlesData;
    RwFrame            *frame;

    RWAPIFUNCTION(RWSTRING("RpParticlesAtomicCreate"));

    atomic = RpAtomicCreate();
    if (!atomic)
    {
        RWRETURN((RpAtomic *) NULL);
    }

    frame = RwFrameCreate();
    if (!frame)
    {
        RWRETURN((RpAtomic *) NULL);
    }

    flags = rpGEOMETRYPRELIT;

    if (RpMaterialGetTexture(material))
    {
        flags |= rpGEOMETRYTEXTURED;
    }

    geometry = RpGeometryCreate(1, 1, flags);
    if (!geometry)
    {
        RWRETURN((RpAtomic *) NULL);
    }

    particlesData = ParticlesDataCreate();
    if (!particlesData)
    {
        RWRETURN((RpAtomic *) NULL);
    }
    *PARTICLESATOMICGETDATA(atomic) = particlesData;

    particlesData->numParticles = numParticles;

    if (RpGeometryLock
        (geometry, rpGEOMETRYLOCKPOLYGONS | rpGEOMETRYLOCKVERTICES))
    {
        RpMorphTarget      *morph;
        RwV3d              *verts;
        RpTriangle         *triangles;

        /* set the one and only vertex */
        morph = RpGeometryGetMorphTarget(geometry, 0);
        verts = RpMorphTargetGetVertices(morph);
        verts->x = ((RwReal) 0);
        verts->y = ((RwReal) 0);
        verts->z = ((RwReal) 0);

        /* set the one and only triangle */
        triangles = RpGeometryGetTriangles(geometry);

        RpGeometryTriangleSetVertexIndices(geometry, triangles, 0, 0,
                                           0);
        RpGeometryTriangleSetMaterial(geometry, triangles, material);

        RpGeometryUnlock(geometry);

        RpAtomicSetGeometry(atomic, geometry, 0);
        /* reduce the ref count */
        RpGeometryDestroy(geometry);
        RpAtomicSetFrame(atomic, frame);

        /* set the particle pipeline */
        RpAtomicSetPipeline(atomic, GParticlesObjectPipe);
        RpMaterialSetPipeline(material, GParticlesMaterialPipe);

        _rpParticleAddGeomData(atomic, numParticles);

        RWRETURN(atomic);
    }

    RWRETURN((RpAtomic *) NULL);
}

/**
 * \ingroup rpprtsys
 * \ref RpParticlesAtomicIsAParticlesAtomic is used to find out if this atomic
 *  has a particle system.
 *
 * \param atomic A RpAtomic * the atomic representing the particle system
 *
 * \return Returns TRUE if it does or FALSE if it doesn't
 *
 */
RwBool
RpParticlesAtomicIsAParticlesAtomic(RpAtomic * atomic)
{
    RwBool              result;

    RpParticlesData    *particlesData;

    RWAPIFUNCTION(RWSTRING("RpParticlesAtomicIsAParticlesAtomic"));

    result = ((RpAtomic *) NULL != atomic);

    if (result)
    {
        particlesData = *PARTICLESATOMICGETDATA(atomic);
        result = ((RpParticlesData *) NULL != particlesData);
    }

    RWRETURN(result);
}

/**
 * \ingroup rpprtsys
 * \ref RpParticlesAtomicGetNumParticles gets the number of particles in the
 *  particle system.
 *
 * \param atomic the atomic representing the particle system
 *
 * \return Returns the number of particles in the particle system.
 *
 */
RwInt32
RpParticlesAtomicGetNumParticles(RpAtomic * atomic)
{
    RpParticlesData    *particlesData;

    RWAPIFUNCTION(RWSTRING("RpParticlesAtomicGetNumParticles"));
    RWASSERT(atomic);
    particlesData = *PARTICLESATOMICGETDATA(atomic);
    RWASSERT(particlesData != 0);

    RWRETURN(particlesData->numParticles);
}

/**
 * \ingroup rpprtsys
 * \ref RpParticlesAtomicSetEmitterSize is used to specify
 * the size of the emitter rectangle in the xz plane of
 * the emitters local space. 
 * The frame attached to
 * the atomic can later be used to position and orientate the
 * emitter in world space.
 * Particles will be emitted at random positions across this plane.
 *
 * \param atomic the atomic representing the particle system
 * \param width  a RwReal value equal to the width of the emitter
 * \param length a RwReal value equal to the length of the emitter
 *
 * \return Returns a pointer to the atomic if successful or NULL if
 * there is an error.
 *
 */
RpAtomic           *
RpParticlesAtomicSetEmitterSize(RpAtomic * atomic,
                                RwReal width, RwReal length)
{

    RpParticlesData    *particlesData;

    RWAPIFUNCTION(RWSTRING("RpParticlesAtomicSetEmitterSize"));
    RWASSERT(atomic);
    particlesData = *PARTICLESATOMICGETDATA(atomic);
    if (particlesData)
    {
        particlesData->emitterSize.x = width;
        particlesData->emitterSize.y = length;
        particlesData->flags = (RpParticlesFlag)
            (rpPARTICLESDIRTY | (RwUInt32) particlesData->flags);
        particlesData->flags = (RpParticlesFlag)
            (rpPARTICLESSETBSPHERE | (RwUInt32) particlesData->flags);
    }
    else
    {

        atomic = ((RpAtomic *) NULL);
    }

    RWRETURN(atomic);
}

/**
 * \ingroup rpprtsys
 * \ref RpParticlesAtomicGetEmitterSize gets the size of the emitter.
 *
 * \param atomic the atomic representing the particle system
 *
 * \return Returns the size of the emitter.
 *
 */
RwV2d
RpParticlesAtomicGetEmitterSize(RpAtomic * atomic)
{
    RwV2d               result;
    RpParticlesData    *particlesData;

    RWAPIFUNCTION(RWSTRING("RpParticlesAtomicGetEmitterSize"));
    RWASSERT(atomic);
    particlesData = *PARTICLESATOMICGETDATA(atomic);
    RWASSERT((RpParticlesData *) NULL != particlesData);
    result = particlesData->emitterSize;
    RWRETURN(result);
}

/**
 * \ingroup rpprtsys
 * \ref RpParticlesAtomicSetEmitterAngle is used to specify
 * the angle of a cone within which each particle will be emitted.
 * The initial angle of the particles velocity to the emitter
 * plane will vary randomly within this cone.
 * The axis of the cone is perpendicular to the emitter plane.
 *
 * \param atomic the atomic representing the particle system
 * \param angle  a RwReal value equal to the angle of
 *               the cone within which particles are emitted
 *
 * \return Returns a pointer to the atomic if successful or NULL if
 * there is an error.
 *
 */
RpAtomic           *
RpParticlesAtomicSetEmitterAngle(RpAtomic * atomic, RwReal angle)
{
    RpParticlesData    *particlesData;

    RWAPIFUNCTION(RWSTRING("RpParticlesAtomicSetEmitterAngle"));
    RWASSERT(atomic);
    particlesData = *PARTICLESATOMICGETDATA(atomic);
    if (particlesData)
    {
        /* convert into range 0->90 into range 0->1 */
        angle *= rwPI / ((RwReal) 180);
        particlesData->angle = angle / (rwPI * ((RwReal) 0.5));
        particlesData->flags = (RpParticlesFlag)
            (rpPARTICLESDIRTY | (RwUInt32) particlesData->flags);
        particlesData->flags = (RpParticlesFlag)
            (rpPARTICLESSETBSPHERE | (RwUInt32) particlesData->flags);
    }
    else
    {

        atomic = (RpAtomic *) NULL;
    }

    RWRETURN(atomic);
}

/**
 * \ingroup rpprtsys
 * \ref RpParticlesAtomicGetEmitterAngle gets the angle of the emitter.
 *
 * \param atomic the atomic representing the particle system
 *
 * \return Returns the angle of the emitter.
 *
 */
RwReal
RpParticlesAtomicGetEmitterAngle(RpAtomic * atomic)
{
    RwReal              result;
    RpParticlesData    *particlesData;

    RWAPIFUNCTION(RWSTRING("RpParticlesAtomicGetEmitterAngle"));
    RWASSERT(atomic);
    particlesData = *PARTICLESATOMICGETDATA(atomic);
    RWASSERT(particlesData != 0);
    result = particlesData->angle * ((RwReal) 90);
    RWRETURN(result);
}

/**
 * \ingroup rpprtsys
 * \ref RpParticlesAtomicSetSize is used to specify the initial particle size,
 * growth factor and particle aspect ratio.
 * The size is measured in world units.
 * The growth factor is the ratio of destination size over initial size.
 * The aspect ratio is the ratio of width over height.
 *
 * \param atomic the atomic representing the particle system
 * \param size a RwReal value equal to initial height of the particle
 *              (length is determined using the specified aspect ratio)
 * \param growth a RwReal value equal to the growth factor of the particle
 *               across its flight time
 * \param aspectRatio a RwReal value equal to the aspect ratio of the particles
 *
 * \return Returns a pointer to the atomic if successful or NULL if
 * there is an error.
 *
 */
RpAtomic           *
RpParticlesAtomicSetSize(RpAtomic * atomic,
                         RwReal size, RwReal growth, RwReal aspectRatio)
{

    RpParticlesData    *particlesData;

    RWAPIFUNCTION(RWSTRING("RpParticlesAtomicSetSize"));
    RWASSERT(atomic);
    particlesData = *PARTICLESATOMICGETDATA(atomic);
    if (particlesData)
    {
        particlesData->particleSize.x = size;
        particlesData->particleSize.y = size * aspectRatio;
        particlesData->growth = growth;
        particlesData->flags = (RpParticlesFlag)
            (rpPARTICLESDIRTY | (RwUInt32) particlesData->flags);
        particlesData->flags = (RpParticlesFlag)
            (rpPARTICLESSETBSPHERE | (RwUInt32) particlesData->flags);
    }
    else
    {
        atomic = (RpAtomic *) NULL;
    }

    RWRETURN(atomic);
}

/**
 * \ingroup rpprtsys
 * \ref RpParticlesAtomicGetSize gets the size of the particles.
 *
 * \param atomic the atomic representing the particle system
 * \param angle a RwReal value equal to initial height of the particle
 *              (length is determined using the specified aspect ratio)
 * \param growth a RwReal value equal to the growth factor of the particle
 *                across its flight time
 * \param aspectRatio a RwReal value equal to the aspect ratio of the particles
 *
 * \return Returns the size of the particles in the parameters.
 *
 */
void
RpParticlesAtomicGetSize(RpAtomic * atomic,
                         RwReal * size, RwReal * growth,
                         RwReal * aspectRatio)
{
    RpParticlesData    *particlesData;

    RWAPIFUNCTION(RWSTRING("RpParticlesAtomicGetSize"));
    RWASSERT(atomic);
    particlesData = *PARTICLESATOMICGETDATA(atomic);
    RWASSERT(particlesData != 0);
    *size = particlesData->particleSize.x;
    *growth = particlesData->growth;
    RWASSERT(0 < particlesData->particleSize.x);
    *aspectRatio = (particlesData->particleSize.y /
                    particlesData->particleSize.x);
    RWRETURNVOID();
}

/**
 * \ingroup rpprtsys
 * \ref RpParticlesAtomicSetSpeed is used to specify
 *  the particles speed range from speedMin to speedMax.
 *  A particles initial velocity will be a random value within this
 * speed range.
 *  It also specifies a dampening factor, which is used to vary the
 * particles initial speed across the emitter plane.
 * The dampening varies from its maximum value at the edges to
 * zero at the centre.
 * This can be used to simulate effects such as water coming out
 * of a hose where dampening is greatest at the edges.
 *
 * \param atomic the atomic representing the particle system
 * \param speedMin a RwReal value equal to the minimum value of the
 *                 particles initial velocity range
 * \param speedMax a RwReal value equal to the maximum value of the
 *                 particles initial velocity range
 * \param dampening a RwReal value equal dampening applied to the particles
 *                initial velocity
 *
 * \return Returns a pointer to the modified atomic if successful or NULL if
 * there is an error.
 *
 */
RpAtomic           *
RpParticlesAtomicSetSpeed(RpAtomic * atomic,
                          RwReal speedMin, RwReal speedMax,
                          RwReal dampening)
{

    RpParticlesData    *particlesData;
    RwReal              speed, speedVariation;

    RWAPIFUNCTION(RWSTRING("RpParticlesAtomicSetSpeed"));
    RWASSERT(atomic);
    particlesData = *PARTICLESATOMICGETDATA(atomic);
    if (particlesData)
    {
        speed = (speedMin + speedMax) / ((RwReal) 2);
        speedVariation = speedMax - speed;
        particlesData->speed = speed;
        particlesData->speedVariation = speedVariation;
        particlesData->dampening = -dampening;
        particlesData->flags = (RpParticlesFlag)
            (rpPARTICLESDIRTY | (RwUInt32) particlesData->flags);
        particlesData->flags = (RpParticlesFlag)
            (rpPARTICLESSETBSPHERE | (RwUInt32) particlesData->flags);
    }
    else
    {
        atomic = (RpAtomic *) NULL;
    }

    RWRETURN(atomic);
}

/**
 * \ingroup rpprtsys
 * \ref RpParticlesAtomicGetSpeed is used to get the particles
 * speed range from speedMin to speedMax.
 *
 * \param atomic the atomic representing the particle system
 * \param speedMin a RwReal value equal to the minimum value of the
 *                 particles initial velocity range
 * \param speedMax a RwReal value equal to the maximum value of the
 *                 particles initial velocity range
 * \param dampening a RwReal value equal dampening applied to the
 *                particles initial velocity
 *
 */
void
RpParticlesAtomicGetSpeed(RpAtomic * atomic, RwReal * speedMin,
                          RwReal * speedMax, RwReal * dampening)
{
    RpParticlesData    *particlesData;

    RWAPIFUNCTION(RWSTRING("RpParticlesAtomicGetSpeed"));
    RWASSERT(atomic);
    particlesData = *PARTICLESATOMICGETDATA(atomic);
    RWASSERT(particlesData != 0);
    *speedMin = particlesData->speed - particlesData->speedVariation;
    *speedMax = particlesData->speed + particlesData->speedVariation;
    *dampening = particlesData->dampening;
    RWRETURNVOID();
}

/**
 * \ingroup rpprtsys
 * \ref RpParticlesAtomicSetFlightTime is used to specify
 * the period of time over which the particles
 * travels along their trajectories.
 * Once a particle has completed its path, it is recycled and emitted
 * from the start in a continuous cycle.
 *
 * \param atomic the atomic representing the particle system
 * \param flightTime a RwReal value equal flight time of the particle
 *
 * \return Returns a pointer to the modified atomic if successful or NULL if
 * there is an error.
 *
 */
RpAtomic           *
RpParticlesAtomicSetFlightTime(RpAtomic * atomic, RwReal flightTime)
{
    RpParticlesData    *particlesData;

    RWAPIFUNCTION(RWSTRING("RpParticlesAtomicSetFlightTime"));
    RWASSERT(atomic);
    particlesData = *PARTICLESATOMICGETDATA(atomic);
    if (particlesData)
    {

        RWASSERT(0 <= flightTime);
        particlesData->flightTime = flightTime;
        /* temp hack for testing */
        particlesData->duration = flightTime * ((RwReal) 1);
        particlesData->flags = ((RpParticlesFlag)
                                (rpPARTICLESDIRTY |
                                 (RwUInt32) particlesData->flags));
        particlesData->flags = ((RpParticlesFlag)
                                (rpPARTICLESSETBSPHERE |
                                 (RwUInt32) particlesData->flags));
    }
    else
    {

        atomic = (RpAtomic *) NULL;
    }

    RWRETURN(atomic);
}

/**
 * \ingroup rpprtsys
 * \ref RpParticlesAtomicGetFlightTime gets the particles flight time.
 *
 * \param atomic the atomic representing the particle system
 *
 * \return Returns the particles flight time.
 *
 */
RwReal
RpParticlesAtomicGetFlightTime(RpAtomic * atomic)
{
    RwReal              result = ((RwReal) 0);
    RpParticlesData    *particlesData;

    RWAPIFUNCTION(RWSTRING("RpParticlesAtomicGetFlightTime"));
    RWASSERT(atomic);
    particlesData = *PARTICLESATOMICGETDATA(atomic);
    RWASSERT(particlesData != 0);
    result = particlesData->flightTime;
    RWASSERT(0 <= result);
    RWRETURN(result);
}

/**
 * \ingroup rpprtsys
 * \ref RpParticlesAtomicSetForce is used the specify a global
 *  force that acts on all the particles in the system.
 * The force is specified in the emitters local coordinate space.
 *
 * \param atomic the atomic representing the particle system
 * \param force  the vector representing the force applied
 *              to each of the particles in the system
 *
 * \return Returns a pointer to the modified atomic if successful or NULL if
 * there is an error.
 *
 */
RpAtomic           *
RpParticlesAtomicSetForce(RpAtomic * atomic, RwV3d * force)
{
    RpParticlesData    *particlesData;
    RwV3d               f;

    RWAPIFUNCTION(RWSTRING("RpParticlesAtomicSetForce"));
    RWASSERT(atomic);
    particlesData = *PARTICLESATOMICGETDATA(atomic);
    if (particlesData)
    {

        RwV3dScale(&f, force, ((RwReal) 0.5));
        particlesData->force = f;
        particlesData->flags = (RpParticlesFlag)
            (rpPARTICLESDIRTY | (RwUInt32) particlesData->flags);
        particlesData->flags = (RpParticlesFlag)
            (rpPARTICLESSETBSPHERE | (RwUInt32) particlesData->flags);
    }
    else
    {
        atomic = (RpAtomic *) NULL;
    }

    RWRETURN(atomic);
}

/**
 * \ingroup rpprtsys
 * \ref RpParticlesAtomicGetForce gets the global force.
 *
 * \param atomic the atomic representing the particle system
 *
 * \return Returns the global force.
 *
 */
RwV3d
RpParticlesAtomicGetForce(RpAtomic * atomic)
{
    RpParticlesData    *particlesData;
    RwV3d               force;

    RWAPIFUNCTION(RWSTRING("RpParticlesAtomicGetForce"));
    RWASSERT(atomic);
    particlesData = *PARTICLESATOMICGETDATA(atomic);
    RWASSERT(particlesData != 0);
    RwV3dScale(&force, &particlesData->force, ((RwReal) 2));
    RWRETURN(force);
}

/**
 * \ingroup rpprtsys
 * \ref RpParticlesAtomicSetColors is used to set the start color
 * and the destination color.
 * The colors plus the alpha are linearly interpolated over
 * the flight time of the particle in RGBA color space.
 *
 * \param atomicthe atomic representing the particle system
 * \param colStart the initial color of the particles
 *                            in the system
 * \param colEnd the destination color of the particles
 *                          in the system
 *
 * \return Returns a pointer to the modified atomic if successful or NULL if
 * there is an error.
 *
 */
RpAtomic           *
RpParticlesAtomicSetColors(RpAtomic * atomic,
                           RwRGBA * colStart, RwRGBA * colEnd)
{

    RpParticlesData    *particlesData;

    RWAPIFUNCTION(RWSTRING("RpParticlesAtomicSetColors"));
    RWASSERT(atomic);
    particlesData = *PARTICLESATOMICGETDATA(atomic);
    if (particlesData)
    {

        particlesData->startCol.red = (RwReal) colStart->red * COLSCALE;
        particlesData->startCol.green =
            (RwReal) colStart->green * COLSCALE;
        particlesData->startCol.blue =
            (RwReal) colStart->blue * COLSCALE;
        particlesData->startCol.alpha =
            (RwReal) colStart->alpha * COLSCALE;
        particlesData->endCol.red = (RwReal) colEnd->red * COLSCALE;
        particlesData->endCol.green = (RwReal) colEnd->green * COLSCALE;
        particlesData->endCol.blue = (RwReal) colEnd->blue * COLSCALE;
        particlesData->endCol.alpha = (RwReal) colEnd->alpha * COLSCALE;
        particlesData->flags =
            (RpParticlesFlag) (rpPARTICLESDIRTY | (RwUInt32)
                               particlesData->flags);
        /*
         * particlesData->flags = (RpParticlesFlag)
         * ( rpPARTICLESSETBSPHERE | (RwUInt32) particlesData->flags );
         */
    }
    else
    {

        atomic = (RpAtomic *) NULL;
    }

    RWRETURN(atomic);
}

/**
 * \ingroup rpprtsys
 * \ref RpParticlesAtomicGetColors gets
 * the start and end colors for the particle system.
 *
 * \param atomic the atomic representing the particle system
 * \param index specifying which color to get (0=start, 1=end)
 *
 * \return Returns the color.
 */
RwRGBA
RpParticlesAtomicGetColors(RpAtomic * atomic, RwInt32 index)
{
    RpParticlesData    *particlesData;
    RwRGBA              col;

    RWAPIFUNCTION(RWSTRING("RpParticlesAtomicGetColors"));
    RWASSERT(atomic);
    particlesData = *PARTICLESATOMICGETDATA(atomic);
    RWASSERT(particlesData != 0);
    RWASSERT(index == 0 || index == 1);
    if (index == 0)
    {
        ParticleColorToRwRGBAMacro(&col, &particlesData->startCol);
    }
    else
    {
        ParticleColorToRwRGBAMacro(&col, &particlesData->endCol);
    }

    RWRETURN(col);
}

/**
 * \ingroup rpprtsys
 * \ref RpParticlesAtomicUpdate is used to increase
 * the state of the particle by amount timeDelta.
 *
 * \param atomic the atomic representing the particle system
 * \param timeDelta a RwReal value equal to the amount of
 *                  time the state of the particle system
 *                  is to be progressed
 *
 * \return Returns a pointer to the modified atomic if successful or NULL if
 * there is an error.
 *
 */
RpAtomic           *
RpParticlesAtomicUpdate(RpAtomic * atomic, RwReal timeDelta)
{
    RpParticlesData    *particlesData;

    RWAPIFUNCTION(RWSTRING("RpParticlesAtomicUpdate"));
    RWASSERT(atomic);
    particlesData = *PARTICLESATOMICGETDATA(atomic);
    if (particlesData)
    {
        const RwReal        flightTime = particlesData->flightTime;

        /*
         * Is the bounding sphere dirty ?
         */
        if (particlesData->flags & rpPARTICLESSETBSPHERE)
        {
            ParticlesAtomicSetBoundingSphere(atomic);

            particlesData->flags = (RpParticlesFlag)
                ((~rpPARTICLESSETBSPHERE) & 
                 (RwUInt32) particlesData-> flags);
        }

        if (((RwReal) 0) < timeDelta)
        {
            particlesData->time += timeDelta;
        }

        if (0 < flightTime)
        {
            while (particlesData->time > flightTime)
            {
                particlesData->time -= flightTime;
            }
        }
    }
    else
    {
        atomic = (RpAtomic *) NULL;
    }

    RWRETURN(atomic);
}

/**
 * \ingroup rpprtsys
 * \ref RpParticlesAtomicSetMaterial is used to specify the material
 *  applied to each of the particles within the system.
 *
 * \param atomic  the atomic representing the particle system
 * \param material the material that is to be
 *                 applied to each of the particles in the system
 *
 * \return Returns a pointer to the modified atomic if successful or NULL if
 *         there is an error.
 *
 */
RpAtomic           *
RpParticlesAtomicSetMaterial(RpAtomic * atomic, RpMaterial * material)
{

    RpParticlesData    *particlesData;
    RpGeometry         *geometry;
    RpTriangle         *triangles;
    RwInt32             numTriangles, triNum;

    RWAPIFUNCTION(RWSTRING("RpParticlesAtomicSetMaterial"));
    RWASSERT(atomic);
    particlesData = *PARTICLESATOMICGETDATA(atomic);
    if (particlesData)
    {

        geometry = RpAtomicGetGeometry(atomic);
        if (RpGeometryLock(geometry, rpGEOMETRYLOCKPOLYGONS))
        {
            numTriangles = RpGeometryGetNumTriangles(geometry);
            triangles = RpGeometryGetTriangles(geometry);
            for (triNum = 0; triNum < numTriangles; triNum++)
            {
                RpGeometryTriangleSetMaterial(geometry, triangles,
                                              material);
                triangles++;
            }

            RpGeometryUnlock(geometry);
        }

        RpAtomicSetPipeline(atomic, GParticlesObjectPipe);
        RpMaterialSetPipeline(material, GParticlesMaterialPipe);
    }
    else
    {
        atomic = (RpAtomic *) NULL;
    }

    RWRETURN(atomic);
}

/**
 * \ingroup rpprtsys
 * \ref RpParticlesAtomicGetMaterial is used to get
 * the material applied to each of the particles within the system.
 *
 * \param atomic the atomic representing the particle system
 *
 * \return Returns a pointer to the material if successful or NULL if
 *         there is an error.
 *
 */
RpMaterial         *
RpParticlesAtomicGetMaterial(RpAtomic * atomic)
{
    RpParticlesData    *particlesData;
    RpGeometry         *geometry;
    RpTriangle         *triangles;
    RpMaterial         *material = (RpMaterial *) NULL;

    RWAPIFUNCTION(RWSTRING("RpParticlesAtomicGetMaterial"));
    RWASSERT(atomic);
    particlesData = *PARTICLESATOMICGETDATA(atomic);
    RWASSERT(particlesData != 0);
    geometry = RpAtomicGetGeometry(atomic);
    if (RpGeometryLock(geometry, rpGEOMETRYLOCKPOLYGONS))
    {
        triangles = RpGeometryGetTriangles(geometry);
        material = RpGeometryTriangleGetMaterial(geometry, triangles);
        RpGeometryUnlock(geometry);
    }

    RWRETURN(material);
}
