/*=============================================================================
    Name    : particle.c
    Purpose : maintain and render particle systems

    Created 11/13/1997 by khent
    Copyright Relic Entertainment, Inc.  All rights reserved.
=============================================================================*/

#define PART_TEST_MORPH 0

#include <stdio.h>
#include <math.h>
#include <float.h>
#include <windows.h>
#include "glinc.h"
#include "types.h"
#include "vector.h"
#include "randy.h"
#include "debug.h"
#include "color.h"
#include "matrix.h"
#include "memory.h"
#include "particle.h"
#include "spaceobj.h"
#include "render.h"
#include "mesh.h"
#include "texreg.h"
#include "fastmath.h"
#include "mainrgn.h"
#include "task.h"
#include "universe.h"
#include "autolod.h"
#include "shader.h"
#include "glcaps.h"
#include "devstats.h"

extern unsigned int gDevcaps;

#ifndef RUB
#define RUB(c) (ubyte)(c * 255.0f)
#endif

#define TEXX(X) ((real32)(X) / (real32)texWidth)
#define TEXY(Y) ((real32)(Y) / (real32)texHeight)

#define MIN2(a,b) ((a) < (b) ? (a) : (b))

/*=============================================================================
    Data:
=============================================================================*/

static sdword alternateIndex = 0;
typedef struct alternate_s
{
    sdword trhandle;    //of the etg texture
    udword glhandle;    //of the alternate
} alternate_t;

static alternate_t alternates[PART_NUM_ALTERNATES];

static GLfloat illumSave[4];
static bool wasStippled;

//function called for each particle created
void (*partCreateCallback)(sdword userValue, void *userData) = NULL;
sdword partCreateUserValue = 0;
ubyte *partCreateUserData = NULL;

sdword partAdvanceMeshMorph(meshSystem* psys, particle* p);
meshdata* partMeshNextMesh(meshSystem* psys, particle* p);

typedef struct
{
    vector  worldVel;
    //misc
    udword  flags;
    real32  drag;
    real32  scale;
    real32  xScale, yScale, zScale;
    real32  scaleDist;
    real32  deltaScale;
    real32  deltaScaleDist;
    //position
    real32  offsetLOF;
    real32  offsetR;
    real32  offsetTheta;
    vector *offsetArray;
    real32  deltaLOF;
    real32  deltaLOFDist;
    real32  deltaR[2];
    real32  deltaRDist[2];
    //velocity
    real32  velLOF;
    real32  velLOFDist;
    real32  velR;
    real32  velRDist;
    real32  deltaVelLOF;
    real32  deltaVelLOFDist;
    real32  deltaVelR;
    real32  deltaVelRDist;
    real32  ang;
    real32  angDist;
    real32  angDelta;
    real32  angDeltaDist;
    //line thingz
    real32  length;
    real32  lengthDist;
    real32  deltaLength;
    real32  deltaLengthDist;
    //appearance
    real32  icolor[4];
    real32  colorDist[4];
    real32  deltaColor[4];
    real32  deltaColorDist[4];
    bool8   lit;
    real32  illum;
    real32  illumDist;
    real32  deltaIllum;
    real32  deltaIllumDist;
    //lifespan
    real32  lifespan;
    real32  lifespanDist;
    real32  waitspan;
    real32  waitspanDist;
    //mesh
    meshdata *mesh;
    real32  exponent;
    real32  exponentDist;
    ubyte   colorScheme;
    sdword  loopCount;
    //texture
    trhandle tex;
    //billboard
    real32  position[3];
    matrix* mat;
    udword  slices;
    udword  uv[2][2];
    void*   tstruct;
    void*   mstruct;
    real32  rate;
    real32  meshRate;
    real32  startMeshFrame;
    bool8   loop;
    uword   startFrame;
    //mesh tumble attribs
    real32  tumble[3];
    real32  tumbleDist[3];
    real32  deltaTumble[3];
    real32  deltaTumbleDist[3];
    //color biasing
    real32  rbias;
    real32  gbias;
    real32  bbias;
    real32  rbiasDist;
    real32  gbiasDist;
    real32  bbiasDist;
    //flags
    bool    specularOnly;
    bool    stippleAlpha;
    bool    noDepthWrite;
    bool    additiveBlends;
    bool    pseudoBillboard;
    bool    trueBillboard;
} particleAttribs;

particleAttribs defaultParticleAttribs =
{
    {0.0f, 0.0f, 0.0f}, //worldVel
    0,                  //flags.
    1.0f,               //drag
    5.0f,               //scale [3]
    1.0f, 1.0f, 1.0f,   //XYZ scale
    0.0f,               //scaleDist
    0.0f,               //deltaScale
    0.0f,               //deltaScaleDist

    0.0f,               //offsetLOF
    0.0f,               //offsetR
    0.0f,               //offsetTheta
    NULL,               //offsetArray
    0.0f,               //deltaLOF
    0.0f,               //deltaLOFDist
    {0.0f, TWOPI},      //deltaR[]
    {0.0f, TWOPI},      //deltaRDist[]
    0.0f,               //velLOF
    0.0f,               //velLOFDist
    0.0f,               //velR
    0.0f,               //velRDist
    0.0f,               //deltaVelLOF
    0.0f,               //deltaVelLOFDist
    0.0f,               //deltaVelR
    0.0f,               //deltaVelRDist
    0.0f,               //ang
    0.0f,               //angDist
    0.0f,               //angDelta
    0.0f,               //angDeltaDist

    5.0f,               //length
    0.0f,               //lengthDist
    0.0f,               //deltaLength
    0.0f,               //deltaLengthDist

    {1.0f, 1.0f, 1.0f, 1.0f},       //icolor
    {0.0f, 0.0f, 0.0f, 0.0f},       //colorDist
    {0.0f, 0.0f, 0.0f, 0.0f},       //deltaColor
    {0.0f, 0.0f, 0.0f, 0.0f},       //deltaColorDist
    FALSE,                          //lit
    0.0f,                           //illum
    0.0f,                           //illumDist
    0.0f,                           //deltaIllum
    0.0f,                           //deltaIllumDist

    2.0f,               //lifespan (s) [2]
    0.0f,               //lifespanDist
    0.0f,               //waitspan
    0.0f,               //waitspanDist

    NULL,               //meshdata
    -1.0f,              //exponent
    0.0f,               //exponentDist
    PART_NOCOLORSCHEME, //color scheme
    -1,                 //loop count
    TR_Invalid,         //trhandle

    {0.0f, 0.0f, 0.0f}, //position
    NULL,
    8,                  //slices
    {{0, 0},            //uv
     {1, 1}},
    NULL,               //tstruct
    NULL,               //mstruct
    10.0f,              //rate (f/s)
    5.0f,               //mesh framerate (f/s)
    0.0f,               //starting mesh frame
    TRUE,               //loopFlag
    0,                  //startFrame

    {0.0f, 0.0f, 0.0f}, //tumble angles (radians)
    {0.0f, 0.0f, 0.0f}, //tumble distribution
    {0.0f, 0.0f, 0.0f}, //deltaTumble (rads/s)
    {0.0f, 0.0f, 0.0f}, //deltaTumble distribution

    //color bias
    0.0f,               //red bias
    0.0f,               //green bias
    0.0f,               //blue bias
    0.0f,               //red bias distribution
    0.0f,               //green bias dist
    0.0f,               //blue bias dist

    //flags
    FALSE,              //specularOnly
    FALSE,              //stippleAlpha
    FALSE,              //noDepthWrite
    TRUE,               //additiveBlends
    FALSE,              //pseudoBillboard
    FALSE               //trueBillboard
};

static particleAttribs pat;

/*-----------------------------------------------------------------------------
    Name        : partStartup
    Description : startup the particle module
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void partStartup(void)
{
    partShutdown();
}

/*-----------------------------------------------------------------------------
    Name        : partFreeAlternates
    Description : frees "alternates"
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void partFreeAlternates(void)
{
    sdword i;

    if (alternateIndex != 0)
    {
#if PART_Verbose > 0
        dbgMessagef("\nfreeing %d particle alternates", alternateIndex);
#endif
        for (i = 0; i < alternateIndex; i++)
        {
            alternates[i].trhandle = TR_InvalidHandle;
            if (alternates[i].glhandle != 0)
            {
                glDeleteTextures(1, &alternates[i].glhandle);
                alternates[i].glhandle = 0;
            }
        }

        alternateIndex = 0;
    }
}

/*-----------------------------------------------------------------------------
    Name        : partShutdown
    Description : shutdown the particle module.  frees "alternates"
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void partShutdown(void)
{
    partFreeAlternates();
}

/*-----------------------------------------------------------------------------
    Name        : partSetDefaults
    Description : reset particle attributes structure to default values
    Inputs      :
    Outputs     : pat is filled with defaultParticleAttribs
    Return      :
----------------------------------------------------------------------------*/
void partSetDefaults()
{
    memcpy(&pat, &defaultParticleAttribs, sizeof(particleAttribs));
}

#define RealClamp(r) \
        if ((r) < 0.0f) r = 0.0f; \
        else if ((r) > 1.0f) r = 1.0f;
#define RealClampNeg(r) \
        if ((r) < -1.0f) r = -1.0f; \
        else if ((r) > 1.0f) r = 1.0f;

static psysPtr g_psys = NULL;
static hmatrix particleHMatrix;
static matrix  shiprotmatrix;
static vector  shipposition;
static vector  *partEffectVelocity;              //retained pointer to velocity of effect which spawned said particles

matrix partEffectOwnerSystem;
vector partEffectOwnerPosition;
color  partEffectColor;
real32 partNLips;

//box helper
static void drawBox(GLfloat size, GLenum type)
{
    static GLfloat n[6][3] =
    {
        {-1.0f, 0.0f, 0.0f},
        {0.0f, 1.0f, 0.0f},
        {1.0f, 0.0f, 0.0f},
        {0.0f, -1.0f, 0.0f},
        {0.0f, 0.0f, 1.0f},
        {0.0f, 0.0f, -1.0f}
    };
    static GLint faces[6][4] =
    {
        {0, 1, 2, 3},
        {3, 2, 6, 7},
        {7, 6, 5, 4},
        {4, 5, 1, 0},
        {5, 6, 2, 1},
        {7, 4, 0, 3}
    };
    GLfloat v[8][3];
    GLint i;

    v[0][0] = v[1][0] = v[2][0] = v[3][0] = -size / 2;
    v[4][0] = v[5][0] = v[6][0] = v[7][0] = size / 2;
    v[0][1] = v[1][1] = v[4][1] = v[5][1] = -size / 2;
    v[2][1] = v[3][1] = v[6][1] = v[7][1] = size / 2;
    v[0][2] = v[3][2] = v[4][2] = v[7][2] = -size / 2;
    v[1][2] = v[2][2] = v[5][2] = v[6][2] = size / 2;

    for (i = 0; i < 6; i++)
    {
        glBegin(type);
        glNormal3fv(&n[i][0]);
        glVertex3fv(&v[faces[i][0]][0]);
        glVertex3fv(&v[faces[i][1]][0]);
        glVertex3fv(&v[faces[i][2]][0]);
        glVertex3fv(&v[faces[i][3]][0]);
        glEnd();
    }
}

//like primCircleSolid3, but w/alpha
void partCircleSolid3(vector *centre, real32 radius, sdword nSlices, color c)
{
    sdword index;
    GLfloat v[3];
    double theta;

    glColor4ub(colRed(c), colGreen(c), colBlue(c), colAlpha(c));
    v[0] = centre->x;
    v[1] = centre->y;
    v[2] = centre->z;
    glBegin(GL_TRIANGLE_FAN);
    glVertex3fv(v);
    for (index = 0, theta = 0.0; index < nSlices; index++)
    {
        v[0] = centre->x + (real32)(sin(theta)) * radius;
        v[1] = centre->y + (real32)(cos(theta)) * radius;
        theta += 2.0 * PI / (double)nSlices;
        glVertex3fv(v);
    }
    v[0] = centre->x;
    v[1] = centre->y + radius;
    glVertex3fv(v);
    glEnd();
}

/*-----------------------------------------------------------------------------
    Name        : partBillboardEnable
    Description : sets up sprite billboard mode for worldspace & effectspace systems
    Inputs      : v - position of the sprite
                  isWorldspace - TRUE or FALSE
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void partBillboardEnable(vector *v, bool isWorldspace)
{
    if (isWorldspace)
    {
        //enable billboarding
        rndBillboardEnable(v);
    }
    else
    {
        vector vec;

        //object -> world
        vecSubFrom(*v,shipposition);
        matMultiplyMatByVec(&vec, &shiprotmatrix, v);
        vecAddTo(vec,shipposition);
        //enable billboarding
        rndBillboardEnable(&vec);
    }
}

/*-----------------------------------------------------------------------------
    Name        : partBillboardDisable
    Description : disables billboarding
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void partBillboardDisable()
{
    //disable billboarding
    rndBillboardDisable();
}

/*-----------------------------------------------------------------------------
    Name        : storeIllum
    Description : saves GL illumination parameters (LIGHT_MODEL_AMBIENT)
    Inputs      :
    Outputs     : global illumSave contains parameters
    Return      :
----------------------------------------------------------------------------*/
void storeIllum()
{
    glGetFloatv(GL_LIGHT_MODEL_AMBIENT, illumSave);
}

/*-----------------------------------------------------------------------------
    Name        : handleIllum
    Description : alters LIGHT_MODEL_AMBIENT according to passed particle's vals
    Inputs      : p - the particle
    Outputs     : the lighting model may be modified
    Return      :
----------------------------------------------------------------------------*/
void handleIllum(particle *p)
{
    GLfloat illum[4];
    if (p->illum != 0.0f)
    {
        illum[0] = p->illum;
        illum[1] = p->illum;
        illum[2] = p->illum;
        illum[3] = 1.0f;
        glLightModelfv(GL_LIGHT_MODEL_AMBIENT, illum);
    }
}

