Lesson 13: Particle Systems

Particle Systems

A particle system is basically just a bunch of tiny particles moving around according to simple rules. Particle systems are useful for simulating certain phenomena, such as sparks, fire, explosions, and snow. With particle systems, it's relatively simple to make various good-looking effects. In our program, we're going to have a fountain, which rotates as it shoots out particles of changing colors. It looks like the picture below:

Particle system program screenshot

Textures With Alpha Channels

In order to make our particle fountain, we're going to use alpha channels. Basically, alpha channels let us give each pixel in an image a particular transparency. Each pixel in the image will have not only a red, green, and blue component, but also an alpha component indicating its opacity.

In our program, we'll use two separate image files to store our texture: one to store the red, green, and blue components of the pixels in the image, and one to store the alpha components of the image, using a grayscale image. The grayscale image is our alpha channel. A white pixel in the image indicates an alpha value of 1, while a black pixel indicates an alpha value of 0.

There are some image formats, such as PNG, which allow you to have an image and an alpha channel in a single image file. But the bitmap file format doesn't really like alpha channels, so we're using two separate images instead.

This is what the two image files look like:

Particle texture

We're going to have to have some new code in order to load in textures with alpha channels, and we'll start with the addAlphaChannel function below.

//Returns an array indicating pixel data for an RGBA image that is the same as
//image, but with an alpha channel indicated by the grayscale image alphaChannel
char* addAlphaChannel(Image* image, Image* alphaChannel) {
    char* pixels = new char[image->width * image->height * 4];
    for(int y = 0; y < image->height; y++) {
        for(int x = 0; x < image->width; x++) {
            for(int j = 0; j < 3; j++) {
                pixels[4 * (y * image->width + x) + j] =
                    image->pixels[3 * (y * image->width + x) + j];
            }
            pixels[4 * (y * image->width + x) + 3] =
                alphaChannel->pixels[3 * (y * image->width + x)];
        }
    }
    
    return pixels;
}

If you recall, when we load in an image, there is an array that stores the red, green, and blue components of each pixel, and the array goes through all of the pixels in the image in a particular order. Since each pixel now needs an alpha component, we'll need a different array for storing the image, one that goes through not only the red, green, and blue components of each pixel, but also the alpha components.

That's where the addAlphaChannel function comes in. It goes through each pixel of the image. It sets the red, green, and blue components indicated by the pixels array to be the same as the red, green, and blue components in image. It sets the alpha component for the pixels array to be the red component of the pixels in the alphaChannel Image object. (It's a grayscale image, so we could have used the green or blue components instead.)

//Makes the image into a texture, using the specified grayscale image as an
//alpha channel and returns the id of the texture
GLuint loadAlphaTexture(Image* image, Image* alphaChannel) {
    char* pixels = addAlphaChannel(image, alphaChannel);
    
    GLuint textureId;
    glGenTextures(1, &textureId);
    glBindTexture(GL_TEXTURE_2D, textureId);
    glTexImage2D(GL_TEXTURE_2D,
                 0,
                 GL_RGBA,
                 image->width, image->height,
                 0,
                 GL_RGBA,
                 GL_UNSIGNED_BYTE,
                 pixels);
    
    delete pixels;
    return textureId;
}

Now we need a new version of the loadTexture function, one that can handle an alpha channel. We'll call this new function "loadAlphaTexture". First, it calls addAlphaChannel to get all of the pixels' color components all set up in an array. The rest of the function is basically the same as the loadTexture function from previous lessons, except that the third and seventh parameters are GL_RGBA rather than GL_RGB, indicating that the pixels array includes an alpha component for each pixel.

void initRendering() {
    //...
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    
    Image* image = loadBMP("circle.bmp");
    Image* alphaChannel = loadBMP("circlealpha.bmp");
    _textureId = loadAlphaTexture(image, alphaChannel);
    delete image;
    delete alphaChannel;
}

