If you're running AdBlock, please consider whitelisting this site if you'd like to support LearnOpenGL; and no worries, I won't be mad if you don't :)

Particles

In-Practice/2D-Game/Particles

A particle, as seen from OpenGL's perspective, is a tiny 2D quad that is always faced at the camera (billboarding) and (usually) contains a texture with large parts of the sprite being transparent. A particle by itself is basically just a sprite as we've been extensively using so far, but when you put together hundreds or even thousands of these particles together you can create amazing effects.

When working with particles, there is usually an object called a particle emitter or particle generator that, from its location, continously spawns new particles that decay over time. If such a particle emitter would for example spawn tiny particles with a smoke-like texture, color them less bright the larger the distance from the emitter and give them a glowy appearance you'd get a fire-like effect:

Example of particles as a fire

A single particle often has a life variable that slowly decays once it is spawned. Once its life is less than a certain threshold (usually 0) we kill the particle so it can be replaced with a new particle object when the next particle spawns. A particle emitter controls all its spawned particles and changes their behavior based on their attributes. A particle generally has the following attributes:


struct Particle {
    glm::vec2 Position, Velocity;
    glm::vec4 Color;
    GLfloat Life;
  
    Particle() 
      : Position(0.0f), Velocity(0.0f), Color(1.0f), Life(0.0f) { }
};    

Looking at the fire example, the particle emitter probably spawns each particle with a position close to the emitter and with an upwards velocity so each particle moves in the positive y direction. It seems to have 3 different regions so it probably gives some particles a higher velocity than others. We can also see that the higher the y position of the particle, the less yellow or bright its color becomes. After the particles have reached a certain height, their life is depleted and the particles are killed; never reaching the stars.

You can imagine that with systems like these we can create interesting effects like fire, smoke, fog, magic effects, gunfire residue etc. In Breakout we're going to add a simple particle generator following the ball to make it all look more interesting. It'll look a bit like this:

Here the particle generator spawns each particle at the ball's position, gives it a velocity equal to a fraction of the ball's velocity and changes the color of the particle based on how long it lived.

For rendering the particles we'll be using a different set of shaders:


#version 330 core
layout (location = 0) in vec4 vertex; // <vec2 position, vec2 texCoords>

out vec2 TexCoords;
out vec4 ParticleColor;

uniform mat4 projection;
uniform vec2 offset;
uniform vec4 color;

void main()
{
    float scale = 10.0f;
    TexCoords = vertex.zw;
    ParticleColor = color;
    gl_Position = projection * vec4((vertex.xy * scale) + offset, 0.0, 1.0);
}

And the fragment shader:


#version 330 core
in vec2 TexCoords;
in vec4 ParticleColor;
out vec4 color;

uniform sampler2D sprite;

void main()
{
    color = (texture(sprite, TexCoords) * ParticleColor);
}  

We take the standard position and texture attributes per particle and also accept an offset and a color uniform for changing the outcome per particle. Note that in the vertex shader we scale the particle quad by 10.0f; you could also set the scale as a uniform and control this individually per particle.

First, we need a list of particles that we then instantiate with default Particle structs.


GLuint nr_particles = 500;
std::vector<Particle> particles;
  
for (GLuint i = 0; i < nr_particles; ++i)
    particles.push_back(Particle());

Then in each frame, we spawn several new particles with starting values and then for each particle that is (still) alive we update their values.


GLuint nr_new_particles = 2;
// Add new particles
for (GLuint i = 0; i < nr_new_particles; ++i)
{
    int unusedParticle = FirstUnusedParticle();
    RespawnParticle(particles[unusedParticle], object, offset);
}
// Uupdate all particles
for (GLuint i = 0; i < nr_particles; ++i)
{
    Particle &p = particles[i];
    p.Life -= dt; // reduce life
    if (p.Life > 0.0f)
    {	// particle is alive, thus update
        p.Position -= p.Velocity * dt;
        p.Color.a -= dt * 2.5;
    }
}  

The first loop might look a little daunting. Because particles die over time we want to spawn nr_new_particles particles each frame, but since we've decided from the start that the total amount of particles we'll be using is nr_particles we can't simply push the new particles to the end of the list. This way we'll quickly get a list filled with thousands of particles which isn't really efficient considering only a small portion of that list has particles that are alive.

What we want is to find the first particle that is dead (life < 0.0f) and update that particle as a new respawned particle.

The function FirstUnusedParticle tries to find the first particle that is dead and returns its index to the caller.