/*-----------------------------------------------------------------------------
    Name        : restoreIllum
    Description : restores saved GL illumination parameters
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void restoreIllum()
{
    glLightModelfv(GL_LIGHT_MODEL_AMBIENT, illumSave);
}

/*-----------------------------------------------------------------------------
    Name        : partPerformHacking
    Description : sets up a billboard-friendly MODELVIEW for non-worldspace
                  sprite systems
    Inputs      : partMat - the coordinate system
                  particleTranslation - position in the space
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void partPerformHacking(matrix* partMat, vector particleTranslation)
{
    hmatrix mat;
    hvector hvec = {0.0f, 0.0f, 0.0f, 1.0f};

    //build a homogenous matrix
    hmatMakeHMatFromMat(&particleHMatrix, partMat);
    hmatPutVectIntoHMatrixCol4(particleTranslation, particleHMatrix);

    //save position globally
    shipposition = particleTranslation;
    //save rotation globally
    matGetMatFromHMat(&shiprotmatrix, &particleHMatrix);

    //obtain current modelview
    glGetFloatv(GL_MODELVIEW_MATRIX, (GLfloat*)&mat);
    glPushMatrix();
    //zero translation elements
    hmatPutHVectIntoHMatrixCol4(hvec, mat);
    //load rotation but not translation into modelview
    glLoadMatrixf((GLfloat*)&mat);
}

/*-----------------------------------------------------------------------------
    Name        : partFilter
    Description : enables bilinear filtering for textures without regard to the
                  global filter setting
    Inputs      : on - TRUE or FALSE (yes or no)
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void partFilter(bool on)
{
    if (on)
    {
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    }
    else
    {
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    }
}

//save GL stipple setting
void stippleSave()
{
    if (RGL)
        wasStippled = (bool)glIsEnabled(GL_POLYGON_STIPPLE);
}

//restore previous GL stipple setting
void stippleRestore()
{
    if (RGL)
    {
        if (wasStippled)
        {
            glEnable(GL_POLYGON_STIPPLE);
        }
        else
        {
            glDisable(GL_POLYGON_STIPPLE);
        }
    }
}

//decide whether to enable stippling or not
void handleStipple(particle* p)
{
    if (!RGL)
        return;

    if (wasStippled)
    {
        //leave stippling alone if global stippling enabled
        return;
    }

    if (bitTest(p->flags, PART_STIPPLE))
    {
        glEnable(GL_POLYGON_STIPPLE);
    }
    else if (!wasStippled)
    {
        glDisable(GL_POLYGON_STIPPLE);
    }
}

/*-----------------------------------------------------------------------------
    Name        : partSaturateAdd
    Description : adds then normalizes input colour values
    Inputs      : out - output array of colours
                  add - input array 0
                  base - input array 1
    Outputs     : out is filled w/ saturated in+base
    Return      :
----------------------------------------------------------------------------*/
void partSaturateAdd(real32* out, real32* add, real32* base)
{
    real32 max, scale;
    real32 in[3];
    sdword i;

    for (i = 0; i < 3; i++)
    {
        in[i] = base[i] + add[i];
    }

    max = in[0];
    if (in[1] > max) max = in[1];
    if (in[2] > max) max = in[2];
    scale = (max == 0.0f) ? 1.0f : (1.0f / max);

    for (i = 0; i < 3; i++)
    {
        out[i] = in[i] * scale;
    }
}

void partBindAlternate(trhandle tex)
{
    sdword i;
    udword alternate;
    ubyte* data;

    texreg* reg = trStructureGet(tex);
    if (!bitTest(reg->flags, TRF_Alpha))
    {
        //do nothing for non-RGBA textures
        return;
    }

    //check devcaps for valid GetTexImage
    if (bitTest(gDevcaps, DEVSTAT_NO_GETTEXIMAGE))
    {
        return;
    }

    //check alternates
    for (i = 0; i < alternateIndex; i++)
    {
        if (alternates[i].trhandle == tex)
        {
            trClearCurrent();
            glBindTexture(GL_TEXTURE_2D, alternates[i].glhandle);
            return;
        }
    }

    //create a new alternate
    glGenTextures(1, &alternate);
    alternates[alternateIndex].trhandle = tex;
    alternates[alternateIndex++].glhandle = alternate;   //store alternate

    if (alternateIndex >= PART_NUM_ALTERNATES)
    {
        //FIXME: this is not an efficient queue
        partFreeAlternates();
    }

    // ... from the data of the old texture (ASSERT: currently bound)
    data = (ubyte*)memAlloc(4*reg->scaledWidth*reg->scaledHeight, "temp part alternate", Pyrophoric);
    glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
    for (i = 0; i < 4*reg->scaledWidth*reg->scaledHeight; i += 4)
    {
        data[i+0] = data[i+1] = data[i+2] = 200;
    }

    trClearCurrent();
    glBindTexture(GL_TEXTURE_2D, alternate);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
                 reg->scaledWidth, reg->scaledHeight,
                 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

    memFree(data);
}

/*-----------------------------------------------------------------------------
    Name        : partRenderBillSystem
    Description : render a billboard system
    Inputs      : n - number of particles
                  p - pointer to first particle
                  flags - misc flags
                  particleTranslation - position of the system, used for billboarding
                  uv - texture uv coordinates
                  bpart - system header pointer
                  isWorldspace - TRUE or FALSE
    Outputs     :
    Return      : number of live particles; kill the system if 0
----------------------------------------------------------------------------*/
udword partRenderBillSystem(udword n, particle* p, udword flags,
                            vector particleTranslation, udword uv[2][2],
                            billSystem* bpart, bool isWorldspace)
{
    udword i, hits;
    vector pos;
    color col;
    real32 sx;
    real32 sy;
    trhandle currentTex;
    udword texWidth, texHeight;
    static vector origin = {0.0f, 0.0f, 0.0f};
    texreg *reg;
    udword  slices;
    matrix* partMat;
    trhandle tex;
    real32 saturatedBias[3];
    sdword blended;
    bool canTexAdd;

    slices = bpart->slices;
    partMat = bpart->partMat;
    tex = bpart->tex;

    currentTex = tex;

//    rndAdditiveBlends(FALSE);

    glDisable(GL_CULL_FACE);
    glDepthMask(GL_FALSE);
    glShadeModel(GL_SMOOTH);

    if (RGLtype != SWtype)
    {
        rndTextureEnvironment(RTE_Modulate);
    }

    if (currentTex != TR_Invalid)
    {
        reg = trStructureGet(tex);
        texWidth = reg->diskWidth;
        texHeight = reg->diskHeight;
        rndTextureEnable(TRUE);
/*
        trMakeCurrent(tex);
        if (bitTest(reg->flags, TRF_Alpha))
        {
            glEnable(GL_BLEND);
            glDisable(GL_ALPHA_TEST);
            rndAdditiveBlends(FALSE);
        }
*/
    }

    canTexAdd = glCapFeatureExists(RGL_COLOROP_ADD);

    for (i = hits = 0; i < n; i++, p++)
    {
        if (p->lifespan < 0.0f || p->waitspan > 0.0f)
            continue;
        else
            hits++;
        if (p->waitspan > 0.0f)
            continue;
        pos = p->position;
        if (!isWorldspace)
        {
            vecAddTo(pos, particleTranslation);
        }
        partBillboardEnable(&pos, isWorldspace);

        if (p->tstruct == NULL && p->tex != TR_Invalid)
        {
            currentTex = p->tex;
        }

        handleIllum(p);
        //set appropriate stipple mode
        handleStipple(p);

        blended = 0;
        if (canTexAdd)
        {
            glPixelTransferf(GL_RED_BIAS, p->bias[0]);
            glPixelTransferf(GL_GREEN_BIAS, p->bias[1]);
            glPixelTransferf(GL_BLUE_BIAS, p->bias[2]);
        }
        else
        {
            saturatedBias[0] = p->bias[0];
            saturatedBias[1] = p->bias[1];
            saturatedBias[2] = p->bias[2];

            //see if device can handle multiple pass rendering
            if (!bitTest(gDevcaps, DEVSTAT_NO_GETTEXIMAGE))
            {
                if ((p->bias[0] + p->bias[1] + p->bias[2]) > 0.0f)
                {
                    blended = 1;
                }
            }
        }

        if (p->icolor[3] == 1.0f)
        {
            glColor3f(p->icolor[0], p->icolor[1], p->icolor[2]);
            glDisable(GL_BLEND);
        }
        else
        {
            glColor4f(p->icolor[0], p->icolor[1], p->icolor[2], p->icolor[3]);
            glEnable(GL_BLEND);
        }

        if (currentTex == TR_Invalid)
        {
            col = colRGBA(colRealToUbyte(p->icolor[0]), colRealToUbyte(p->icolor[1]),
                          colRealToUbyte(p->icolor[2]), colRealToUbyte(p->icolor[3]));
            partCircleSolid3(&origin, p->scale, slices, col);
        }
        else
        {
            real32 cosTheta, sinTheta;

            rndTextureEnable(TRUE);

            trMakeCurrent(currentTex);
            reg = trStructureGet(tex);
            if (bitTest(reg->flags, TRF_Alpha))
            {
                glEnable(GL_BLEND);
                glDisable(GL_ALPHA_TEST);
                if (bitTest(p->flags, PART_ADDITIVE))
                {
                    rndAdditiveBlends(TRUE);
                }
                else
                {
                    rndAdditiveBlends(FALSE);
                }
            }
            partFilter(TRUE);

            cosTheta = (real32)cos((real64)p->rot);
            sinTheta = (real32)sin((real64)p->rot);

            sx = sy = p->scale * 0.5f;

            if (p->rot == 0.0f)
            {
AGAIN0:
                glBegin(GL_QUADS);
                glTexCoord2f(TEXX(uv[0][0]), TEXY(uv[0][1]));
                glVertex3f(-sx, -sy, 0.0f);
                glTexCoord2f(TEXX(uv[1][0]), TEXY(uv[0][1]));
                glVertex3f(sx, -sy, 0.0f);
                glTexCoord2f(TEXX(uv[1][0]), TEXY(uv[1][1]));
                glVertex3f(sx, sy, 0.0f);
                glTexCoord2f(TEXX(uv[0][0]), TEXY(uv[1][1]));
                glVertex3f(-sx, sy, 0.0f);
                glEnd();
                if (blended)
                {
                    blended--;
                    rndAdditiveBlends(TRUE);
                    glColor4f(saturatedBias[0], saturatedBias[1], saturatedBias[2], p->icolor[3]);
                    //render this guy again, additively blending over the last
                    partBindAlternate(currentTex);
                    partFilter(TRUE);
                    goto AGAIN0;
                }
            }
            else
            {
AGAIN1:
                glBegin(GL_QUADS);
                glTexCoord2f(TEXX(uv[0][0]), TEXY(uv[0][1]));
                glVertex3f((-sx*cosTheta) - (-sy*sinTheta),
                           (-sx*sinTheta) + (-sy*cosTheta),
                           0.0f);
                glTexCoord2f(TEXX(uv[1][0]), TEXY(uv[0][1]));
                glVertex3f((sx*cosTheta) - (-sy*sinTheta),
                           (sx*sinTheta) + (-sy*cosTheta),
                           0.0f);
                glTexCoord2f(TEXX(uv[1][0]), TEXY(uv[1][1]));
                glVertex3f((sx*cosTheta) - (sy*sinTheta),
                           (sx*sinTheta) + (sy*cosTheta),
                           0.0f);
                glTexCoord2f(TEXX(uv[0][0]), TEXY(uv[1][1]));
                glVertex3f((-sx*cosTheta) - (sy*sinTheta),
                           (-sx*sinTheta) + (sy*cosTheta),
                           0.0f);
                glEnd();
                if (blended)
                {
                    blended--;
                    rndAdditiveBlends(TRUE);
                    glColor4f(saturatedBias[0], saturatedBias[1], saturatedBias[2], p->icolor[3]);
                    //render again, additively blending over previous
                    partBindAlternate(currentTex);
                    partFilter(TRUE);
                    goto AGAIN1;
                }
            }
        }

        partBillboardDisable();
    }
    rndTextureEnable(FALSE);
    glEnable(GL_CULL_FACE);
    glDepthMask(GL_TRUE);
    glDisable(GL_BLEND);

    if (canTexAdd)
    {
        glPixelTransferf(GL_RED_BIAS, 0.0f);
        glPixelTransferf(GL_GREEN_BIAS, 0.0f);
        glPixelTransferf(GL_BLUE_BIAS, 0.0f);
    }

    partFilter(FALSE);
    rndAdditiveBlends(FALSE);
    return hits;
}

