# OpenGL:Tutorials:Tutorial Framework:Particles

## Contents

In this example, We're going to extend the principles used in the Ortho example to produce a nice particle effect.

## Setting Up

Each particle has unique position, direction vector, color and a 'life' values. These values are contained by a structure:

```   typedef struct
{
float xPos,yPos,zPos;
float xVec,yVec,zVec;
float r,g,b,life;
}SpriteInfo;```

We'll also specify a few parameters for the particles which can be tweaked to change the appearance of our effect:

```    const float PARTICLE_SIZE = 0.5f;
const int NUM_PARTICLES = 10000;
const float SPEED_DECAY = 0.00005f; // (Gravity)```

Another new thing here is time-based movement. This code was written across a few different machines, with varying specs. Obviously the results were very slow on a vanilla, on-board graphics system compared to those produced by a 3D accelerated system. To get around this we can use time-based modelling to ensure that the objects move at a constant speed, regardless of the frame per second rate. It's not as scary as it sounds, we simply have to work out how many 'ticks' have elapsed between frames, and multiply all our movement factors by that value.

Here we use three long values to manage the time:

`   long Time1,Time2,Ticks;`

Time1 stores the tick count of the last frame, Time2 is the tick count of the current frame and Ticks is the difference between them. This is the value we will use to calculate our movement.

## Managing the Particles

The first thing we must do with our particles is set them all to a known state. Setting all the 'life' attributes to zero means that each particle will be initialised by the main loop.

```   for(Index=0;Index!=NUM_PARTICLES;Index++)
{
Spr[Index].life=0.0f;
Spr[Index].r=1.0f;
Spr[Index].b=0.0f;
}```

We also set the red and blue color values here as they will not change during the program.

Once we have set up the view and drawn the floor quad, we can get on with handling our particles.

If a particle is live (life>0), we add the direction vectors to the position. We use the Ticks value multiplied by the direction vectors to calculate the correct distance:

```   if(Spr[Index].life>0.0f)
{
Spr[Index].xPos+=(Spr[Index].xVec*Ticks);
Spr[Index].yPos+=(Spr[Index].yVec*Ticks);
Spr[Index].zPos+=(Spr[Index].zVec*Ticks);
Spr[Index].yVec-=(SPEED_DECAY*Ticks);```

We also subtract the 'SPEED_DECAY' value from the Y vector to give the illusion of gravity.

We have a 'floor' in our example, so, if the particle is within the floor area, and it's position is below it, we'll reverse the direction of the Y vector to bounce the particle. During the bounce we'll also scrub off a little speed and life to make for a more realistic effect:

```      // 'Bounce' particle if on the floor square
if(Spr[Index].xPos>-10.0f && Spr[Index].xPos<10.0f &&
Spr[Index].zPos>-10.0f && Spr[Index].zPos<10.0f)
{
if(Spr[Index].yPos<PARTICLE_SIZE)
{
Spr[Index].yPos=PARTICLE_SIZE;
Spr[Index].life-=0.01f;
Spr[Index].yVec*=-0.6f;
}
}```

Lastly, we reduce the life value of each particle to give a finite lifespan:

```     Spr[Index].life-=(0.0001f*Ticks);
}```

For particles which have expired (life<0), we generate a new set of parameters. The position values are reset to the base of our 'fountain':

```   // Reset position
Spr[Index].xPos=0.0f;
Spr[Index].yPos=PARTICLE_SIZE;
Spr[Index].zPos=0.0f;```

The direction vector is derived from a random offset from the vertical, rotated around the Y axis by a random value in the range 0 - 1.57 radians (90 degrees).

```   // Get a random spread and direction
Angle=(float)(rand()%157)/100.0f; // Quarter circle

// Calculate X and Z vectors

Next we randomly reverse the X and Z vectors to make a complete circle of possible directions.

```   // Randomly reverse X and Z vector to complete the circle
if(rand()%2)
Spr[Index].xVec= - Spr[Index].xVec;

if(rand()%2)
Spr[Index].zVec= - Spr[Index].zVec;```

Finally, we choose a random initial speed, color and lifespan.

```   // Get a random initial speed
Spr[Index].yVec=(float)(rand()%500)/10000.0f;

