Particles Tutorial

By Alan Baylis 14/08/2002
My Homepage   Message Board   Email





Particles are basically just small billboards, textured quads that always face the camera, but because the particles are generally small and numerous a different way of handling them within the program is needed. The most common method of handling up to thousands of particles is with a particle manager. The particle manager is responsible for maintaining a group of particle systems and the particle systems in turn handle groups of particles. The method is well described in a tutorial by John van der Burg, it is well worth a read and I have used it as the basis for my program. However, I believe my final method is different enough to be considered my own work and therefore you can use my source code as you see fit, just don't be surprised if there's a bug or two in there.

Download the Demo with Source



First we have two structures which are used to set the properties of the particle systems and particles. Not all the properties are used yet, some are there for future use and some may be used by one particle system and not another. The struct called SystemInfo is a member of each particle system class and the struct called ParticleInfo is a member of each particle class.

typedef struct
{
    int Id;
    int numAlive;
    bool Visibility;
    int numParticles;
    ParticleType Type;
    int BlendMode;
    unsigned int TexID;
    GLfloat Color[4];
    VECTOR Pos;
    VECTOR Normal;
    VECTOR InitialVelocity;
} SystemInfo;

typedef struct
{
    bool Alive;
    int Leaf;
    VECTOR Pos;
    VECTOR OldPos;
    VECTOR OrigPos;
    VECTOR Velocity;
    VECTOR Vertex[4];
    GLfloat Color[4];
    GLfloat Energy;
    GLfloat SizeX;
    GLfloat SizeY;
} ParticleInfo;


Next we have the particle class which includes the ParticleInfo struct. The Compare and other methods are for use by the linked list that holds the particles.

class PARTICLE
{
    public:
        PARTICLE(){};
        ~PARTICLE(){};

	    int Compare(const PARTICLE& Particle);
	    int GetMyPosition() const {return linkPosition;}
	    void SetMyPosition(int newPosition) {linkPosition = newPosition;}
	    int linkPosition;

        ParticleInfo PartInfo;
};

int PARTICLE::Compare(const PARTICLE& Particle)
{
    if (linkPosition < Particle.linkPosition)
        return smaller;
    if (linkPosition > Particle.linkPosition)
        return bigger;
    else
        return same;
}


The following ParticleSystem class includes the SystemInfo struct and a linked list of particles called ParticleList. There are also a few methods to initialize, add and remove particles from the system. The virtual methods will provide default behaviour to any particle systems that derive from this base class, but note that I haven't written these methods yet.

class ParticleSystem
{
    public:
      	ParticleSystem(){};
  	    ~ParticleSystem(){};

	    int Compare(const ParticleSystem& ParticleSys);
	    int GetMyPosition() const {return linkPosition;}
	    void SetMyPosition(int newPosition) {linkPosition = newPosition;}
	    int linkPosition;

        LinkedList ParticleList;
        SystemInfo SysInfo;

        int GetNumAlive();
        void SetupParticles();
        PARTICLE* Add();
        void Remove();

        virtual void SetDefaults(PARTICLE* Particle);
        virtual void SetShape(PARTICLE* Particle);
        virtual void Update();
        virtual void Render();
};

int ParticleSystem::Compare(const ParticleSystem& ParticleSys)
{
  if (linkPosition < ParticleSys.linkPosition)
    return smaller;
  if (linkPosition > ParticleSys.linkPosition)
    return bigger;
  else
    return same;
}

void ParticleSystem::SetDefaults(PARTICLE* Particle)
{
   // Add a default method
}

void ParticleSystem::SetShape(PARTICLE* Particle)
{
   // Add a default method
}

void ParticleSystem::Update()
{
   // Add a default method
}

void ParticleSystem::Render(int nodeid)
{
   // Add a default method
}

int ParticleSystem::GetNumAlive()
{
    int numParticles = 0;
    PARTICLE* tempParticle;
    for (int loop = 1; loop <= SysInfo.numParticles; loop++)
    {
        tempParticle = ParticleList.Get(loop);
        if (tempParticle->PartInfo.Alive)
            numParticles++;
    }
    return numParticles;
}

// Add initial particles to the empty list
void ParticleSystem::SetupParticles()
{
    for (int loop = 1; loop <= SysInfo.numParticles; loop++)
    {
        PARTICLE* newParticle = new PARTICLE;
        SetDefaults(newParticle);
        SetShape(newParticle);
        newParticle->linkPosition = loop;
        ParticleList.Insert(newParticle);
    }
}

PARTICLE* ParticleSystem::Add()
{
    PARTICLE* newParticle = new PARTICLE;
    SetDefaults(newParticle);
    SetShape(newParticle);
    newParticle->linkPosition = ++SysInfo.numParticles;
    ParticleList.Insert(newParticle);
    return newParticle;
}

void ParticleSystem::Remove()
{
    if (SysInfo.numParticles > 0)
    {
        ParticleList.Delete(1);
        --SysInfo.numParticles;
    }
}


This then brings us to the particle manager class itself. It includes a linked list of particle systems called SystemList and a counter that keeps track of the number of systems in the list. Again, most of the methods here are for future use and are untested. The main methods here are the Update and Render methods.

class ParticleManager
{
    public:
      	ParticleManager(){numSystems = 0;}
  	    ~ParticleManager(){};

        int numSystems;
        LinkedList SystemList;

        void SetVisibility(int Id, bool State);
        void ToggleVisibility(int Id);
        void SetType(int Id, ParticleType Type);
        void SetBlendMode(int Id, int BlendMode);
        void SetTextureId(int Id, unsigned int TexID);
        void SetId(int Id, int newId);
        void Remove(int Id);
        void RemoveType(ParticleType Type);
        void Update();
        void Render();
        ParticleSystem* Add(ParticleSystem* PartSys);
};

void ParticleManager::Update()
{
    int loop, innerloop;
    ParticleSystem* PartSys;
    PARTICLE* tempParticle;
    for (loop = 1; loop <= numSystems; loop++)
    {
        PartSys = SystemList.Get(loop);

        if (!PartSys->GetNumAlive())
        {
            SystemList.Delete(PartSys->linkPosition);
            numSystems--;
        }
        else
            PartSys->Update();
    }
}

void ParticleManager::Render()
{
    for (int loop = 1; loop <= numSystems; loop++)
    {
        ParticleSystem* PartSys = SystemList.Get(loop);
        PartSys->Render();
    }
}

void ParticleManager::SetId(int Id, int newId)
{
    for (int loop = 1; loop <= numSystems; loop++)
    {
        ParticleSystem* PartSys = SystemList.Get(loop);
        if (Id == PartSys->SysInfo.Id)
            PartSys->SysInfo.Id = newId;
    }
}

void ParticleManager::SetTextureId(int Id, unsigned int TexID)
{
    for (int loop = 1; loop <= numSystems; loop++)
    {
        ParticleSystem* PartSys = SystemList.Get(loop);
        if (Id == PartSys->SysInfo.Id)
            PartSys->SysInfo.TexID = TexID;
    }
}

void ParticleManager::SetBlendMode(int Id, int BlendMode)
{
    for (int loop = 1; loop <= numSystems; loop++)
    {
        ParticleSystem* PartSys = SystemList.Get(loop);
        if (Id == PartSys->SysInfo.Id)
            PartSys->SysInfo.BlendMode = BlendMode;
    }
}

void ParticleManager::SetType(int Id, ParticleType Type)
{
    for (int loop = 1; loop <= numSystems; loop++)
    {
        ParticleSystem* PartSys = SystemList.Get(loop);
        if (Id == PartSys->SysInfo.Id)
            PartSys->SysInfo.Type = Type;
    }
}

void ParticleManager::SetVisibility(int Id, bool State)
{
    for (int loop = 1; loop <= numSystems; loop++)
    {
        ParticleSystem* PartSys = SystemList.Get(loop);
        if (Id == PartSys->SysInfo.Id)
            PartSys->SysInfo.Visibility = State;
    }
}

void ParticleManager::ToggleVisibility(int Id)
{
    for (int loop = 1; loop <= numSystems; loop++)
    {
        ParticleSystem* PartSys = SystemList.Get(loop);
        if (Id == PartSys->SysInfo.Id)
            PartSys->SysInfo.Visibility = !PartSys->SysInfo.Visibility;
    }
}

ParticleSystem* ParticleManager::Add(ParticleSystem* PartSys)
{
    PartSys->SetupParticles();
    PartSys->linkPosition = ++numSystems;
    SystemList.Insert(PartSys);
    return PartSys;
}

void ParticleManager::Remove(int Id)
{
    for (int loop = 1; loop <= numSystems; loop++)
    {
        ParticleSystem* PartSys = SystemList.Get(loop);
        if (Id == PartSys->SysInfo.Id)
        {
            SystemList.Delete(PartSys->linkPosition);
            --numSystems;
        }
    }
}

void ParticleManager::RemoveType(ParticleType Type)
{
    for (int loop = 1; loop <= numSystems; loop++)
    {
        ParticleSystem* PartSys = SystemList.Get(loop);
        if (Type == PartSys->SysInfo.Type)
        {
            SystemList.Delete(PartSys->linkPosition);
            --numSystems;
        }
    }
}


Now that the main classes are defined we can create new types of particle systems that derive from the base class. The example class here is called Spark.


enum ParticleType{spark};   // Particle types

// spark type
class Spark : public ParticleSystem
{
    public:
        Spark(){};
        ~Spark(){};

        // Override base methods
        void Update();
        void Render();
        void SetShape(PARTICLE* Particle);
        void SetDefaults(PARTICLE* Particle);
};

void Spark::Update()
{
    bool CollisionFlag;
    VECTOR VelocityVector, normal, pos, oldpos, temppos;
    PARTICLE* tempParticle;
    // Loop through all the particles of this system
    for (int loop = 1; loop <= SysInfo.numParticles; loop++)
    {
        // Get the particle from the list
        tempParticle = ParticleList.Get(loop);
        // Set the old position
        tempParticle->PartInfo.OldPos = tempParticle->PartInfo.Pos;
        // Apply gravity
        tempParticle->PartInfo.Velocity.x += 0.0;
        tempParticle->PartInfo.Velocity.y += -0.002;
        tempParticle->PartInfo.Velocity.z += 0.0;

        if (tempParticle->PartInfo.Energy > 0.0)
        {
            // Set the alpha channel to the energy value
            tempParticle->PartInfo.Color[3] = tempParticle->PartInfo.Energy;
            // Decrease the energy
            tempParticle->PartInfo.Energy -= 0.002;
            // Update the position
            oldpos = tempParticle->PartInfo.Pos;
            pos = tempParticle->PartInfo.Pos + tempParticle->PartInfo.Velocity;
            temppos = pos;
            // Check for a collision and get the normal of the collision polygon
            CollisionFlag = CheckForParticleCollision(tempParticle, oldpos, &temppos, &normal);
            // If there was a collision then reflect the velocity vector
            if (CollisionFlag)
            {
                VECTOR vectn = normal * (normal.dot(tempParticle->PartInfo.Velocity));
                VECTOR vectt = tempParticle->PartInfo.Velocity - vectn;
                VECTOR vel = (vectt - vectn);
                tempParticle->PartInfo.Pos.x += vel.x;
                tempParticle->PartInfo.Pos.y += vel.y;
                tempParticle->PartInfo.Pos.z += vel.z;
                // Decrease the velocity
                tempParticle->PartInfo.Velocity.x = vel.x / 3.0;
                tempParticle->PartInfo.Velocity.y = vel.y / 3.0;
                tempParticle->PartInfo.Velocity.z = vel.z / 3.0;
                // Reduce the particles energy due to the collision
                tempParticle->PartInfo.Energy -= 0.2;
            }
            else // Update the position as normal
            {
                tempParticle->PartInfo.Pos.x += tempParticle->PartInfo.Velocity.x;
                tempParticle->PartInfo.Pos.y += tempParticle->PartInfo.Velocity.y;
                tempParticle->PartInfo.Pos.z += tempParticle->PartInfo.Velocity.z;
            }
        }
        else // The particles energy has reduced to zero
        {
            tempParticle->PartInfo.Alive = false;
        }
    }
}

void Spark::Render()
{
    MATRIX mat;
    VECTOR up;
    VECTOR right;
    PARTICLE* tempParticle;
    // Get the current modelview matrix
    glGetFloatv(GL_MODELVIEW_MATRIX, mat.Element);
    // Set texture states
    glEnable(GL_BLEND);
    glDepthMask(0);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE);
    glDisable(GL_LIGHTING);
    glBindTexture(GL_TEXTURE_2D, SysInfo.TexID);
    // Loop through all the particles in this system
    for (int loop = 1; loop <= SysInfo.numParticles; loop++)
    {
        // Get the particle from the list
        tempParticle = ParticleList.Get(loop);
        // If the particle is still alive
        if (tempParticle->PartInfo.Alive)
        {
            // Set the shape, should really be performed in Spark::SetShape()
            right.x = mat.Element[0];
            right.y = mat.Element[4];
            right.z = mat.Element[8];
            right.Normalize();
            right.x *= tempParticle->PartInfo.SizeX / 2;
            right.y *= tempParticle->PartInfo.SizeX / 2;
            right.z *= tempParticle->PartInfo.SizeX / 2;

            up.x = mat.Element[1];
            up.y = mat.Element[5];
            up.z = mat.Element[9];
            up.Normalize();
            up.x *= tempParticle->PartInfo.SizeY / 2;
            up.y *= tempParticle->PartInfo.SizeY / 2;
            up.z *= tempParticle->PartInfo.SizeY / 2;
            // Set the color to the default values
            glColor4f(tempParticle->PartInfo.Color[0],
                        tempParticle->PartInfo.Color[1],
                        tempParticle->PartInfo.Color[2],
                        tempParticle->PartInfo.Color[3]);
            // Render the billboarded particle
            glBegin(GL_QUADS);
                glTexCoord2f(0.0f, 0.0f);
                glVertex3f(tempParticle->PartInfo.Pos.x + (-right.x - up.x),
                            tempParticle->PartInfo.Pos.y + (-right.y - up.y),
                            tempParticle->PartInfo.Pos.z + (-right.z - up.z));
                glTexCoord2f(1.0f, 0.0f);
                glVertex3f(tempParticle->PartInfo.Pos.x + (right.x - up.x),
                            tempParticle->PartInfo.Pos.y + (right.y - up.y),
                            tempParticle->PartInfo.Pos.z + (right.z - up.z));
                glTexCoord2f(1.0f, 1.0f);
                glVertex3f(tempParticle->PartInfo.Pos.x + (right.x + up.x),
                            tempParticle->PartInfo.Pos.y + (right.y + up.y),
                            tempParticle->PartInfo.Pos.z + (right.z + up.z));
                glTexCoord2f(0.0f, 1.0f);
                glVertex3f(tempParticle->PartInfo.Pos.x + (up.x - right.x),
                            tempParticle->PartInfo.Pos.y + (up.y - right.y),
                            tempParticle->PartInfo.Pos.z + (up.z - right.z));
            glEnd();
        }
    }
    // Reset texture states
    glEnable(GL_LIGHTING);
    glDepthMask(1);
    glDisable(GL_BLEND);
}

void Spark::SetDefaults(PARTICLE* Particle)
{
    // Set this particle systems properties
    Particle->PartInfo.Alive = true;
    Particle->PartInfo.Pos = SysInfo.Pos;
    Particle->PartInfo.OldPos = SysInfo.Pos;
    Particle->PartInfo.OrigPos = SysInfo.Pos;
    // Reflect the initial velocity
    VECTOR Velocity = SysInfo.InitialVelocity;
    VECTOR vectn = SysInfo.Normal * (SysInfo.Normal.dot(Velocity));
    VECTOR vectt = Velocity - vectn;
    VECTOR vel = (vectt - vectn);
    Velocity.normalize();
    // Create a random spread for each particle
    GLfloat x;
    x = (float)rand()/(float)RAND_MAX;
    Particle->PartInfo.Velocity.x = Velocity.x + (x - 0.5);
    Particle->PartInfo.Velocity.x /= 2.0;
    x = (float)rand()/(float)RAND_MAX;
    Particle->PartInfo.Velocity.y = Velocity.y + (x - 0.5);
    Particle->PartInfo.Velocity.y /= 2.0;
    x = (float)rand()/(float)RAND_MAX;
    Particle->PartInfo.Velocity.z = Velocity.z + (x - 0.5);
    Particle->PartInfo.Velocity.z /= 2.0;
    // Set the color
    Particle->PartInfo.Color[0] = 1.0;
    Particle->PartInfo.Color[1] = 1.0;
    Particle->PartInfo.Color[2] = 1.0;
    Particle->PartInfo.Color[3] = 1.0;
    // Create a random energy value between 0.5 and 1.0
    x = (float)rand()/(float)RAND_MAX;
    Particle->PartInfo.Energy = x + 0.5;
    if (Particle->PartInfo.Energy > 1.0)
        Particle->PartInfo.Energy = 1.0;
    // Set the size of the particle
    Particle->PartInfo.SizeX = 0.8;
    Particle->PartInfo.SizeY = 0.8;
}

void Spark::SetShape(PARTICLE* Particle)
{
    // Unused for now
}


To create a new particle system you make a new Spark object and assign it to a ParticleSystem pointer. Using this pointer, you fill in the SystemInfo struct with the properties you want and then add the object to the particle manager using the Add method. The following function demonstrates one way to do this.


// global instance of particle manager
ParticleManager PManager;

void CreateSparks(VECTOR OldPos, VECTOR Pos, VECTOR Normal)
{

    SystemInfo SI;
    ZeroMemory(&SI, sizeof(SI));
    SI.Visibility = true;
    SI.numParticles = rand()%6 + 2;
    SI.Type = spark;
    SI.TexID = texture[8].TexID;
    SI.Id = 1;
    SI.Pos = Pos;
    SI.Normal = Normal;
    SI.InitialVelocity = Pos - OldPos;

    ParticleSystem* sparky = new Spark;
    sparky->SysInfo = SI;
    PManager.Add(sparky);
}





I hope this helps you to quickly and easily add particles to your programs. The demo is slightly modified to draw the particles in the right order with the world and has two other types of particle systems. If you have any suggestions, problems or error reports then send me an email.

Al.



References:

John van der Burg
http://www.gamasutra.com/features/20000623/vanderburg_01.htm

Nate 'm|d' Miller
http://nate.scuzzy.net/programming/ (Particles of Authority)



(Alan Baylis a.k.a. TheGoodAlien)
My Homepage

Email


Add Me!