/*-----------------------------------------------------------------------------
    Name        : partMeshMaterialPrepare
    Description : Prepare to render a mesh in singl-texture more by setting up
                    material attributes.
    Inputs      : currentTex - handle of texture we're going to render in.
                  p - particle we're going to render.
                  material - first material of the mesh
                  alpha - enable GL_BLEND if alpha > 0 regardless of other settings
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void partMeshMaterialPrepare(particle *p, trhandle currentTex, materialentry *material, bool alpha)
{
    texreg *reg;
    GLfloat attribs[4];
    real32 ambientFactor;
#if MESH_SPECULAR
    real32 specularFactor;
#endif
    real32 diffuseRed, otherRed;
    GLenum face = GL_FRONT_AND_BACK;  //!!!

    rndTextureEnable(TRUE);

    trMakeCurrent(currentTex);
    reg = trStructureGet(currentTex);
    if (bitTest(reg->flags, TRF_Alpha) || alpha)
    {
        glEnable(GL_BLEND);
        glDisable(GL_ALPHA_TEST);
        if (bitTest(p->flags, PART_ADDITIVE))
        {
            rndAdditiveBlends(TRUE);
        }
        else
        {
            rndAdditiveBlends(FALSE);
        }
    }
    partFilter(TRUE);
    if (p->lit)
    {                                           //if particle is lit
        //this is an attempt to extract the ambient and specular properties from
        //the materials of the mesh without actually using the ambient and
        //specular colors.  It should work in most cases.  It uses the ratio of
        //ambient/diffuse and ambient/specular reds in the first material of the mesh.
        diffuseRed = colUbyteToReal(colRed(material->diffuse));
        otherRed = colUbyteToReal(colRed(material->ambient));
        ambientFactor = otherRed / diffuseRed;
#if MESH_SPECULAR
        otherRed = colUbyteToReal(colRed(material->specular));
        specularFactor = otherRed / diffuseRed;
#endif

        attribs[0] = p->icolor[0] * ambientFactor;
        attribs[1] = p->icolor[1] * ambientFactor;
        attribs[2] = p->icolor[2] * ambientFactor;
        attribs[3] = 1.0f;
        glMaterialfv(face, GL_AMBIENT, attribs);
#if MESH_SPECULAR
        attribs[0] = p->icolor[0] * specularFactor;
        attribs[1] = p->icolor[1] * specularFactor;
        attribs[2] = p->icolor[2] * specularFactor;
        glMaterialfv(face, GL_SPECULAR, attribs);
#endif
        attribs[0] = p->icolor[0];
        attribs[1] = p->icolor[1];
        attribs[2] = p->icolor[2];
        attribs[3] = p->icolor[3];
        glMaterialfv(face, GL_DIFFUSE, attribs);
        rndLightingEnable(TRUE);
    }
    else
    {
        rndLightingEnable(FALSE);
    }
}

void calcBillboardMatrixWorld(matrix* mat, vector* pos, particle* p)
{
    //nothing here
}

void calcBillboardMatrixNotWorld(matrix* mat, vector* pos)
{
    //nothing here
}

void undoBillboardMatrix()
{
    //nothing here
}

/*-----------------------------------------------------------------------------
    Name        : calcPseudoBillboardMatrixWorld
    Description : Calculate a pseudo-billboard matrix for a world-space mesh particle
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void calcPseudoBillboardMatrixWorld(matrix* mat, vector* pos, particle* p, meshSystem* meshPart)
{
    vector velocity, veye, up, right;
    matrix matResult;
    hmatrix hmatResult;

    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();

    velocity = p->wVel;
    if (!bitTest(meshPart->flags, PART_WORLDSPACEVELOCITY))
    {
        vecSub(velocity, velocity, *partEffectVelocity);
    }
    if (velocity.x == 0.0f && velocity.y == 0.0f && velocity.z == 0.0f)
    {
        velocity.z = 1.0f;
    }
    else
    {
        vecNormalize(&velocity);
    }

    vecSub(veye, p->position, mrCamera->eyeposition);
    vecNormalize(&veye);

    vecCrossProduct(up, veye, velocity);
    vecCrossProduct(right, velocity, up);
    vecNormalize(&right);
    vecNormalize(&up);

    matPutVectIntoMatrixCol1(up, matResult);
    matPutVectIntoMatrixCol2(right, matResult);
    matPutVectIntoMatrixCol3(velocity, matResult);

    hmatMakeHMatFromMat(&hmatResult, &matResult);

    glMultMatrixf((GLfloat*)&hmatResult);
}

void partInverselyTransform(vector* out, vector* in, matrix* mat, vector* localPos)
{
    vector  spacedIn;

    vecSub(spacedIn, *in, *localPos);
    //transform this way to perform "inverse" operation
    matMultiplyVecByMat(out, &spacedIn, mat);
}

/*-----------------------------------------------------------------------------
    Name        : calcPseudoBillboardMatrixNotWorld
    Description : Calculate a pseudo-billboard matrix for a local-space mesh particle
    Inputs      :
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void calcPseudoBillboardMatrixNotWorld(matrix* mat, vector* pos, particle* p, meshSystem* meshPart)
{
    vector  velocity, veye, up, right;
    vector  camera_effect, origin_world;
    matrix  matResult;
    hmatrix hmatResult;

    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();

    //obtain velocity vector in effectspace
    vecScalarMultiply(velocity, p->rvec, p->velR);
    if (!bitTest(meshPart->flags, PART_WORLDSPACEVELOCITY))
    {
        vecSub(velocity, velocity, *partEffectVelocity);
    }
    if (velocity.x == 0.0f && velocity.y == 0.0f && velocity.z == 0.0f)
    {
        velocity.z = 1.0f;
    }
    else
    {
        vecNormalize(&velocity);
    }

    //obtain particle's worldspace position
    matMultiplyMatByVec(&origin_world, mat, &p->position);
    vecAddTo(origin_world, *pos);

    //obtain camera's position in effectspace
    partInverselyTransform(&camera_effect, &mrCamera->eyeposition, mat, &origin_world);
    //obtain veye
    vecSub(veye, p->position, camera_effect);
    vecNormalize(&veye);

    //obtain up vector from veye & velocity
    vecCrossProduct(up, veye, velocity);
    vecNormalize(&up);
    //obtain right vector from velocity & up
    vecCrossProduct(right, velocity, up);
    vecNormalize(&right);

    //form a coordinate system
    matPutVectIntoMatrixCol1(up, matResult);
    matPutVectIntoMatrixCol2(right, matResult);
    matPutVectIntoMatrixCol3(velocity, matResult);

    hmatMakeHMatFromMat(&hmatResult, &matResult);

    glMultMatrixf((GLfloat*)&hmatResult);
}

void undoPseudoBillboardMatrix()
{
    //ASSERT: MatrixMode == MODELVIEW
    glPopMatrix();
}

/*-----------------------------------------------------------------------------
    Name        : partMeshOrient
    Description : Create a matrix for the mesh
    Inputs      : p - particle to work with
                  bRescaleNormal - TRUE if normals are to be normalized (as in scaled meshes)
                  meshPart - pointer to mesh particle
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
void partMeshOrient(particle* p, bool bRescaleNormal, meshSystem* meshPart)
{
    vector velocity;
    hmatrix velHMatrix;
    matrix velMatrix;
    real32 length, scale;

    if (bitTest(p->flags, PART_XYZSCALE))
    {
        hmatMakeHMatFromMat(&velHMatrix, &partEffectOwnerSystem/*meshPart->partMat*/);
        hmatPutVectIntoHMatrixCol4(partEffectOwnerPosition/*p->position*/, velHMatrix);
        glMultMatrixf((GLfloat*)&velHMatrix);
/*
        velocity.x = 0.0f;
        velocity.y = 0.0f;
        velocity.z = -1.0f;

        matCreateCoordSysFromHeading(&velMatrix, &velocity);
        hmatMakeHMatFromMat(&velHMatrix, &velMatrix);
        glMultMatrixf((GLfloat*)&velHMatrix);
*/
        hmatMakeRotAboutX(&velHMatrix, (real32)cos(DEG_TO_RAD(180.0)), (real32)sin(DEG_TO_RAD(180.0)));
        glMultMatrixf((GLfloat*)&velHMatrix);

        glScalef(p->tumble[0], p->tumble[1], p->tumble[2]);
        glEnable(GL_NORMALIZE);

        return;
    }

    //translate the mesh
    glTranslatef(p->position.x, p->position.y, p->position.z);

    //align mesh along particle's velocity vector
    if (!(p->flags & (PART_PSEUDOBILLBOARD | PART_TRUEBILLBOARD)))
    {
        if (bitTest(meshPart->flags, PART_WORLDSPACE))
        {
            velocity = p->wVel;
            if (!bitTest(meshPart->flags, PART_WORLDSPACEVELOCITY))
            {
                vecSub(velocity, velocity, *partEffectVelocity);
            }
            if (velocity.x == 0.0f && velocity.y == 0.0f && velocity.z == 0.0f)
            {
                velocity.z = 1.0f;
            }
            else
            {
                vecNormalize(&velocity);
            }
        }
        else
        {
            vecScalarMultiply(velocity, p->rvec, p->velR);
            if (velocity.x == 0.0f && velocity.y == 0.0f && velocity.z == 0.0f)
            {
                velocity.z = 1.0f;
            }
            else
            {
                vecNormalize(&velocity);
            }
        }
        matCreateCoordSysFromHeading(&velMatrix, &velocity);
        hmatMakeHMatFromMat(&velHMatrix, &velMatrix);
        glMultMatrixf((GLfloat*)&velHMatrix);
    }

    //tumble the mesh
    if (p->tumble[0] != 0.0f)
    {
        glRotatef(RAD_TO_DEG(p->tumble[0]), 1.0f, 0.0f, 0.0f);
    }
    if (p->tumble[1] != 0.0f)
    {
        glRotatef(RAD_TO_DEG(p->tumble[1]), 0.0f, 1.0f, 0.0f);
    }
    if (p->tumble[2] != 0.0f)
    {
        glRotatef(RAD_TO_DEG(p->tumble[2]), 0.0f, 0.0f, 1.0f);
    }

    //scale the mesh, if applicable
    scale = p->scale / partNLips;
    length = scale;
    if (p->length != 5.0f)
    {
        length = p->length / partNLips;
    }
    if (p->scale != 1.0f)
    {
        glScalef(scale, scale, length);
        if (bRescaleNormal && (scale == length))
        {
            glEnable(GL_RESCALE_NORMAL);
        }
        else
        {
            glEnable(GL_NORMALIZE);
        }
    }
}

/*-----------------------------------------------------------------------------
    Name        : partRenderMeshSystem
    Description : render a mesh system
    Inputs      : n - number of particles
                  p - pointer to first particle
                  flags - misc flags
    Outputs     :
    Return      : number of live particles; kill the system if 0
----------------------------------------------------------------------------*/
udword partRenderMeshSystem(udword n, particle *p, udword flags, trhandle tex, meshSystem* meshPart)
{
    udword i, hits;
    meshdata* mesh;
    meshdata* mesh2;
    polyentry *polyList;
    materialentry *materialList;
    real32 frac;
    trhandle currentTex = tex;
    bool bRescaleNormal, canTexAdd;
    bool hsColor;
    extern bool8 g_SpecHack;

    glColor3ub(200,200,200);

    bRescaleNormal = glCapFeatureExists(GL_RESCALE_NORMAL);

    hsColor = FALSE;
    if (bitTest(p->flags, PART_XYZSCALE))
    {
        //addColor makes hs effect too distinct
        canTexAdd = FALSE;
        if (partEffectColor != colBlack)
        {
            hsColor = TRUE;
        }
    }
    else
    {
        canTexAdd = glCapFeatureExists(RGL_COLOROP_ADD);
    }

    if (!canTexAdd)
    {
        rndTextureEnvironment(RTE_Modulate);
    }

    for (i = hits = 0; i < n; i++, p++)
    {
        if (p->lifespan < 0.0f)
        {
            continue;
        }
        else
        {
            hits++;
        }
        if (p->waitspan > 0.0f)
        {
            continue;
        }

        handleIllum(p);
        handleStipple(p);

        if (bitTest(p->flags, PART_SPECULAR))
        {
            g_SpecHack = TRUE;
            if (usingShader)
                meshSetSpecular(0, 200,200,200,200);
        }
        else
        {
            g_SpecHack = FALSE;
            if (usingShader)
                meshSetSpecular(-1, 0,0,0,0);
        }

        if (bitTest(p->flags, PART_NODEPTHWRITE))
        {
            glDepthMask(GL_FALSE);
        }
        else
        {
            glDepthMask(GL_TRUE);
        }

        glPushMatrix();

        mesh = p->mesh;
        if ((mesh != NULL) && ((sdword)mesh != -1))
        {
            partMeshOrient(p, bRescaleNormal, meshPart);

            if (g_SpecHack)
            {
                if (usingShader)
                    shSetExponent(0, p->exponent);
                else if (RGL)
                    rglSpecExp(0, p->exponent);
            }

            if (bitTest(flags, PART_ALPHA))
            {
                glColor4f(p->icolor[0], p->icolor[1], p->icolor[2], p->icolor[3]);
                glEnable(GL_BLEND);
                if (usingShader && g_SpecHack)
                {
                    meshSetSpecular(0, RUB(p->icolor[0]), RUB(p->icolor[1]), RUB(p->icolor[2]), RUB(p->icolor[3]));
                }
            }
            else
            {
                glColor3f(p->icolor[0], p->icolor[1], p->icolor[2]);
                if (usingShader && g_SpecHack)
                {
                    meshSetSpecular(0, RUB(p->icolor[0]), RUB(p->icolor[1]), RUB(p->icolor[2]), 255);
                }
            }

            if (canTexAdd)
            {
                glPixelTransferf(GL_RED_BIAS, p->bias[0]);
                glPixelTransferf(GL_GREEN_BIAS, p->bias[1]);
                glPixelTransferf(GL_BLUE_BIAS, p->bias[2]);
            }
            else
            {
                //simulate colorop_add in a crude manner

                real32 csat[3];

                if ((p->bias[0] + p->bias[1] + p->bias[2]) > 0.0f)
                {
                    if (hsColor)
                    {
                        p->bias[0] = colReal32(colRed(partEffectColor));
                        p->bias[1] = colReal32(colGreen(partEffectColor));
                        p->bias[2] = colReal32(colBlue(partEffectColor));
                    }
                    partSaturateAdd(csat, p->bias, p->icolor);

                    if (bitTest(flags, PART_ALPHA))
                    {
                        glColor4f(csat[0], csat[1], csat[2], p->icolor[3]);
                    }
                    else
                    {
                        glColor3f(csat[0], csat[1], csat[2]);
                    }
                }
            }

            //logic to determine what texture to use on this mesh, if any
            if (p->tstruct == NULL && p->tex != TR_Invalid)
            {
                currentTex = p->tex;
            }

#if MESH_VERBOSE_LEVEL
            if (mesh->nPolygonObjects > 1)
            {                                               //display warning if there are more than 1 object in the mesh
                dbgMessagef("\nThere are %d polygon objects in mesh ('%s').", mesh->nPolygonObjects,
#if MESH_RETAIN_FILENAMES
                    mesh->fileName);
#else
                    "N/A");
#endif //MESH_RETAIN_FILENAMES
            }
#endif //MESH_VERBOSE_LEVEL

            if (bitTest(p->flags, PART_PSEUDOBILLBOARD))
            {
                if (bitTest(flags, PART_WORLDSPACE))
                {
                    calcPseudoBillboardMatrixWorld(meshPart->partMat, &meshPart->partPos, p, meshPart);
                }
                else
                {
                    calcPseudoBillboardMatrixNotWorld(meshPart->partMat, &meshPart->partPos, p, meshPart);
                }
            }
            else if (bitTest(p->flags, PART_TRUEBILLBOARD))
            {
                if (bitTest(flags, PART_WORLDSPACE))
                {
                    calcBillboardMatrixWorld(meshPart->partMat, &meshPart->partPos, p);
                }
                else
                {
                    calcBillboardMatrixNotWorld(meshPart->partMat, &meshPart->partPos);
                }
            }

            if (p->mstruct == NULL)
            {                                               //if no morph animation
                if (currentTex != TR_Invalid)
                {                                           //if there is a texture
                    partMeshMaterialPrepare(p, currentTex, &mesh->localMaterial[0], bitTest(flags, PART_ALPHA));
                    rndGLStateLog("meshSystem");
                    meshObjectRenderTex(&mesh->object[0], mesh->localMaterial);
                }
                else
                {                                           //no texture, render solid surfaces
                    rndTextureEnable(FALSE);
                    rndGLStateLog("meshSystem");
                    meshObjectRender(&mesh->object[0], mesh->localMaterial, p->colorScheme);
                }
            }
            else
            {                                               //else there is a morph animation
                mesh2 = partMeshNextMesh(meshPart, p);
                frac = p->meshFrame - (real32)((sdword)p->meshFrame);

                if (bRescaleNormal)
                {
                    glDisable(GL_RESCALE_NORMAL);
                }
                glEnable(GL_NORMALIZE);
                polyList = ((meshAnim*)p->mstruct)->mesh->object[0].pPolygonList;
                materialList = ((meshAnim*)p->mstruct)->mesh->localMaterial;
                if (currentTex != TR_Invalid)
                {                                           //if there is a texture
                    partMeshMaterialPrepare(p, currentTex, &materialList[0], bitTest(flags, PART_ALPHA));
                    meshMorphedObjectRenderTex(&mesh->object[0], &mesh2->object[0], polyList, materialList, frac, p->colorScheme);
                }
                else
                {                                           //morphed mesh, mo texture
                    meshMorphedObjectRender(&mesh->object[0], &mesh2->object[0], polyList, materialList, frac, p->colorScheme);
                }
                glDisable(GL_NORMALIZE);
            }

            if (bitTest(p->flags, PART_PSEUDOBILLBOARD))
            {
                undoPseudoBillboardMatrix();
            }
            else if (bitTest(p->flags, PART_TRUEBILLBOARD))
            {
                undoBillboardMatrix();
            }

            if (p->scale != 1.0f)
            {
                if (bRescaleNormal)
                {
                    glDisable(GL_RESCALE_NORMAL);
                }
                else
                {
                    glDisable(GL_NORMALIZE);
                }
            }
        }

        glPopMatrix();
    }

    if (RGL)
    {
        if (usingShader)
        {
            shSetExponent(0, -1.0f);
            meshSetSpecular(-1, 0, 0, 0, 0);
        }
        else
        {
            rglSpecExp(0, -1.0f);
        }
    }
    else
    {
        shSetExponent(0, -1.0f);
        meshSetSpecular(-1, 0, 0, 0, 0);
    }

    if (canTexAdd)
    {
        glPixelTransferf(GL_RED_BIAS, 0.0f);
        glPixelTransferf(GL_GREEN_BIAS, 0.0f);
        glPixelTransferf(GL_BLUE_BIAS, 0.0f);
    }

    g_SpecHack = FALSE;
    glDepthMask(GL_TRUE);

    glDisable(GL_BLEND);

    return hits;
}