// Get a random life and 'yellowness'
Spr[Index].life=(float)(rand()%100)/100.0f;
Spr[Index].g=0.2f+((float)(rand()%50)/100.0f);```

## Billboarding

So, now we have updated all our particles, it's time to actually draw them.

The technique of billboarding is used to give the illusion of complexity by aligning a textured polygon to face the viewer as the scene or observer moves. Cylindrical billboarding uses a single axis to align the polygon and is often used for trees and other objects which have a fixed position. The problem here is that the illusion is lost if the viewer moves above the object. Spherical billboarding uses two axes alignment and is used for clouds, explosions and particle effects. It does not suffer the same problems as the cylindrical method.

There are two alignment methods for billboarding; Simple or scene based alignment uses the same angles for all polygons, this is the fastest method and works well for most applications. Complex or camera based alignment calculates the facing for each polygon individually, aligning it exactly with the viewer. This is time consuming, but produces better results for large billboards.

We will use simple spherical billboarding in this example. First, we'll turn off depth writes to prevent interference patterns from appearing in the output (we're going to be drawing lots of overlapping polygons). This way, the quads are still affected any depth information in the scene:

`   glDepthMask(GL_FALSE);`

Now we use the glPushMatrix() / glPopMatrix() technique to move each particle into place.

Note the reverse application of the view rotations to align the quad with the viewer.

```   for(Index=0;Index!=MaxParticles;Index++)
{
glPushMatrix();

// Place the quad and rotate to face the viewer
glColor4f(Spr[Index].r,Spr[Index].g,Spr[Index].b,Spr[Index].life);
glTranslatef(Spr[Index].xPos,Spr[Index].yPos,Spr[Index].zPos);
glRotatef(-ViewYaw,0.0f,1.0f,0.0f);
glRotatef(-ViewPitch,1.0f,0.0f,0.0f);

glTexCoord2f(0.0f,0.0f); glVertex3f(-PARTICLE_SIZE, PARTICLE_SIZE,0.0f);
glTexCoord2f(0.0f,1.0f); glVertex3f(-PARTICLE_SIZE,-PARTICLE_SIZE,0.0f);
glTexCoord2f(1.0f,1.0f); glVertex3f( PARTICLE_SIZE,-PARTICLE_SIZE,0.0f);
glTexCoord2f(1.0f,0.0f); glVertex3f( PARTICLE_SIZE, PARTICLE_SIZE,0.0f);
glEnd();

glPopMatrix();
}```

If all goes well, we should have a nice fountain of particles like this:

During execution, use the left/right keys to control the spread and the up/down keys to control the particle count.
Moving the mouse adjusts the view.

## Source Code

The source to Render.cpp, compile this demo using the OpenGL Tutorial Framework.

```#include "Framework.h"
#include <cmath>
#include <ctime>
#include <cstdlib>
#include "tga.h"

// A few parameter definitions, tweak these to alter the appearance of the spray
const float PARTICLE_SIZE = 0.5f;
const int NUM_PARTICLES = 10000;
const float SPEED_DECAY = 0.00005f; // (Gravity)

// Particle structure
typedef struct
{
float xPos,yPos,zPos;
float xVec,yVec,zVec;
float r,g,b,life;
}SpriteInfo;

// Function declarations

// Here we go!
void Render(void)
{
GLuint Texture[128];           // Handles to our textures
SpriteInfo Spr[NUM_PARTICLES]; // Array of particles

float ViewYaw, ViewPitch;
long Time1,Time2,Ticks;

// Allocate all textures in one go
glGenTextures(128,Texture);

// Background color
glClearColor(0.0f,0.0f,0.0f,1.0f);

// Setup our screen
glMatrixMode(GL_PROJECTION);
glViewport(0,0,800,600);
glFrustum(-.5f,.5f,-.5f*(600.0f/800.0f),.5f*(600.0f/800.0f),1.0f,500.0f);
glMatrixMode(GL_MODELVIEW);

// Enable z-buffer
glEnable(GL_DEPTH_TEST);

// Enable blending
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);

glEnable(GL_TEXTURE_2D);

// Seed the randomiser
srand(time(NULL));

// Set up adjustable parameters and timing variables
MaxParticles=NUM_PARTICLES/2;

Time1=Time2=clock();

// Set all particles to dead
for(Index=0;Index!=NUM_PARTICLES;Index++)
{
Spr[Index].life=0.0f;
Spr[Index].r=1.0f;
Spr[Index].b=0.0f;
}

// Main Loop
while(RunLevel)
{
if(Keys[VK_ESCAPE])
RunLevel=0;

// Tighten spray
if(Keys[VK_LEFT])
{
}

// Widen spray
if(Keys[VK_RIGHT])
{
}

// Reduce particle count (This takes a while to be noticable)
if(Keys[VK_DOWN])
{
MaxParticles-=1;
if(MaxParticles<1)
MaxParticles=1;
}

// Increase particle count
if(Keys[VK_UP])
{
MaxParticles+=1;
if(MaxParticles>NUM_PARTICLES)
MaxParticles=NUM_PARTICLES;

// Kill new particle to ensure good start point
Spr[MaxParticles].life=0.0f;
}

// Reset view
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);

// Get ticks since last frame
Time2=clock();
Ticks=Time2-Time1;
Time1=Time2;

// Calculate view angles
ViewPitch=(Mouse.My-300)/4.0f;
ViewYaw=(Mouse.Mx-300)/3.0f;