In our initRendering function, we need to enable alpha blending and set up the blending function, in order for the transparent pixels to work. (We also need alpha blending for a fade-out effect that I'll mention later.) We also have our call to loadAlphaTexture to load in the texture.

The Particle Fountain

Now, let's get to some code more particular to the particle fountain.

//Represents a single particle.
struct Particle {
    Vec3f pos;
    Vec3f velocity;
    Vec3f color;
    float timeAlive; //The amount of time that this particle has been alive.
    float lifespan;  //The total amount of time that this particle is to live.
};

We use the structure Particle to store information about the individual particles. Each particle has a particular position, velocity, and color, which we represent as three-dimensional vectors. Each particle also has a lifespan indicating how many seconds it will live, and a field timeAlive indicating how long the particle has been alive so far.

We'll use the timeAlive and lifespan fields to add a fade-out effect. Each particle will start out looking like the texture we saw earlier. When a particle is at half its lifespan, we'll multiply each of the alpha values of the pixels of the texture by 0.5. When a particle is almost dead, we'll multiply the alpha values of the pixels of the texture by almost 0. This will have the effect of making each particle appear to fade out over its lifespan.

//Rotates the vector by the indicated number of degrees about the specified axis
Vec3f rotate(Vec3f v, Vec3f axis, float degrees) {
    axis = axis.normalize();
    float radians = degrees * PI / 180;
    float s = sin(radians);
    float c = cos(radians);
    return v * c + axis * axis.dot(v) * (1 - c) + v.cross(axis) * s;
}

//Returns the position of the particle, after rotating the camera.
Vec3f adjParticlePos(Vec3f pos) {
    return rotate(pos, Vec3f(1, 0, 0), -30);
}

We have a function that rotates a vector a certain number of degrees about a particular axis. We'll need this function because we want the camera to be rotated 30 degrees about the x axis. We need to know the exact position of each particle, rather than letting glRotatef figure it out, because we have to sort the particles from back to front, as we always have to do when using alpha blending. So we'll use adjParticlePos to tell us the position of each particle, adjusted based on the camera angle.

//Returns whether particle1 is in back of particle2.
bool compareParticles(Particle* particle1, Particle* particle2) {
    return adjParticlePos(particle1->pos)[2] <
        adjParticlePos(particle2->pos)[2];
}

The compareParticles function is used to sort the particles from back to front. It returns whether the adjusted position of particle1 is in back of the adjusted position of particle2.

const float GRAVITY = 3.0f;
const int NUM_PARTICLES = 1000;
//The interval of time, in seconds, by which the particle engine periodically
//steps.
const float STEP_TIME = 0.01f;
//The length of the sides of the quadrilateral drawn for each particle.
const float PARTICLE_SIZE = 0.05f;

Next, we have a few constants. We have the gravity that will be applied to each of the particles. We have a constant indicating the number of particles. Each time a particle dies, we'll create a new one in its place, so the number of particles will not change.

To animate the particle fountain, we'll use a step method that advances the fountain by a particular amount of time. This amount of time is STEP_TIME seconds.

Each particle is rendered as a textured square that is parallel to the camera. PARTICLE_SIZE indicates the length of each side of the squares.

class ParticleEngine {
    private:
        GLuint textureId;
        Particle particles[NUM_PARTICLES];
        //The amount of time until the next call to step().
        float timeUntilNextStep;

Here, we have our ParticleEngine class, which stores all of the information regarding the particle fountain and the particles it contains. It has the id of the texture. It has an array storing each of the particles. It has a variable called "timeUntilNextStep" which indicates the amount of time until we have to call step() again.

        //The color of particles that the fountain is currently shooting.  0
        //indicates red, and when it reaches 1, it starts over at red again.  It
        //always lies between 0 and 1.
        float colorTime;

We have a colorTime field, which is used to change the colors of the particles over time. Initially, colorTime is 0, indicating a color of red. It constantly increases. When it reaches one third, the current color is green, and when it's at two thirds, the current color is blue. When it reaches 1, it jumps back to 0, and we start over at red. colorTime is always between 0 and 1.

        //The angle at which the fountain is shooting particles, in radians.
        float angle;

The angle field indicates the angle at which particles are being shot out, in radians. We increase it over time so that the fountain continuously rotates.

        //Returns the current color of particles produced by the fountain.
        Vec3f curColor() {
            Vec3f color;
            if (colorTime < 0.166667f) {
                color = Vec3f(1.0f, colorTime * 6, 0.0f);
            }
            else if (colorTime < 0.333333f) {
                color = Vec3f((0.333333f - colorTime) * 6, 1.0f, 0.0f);
            }
            else if (colorTime < 0.5f) {
                color = Vec3f(0.0f, 1.0f, (colorTime - 0.333333f) * 6);
            }
            else if (colorTime < 0.666667f) {
                color = Vec3f(0.0f, (0.666667f - colorTime) * 6, 1.0f);
            }
            else if (colorTime < 0.833333f) {
                color = Vec3f((colorTime - 0.666667f) * 6, 0.0f, 1.0f);
            }
            else {
                color = Vec3f(1.0f, 0.0f, (1.0f - colorTime) * 6);
            }

We have a function curColor which indicates the color that a particle would be if it were shot out right now. If you look at the code, you'll notice that it ranges from red at colorTime == 0 to green at colorTime == 1.0 / 3.0f to blue at colorTime == 2.0f / 3.0f back to red at colorTime == 1.

            //Make sure each of the color's components range from 0 to 1
            for(int i = 0; i < 3; i++) {
                if (color[i] < 0) {
                    color[i] = 0;
                }
                else if (color[i] > 1) {
                    color[i] = 1;
                }
            }
            
            return color;
        }

Now we make sure that each component of the color ranges from 0 to 1. This code corrects for rounding errors that might make some component of the color less than 0 or greater than 1.

        //Returns the average velocity of particles produced by the fountain.
        Vec3f curVelocity() {
            return Vec3f(2 * cos(angle), 2.0f, 2 * sin(angle));
        }

The curVelocity function indicates the average velocity of any particles that would be shot out now. The starting velocity of a newly created particle is a little random, to create a nice dispersal of the particles, but is centered around the return value of curVelocity().

        //Alters p to be a particle newly produced by the fountain.
        void createParticle(Particle* p) {
            p->pos = Vec3f(0, 0, 0);
            p->velocity = curVelocity() + Vec3f(0.5f * randomFloat() - 0.25f,
                                                0.5f * randomFloat() - 0.25f,
                                                0.5f * randomFloat() - 0.25f);
            p->color = curColor();
            p->timeAlive = 0;
            p->lifespan = randomFloat() + 1;
        }

The createParticle function takes a dead particle p and alters its fields so that it is a newly created one. The position is set to the origin. The initial velocity is curVelocity() plus a random vector. The color is set to be curColor(). The timeAlive field is set to 0, as the particle has not been alive for any time. The particle is given a random lifespan of between 1 and 2 seconds.

        //Advances the particle fountain by STEP_TIME seconds.
        void step() {
            colorTime += STEP_TIME / 10;
            while (colorTime >= 1) {
                colorTime -= 1;
            }
            
            angle += 0.5f * STEP_TIME;
            while (angle > 2 * PI) {
                angle -= 2 * PI;
            }

Here's our step method, which advances the state of the fountain by STEP_TIME seconds. First, we increase the colorTime and angle fields by amounts proportional to STEP_TIME. We make sure to keep colorTime between 0 and 1 and to keep angle between 0 and 2 * PI.

            for(int i = 0; i < NUM_PARTICLES; i++) {
                Particle* p = particles + i;
                
                p->pos += p->velocity * STEP_TIME;
                p->velocity += Vec3f(0.0f, -GRAVITY * STEP_TIME, 0.0f);
                p->timeAlive += STEP_TIME;
                if (p->timeAlive > p->lifespan) {
                    createParticle(p);
                }
            }

Now, we update each of the particles. We add STEP_TIME times the velocity to each particle's position vector. We decrease the y components of the particles' velocities by GRAVITY * STEP_TIME to simulate the effects of gravity. We increase timeAlive by STEP_TIME, and if a particle has exceeded its lifespan, we replace it with a new particle.

    public:
        ParticleEngine(GLuint textureId1) {
            textureId = textureId1;
            timeUntilNextStep = 0;
            colorTime = 0;
            angle = 0;
            for(int i = 0; i < NUM_PARTICLES; i++) {
                createParticle(particles + i);
            }
            for(int i = 0; i < 5 / STEP_TIME; i++) {
                step();
            }
        }

In our constructor, we initialize some variables and create all of the particles. Then, we advance the fountain by 5 seconds, to make it look like it's been running for a while. It would be weird if we didn't, as all of the particles would be in the center of the fountain at the beginning of the program.

        //Advances the particle fountain by the specified amount of time.
        void advance(float dt) {
            while (dt > 0) {
                if (timeUntilNextStep < dt) {
                    dt -= timeUntilNextStep;
                    step();
                    timeUntilNextStep = STEP_TIME;
                }
                else {
                    timeUntilNextStep -= dt;
                    dt = 0;
                }
            }
        }

The advance method advances the particle fountain by a particular amount of time. It does this just by calling step() some number of times and altering the timeUntilNextStep field.

        //Draws the particle fountain.
        void draw() {
            vector<Particle*> ps;
            for(int i = 0; i < NUM_PARTICLES; i++) {
                ps.push_back(particles + i);
            }
            sort(ps.begin(), ps.end(), compareParticles);

Here's where we actually draw the particles. First, we sort the particles from back to front using the sort function.

            glEnable(GL_TEXTURE_2D);
            glBindTexture(GL_TEXTURE_2D, textureId);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

We set up some texture stuff.

            glBegin(GL_QUADS);
            for(unsigned int i = 0; i < ps.size(); i++) {
                Particle* p = ps[i];
                glColor4f(p->color[0], p->color[1], p->color[2],
                          (1 - p->timeAlive / p->lifespan));
                float size = PARTICLE_SIZE / 2;
                
                Vec3f pos = adjParticlePos(p->pos);
                
                glTexCoord2f(0, 0);
                glVertex3f(pos[0] - size, pos[1] - size, pos[2]);
                glTexCoord2f(0, 1);
                glVertex3f(pos[0] - size, pos[1] + size, pos[2]);
                glTexCoord2f(1, 1);
                glVertex3f(pos[0] + size, pos[1] + size, pos[2]);
                glTexCoord2f(1, 0);
                glVertex3f(pos[0] + size, pos[1] - size, pos[2]);
            }
            glEnd();

Finally, we actually draw all of the particles. As I mentioned earlier, each particle is just a square parallel to the camera, with our texture applied to it.

The call to glColor4f gives the fade-out effect I mentioned earlier. It specifies an amount by which to multiply the alpha component of each pixel of the texture. When a particle is just created, this amount will be 1, when the particle is at half its lifespan, it will be 0.5, and when the particle is almost dead, it will be almost 0.

That's all there is to the program, other than a call to this draw method in drawScene and a call to the ParticleEngine constructor in main. This explains how you can make a particle fountain, and how particle systems tend to work in general.

Next is "Lesson 14: Drawing Reflections".