/*-----------------------------------------------------------------------------
    Name        : partRenderLineSystem
    Description : render a line system
    Inputs      : n - number of particles
                  p - pointer to first particle
                  flags - misc flags
    Outputs     :
    Return      : number of live particles; kill the system if 0
----------------------------------------------------------------------------*/
udword partRenderLineSystem(udword n, particle *p, udword flags)
{
    udword i, hits;
    vector pos;
    bool texEnabled, lightEnabled;
    bool alpha = FALSE;

    glPushAttrib(GL_LINE_BIT);
    if (bitTest(flags, PART_ALPHA))
    {
        alpha = TRUE;
        rndAdditiveBlends(FALSE);
        glEnable(GL_BLEND);
    }

    texEnabled = rndTextureEnable(FALSE);
    lightEnabled = rndLightingEnable(FALSE);

    for (i = hits = 0; i < n; i++, p++)
    {
        if (p->lifespan < 0.0f)
            continue;
        else
            hits++;
        if (p->waitspan > 0.0f)
            continue;

        handleIllum(p);
        handleStipple(p);

        if (bitTest(p->flags, PART_ADDITIVE))
        {
            rndAdditiveBlends(TRUE);
        }
        else
        {
            rndAdditiveBlends(FALSE);
        }

        glLineWidth(p->scale);
        glBegin(GL_LINES);
        if (alpha)
        {
            glColor4f(p->icolor[0], p->icolor[1], p->icolor[2], p->icolor[3]);
        }
        else
        {
            glColor3f(p->icolor[0], p->icolor[1], p->icolor[2]);
        }
        pos = p->position;
        if (bitTest(flags, PART_WORLDSPACE))
        {
            vector rv = p->wVel;
            vecNormalize(&rv);
            if (_isnan((double)rv.x) || _isnan((double)rv.y) || _isnan((double)rv.z) || 
				abs(rv.x) < REALlySmall && abs(rv.y) <= REALlySmall && abs(rv.z) <= REALlySmall)
            {
                rv.z = 1.0f;
            }
            if (!_isnan((double)p->length))
            {
                vecMultiplyByScalar(rv, p->length);
            }
            vecAddTo(pos, rv);
        }
        else
        {
            vector rv = p->rvec;
            vecMultiplyByScalar(rv, p->velR);
            rv.z += p->velLOF;
            if (rv.x == 0.0f && rv.y == 0.0f && rv.z == 0.0f)
            {
                rv.z = 1.0f;
            }
            vecNormalize(&rv);
            vecMultiplyByScalar(rv, p->length);
            vecAddTo(pos, rv);
        }
        glVertex3f(p->position.x, p->position.y, p->position.z);
        glVertex3f(pos.x, pos.y, pos.z);
        glEnd();
    }

    if (alpha)
    {
        glDisable(GL_BLEND);
    }
    rndAdditiveBlends(FALSE);

    rndTextureEnable(texEnabled);
    rndLightingEnable(lightEnabled);

    glPopAttrib();
    return hits;
}

/*-----------------------------------------------------------------------------
    Name        : partRenderCubeSystem
    Description : render a cube system
    Inputs      : n - number of particles
                  p - pointer to first particle
                  flags - misc flags
    Outputs     :
    Return      : number of live particles; kill the system if 0
----------------------------------------------------------------------------*/
udword partRenderCubeSystem(udword n, particle *p, udword flags)
{
    udword i, hits;
    bool alpha = FALSE;

    if (bitTest(flags, PART_ALPHA))
    {
        alpha = TRUE;
        rndAdditiveBlends(FALSE);
        glEnable(GL_BLEND);
    }

    for (i = hits = 0; i < n; i++, p++)
    {
        if (p->lifespan < 0.0f)
            continue;
        else
            hits++;
        if (p->waitspan > 0.0f)
            continue;
        glPushMatrix();
        glTranslatef(p->position.x, p->position.y, p->position.z);
        if (alpha)
            glColor4f(p->icolor[0], p->icolor[1], p->icolor[2], p->icolor[3]);
        else
            glColor3f(p->icolor[0], p->icolor[1], p->icolor[2]);
        handleIllum(p);
        drawBox(p->scale * 0.5f, GL_QUADS);
        glPopMatrix();
    }

    if (alpha)
        glDisable(GL_BLEND);

    return hits;
}

/*-----------------------------------------------------------------------------
    Name        : partRenderPointSystem
    Description : render a point system
    Inputs      : n - number of particles
                  p - pointer to first particle
                  flags - misc flags
    Outputs     :
    Return      : number of live particles; kill the system if 0
----------------------------------------------------------------------------*/
udword partRenderPointSystem(udword n, particle *p, udword flags)
{
    udword i, hits;
    bool alpha = FALSE;

    bool texEnabled, lightEnabled;

    texEnabled = rndTextureEnable(FALSE);
    lightEnabled = rndLightingEnable(FALSE);

    glPushAttrib(GL_POINT_BIT);
    if (bitTest(flags, PART_ALPHA))
    {
        alpha = TRUE;
        glEnable(GL_BLEND);
        if (bitTest(p->flags, PART_ADDITIVE))
        {
            rndAdditiveBlends(TRUE);
        }
        else
        {
            rndAdditiveBlends(FALSE);
        }
    }

    for (i = hits = 0; i < n; i++, p++)
    {
        if (p->lifespan < 0.0f)
            continue;
        else
            hits++;
        if (p->waitspan > 0.0f)
            continue;

        handleIllum(p);

        glPointSize((p->scale <= 3.0f) ? p->scale : 3.0f);
        if (RGL)
            rglFeature(RGL_EFFECTPOINT);
        glBegin(GL_POINTS);
        if (alpha)
            glColor4f(p->icolor[0], p->icolor[1], p->icolor[2], p->icolor[3]);
        else
            glColor3f(p->icolor[0], p->icolor[1], p->icolor[2]);
        glVertex3f(p->position.x, p->position.y, p->position.z);
        glEnd();
    }

    if (alpha)
    {
        glDisable(GL_BLEND);
    }

    rndTextureEnable(texEnabled);
    rndLightingEnable(lightEnabled);

    glPopAttrib();
    rndAdditiveBlends(FALSE);
    return hits;
}

/*-----------------------------------------------------------------------------
    Name        : partRenderSystem
    Description : render a particle system.  passes control to other more
                  specific functions.  assumes MODELVIEW already set.
    Inputs      : psys - the particle system
    Outputs     : sets psys->isAlive to FALSE if no particle is left alive
    Return      :
----------------------------------------------------------------------------*/
void partRenderSystem(psysPtr psys)
{
    pointSystem *pp;
    particle *p = NULL;
    udword hits;
    sdword wasLit;
    bool isWorldspace;
    vector position;

    billSystem* billPart;

    wasLit = rndLightingEnable(FALSE);

    pp = (pointSystem*)psys;
    storeIllum();
    stippleSave();
    isWorldspace = bitTest(pp->flags, PART_WORLDSPACE) ? TRUE : FALSE;

    p = (particle*)(psys + partHeaderSize(psys));
    if (p->lit)
    {
        rndLightingEnable(TRUE);
    }

    switch (pp->t)
    {
    case PART_BILLBOARD:
        billPart = (billSystem*)pp;
        memcpy(&position, &billPart->position, sizeof(vector));
        if (!isWorldspace)
        {
            //will push matrix
            partPerformHacking(((billSystem*)pp)->partMat, position);
        }
        hits = partRenderBillSystem(pp->n, p, pp->flags, position,
                                    billPart->uv, billPart, isWorldspace);
        if (!isWorldspace)
        {
            //pop the matrix
            glPopMatrix();
        }
        break;

    case PART_MESH:
        alodEnable(FALSE);
        hits = partRenderMeshSystem(pp->n, p, pp->flags, ((meshSystem*)pp)->tex,
                                    (meshSystem*)pp);
        alodEnable(TRUE);
        break;

    case PART_LINES:
        hits = partRenderLineSystem(pp->n, p, pp->flags);
        break;

    case PART_CUBES:
        hits = partRenderCubeSystem(pp->n, p, pp->flags);
        break;

    case PART_POINTS:
        hits = partRenderPointSystem(pp->n, p, pp->flags);
        break;

    default:
#if PART_Verbose > 0
        dbgMessagef("\nunknown particle system type: %d", pp->t);
#endif
        return;
    }

    restoreIllum();
    stippleRestore();

    if (!hits)                      //kill the system
    {
        pp->isAlive = FALSE;
    }
    rndLightingEnable(wasLit);
    rndAdditiveBlends(FALSE);
}

//constant delta
real32 partRealDelta(real32 n, real32 d)
{
    return n + d;
}

//randomized delta
real32 partRealDist(real32 n, real32 d)
{
    real32 r = (real32)((real64)(ranRandom(RAN_ParticleStream) % 1000000) / 1000000.0);
    real32 sign = (ranRandom(RAN_ParticleStream) % 2 == 0) ? -1.0f : 1.0f;
    if (d < 0.0f)
        sign = -1.0f;
    return n + sign*r*d;
}

//constant delta, will handle alpha
void partColorDelta(real32 *c, real32 *d, udword flags, real32 dt)
{
    real32 cr, cg, cb, ca;
    if (d[0] != 0.0f)
    {
        cr = partRealDelta(c[0], d[0] * dt);
        RealClamp(cr);
        c[0] = cr;
    }
    if (d[1] != 0.0f)
    {
        cg = partRealDelta(c[1], d[1] * dt);
        RealClamp(cg);
        c[1] = cg;
    }
    if (d[2] != 0.0f)
    {
        cb = partRealDelta(c[2], d[2] * dt);
        RealClamp(cb);
        c[2] = cb;
    }
    if (bitTest(flags, PART_ALPHA) && d[3] != 0.0f)
    {
        ca = partRealDelta(c[3], d[3] * dt);
        RealClamp(ca);
        c[3] = ca;
    }
}

//randomized color delta
void partColorDist(real32 *out, real32 *c, real32 dr, real32 dg, real32 db)
{
    real32 cr, cg, cb;
    cr = partRealDist(c[0], dr);
    cg = partRealDist(c[1], dg);
    cb = partRealDist(c[2], db);
    RealClampNeg(cr);
    RealClampNeg(cg);
    RealClampNeg(cb);
    out[0] = cr;
    out[1] = cg;
    out[2] = cb;
}

//like partColorDist, but with alpha
void partColorDistA(real32 *out, real32 *c, real32 dr, real32 dg, real32 db, real32 da)
{
    real32 ca;
    partColorDist(out, c, dr, dg, db);
    ca = c[3];
    ca = partRealDist(ca, da);
    RealClampNeg(ca);
    out[3] = ca;
}

real32 fracPart(real32 f)
{
    sdword i = (sdword)f;
    return (real32)fabs((real64)((real32)i - f));
}

#if PART_TEST_MORPH
meshAnim* partSetupTestMeshMorphBlock()
{
    meshAnim* ma;
    meshAnim* a;

    ma = (meshAnim*)memAlloc(5*sizeof(meshAnim), "mesh animation block", 0);

    a = ma;
    a->mesh = meshLoad("etg\\meshes\\head00.geo");
    a->flags = PART_LOOPSTART;

    a++;
    a->mesh = meshLoad("etg\\meshes\\head01.geo");
    a->flags = 0;

    a++;
    a->mesh = meshLoad("etg\\meshes\\head02.geo");
    a->flags = 0;

    a++;
    a->mesh = meshLoad("etg\\meshes\\head03.geo");
    a->flags = PART_LOOP;

    a++;
    a->mesh = NULL;
    a->flags = 0;

    return ma;
}
#endif

