These tutorials focus mainly on OpenGL, Win32 programming and the ODE physics engine. OpenGL has moved on to great heights and I don't cover the newest features but cover all of the basic concepts you will need with working example programs.
Working with the Win32 API is a great way to get to the heart of Windows and is just as relevant today as ever before. Whereas ODE has been marginalized as hardware accelerated physics becomes more common.
Games and graphics utilities can be made quickly and easily using game engines like Unity so this and Linux development in general will be the focus of my next tutorials.
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.
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 behavior 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; LinkedListParticleList; 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; LinkedListSystemList; 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.
References: