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 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:
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.