/*-----------------------------------------------------------------------------
    Name        : partMeshNextMesh
    Description : mesh animations track the CURRENT frame of animation; this fn
                  returns the NEXT frame of animation for lerping
    Inputs      : psys - particle system header
                  p - the particular particle
    Outputs     :
    Return      : mesh object of the next animation frame
----------------------------------------------------------------------------*/
meshdata* partMeshNextMesh(meshSystem* psys, particle* p)
{
    meshAnim* animblock;
    meshAnim* frame;
    sdword frameNo;

    frameNo = partAdvanceMeshMorph(psys, p);
    animblock = (meshAnim*)p->mstruct;
    frame = animblock + frameNo;
    return frame->mesh;
}

/*-----------------------------------------------------------------------------
    Name        : partAdvanceMeshMorph
    Description : actually updates the frame of a mesh animation.
                  p->meshFrame is ASSUMED VALID
    Inputs      : p - the partice to update
    Outputs     :
    Return      : the frame of animation is returned
----------------------------------------------------------------------------*/
//this fn assumes that the current frame is valid (ie. not an end or overflow)
sdword partAdvanceMeshMorph(meshSystem* psys, particle* p)
{
    meshAnim* animblock;
    meshAnim* prev;
    meshAnim* next;
    sdword    frame;

    animblock = (meshAnim*)p->mstruct;
    prev = animblock + (sdword)p->meshFrame;     //floor for previous

    //-- seek to next position --

    //maybe we're a loopend
    if (prev->flags == PART_LOOP)
    {
        if (p->loopCount > 0)
        {
            p->loopCount--;
        }
        else if (p->loopCount == 0)
        {
            goto NO_LOOP;
        }
        //seek to loopstart
        for (next = animblock, frame = 0;; frame++, next++)
        {
            if (next->flags == PART_LOOPSTART)
            {
                return frame;   //found the start of the loop
            }
            else if (next->mesh == NULL)
            {
                return -1;      //error condition
            }
        }
    }

 NO_LOOP:

    //find the trivial next
    frame = (sdword)p->meshFrame + 1;
    next = animblock + frame;
    if (next->mesh == NULL || (sdword)next->mesh == -1)
    {
        if (p->loopCount == 0)
        {
            //we've exceeded our loopcount, so don't wrap around
            frame = (sdword)p->meshFrame;
        }
        else
        {
            next = animblock;       //wrapped around
            frame = 0;
        }
    }

    return frame;
}

/*-----------------------------------------------------------------------------
    Name        : partSetMeshFromAnimation
    Description : sets the mesh on a particle from the current frame of animation.
                  p->meshFrame is ASSUMED VALID
    Inputs      : p - the particle
    Outputs     : p->mesh is updated
    Return      :
----------------------------------------------------------------------------*/
void partSetMeshFromAnimation(particle* p)
{
    meshAnim* animblock;
    meshAnim* frame;

    animblock = (meshAnim*)p->mstruct;
    frame = animblock + (sdword)p->meshFrame;
    p->mesh = frame->mesh;
}

/*-----------------------------------------------------------------------------
    Name        : partUpdateMeshAnimation
    Description : updates the frame of a mesh animation
    Inputs      : psys - mesh particle system header
                  part - the particle to update
                  dt - elapsed time
    Outputs     :
    Return      :
----------------------------------------------------------------------------*/
//when rendering, use floor(meshFrame) as "from", and the result of
//partAdvanceMeshMorph(..) as "to"
void partUpdateMeshAnimation(meshSystem* psys, particle* part, real32 dt)
{
    real32 prevFrame, nextFrame, frac;
    sdword iFrames, frame, i;

    if (part->meshRate == 0.0f)         //nothing to update
    {
        return;
    }

    prevFrame = part->meshFrame;
    nextFrame = prevFrame + dt * part->meshRate;

    if ((sdword)nextFrame == (sdword)prevFrame)
    {
        //no animation advance, just new lerp parameters
        part->meshFrame = nextFrame;
        partSetMeshFromAnimation(part);
        return;
    }

    iFrames = (sdword)nextFrame - (sdword)prevFrame;
    frac = nextFrame - (real32)floor((real64)nextFrame);
    //seek iFrames into the animation
    for (i = 0; i < iFrames; i++)
    {
        frame = partAdvanceMeshMorph(psys, part);
        part->meshFrame = (real32)frame + frac;
    }

    partSetMeshFromAnimation(part);
}

/*-----------------------------------------------------------------------------
    Name        : partUpdateTexAnim
    Description : returns an incremented frame counter for a texture animation
                  (frame++)
    Inputs      : bsys - billboard particle system header
                  p - the particular particle we're interested in
    Outputs     :
    Return      : incremented frame counter
----------------------------------------------------------------------------*/
sdword partAdvanceTexAnim(billSystem* bsys, particle* p)
{
    texAnim* animblock;
    texAnim* prev;
    texAnim* next;
    sdword   frame;

    animblock = (texAnim*)p->tstruct;
    prev = animblock + (sdword)p->currentFrame;     //floor for previous

    //-- seek to next position --

    //maybe we're a loopend
    if (p->loop && prev->flags == PART_LOOP)
    {
        //seek to loopstart
        for (next = animblock, frame = 0;; frame++, next++)
        {
            if (next->flags == PART_LOOPSTART)
            {
                return frame;   //found the start of the loop
            }
            else if ((sdword)next->tex == -1)
            {
                return -1;      //error condition
            }
        }
    }

    //find the trivial next
    frame = (sdword)p->currentFrame + 1;
    next = animblock + frame;
    if ((sdword)next->tex == -1)
    {
        if (!p->loop)
        {
            //we've exceeded our loopcount, so don't wrap around
            frame = (sdword)p->currentFrame;
        }
        else
        {
            next = animblock;       //wrapped around
            frame = 0;
        }
    }

    return frame;
}

/*-----------------------------------------------------------------------------
    Name        : partSetTexFromAnimation
    Description : sets the texture on a particle from the current frame of animation.
                  part->currentFrame is ASSUMED VALID
    Inputs      : bsys - system header
                  part - individual particle
    Outputs     : system's texture is modified
    Return      :
----------------------------------------------------------------------------*/
void partSetTexFromAnimation(psysPtr bsys, particle* part)
{
    texAnim* animblock;
    texAnim* frame;

    animblock = (texAnim*)part->tstruct;
    frame = animblock + (sdword)part->currentFrame;
    partModifyTexture(bsys, frame->tex,
                      (udword)frame->uv[0][0], (udword)frame->uv[0][1],
                      (udword)frame->uv[1][0], (udword)frame->uv[1][1]);
}

/*-----------------------------------------------------------------------------
    Name        : partUpdateAnimation
    Description : updates the frame of a texture animation
    Inputs      : bsys - system header
                  part - particular particle
                  dt - time elapsed
    Outputs     : system's texture is modified, particle's frame counter is updated
    Return      :
----------------------------------------------------------------------------*/
void partUpdateAnimation(billSystem* bsys, particle* part, real32 dt)
{
    real32 prevFrame, nextFrame, frac;
    sdword iFrames, frame, i;

    if (part->rate == 0.0f)
    {
        return;
    }

    prevFrame = part->currentFrame;
    nextFrame = prevFrame + dt * part->rate;

    if ((sdword)nextFrame == (sdword)prevFrame)
    {
        part->currentFrame = nextFrame;
        partSetTexFromAnimation((psysPtr)bsys, part);
        return;
    }

    iFrames = (sdword)nextFrame - (sdword)prevFrame;
    frac = nextFrame - (real32)floor((real64)nextFrame);
    for (i = 0; i < iFrames; i++)
    {
        frame = partAdvanceTexAnim(bsys, part);
        part->currentFrame = (real32)frame + frac;
    }

    partSetTexFromAnimation((psysPtr)bsys, part);
}

/*-----------------------------------------------------------------------------
    Name        : partUpdateSystem
    Description : ticks a particle system by given elapsed time
    Inputs      : psys - particle system pointer
                  dt - elapsed time since last call
                  velvec - velocity vector to be added to velLOF
                           and velR
    Outputs     :
    Return      : TRUE if system is dead, FALSE otherwise
----------------------------------------------------------------------------*/
bool8 partUpdateSystem(psysPtr psys, real32 dt, vector* velvec)
{
    billSystem *pp;
    particle *p = NULL;
    udword n, i, hits;
    vector rvec;
    bool draggin;

    pp = (billSystem*)psys;      //default assumption
    n = pp->n;
    if (pp->angDelta != 0.0f)
    {
        pp->ang += dt * pp->angDelta;
    }

    p = (particle*)(psys + partHeaderSize(psys));

    if (p == NULL)
    {
        dbgMessage("\n!! invalid particle system !!");
        return TRUE;
    }

    draggin = (pp->drag != 1.0f);

    for (i = hits = 0; i < n; i++, p++)
    {
        //mesh tumble specifics
        if (pp->t == PART_MESH)
        {
            p->tumble[0] += dt * p->deltaTumble[0];
            p->tumble[1] += dt * p->deltaTumble[1];
            p->tumble[2] += dt * p->deltaTumble[2];
        }

        if (p->waitspan > 0.0f)
        {
            p->waitspan -= dt;
            if (p->waitspan < 0.0f)
            {
                p->waitspan = 0.0f;
            }
            hits++;
            continue;       //don't dec lifespan if waiting
        }

        //sprite/mesh system specifics
        if (p->tstruct != NULL)
        {
            partUpdateAnimation(pp, p, dt);
        }
        if (p->mstruct != NULL)
        {
            partUpdateMeshAnimation((meshSystem*)pp, p, dt);
        }
        if (p->deltaRot != 0.0f)
        {
            p->rot += dt * p->deltaRot;
        }

        //lines
        if (p->deltaLength)
        {
            p->length += dt * p->deltaLength;
            if (_isnan((double)p->length))
            {
                p->length = 1.0f;
            }
        }

        //kinematics
        if (bitTest(pp->flags, PART_WORLDSPACE))
        {
            vector adjWVel, adjWAccel;

            //don't update position if XYZ scaling (hyperspace effect)
            if (!bitTest(p->flags, PART_XYZSCALE))
            {
                //setup
                adjWVel = p->wVel;
                adjWAccel = p->wAccel;
                vecMultiplyByScalar(adjWVel, dt);
                vecMultiplyByScalar(adjWAccel, dt);
                //velocity
                vecAddTo(adjWVel, adjWAccel);
                //update
                vecAddTo(p->position, adjWVel);
                vecAddTo(p->wVel, adjWAccel);
                //drag
                vecMultiplyByScalar(p->wVel, pp->drag);
                vecMultiplyByScalar(p->wAccel, pp->drag);
            }
        }
        else
        {
            //position
            p->position.z += dt * p->velLOF;
            rvec = p->rvec;
            vecMultiplyByScalar(rvec, dt * p->velR);
            vecAddTo(p->position, rvec);

            //velocity
            p->velLOF += dt * p->deltaVelLOF;
            p->velR += dt * p->deltaVelR;
            if (draggin)
            {
                p->velLOF *= pp->drag;
                p->velR *= pp->drag;
            }
        }

        //appearance
        if (p->deltaScale != 0.0f)
        {                                                   //scale the particle
            p->scale += dt * p->deltaScale;
            if (p->scale < 0.0f)
            {                                               //clamp at zero
                p->scale = 0.0f;
            }
        }

        partColorDelta(p->icolor, p->deltaColor, pp->flags, dt);

        //FIXME: does this make sense?
        if (p->icolor[3] < 1.0f)
        {
            pointSystem* sys = (pointSystem*)psys;
            bitSet(sys->flags, PART_ALPHA);
        }

        if (p->lit)
        {
            p->illum += dt * p->deltaIllum;
            RealClamp(p->illum);
        }

        //lifespan
        p->lifespan -= dt;
        if (p->lifespan > 0.0f)
        {
            hits++;
        }
    }

    return((bool8)((hits == 0) ? TRUE : FALSE));
}

bool g_Billboarded;

real64 randomAngle(udword a)
{
    udword r = ranRandom(RAN_ParticleStream) % a;
    return DEG_TO_RAD((real64)r);
}

//make a spherically distributed particle
vector partMakeSPos(real32 offsetLOF, real32 offsetR, real32 offsetTheta,
                    real32 deltaLOF, real32 deltaLOFDist,
                    real32 deltaR, real32 deltaRDist,
                    real32 deltaTheta, real32 deltaThetaDist,
                    vector *rVec)
{
    vector xyz;
    hvector hvec, rvec;
    real32 sinTheta, cosTheta;
    hmatrix result;

    //deltas
    deltaLOF = partRealDist(deltaLOF, deltaLOFDist);
    deltaR = partRealDist(deltaR, deltaRDist);
    deltaTheta = partRealDist(deltaTheta, deltaThetaDist);

    //offsets
    offsetLOF = partRealDelta(offsetLOF, deltaLOF);
    offsetR = partRealDelta(offsetR, deltaR);
    offsetTheta = partRealDelta(offsetTheta, deltaTheta);

    //cartesian
    {
        real64 a = randomAngle(360);
        sinTheta = (real32)sin(a);
        cosTheta = (real32)cos(a);
    }
    vecZeroVector(xyz);
    xyz.x = 1.0f;
    vecMakeHVecFromVec(hvec, xyz);
    hmatMakeRotAboutZ(&result, sinTheta, cosTheta);
    hmatMultiplyHMatByHVec(&rvec, &result, &hvec);
    {
        real64 a = randomAngle(360);
        sinTheta = (real32)sin(a);
        cosTheta = (real32)cos(a);
    }
    hmatMakeRotAboutX(&result, sinTheta, cosTheta);
    hmatMultiplyHMatByHVec(&hvec, &result, &rvec);
    rvec = hvec;
    hmatMultiplyHMatByHVec(&rvec, &result, &hvec);
    vecHomogenize(&xyz, &rvec);
    *rVec = xyz;
    vecNormalize(rVec);
    vecMultiplyByScalar(xyz, offsetR);
    xyz.z += offsetLOF;

    //done
    return xyz;
}

/*-----------------------------------------------------------------------------
    Name        : partMakePosition
    Description : turns a many-variabled cylindrical coordinate system
                  interpretation into something nice.
    Inputs      : offsetLOF - distance along line of fire
                  offsetR - radius component
                  offsetTheta - theta component
                  deltaLOF - variance in offsetLOF
                  deltaLOFDist - variance in deltaLOF
                  deltaR - variance in offsetR
                  deltaRDist - variance in deltaR
                  deltaTheta - variance in theta
                  deltaThetaDist - variance in deltaTheta
                  rVec - pointer to a vector to recieve radial velocity
    Outputs     : rVec - unitized offsetR rotated by theta
    Return      : a 3d vector (x,y,z)
----------------------------------------------------------------------------*/
vector partMakePosition(real32 offsetLOF, real32 offsetR, real32 offsetTheta,
                        real32 deltaLOF, real32 deltaLOFDist,
                        real32 deltaR, real32 deltaRDist,
                        real32 deltaTheta, real32 deltaThetaDist,
                        vector *rVec)
{
    vector xyz;
    vector hvec;
    real32 sinTheta, cosTheta;
    matrix result;

    //deltas
    deltaLOF = partRealDist(deltaLOF, deltaLOFDist);
    deltaR = partRealDist(deltaR, deltaRDist);
    deltaTheta = partRealDist(deltaTheta, deltaThetaDist);

    //offsets
    offsetLOF = partRealDelta(offsetLOF, deltaLOF);
    offsetR = partRealDelta(offsetR, deltaR);
    offsetTheta = partRealDelta(offsetTheta, deltaTheta);

    //cartesian
    sinTheta = (real32)sin((real64)offsetTheta);
    cosTheta = (real32)cos((real64)offsetTheta);
    vecZeroVector(xyz);
    xyz.x = offsetR;
    hvec = xyz;
    matMakeRotAboutZ(&result, sinTheta, cosTheta);
    matMultiplyMatByVec(&xyz, &result, &hvec);
    hvec.x = 1.0f;
    hvec.y = hvec.z = 0.0f;
    matMultiplyMatByVec(rVec, &result, &hvec);

    xyz.z += offsetLOF;

    //done
    return xyz;
}

/*-----------------------------------------------------------------------------
    Name        : partFillGenericParticles
    Description : fills up the generic portion of a particle structure with the
                  current particle state (particleAttribs)
    Inputs      : n - number of particles
                  psys - the particle system, type-specific header + generic portion
                  headerlength - length of the type-specific portion
                  dist - PART_CYLINDER or PART_SPHERE
    Outputs     : fills up the generic portion of psys appropriately
    Return      :
----------------------------------------------------------------------------*/
void partFillGenericParticles(udword n, psysPtr psys,
                              udword headerlength, udword dist,
                              bool isWorldspace)
{
    udword i, pos, size;
    vector rvec;
    particle *p;
    bool alpha = FALSE;

    if (bitTest(pat.flags, PART_ALPHA))
        alpha = TRUE;

    for (i = 0; i < n; i++)
    {
        //call the particle creation callback, if any
        if (partCreateCallback)
        {
            partCreateCallback(partCreateUserValue, partCreateUserData);
        }
        size = sizeof(particle);
        pos = headerlength + i*size;
        p = (particle*)(psys + pos);

        //flags
        p->flags = 0;
        if (pat.specularOnly)
        {
            bitSet(p->flags, PART_SPECULAR);
        }
        if (pat.stippleAlpha)
        {
            bitSet(p->flags, PART_STIPPLE);
        }
        if (pat.additiveBlends)
        {
            bitSet(p->flags, PART_ADDITIVE);
        }
        if (pat.pseudoBillboard)
        {
            bitSet(p->flags, PART_PSEUDOBILLBOARD);
        }
        if (pat.trueBillboard)
        {
            bitSet(p->flags, PART_TRUEBILLBOARD);
        }
        if (pat.noDepthWrite)
        {
            bitSet(p->flags, PART_NODEPTHWRITE);
        }

        //sprite system specifics
        p->tex = TR_Invalid;
        p->rot = partRealDist(pat.ang, pat.angDist);
        p->deltaRot = partRealDist(pat.angDelta, pat.angDeltaDist);

        p->tstruct = pat.tstruct;
        p->rate = pat.rate;
        p->meshRate = pat.meshRate;
        p->loop = pat.loop;
        p->currentFrame = (real32)pat.startFrame;

        //position
        if (dist == PART_CYLINDER)
        {
            p->position = partMakePosition(pat.offsetLOF, pat.offsetR, pat.offsetTheta,
                                           pat.deltaLOF, pat.deltaLOFDist,
                                           pat.deltaR[0], pat.deltaRDist[0],
                                           pat.deltaR[1], pat.deltaRDist[1],
                                           &rvec);
        }
        else
        {
            p->position = partMakeSPos(pat.offsetLOF, pat.offsetR, pat.offsetTheta,
                                       pat.deltaLOF, pat.deltaLOFDist,
                                       pat.deltaR[0], pat.deltaRDist[0],
                                       pat.deltaR[1], pat.deltaRDist[1],
                                       &rvec);
        }
        if (pat.offsetArray != NULL)
        {
            vecAddTo(p->position, pat.offsetArray[i]);
            //comment out the previous line and uncomment this section to get a mapping from
            //Lightwave ship coords to Homeworld ship coords
            /*
            p->position.x += pat.offsetArray[i].y;
            p->position.y += pat.offsetArray[i].z;
            p->position.z -= pat.offsetArray[i].x;
            */

        }

        p->rvec = rvec;
        vecNormalize(&p->rvec);
        //velocity
        p->deltaVelLOF = partRealDist(pat.deltaVelLOF, pat.deltaVelLOFDist);
        p->deltaVelR = partRealDist(pat.deltaVelR, pat.deltaVelRDist);
        p->velLOF = partRealDist(pat.velLOF, pat.velLOFDist);
        p->velR = partRealDist(pat.velR, pat.velRDist);
        //appearance
        p->scale = partRealDist(pat.scale, pat.scaleDist);
        p->deltaScale = partRealDist(pat.deltaScale, pat.deltaScaleDist);
        if (alpha)
        {
            partColorDistA(p->icolor, pat.icolor, pat.colorDist[0], pat.colorDist[1],
                           pat.colorDist[2], pat.colorDist[3]);
            partColorDistA(p->deltaColor, pat.deltaColor, pat.deltaColorDist[0],
                           pat.deltaColorDist[1], pat.deltaColorDist[2],
                           pat.deltaColorDist[3]);
        }
        else
        {
            partColorDist(p->icolor, pat.icolor, pat.colorDist[0],
                          pat.colorDist[1], pat.colorDist[2]);
            partColorDist(p->deltaColor, pat.deltaColor, pat.deltaColorDist[0],
                          pat.deltaColorDist[1], pat.deltaColorDist[2]);
            p->icolor[3] = 1.0f;
        }
        p->lit = pat.lit;
        p->illum = partRealDist(pat.illum, pat.illumDist);
        p->deltaIllum = partRealDist(pat.deltaIllum, pat.deltaIllumDist);
        p->exponent = partRealDist(pat.exponent, pat.exponentDist);
        p->colorScheme = pat.colorScheme;
        //lifespan
        p->lifespan = partRealDist(pat.lifespan, pat.lifespanDist);
        p->waitspan = partRealDist(pat.waitspan, pat.waitspanDist);
        //line-specifics
        p->length = partRealDist(pat.length, pat.lengthDist);
        p->deltaLength = partRealDist(pat.deltaLength, pat.deltaLengthDist);

        p->bias[0] = partRealDist(pat.rbias, pat.rbiasDist);
        p->bias[1] = partRealDist(pat.gbias, pat.gbiasDist);
        p->bias[2] = partRealDist(pat.bbias, pat.bbiasDist);

        //mesh specifics
        p->mesh = pat.mesh;
        p->tumble[0] = partRealDist(pat.tumble[0], pat.tumbleDist[0]);
        p->tumble[1] = partRealDist(pat.tumble[1], pat.tumbleDist[1]);
        p->tumble[2] = partRealDist(pat.tumble[2], pat.tumbleDist[2]);
        p->deltaTumble[0] = partRealDist(pat.deltaTumble[0], pat.deltaTumbleDist[0]);
        p->deltaTumble[1] = partRealDist(pat.deltaTumble[1], pat.deltaTumbleDist[1]);
        p->deltaTumble[2] = partRealDist(pat.deltaTumble[2], pat.deltaTumbleDist[2]);
        p->loopCount = (sbyte)pat.loopCount;
#if PART_TEST_MORPH
        if (partTypeof(psys) == PART_MESH)
        {
            p->mstruct = partSetupTestMeshMorphBlock();
        }
        else
#endif
        {
            p->mstruct = pat.mstruct;
        }

        p->meshFrame = pat.startMeshFrame;

        if (bitTest(pat.flags, PART_XYZSCALE))
        {
            bitSet(p->flags, PART_XYZSCALE);
            p->tumble[0] = pat.xScale;
            p->tumble[1] = pat.yScale;
            p->tumble[2] = pat.zScale;
        }
        else
        {
            bitClear(p->flags, PART_XYZSCALE);
        }

        if (isWorldspace)
        {
            vector newVector, worldPosition;
            vector velVec, accelVec;
            vector velLOF, velR;
            vector accelLOF, accelR;
            matrix coordSys;
            //initial setup
            memcpy(&coordSys, pat.mat, 9*sizeof(real32));
            memcpy(&worldPosition, pat.position, sizeof(vector));
            //setup velocity
            velLOF.x = velLOF.y = 0.0f;
            velLOF.z = p->velLOF;
            velR = p->rvec;
            vecMultiplyByScalar(velR, p->velR);
            //setup acceleration
            accelLOF.x = accelLOF.y = 0.0f;
            accelLOF.z = p->deltaVelLOF;
            accelR = p->rvec;
            vecMultiplyByScalar(accelR, p->deltaVelR);
            //get velocity
            vecAdd(velVec, velLOF, velR);
            //get acceleration
            vecAdd(accelVec, accelLOF, accelR);
            //transform position
            matMultiplyMatByVec(&newVector, &coordSys, &p->position);
            vecAddTo(newVector, worldPosition);
            p->position = newVector;
            //transform velocity
            matMultiplyMatByVec(&newVector, &coordSys, &velVec);
            vecAddTo(newVector, pat.worldVel);
            p->wVel = newVector;
            //transform acceleration
            matMultiplyMatByVec(&newVector, &coordSys, &accelVec);
            p->wAccel = newVector;
            //transform rvec
#if 0
            p->rvec = velVec;
            if (p->rvec.x == 0.0f
                && p->rvec.y == 0.0f
                && p->rvec.z == 0.0f)
                p->rvec.z = 1.0f;
            vecNormalize(&p->rvec);
#else
            matMultiplyMatByVec(&newVector, &coordSys, &p->rvec);
#endif
            vecNormalize(&p->rvec);
        }
    }
    partCreateCallback = NULL;
}

psysPtr partCreateSystemWithDelta(particleType t, udword n, udword delta)
{
    real32 d = (real32)delta;
    real32 rn = (real32)n;
    rn = partRealDist(rn, d);
    return partCreateSystem(t, (udword)rn);
}

psysPtr partCreateSphericalSystemWithDelta(particleType t, udword n, udword delta)
{
    real32 d = (real32)delta;
    real32 rn = (real32)n;
    rn = partRealDist(rn, d);
    return partCreateSphericalSystem(t, (udword)rn);
}

psysPtr partCreationHelper(particleType t, udword n, udword dist)
{
    psysPtr p;
    pointSystem *point;
    cubeSystem  *cube;
    lineSystem  *line;
    meshSystem  *mesh;
    billSystem  *bill;
    udword len;
    bool isWorldspace;

    isWorldspace = (bool)bitTest(pat.flags, PART_WORLDSPACE);

    g_Billboarded = FALSE;
    switch (t)
    {
    case PART_BILLBOARD:
        len = sizeof(billSystem) + n*sizeof(particle);
        p = memAlloc(len, "ps(partsys)", Pyrophoric);
        bill = (billSystem*)p;
        bill->t = t;
        bill->n = (uword)n;
        bill->lastUpdated = universe.totaltimeelapsed;
        bill->drag = pat.drag;
        bill->isAlive = TRUE;
        bill->flags = pat.flags;
        bill->ang = pat.ang;
        bill->angDelta = pat.angDelta;
        bill->tex = pat.tex;
        bill->slices = (uword)pat.slices;
        bill->partMat = pat.mat;
        memcpy(bill->uv, pat.uv, 4*sizeof(real32));
        memcpy(&bill->position, pat.position, sizeof(vector));
        g_Billboarded = TRUE;
        partFillGenericParticles(n, p, sizeof(billSystem), dist, isWorldspace);
        break;
    case PART_MESH:
        len = sizeof(meshSystem) + n*sizeof(particle);
        p = memAlloc(len, "ps(partsys)", Pyrophoric);
        mesh = (meshSystem*)p;
        mesh->t = t;
        mesh->n = (uword)n;
        mesh->lastUpdated = universe.totaltimeelapsed;
        mesh->drag = pat.drag;
        mesh->isAlive = TRUE;
        mesh->flags = pat.flags;
        mesh->ang = pat.ang;
        mesh->angDelta = pat.angDelta;
        mesh->tex = pat.tex;
        mesh->partMat = pat.mat;
        memcpy(&mesh->partPos, pat.position, sizeof(vector));
        partFillGenericParticles(n, p, sizeof(meshSystem), dist, isWorldspace);
        break;
    case PART_LINES:
        len = sizeof(lineSystem) + n*sizeof(particle);
        p = memAlloc(len, "ps(partsys)", Pyrophoric);
        line = (lineSystem*)p;
        line->t = t;
        line->n = (uword)n;
        line->lastUpdated = universe.totaltimeelapsed;
        line->drag = pat.drag;
        line->isAlive = TRUE;
        line->flags = pat.flags;
        line->ang = pat.ang;
        line->angDelta = pat.angDelta;
        partFillGenericParticles(n, p, sizeof(lineSystem), dist, isWorldspace);
        break;
    case PART_CUBES:
        len = sizeof(cubeSystem) + n*sizeof(particle);
        p = memAlloc(len, "ps(partsys)", Pyrophoric);
        cube = (cubeSystem*)p;
        cube->t = t;
        cube->n = (uword)n;
        cube->lastUpdated = universe.totaltimeelapsed;
        cube->drag = pat.drag;
        cube->isAlive = TRUE;
        cube->flags = pat.flags;
        cube->ang = pat.ang;
        cube->angDelta = pat.angDelta;
        partFillGenericParticles(n, p, sizeof(cubeSystem), dist, isWorldspace);
        break;
    case PART_POINTS:
        len = sizeof(pointSystem) + n*sizeof(particle);
        p = memAlloc(len, "ps(partsys)", Pyrophoric);
        point = (pointSystem*)p;
        point->t = t;
        point->n = (uword)n;
        point->lastUpdated = universe.totaltimeelapsed;
        point->drag = pat.drag;
        point->isAlive = TRUE;
        point->flags = pat.flags;
        point->ang = pat.ang;
        point->angDelta = pat.angDelta;
        partFillGenericParticles(n, p, sizeof(pointSystem), dist, isWorldspace);
        break;
    default:
#if PART_Verbose > 0
        dbgMessagef("\nunknown particle system type %d", (udword)t);
#endif
        p = NULL;
    }
#if khent
#if PART_Verbose > 1
    dbgMessagef("\nsystem required %dK", len >> 10);
#endif
#endif
    return p;
}

