OpenGL:Tutorials:Tutorial Framework:Particles

From GPWiki
Jump to: navigation, search

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 int INITIAL_PARTICLE_SPREAD = 100;
    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
   Spread=(float)(rand()%MaxSpread)/10000.0f;
   Angle=(float)(rand()%157)/100.0f; // Quarter circle
 
   // Calculate X and Z vectors
   Spr[Index].xVec=cos(Angle)*Spread;
   Spr[Index].zVec=sin(Angle)*Spread;


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);
 
     glBegin(GL_QUADS);
      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:

GLTut6Particle.jpg

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 int INITIAL_PARTICLE_SPREAD = 100;
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
bool LoadTexture(char *TexName, GLuint TexHandle);
 
// Here we go!
void Render(void)
 {
  GLuint Texture[128];           // Handles to our textures
  SpriteInfo Spr[NUM_PARTICLES]; // Array of particles
 
  int MaxSpread,MaxParticles,Index;
  float Spread,Angle;  
  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);
  glLoadIdentity();
  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);
 
  // Load the textures
  LoadTexture("../particle.tga",Texture[0]);
  LoadTexture("../Marble.tga",Texture[1]);
  glEnable(GL_TEXTURE_2D);
 
  // Seed the randomiser
  srand(time(NULL));
 
  // Set up adjustable parameters and timing variables
  MaxSpread=INITIAL_PARTICLE_SPREAD;
  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]) 
       {
        MaxSpread--;
         if(MaxSpread<1)
          MaxSpread=1;
       }
 
      // Widen spray
      if(Keys[VK_RIGHT]) 
       {
        MaxSpread++;
         if(MaxSpread>500)
          MaxSpread=500;
       }
 
      // 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
     glLoadIdentity(); 
     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
     glBegin(GL_QUADS);
      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
           Spread=(float)(rand()%MaxSpread)/10000.0f;
           Angle=(float)(rand()%157)/100.0f; // Quarter circle
 
           // Calculate X and Z vectors
           Spr[Index].xVec=cos(Angle)*Spread;
           Spr[Index].zVec=sin(Angle)*Spread;
 
           // 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);
     glDepthMask(GL_FALSE);
 
      // 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);
 
        glBegin(GL_QUADS);
         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();
       }
 
     glDepthMask(GL_TRUE);
 
     // Show our scene
     FlipBuffers();
    }
 
  // Clean up textures
  glDeleteTextures(128,Texture);
 }
 
 
// Load a TGA texture
bool LoadTexture(char *TexName, GLuint TexHandle)
 {
  TGAImg Img;        // Image loader
 
  // Load our Texture
   if(Img.Load(TexName)!=IMG_OK)
    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;
 }

Downloads

GLTut6_Particles.zip - A zip including Win32 and GLFW source code, images and a Win32 Binary.