GLuint lastUsedParticle = 0;
GLuint FirstUnusedParticle()
{
    // Search from last used particle, this will usually return almost instantly
    for (GLuint i = lastUsedParticle; i < nr_particles; ++i){
        if (particles[i].Life <= 0.0f){
            lastUsedParticle = i;
            return i;
        }
    }
    // Otherwise, do a linear search
    for (GLuint i = 0; i < lastUsedParticle; ++i){
        if (particles[i].Life <= 0.0f){
            lastUsedParticle = i;
            return i;
        }
    }
    // Override first particle if all others are alive
    lastUsedParticle = 0;
    return 0;
}  

The function stores the index of the last dead particle it found, since the next dead particle will most likely be right after this last particle index so we first search from this stored index. If we did not find any dead particles, we simply do a slower linear search. If no particles are dead it will return the index 0 which results in the first particle being overwritten. Note that if it reaches this last case, it means your particles are alive for too long, you need to spawn less particles per frame and/or you simply don't have enough particles reserved.

Then, once the first dead particle in the list is found, we update its values by calling RespawnParticle that takes the particle, a GameObject and an offset vector:


void RespawnParticle(Particle &particle, GameObject &object, glm::vec2 offset)
{
    GLfloat random = ((rand() % 100) - 50) / 10.0f;
    GLfloat rColor = 0.5 + ((rand() % 100) / 100.0f);
    particle.Position = object.Position + random + offset;
    particle.Color = glm::vec4(rColor, rColor, rColor, 1.0f);
    particle.Life = 1.0f;
    particle.Velocity = object.Velocity * 0.1f;
}  

This function simply resets the particle's life to 1.0f, randomly gives it a brightness (via the color vector) starting from 0.5 and assigns a (slightly random) position and velocity based on the game object.

The second loop within the update function loops through all the particles and for each particle reduces its life by the delta time variable; this way each particle's life corresponds to exactly the second(s) it's allowed to live. Then we check if the particle is alive and if so, update its position and color attributes. Here we slowly reduce the alpha component of each particle so it looks like they're slowly disappearing over time.

Then what's left is to actually render the particles:


glBlendFunc(GL_SRC_ALPHA, GL_ONE);
particleShader.Use();
for (Particle particle : particles)
{
    if (particle.Life > 0.0f)
    {
        particleShader.SetVector2f("offset", particle.Position);
        particleShader.SetVector4f("color", particle.Color);
        particleTexture.Bind();
        glBindVertexArray(particleVAO);
        glDrawArrays(GL_TRIANGLES, 0, 6);
        glBindVertexArray(0);
    } 
} 
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

Here, for each particle, we set their offset and color uniform values, bind the texture and render the 2D quad. What is interesting to see here are the two calls to glBlendFunc. When rendering the particles, instead of the default destination blending mode of GL_ONE_MINUS_SRC_ALPHA we use the GL_ONE blending mode that gives the particles a very neat glow effect when stacked upon each other. This is also likely the blending mode used when rendering the fire at the top of the tutorial, since the fire got glowy at the center where most of its particles were.

Because we (like most other parts of the tutorial series) like to keep things organized, another class called ParticleGenerator was created that hosts all the functionality we just talked about. You can find the source code below:

Then within the game code, we create such a particle generator and initialize it with this texture.


ParticleGenerator   *Particles; 

void Game::Init()
{
    [...]
    ResourceManager::LoadShader("shaders/particle.vs", "shaders/particle.frag", nullptr, "particle");
    [...]
    ResourceManager::LoadTexture("textures/particle.png", GL_TRUE, "particle"); 
    [...]
    Particles = new ParticleGenerator(
        ResourceManager::GetShader("particle"), 
        ResourceManager::GetTexture("particle"), 
        500
    );
}

Then we change the game class's Update function by adding an update statement for the particle generator:


void Game::Update(GLfloat dt)
{
    [...]
    // Update particles
    Particles->Update(dt, *Ball, 2, glm::vec2(Ball->Radius / 2));
    [...]
}

Each of the particles will use the game object properties from the ball object, spawn 2 particles each frame and their positions will be offset towards the center of the ball. Last up is rendering the particles:


void Game::Render()
{
    if (this->State == GAME_ACTIVE)
    {
        [...]
        // Draw player
        Player->Draw(*Renderer);
        // Draw particles	
        Particles->Draw();
        // Draw ball
        Ball->Draw(*Renderer);
    }
}  

Note that we render the particles before the ball is rendered and after the other item are rendered so the particles will end up in front of all other objects, but stay behind the ball. You can find the updated game class code here.

If you'd now compile and run your application you should see a trail of particles following the ball, just like at the beginning of this tutorial, giving the game a more modern look. The system can also easily be extended to host more advanced effects so feel free to experiment with the particle generation and see if you can come up with your own creative effects.

HI