/*-----------------------------------------------------------------------------
    Name        : partCreateSphericalSystem
    Description : creates a particle system with the current state,
                  but in the shape of a sphere
    Inputs      : t - type of particles in the system
                  n - number of particles
    Outputs     :
    Return      : returns the psysPtr, or NULL on error
----------------------------------------------------------------------------*/
psysPtr partCreateSphericalSystem(particleType t, udword n)
{
    return partCreationHelper(t, n, PART_SPHERE);
}

/*-----------------------------------------------------------------------------
    Name        : partCreateSystem
    Description : creates a particle system with the current state
    Inputs      : t - type of particles in the system
                  n - number of particles
    Outputs     :
    Return      : returns the psysPtr or NULL on error
----------------------------------------------------------------------------*/
psysPtr partCreateSystem(particleType t, udword n)
{
    return partCreationHelper(t, n, PART_CYLINDER);
}

/*-----------------------------------------------------------------------------
    Name        : partHeaderSize
    Description : returns the size of the header of a system of particles
    Inputs      : psys - the particle system
    Outputs     :
    Return      : returns the header size or 0 if of unrecognized type
----------------------------------------------------------------------------*/
udword partHeaderSize(psysPtr psys)
{
    pointSystem *p = (pointSystem*)psys;
    switch (p->t)
    {
    case PART_POINTS:
        return sizeof(pointSystem);
    case PART_CUBES:
        return sizeof(cubeSystem);
    case PART_LINES:
        return sizeof(lineSystem);
    case PART_MESH:
        return sizeof(meshSystem);
    case PART_BILLBOARD:
        return sizeof(billSystem);
    default:
        return 0;
    }
}

/*============================
  modifiers
 ============================*/
void partSetSystemAlphaFlag(psysPtr psys, real32 a)
{
    pointSystem* sys = (pointSystem*)psys;

    if (a > 0.996f)
    {
        bitClear(sys->flags, PART_ALPHA);
    }
    else
    {
        bitSet(sys->flags, PART_ALPHA);
    }
}

void partModifyTumble(psysPtr psys, vector* t)
{
    if (partTypeof(psys) == PART_MESH)
    {
        udword i;
        particle *p = (particle*)(psys + partHeaderSize(psys));

        for (i = 0; i < partNumberof(psys); i++, p++)
        {
            memcpy(p->tumble, t, sizeof(vector));
        }
    }
    else
    {
        dbgMessage("\nparticle system is not of type PART_MESH");
    }
}

void partModifyDeltaTumble(psysPtr psys, vector* dt)
{
    if (partTypeof(psys) == PART_MESH)
    {
        udword i;
        particle *p = (particle*)(psys + partHeaderSize(psys));

        for (i = 0; i < partNumberof(psys); i++, p++)
        {
            memcpy(p->deltaTumble, dt, sizeof(vector));
        }
    }
    else
    {
        dbgMessage("\nparticle system is not of type PART_MESH");
    }
}

void partModifyExponent(psysPtr psys, real32 exponent)
{
    udword i;
    udword fSpec = (exponent == 0.0f) ? 0 : PART_SPECULAR;

    particle* p = (particle*)(psys + partHeaderSize(psys));
    for (i = 0; i < partNumberof(psys); i++, p++)
    {
        p->exponent = exponent;
        p->flags = ((p->flags & (~PART_SPECULAR)) | fSpec);
    }
}

void partModifyDrag(psysPtr psys, real32 d)
{
    pointSystem *p = (pointSystem*)psys;
    if (fabs((real64)d) < 0.0001)
        d = 0.0f;
    if (d < 0.0f || d > 1.0f)
        dbgMessagef("\ndrag coefficient out of range (%f)", d);
    else
        p->drag = d;
}

void partModifyMesh(psysPtr psys, meshdata *mesh)
{
    udword i;
    particle *p = (particle*)(psys + partHeaderSize(psys));

    for (i = 0; i < partNumberof(psys); i++, p++)
    {
        p->mesh = mesh;
    }
}

void partModifyLighting(psysPtr psys, bool lit)
{
    udword i;
    particle *p = (particle*)(psys + partHeaderSize(psys));
    for (i = 0; i < partNumberof(psys); i++, p++)
        p->lit = (bool8)lit;
}

void partModifyIllum(psysPtr psys, real32 illum)
{
    udword i;
    particle *p = (particle*)(psys + partHeaderSize(psys));
    for (i = 0; i < partNumberof(psys); i++, p++)
        p->illum = illum;
}

void partModifyDeltaIllum(psysPtr psys, real32 deltaIllum)
{
    udword i;
    particle *p = (particle*)(psys + partHeaderSize(psys));
    for (i = 0; i < partNumberof(psys); i++, p++)
        p->deltaIllum = deltaIllum;
}

void partModifyTexture(psysPtr psys, trhandle tex,
                       udword u0, udword v0, udword u1, udword v1)
{
    billSystem *m = (billSystem*)psys;
    udword i;
    particle *p = (particle*)(psys + partHeaderSize(psys));
    if (m->t == PART_BILLBOARD)
    {
        m->tex = tex;
        m->uv[0][0] = u0;
        m->uv[0][1] = v0;
        m->uv[1][0] = u1;
        m->uv[1][1] = v1;
        for (i = 0; i < partNumberof(psys); i++, p++)
            p->tex = tex;
    }
    else if (m->t == PART_MESH)
    {
        m->tex = tex;
        for (i = 0; i < partNumberof(psys); i++, p++)
            p->tex = tex;
    }
    else
    {
        dbgMessage("\nparticle system is not of type PART_BILLBOARD");
    }
}

void partModifyScale(psysPtr psys, real32 s)
{
    udword i;
    particle *p = (particle*)(psys + partHeaderSize(psys));
    for (i = 0; i < partNumberof(psys); i++, p++)
        p->scale = s;
}

void partModifyDeltaScale(psysPtr psys, real32 d)
{
    udword i;
    particle *p = (particle*)(psys + partHeaderSize(psys));
    for (i = 0; i < partNumberof(psys); i++, p++)
        p->deltaScale = d;
}

void partModifyDeltaLength(psysPtr psys, real32 d)
{
    udword i;
    particle *p = (particle*)(psys + partHeaderSize(psys));
    for (i = 0; i < partNumberof(psys); i++, p++)
        p->deltaLength = d;
}

void partModifyLifespan(psysPtr psys, real32 l)
{
    udword i;
    particle *p = (particle*)(psys + partHeaderSize(psys));
    for (i = 0; i < partNumberof(psys); i++, p++)
        p->lifespan = l;
}

void partModifyLength(psysPtr psys, real32 l)
{
    udword i;
    particle *p = (particle*)(psys + partHeaderSize(psys));
    for (i = 0; i < partNumberof(psys); i++, p++)
        p->length = l;
}

void partModifyVelLOF(psysPtr psys, real32 v)
{
    udword i;
    particle *p = (particle*)(psys + partHeaderSize(psys));
    for (i = 0; i < partNumberof(psys); i++, p++)
        p->velLOF = v;
}

void partModifyDeltaVelLOF(psysPtr psys, real32 d)
{
    udword i;
    particle *p = (particle*)(psys + partHeaderSize(psys));
    for (i = 0; i < partNumberof(psys); i++, p++)
        p->deltaVelLOF = d;
}

void partModifyVelR(psysPtr psys, real32 v)
{
    udword i;
    particle *p = (particle*)(psys + partHeaderSize(psys));
    for (i = 0; i < partNumberof(psys); i++, p++)
        p->velR = v;
}

void partModifyDeltaVelR(psysPtr psys, real32 d)
{
    udword i;
    particle *p = (particle*)(psys + partHeaderSize(psys));
    for (i = 0; i < partNumberof(psys); i++, p++)
        p->deltaVelR = d;
}

void partModifyColor(psysPtr psys, real32 r, real32 g, real32 b, real32 a)
{
    udword i;
    particle *p = (particle*)(psys + partHeaderSize(psys));

    partSetSystemAlphaFlag(psys, a);

    for (i = 0; i < partNumberof(psys); i++, p++)
    {
        p->icolor[0] = r;
        p->icolor[1] = g;
        p->icolor[2] = b;
        p->icolor[3] = a;
    }
}

void partModifyColorC(psysPtr psys, color c)
{
    udword i;
    real32 r, g, b, a;
    particle *p = (particle*)(psys + partHeaderSize(psys));

    r = colUbyteToReal(colRed(c));
    g = colUbyteToReal(colGreen(c));
    b = colUbyteToReal(colBlue(c));
    a = colUbyteToReal(colAlpha(c));

    partSetSystemAlphaFlag(psys, a);

    for (i = 0; i < partNumberof(psys); i++, p++)
    {
        p->icolor[0] = r;
        p->icolor[1] = g;
        p->icolor[2] = b;
        p->icolor[3] = a;
    }
}

void partModifyDeltaColorC(psysPtr psys, color c)
{
    udword i;
    real32 r, g, b, a;
    particle *p = (particle*)(psys + partHeaderSize(psys));

    r = colUbyteToReal(colRed(c));
    g = colUbyteToReal(colGreen(c));
    b = colUbyteToReal(colBlue(c));
    a = colUbyteToReal(colAlpha(c));

    for (i = 0; i < partNumberof(psys); i++, p++)
    {
        p->deltaColor[0] = r;
        p->deltaColor[1] = g;
        p->deltaColor[2] = b;
        p->deltaColor[3] = a;
    }
}

void partModifyDeltaColor(psysPtr psys, real32 dr, real32 dg,
                          real32 db, real32 da)
{
    udword i;
    particle *p = (particle*)(psys + partHeaderSize(psys));

    for (i = 0; i < partNumberof(psys); i++, p++)
    {
        p->deltaColor[0] = dr;
        p->deltaColor[1] = dg;
        p->deltaColor[2] = db;
        p->deltaColor[3] = da;
    }
}

void partModifyAnimation(psysPtr psys, void* tstruct)
{
    if (partTypeof(psys) == PART_BILLBOARD ||
        partTypeof(psys) == PART_MESH)
    {
        udword i;
        particle* p = (particle*)(psys + partHeaderSize(psys));
        for (i = 0; i < partNumberof(psys); i++, p++)
        {
            p->tstruct = tstruct;
        }
    }
    else
    {
        dbgMessage("\npartModifyAnimation needs a PART_BILLBOARD or PART_MESH");
    }
}

void partModifyMorph(psysPtr psys, void* mstruct)
{
    if (partTypeof(psys) == PART_MESH)
    {
        udword i;
        particle* p = (particle*)(psys + partHeaderSize(psys));
        for (i = 0; i < partNumberof(psys); i++, p++)
        {
            p->mstruct = mstruct;
        }
    }
    else
    {
        dbgMessage("\npartModifyMorph needs a PART_MESH");
    }
}

void partModifyFramerate(psysPtr psys, real32 rate)
{
    if (partTypeof(psys) == PART_BILLBOARD)
    {
        udword i;
        particle* p = (particle*)(psys + partHeaderSize(psys));
        for (i = 0; i < partNumberof(psys); i++, p++)
        {
            p->rate = rate;
        }
    }
    else
    {
        dbgMessage("\npartModifyFramerate needs a PART_BILLBOARD");
    }
}

void partModifyMorphFramerate(psysPtr psys, real32 meshRate)
{
    if (partTypeof(psys) == PART_MESH)
    {
        udword i;
        particle* p = (particle*)(psys + partHeaderSize(psys));
        for (i = 0; i < partNumberof(psys); i++, p++)
        {
            p->meshRate = meshRate;
        }
    }
    else
    {
        dbgMessage("\npartModifyMorphFramerate needs a PART_MESH");
    }
}

void partModifyBillPosition(psysPtr psys, vector* pos)
{
    if (partTypeof(psys) == PART_BILLBOARD)
    {
        billSystem* bpart = (billSystem*)psys;
        bpart->position = *pos;
    }
    else
    {
        dbgMessage("\npartModifyBillPosition needs a PART_BILLBOARD");
    }
}

void partModifyColorBias(
    psysPtr psys, real32 rb, real32 gb, real32 bb)
{
    udword i;
    particle* p = (particle*)(psys + partHeaderSize(psys));
    for (i = 0; i < partNumberof(psys); i++, p++)
    {
        p->bias[0] = rb;
        p->bias[1] = gb;
        p->bias[2] = bb;
    }
}

void partModifyAddColor(psysPtr psys, color c)
{
    udword i;
    real32 r = colUbyteToReal(colRed(c));
    real32 g = colUbyteToReal(colGreen(c));
    real32 b = colUbyteToReal(colBlue(c));
    particle* p = (particle*)(psys + partHeaderSize(psys));
    for (i = 0; i < partNumberof(psys); i++, p++)
    {
        p->bias[0] = r;
        p->bias[1] = g;
        p->bias[2] = b;
    }
}

void partModifyLoopFlag(psysPtr psys, bool8 willLoop)
{
    if (partTypeof(psys) == PART_BILLBOARD)
    {
        udword i;
        particle* p = (particle*)(psys + partHeaderSize(psys));
        for (i = 0; i < partNumberof(psys); i++, p++)
        {
            p->loop = willLoop;
        }
    }
    else
    {
        dbgMessage("\npartModifyLoopFlag needs a PART_BILLBOARD");
    }
}

void partModifySpecular(psysPtr psys, bool spec)
{
    udword i;
    particle* p;

    p = (particle*)(psys + partHeaderSize(psys));

    if (spec)
    {
        for (i = 0; i < partNumberof(psys); i++, p++)
        {
            bitSet(p->flags, PART_SPECULAR);
        }
    }
    else
    {
        for (i = 0; i < partNumberof(psys); i++, p++)
        {
            bitClear(p->flags, PART_SPECULAR);
        }
    }
}