// Set up the view based on the mouse position
glTranslatef(0.0f,-10.0f,-50.0f);
glRotatef(ViewPitch,1.0f,0.0f,0.0f);
glRotatef(ViewYaw,0.0f,1.0f,0.0f);

// Set up for floor plane
glBindTexture(GL_TEXTURE_2D,Texture[1]);
glColor4f(1.0f,1.0f,1.0f,1.0f);
glDisable(GL_BLEND);

// Draw floor plane
glTexCoord2f(0.0f,1.0f); glVertex3f(-10.0f, 0.0f, 10.0f);
glTexCoord2f(1.0f,1.0f); glVertex3f( 10.0f, 0.0f, 10.0f);
glTexCoord2f(1.0f,0.0f); glVertex3f( 10.0f, 0.0f,-10.0f);
glTexCoord2f(0.0f,0.0f); glVertex3f(-10.0f, 0.0f,-10.0f);
glEnd();

// Update particles, generating new if required
for(Index=0;Index!=MaxParticles;Index++)
{
if(Spr[Index].life>0.0f)
{
Spr[Index].xPos+=(Spr[Index].xVec*Ticks);
Spr[Index].yPos+=(Spr[Index].yVec*Ticks);
Spr[Index].zPos+=(Spr[Index].zVec*Ticks);
Spr[Index].yVec-=(SPEED_DECAY*Ticks);

// 'Bounce' particle if on the floor square
if(Spr[Index].xPos>-10.0f && Spr[Index].xPos<10.0f &&
Spr[Index].zPos>-10.0f && Spr[Index].zPos<10.0f)
{
if(Spr[Index].yPos<PARTICLE_SIZE)
{
Spr[Index].yPos=PARTICLE_SIZE;
Spr[Index].life-=0.01f;
Spr[Index].yVec*=-0.6f;
}
}
Spr[Index].life-=(0.0001f*Ticks);
}
else // Spawn a new particle
{
// Reset position
Spr[Index].xPos=0.0f;
Spr[Index].yPos=PARTICLE_SIZE;
Spr[Index].zPos=0.0f;

// Get a random spread and direction
Angle=(float)(rand()%157)/100.0f; // Quarter circle

// Calculate X and Z vectors

// Randomly reverse X and Z vector to complete the circle
if(rand()%2)
Spr[Index].xVec= - Spr[Index].xVec;

if(rand()%2)
Spr[Index].zVec= - Spr[Index].zVec;

// Get a random initial speed
Spr[Index].yVec=(float)(rand()%500)/10000.0f;

// Get a random life and 'yellowness'
Spr[Index].life=(float)(rand()%100)/100.0f;
Spr[Index].g=0.2f+((float)(rand()%50)/100.0f);
}
}

// Select particle texture
glBindTexture(GL_TEXTURE_2D,Texture[0]);
glEnable(GL_BLEND);

// Draw the particles
for(Index=0;Index!=MaxParticles;Index++)
{
glPushMatrix();

// Place the quad and rotate to face the viewer
glColor4f(Spr[Index].r,Spr[Index].g,Spr[Index].b,Spr[Index].life);
glTranslatef(Spr[Index].xPos,Spr[Index].yPos,Spr[Index].zPos);
glRotatef(-ViewYaw,0.0f,1.0f,0.0f);
glRotatef(-ViewPitch,1.0f,0.0f,0.0f);

glTexCoord2f(0.0f,0.0f); glVertex3f(-PARTICLE_SIZE, PARTICLE_SIZE,0.0f);
glTexCoord2f(0.0f,1.0f); glVertex3f(-PARTICLE_SIZE,-PARTICLE_SIZE,0.0f);
glTexCoord2f(1.0f,1.0f); glVertex3f( PARTICLE_SIZE,-PARTICLE_SIZE,0.0f);
glTexCoord2f(1.0f,0.0f); glVertex3f( PARTICLE_SIZE, PARTICLE_SIZE,0.0f);
glEnd();

glPopMatrix();
}

// Show our scene
FlipBuffers();
}

// Clean up textures
glDeleteTextures(128,Texture);
}

{

return false;

glBindTexture(GL_TEXTURE_2D,TexHandle); // Set our Tex handle as current

// Create the texture
if(Img.GetBPP()==24)
glTexImage2D(GL_TEXTURE_2D,0,3,Img.GetWidth(),Img.GetHeight(),0,
GL_RGB,GL_UNSIGNED_BYTE,Img.GetImg());
else if(Img.GetBPP()==32)
glTexImage2D(GL_TEXTURE_2D,0,4,Img.GetWidth(),Img.GetHeight(),0,
GL_RGBA,GL_UNSIGNED_BYTE,Img.GetImg());
else
return false;

// Specify filtering and edge actions
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP);

return true;
}```