void partModifyStipple(psysPtr psys, bool stip)
{
    udword i;
    particle* p;

    p = (particle*)(psys + partHeaderSize(psys));

    if (stip)
    {
        for (i = 0; i < partNumberof(psys); i++, p++)
        {
            bitSet(p->flags, PART_STIPPLE);
        }
    }
    else
    {
        for (i = 0; i < partNumberof(psys); i++, p++)
        {
            bitClear(p->flags, PART_STIPPLE);
        }
    }
}

void partModifyNoDepthWrite(psysPtr psys, bool noWrite)
{
    udword i;
    particle* p;

    p = (particle*)(psys + partHeaderSize(psys));

    if (noWrite)
    {
        for (i = 0; i < partNumberof(psys); i++, p++)
        {
            bitSet(p->flags, PART_NODEPTHWRITE);
        }
    }
    else
    {
        for (i = 0; i < partNumberof(psys); i++, p++)
        {
            bitClear(p->flags, PART_NODEPTHWRITE);
        }
    }
}

void partModifyAdditiveBlends(psysPtr psys, bool add)
{
    udword i;
    particle* p;

    p = (particle*)(psys + partHeaderSize(psys));

    if (add)
    {
        for (i = 0; i < partNumberof(psys); i++, p++)
        {
            bitSet(p->flags, PART_ADDITIVE);
        }
    }
    else
    {
        for (i = 0; i < partNumberof(psys); i++, p++)
        {
            bitClear(p->flags, PART_ADDITIVE);
        }
    }
}

void partModifyPseudoBillboard(psysPtr psys, bool bill)
{
    udword i;
    particle* p;

    p = (particle*)(psys + partHeaderSize(psys));

    if (bill)
    {
        for (i = 0; i < partNumberof(psys); i++, p++)
        {
            bitSet(p->flags, PART_PSEUDOBILLBOARD);
        }
    }
    else
    {
        for (i = 0; i < partNumberof(psys); i++, p++)
        {
            bitClear(p->flags, PART_PSEUDOBILLBOARD);
        }
    }
}

void partModifyTrueBillboard(psysPtr psys, bool bill)
{
    udword i;
    particle* p;

    p = (particle*)(psys + partHeaderSize(psys));

    if (bill)
    {
        for (i = 0; i < partNumberof(psys); i++, p++)
        {
            bitSet(p->flags, PART_TRUEBILLBOARD);
        }
    }
    else
    {
        for (i = 0; i < partNumberof(psys); i++, p++)
        {
            bitClear(p->flags, PART_TRUEBILLBOARD);
        }
    }
}

void partModifyColorScheme(psysPtr psys, sdword colorScheme)
{
    udword i;
    particle* p;

    p = (particle*)(psys + partHeaderSize(psys));
    for (i = 0; i < partNumberof(psys); i++, p++)
    {
        p->colorScheme = colorScheme;
    }
}

//0 - normal, 1 - additive, 2 - stipple
void partModifyAlphaMode(psysPtr psys, udword mode)
{
    switch (mode)
    {
    case 1:
        partModifyAdditiveBlends(psys, TRUE);
        partModifyStipple(psys, FALSE);
        break;
    case 2:
        partModifyAdditiveBlends(psys, FALSE);
        partModifyStipple(psys, TRUE);
        break;
    default:
        partModifyAdditiveBlends(psys, FALSE);
        partModifyStipple(psys, FALSE);
    }
}


/*============================
  callbacks
 ============================*/
void partSetIsWorldspace(bool8 isWorldspace)
{
    if (isWorldspace)
    {
        bitSet(pat.flags, PART_WORLDSPACE);
    }
    else
    {
        bitClear(pat.flags, PART_WORLDSPACE);
    }
}

void partSetVelocityInWorldSpace(bool8 isLocalSpace)
{
    if (isLocalSpace)
    {
        bitSet(pat.flags, PART_WORLDSPACEVELOCITY);
    }
    else
    {
        bitClear(pat.flags, PART_WORLDSPACEVELOCITY);
    }
}

void partSetExponent(real32 exponent)
{
    pat.exponent = exponent;
}

void patSetExponentDist(real32 exponentDist)
{
    pat.exponentDist = exponentDist;
}

void partSetWorldVel(vector* worldVel)
{
    memcpy(&pat.worldVel, worldVel, sizeof(vector));
    partEffectVelocity = worldVel;
}

void partSetAnimation(void* tstruct)
{
    pat.tstruct = tstruct;
}

void partSetMorph(void* mstruct)
{
    pat.mstruct = mstruct;
}

void partSetMorphFramerate(real32 meshRate)
{
    pat.meshRate = meshRate;
}

void partSetMorphLoopCount(sdword loopCount)
{
    pat.loopCount = loopCount;
}

void partSetMeshStartFrame(real32 frame)
{
    pat.startMeshFrame = frame;
}

void partSetFramerate(real32 rate)
{
    pat.rate = rate;
}

void partSetLoopFlag(bool8 willLoop)
{
    pat.loop = willLoop;
}

void partSetStartFrame(udword frame)
{
    pat.startFrame = (uword)frame;
}

void partSetCoordSys(matrix* mat)
{
    pat.mat = mat;
}

void partSetTumble(real32 x, real32 y, real32 z)
{
    pat.tumble[0] = x;
    pat.tumble[1] = y;
    pat.tumble[2] = z;
}

void partSetTumbleDist(real32 dx, real32 dy, real32 dz)
{
    pat.tumbleDist[0] = dx;
    pat.tumbleDist[1] = dy;
    pat.tumbleDist[2] = dz;
}

void partSetDeltaTumble(real32 x, real32 y, real32 z)
{
    pat.deltaTumble[0] = x;
    pat.deltaTumble[1] = y;
    pat.deltaTumble[2] = z;
}

void partSetDeltaTumbleDist(real32 dx, real32 dy, real32 dz)
{
    pat.deltaTumbleDist[0] = dx;
    pat.deltaTumbleDist[1] = dy;
    pat.deltaTumbleDist[2] = dz;
}

void partSetSlices(udword s)
{
    if (s < 2 || s > 64)
    {
        dbgMessage("\ninsane number of slices, using default of 8");
        pat.slices = 8;
    }
    else
        pat.slices = s;
}

void partSetPosition(vector *pos)
{
    memcpy(pat.position, pos, sizeof(vector));
}

void partSetMeshdata(meshdata *m)
{
    pat.mesh = m;
}

void partSetTrHandle(trhandle t, udword u0, udword v0, udword u1, udword v1)
{
    pat.tex = t;
    pat.uv[0][0] = u0;
    pat.uv[0][1] = v0;
    pat.uv[1][0] = u1;
    pat.uv[1][1] = v1;
}

void partSetDrag(real32 n)
{
    pat.drag = n;
}

void partSetXYZScale(real32 x, real32 y, real32 z)
{
    bitSet(pat.flags, PART_XYZSCALE);
    pat.xScale = x;
    pat.yScale = y;
    pat.zScale = z;
}

void partSetScale(real32 n)
{
    pat.scale = n;
}

void partSetScaleDist(real32 d)
{
    pat.scaleDist = d;
    if (d == 0.0f)
        bitSet(pat.flags, PART_MONOSCALE);
    else
        bitClear(pat.flags, PART_MONOSCALE);
}

void partSetDeltaScale(real32 n)
{
    pat.deltaScale = n;
}

void partSetDeltaScaleDist(real32 d)
{
    pat.deltaScaleDist = d;
}

void partSetOffsetLOF(real32 n)
{
    pat.offsetLOF = n;
}

void partSetOffsetR(real32 n)
{
    pat.offsetR = n;
}

void partSetOffsetTheta(real32 n)
{
    pat.offsetTheta = n;
}

void partSetOffsetArray(vector *offsets)
{
    pat.offsetArray = offsets;
}

void partSetDeltaLOF(real32 n)
{
    pat.deltaLOF = n;
}

void partSetDeltaLOFDist(real32 n)
{
    pat.deltaLOFDist = n;
}

void partSetDeltaR(real32 r, real32 t)
{
    pat.deltaR[0] = r;
    pat.deltaR[1] = t;
}

void partSetDeltaRDist(real32 r, real32 t)
{
    pat.deltaRDist[0] = r;
    pat.deltaRDist[1] = (t == 0.0f) ? 6.28f : t;
}

void partSetVelLOF(real32 v)
{
    pat.velLOF = v;
}

void partSetVelLOFDist(real32 d)
{
    pat.velLOFDist = d;
}

void partSetVelR(real32 v)
{
    pat.velR = v;
}

void partSetVelRDist(real32 d)
{
    pat.velRDist = d;
}

void partSetDeltaVelLOF(real32 d)
{
    pat.deltaVelLOF = d;
}

void partSetDeltaVelLOFDist(real32 d)
{
    pat.deltaVelLOFDist = d;
}

void partSetDeltaVelR(real32 v)
{
    pat.deltaVelR = v;
}

void partSetDeltaVelRDist(real32 v)
{
    pat.deltaVelRDist = v;
}

void partSetAng(real32 a)
{
    pat.ang = a;
}

void partSetAngDist(real32 d)
{
    pat.angDist = d;
}

void partSetAngDelta(real32 d)
{
    pat.angDelta = d;
}

void partSetAngDeltaDist(real32 d)
{
    pat.angDeltaDist = d;
}

void partSetColor(real32 r, real32 g, real32 b)
{
    pat.icolor[0] = r;
    pat.icolor[1] = g;
    pat.icolor[2] = b;
    bitClear(pat.flags, PART_ALPHA);
}

void partSetColorDist(real32 r, real32 g, real32 b)
{
    pat.colorDist[0] = r;
    pat.colorDist[1] = g;
    pat.colorDist[2] = b;
    pat.colorDist[3] = 0.0f;
    bitClear(pat.flags, PART_ALPHA);
}

void partSetColorA(real32 r, real32 g, real32 b, real32 a)
{
    pat.icolor[0] = r;
    pat.icolor[1] = g;
    pat.icolor[2] = b;
    pat.icolor[3] = a;
    bitSet(pat.flags, PART_ALPHA);
}

void partSetColorADist(real32 r, real32 g, real32 b, real32 a)
{
    pat.colorDist[0] = r;
    pat.colorDist[1] = g;
    pat.colorDist[2] = b;
    pat.colorDist[3] = a;
    bitSet(pat.flags, PART_ALPHA);
}

void partSetDeltaColor(real32 r, real32 g, real32 b)
{
    pat.deltaColor[0] = r;
    pat.deltaColor[1] = g;
    pat.deltaColor[2] = b;
    pat.deltaColor[3] = 1.0f;
    bitClear(pat.flags, PART_ALPHA);
}

void partSetDeltaColorDist(real32 r, real32 g, real32 b)
{
    pat.deltaColorDist[0] = r;
    pat.deltaColorDist[1] = g;
    pat.deltaColorDist[2] = b;
    pat.deltaColorDist[3] = 0.0f;
    bitClear(pat.flags, PART_ALPHA);
}

void partSetDeltaColorA(real32 r, real32 g, real32 b, real32 a)
{
    pat.deltaColor[0] = r;
    pat.deltaColor[1] = g;
    pat.deltaColor[2] = b;
    pat.deltaColor[3] = a;
    bitSet(pat.flags, PART_ALPHA);
}

void partSetDeltaColorADist(real32 r, real32 g, real32 b, real32 a)
{
    pat.deltaColorDist[0] = r;
    pat.deltaColorDist[1] = g;
    pat.deltaColorDist[2] = b;
    pat.deltaColorDist[3] = a;
    bitSet(pat.flags, PART_ALPHA);
}

void partSetLighting(bool l)
{
    pat.lit = (bool8)l;
}

void partSetIllum(real32 n)
{
    pat.illum = n;
}

void partSetIllumDist(real32 n)
{
    pat.illumDist = n;
}

void partSetDeltaIllum(real32 n)
{
    pat.deltaIllum = n;
}

void partSetDeltaIllumDist(real32 n)
{
    pat.deltaIllumDist = n;
}

void partSetLifespan(real32 t)
{
    pat.lifespan = t;
}

void partSetLifespanDist(real32 d)
{
    pat.lifespanDist = d;
}

void partSetWaitspan(real32 t)
{
    pat.waitspan = t;
}

void partSetWaitspanDist(real32 d)
{
    pat.waitspanDist = d;
}

void partSetLength(real32 l)
{
    pat.length = l;
}

void partSetLengthDist(real32 d)
{
    pat.lengthDist = d;
}

void partSetDeltaLength(real32 d)
{
    pat.deltaLength = d;
}

void partSetDeltaLengthDist(real32 d)
{
    pat.deltaLengthDist = d;
}

void partSetColorBias(real32 rb, real32 gb, real32 bb)
{
    pat.rbias = rb;
    pat.gbias = gb;
    pat.bbias = bb;
}

void partSetColorBiasDist(real32 drb, real32 dgb, real32 dbb)
{
    pat.rbiasDist = drb;
    pat.gbiasDist = dgb;
    pat.bbiasDist = dbb;
}

void partSetSpecular(bool spec)
{
    pat.specularOnly = spec;
}

void partSetStipple(bool stip)
{
    pat.stippleAlpha = stip;
}

void partSetNoDepthWrite(bool noWrite)
{
    pat.noDepthWrite = noWrite;
}

void partSetAdditiveBlends(bool add)
{
    pat.additiveBlends = add;
}

void partSetPseudoBillboard(bool bill)
{
    pat.pseudoBillboard = bill;
}

void partSetTrueBillboard(bool bill)
{
    pat.trueBillboard = bill;
}

void partSetColorScheme(sdword colorScheme)
{
    pat.colorScheme = colorScheme;
}

//0 - normal, 1 - additive, 2 - stipple
void partSetAlphaMode(udword mode)
{
    switch (mode)
    {
    case 1:
        partSetAdditiveBlends(TRUE);
        partSetStipple(FALSE);
        break;
    case 2:
        partSetAdditiveBlends(FALSE);
        partSetStipple(TRUE);
        break;
    default:
        partSetAdditiveBlends(FALSE);
        partSetStipple(FALSE);
    }
}

void partCreateCallbackSet(void (*function)(sdword userValue, ubyte *userData), sdword userValue, ubyte *userData)
{
    partCreateCallback = function;
    partCreateUserValue = userValue;
    partCreateUserData = userData;
}

