|
|
|
|
|
Welcome
to Tutorial 19. You've learned alot, and now you want to play. I will introduce
one new command in this tutorial... The triangle strip. It's very easy
to use, and can help speed up your programs when drawing alot of triangles.
In this tutorial I will
teach you how to make a semi-complex Particle Engine. Once you understand
how particle engines work, creating effects such as fire, smoke, water
fountains and more will be a piece of cake!
I have to warn you however!
Until today I had never written a particle engine. I had this idea that
the 'famous' particle engine was a very complex piece of code. I've made
attempts in the past, but usually gave up after I realized I couldn't control
all the points without going crazy.
You might not believe
me when I tell you this, but this tutorial was written 100% from scratch.
I borrowed no ones ideas, and I had no technical information sitting in
front of me. I started thinking about particles, and all of a sudden my
head filled with ideas (brain turning on?). Instead of thinking about each
particle as a pixel that had to go from point 'A' to point 'B', and do
this or that, I decided it would be better to think of each particle as
an individual object responding to the environment around it. I gave each
particle life, random aging, color, speed, gravitational influence and
more.
Soon I had a finished
project. I looked up at the clock and realized aliens had come to get me
once again. Another 4 hours gone! I remember stopping now and then to drink
coffee and blink, but 4 hours... ?
So, although this program
in my opinion looks great, and works exactly like I wanted it to, it may
not be the proper way to make a particle engine. I don't care personally,
as long as it works well, and I can use it in my projects! If you are the
type of person that needs to know you're conforming, then spend hours browsing
the net looking for information. Just be warned. The few code snippits
you do find may appear cryptic :)
This tutorial uses the
base code from lesson 1. There is alot of new code however, so I'll rewrite
any section of code that contains changes (makes it easier to understand).
Using the code from lesson
1, we'll add 5 new lines of code at the top of our program. The first line
(stdio.h) allows us to read data from files. It's the same line we've added
to previous tutorials the use texture mapping. The second line defines
how many particles were going to create and display on the screen. Define
just tells our program that MAX_PARTICLES will equal whatever value
we specify. In this case 1000. The third line will be used to toggle 'rainbow
mode' off and on. We'll set it to on by default. sp and rp
are variables we'll use to prevent the spacebar or return key from rapidly
repeating when held down. |
|
|
|
|
#include <windows.h> // Header File For Windows
#include <stdio.h> // Header File For Standard Input/Output ( ADD )
#include <gl\gl.h> // Header File For The OpenGL32 Library
#include <gl\glu.h> // Header File For The GLu32 Library
#include <gl\glaux.h> // Header File For The GLaux Library
#define MAX_PARTICLES 1000 // Number Of Particles To Create ( NEW )
HDC hDC=NULL; // Private GDI Device Context
HGLRC hRC=NULL; // Permanent Rendering Context
HWND hWnd=NULL; // Holds Our Window Handle
HINSTANCE hInstance; // Holds The Instance Of The Application
bool keys[256]; // Array Used For The Keyboard Routine
bool active=TRUE; // Window Active Flag Set To TRUE By Default
bool fullscreen=TRUE; // Fullscreen Flag Set To Fullscreen Mode By Default
bool rainbow=true; // Rainbow Mode? ( ADD )
bool sp; // Spacebar Pressed? ( ADD )
bool rp; // Return Key Pressed? ( ADD )
|
|
|
|
|
The
next 4 lines are misc variables. The variable slowdown controls
how fast the particles move. The higher the number, the slower they move.
The lower the number, the faster they move. If the value is set to low,
the particles will move way too fast! The speed the particles travel at
will affect how they move on the screen. Slow particles will not shoot
out as far. Keep this in mind.
The variables xspeed
and yspeed allow us to control the direction of the tail.
xspeed
will be added to the current speed a particle is travelling on the x axis.
If
xspeed is a positive value our particle will be travelling more
to the right. If
xspeed is a negative value, our particle will travel
more to the left. The higher the value, the more it travels in that direction.
yspeed works the same way, but on the y axis. The reason I say 'MORE'
in a specific direction is because other factors affect the direction our
particle travels. xspeed and yspeed help to move the particle
in the direction we want.
Finally we have the variable
zoom. We use this variable to pan into and out of our scene. With
particle engines, it's nice to see more of the screen at times, and cool
to zoom in real close other times. |
|
|
|
|
float slowdown=2.0f; // Slow Down Particles
float xspeed; // Base X Speed (To Allow Keyboard Direction Of Tail)
float yspeed; // Base Y Speed (To Allow Keyboard Direction Of Tail)
float zoom=-40.0f; // Used To Zoom Out
|
|
|
|
|
Now
we set up a misc loop variable called loop. We'll use this to predefine
the particles and to draw the particles to the screen. col will
be use to keep track of what color to make the particles. delay
will be used to cycle through the colors while in rainbow mode.
Finally, we set aside
storage space for one texture (the particle texture). I decided to use
a texture rather than OpenGL points for a few reasons. The most important
reason is because points are not all that fast, and they look pretty blah.
Secondly, textures are way more cool :) You can use a square particle,
a tiny picture of your face, a picture of a star, etc. More control! |
|
|
|
|
GLuint loop; // Misc Loop Variable
GLuint col; // Current Color Selection
GLuint delay; // Rainbow Effect Delay
GLuint texture[1]; // Storage For Our Particle Texture
|
|
|
|
|
Ok,
now for the fun stuff. The next section of code creates a structure describing
a single particle. This is where we give the particle certain characteristics.
We start off with the
boolean variable active. If this variable is TRUE, our particle
is alive and kicking. If it's FALSE our particle is dead or we've turned
it off! In this program I don't use active, but it's handy to include.
The variables life
and fade control how long the particle is displayed, and how bright
the particle is while it's alive. The variable life is gradually
decreased by the value stored in fade. In this program that will cause
some particles to burn longer than others. |
|
|
|
|
typedef struct // Create A Structure For Particle
{
bool active; // Active (Yes/No)
float life; // Particle Life
float fade; // Fade Speed
|
|
|
|
|
The
variables r, g and b hold the red intensity, green
intensity and blue intensity of our particle. The closer r is to
1.0f, the more red the particle will be. Making all 3 variables 1.0f will
create a white particle. |
|
|
|
|
float r; // Red Value
float g; // Green Value
float b; // Blue Value
|
|
|
|
|
The
variables x, y and z control where the particle will
be displayed on the screen. x holds the location of our particle
on the x axis. y holds the location of our particle on the y axis,
and finally z holds the location of our particle on the z axis. |
|
|
|
|
float x; // X Position
float y; // Y Position
float z; // Z Position
|
|
|
|
|
The
next three variables are important. These three variables control how fast
a particle is moving on specific axis, and what direction to move. If xi
is a negative value our particle will move left. Positive it will move
right. If yi is negative our particle will move down. Positive it
will move up. Finally, if zi is negative the particle will move
into the screen, and postive it will move towards the viewer. |
|
|
|
|
float xi; // X Direction
float yi; // Y Direction
float zi; // Z Direction
|
|
|
|
|
Lastly,
3 more variables! Each of these variables can be thought of as gravity.
If xg is a positive value, our particle will pull to the right.
If it's negative our particle will be pulled to the left. So if our particle
is moving left (negative) and we apply a positive gravity, the speed will
eventually slow so much that our particle will start moving the opposite
direction. yg pulls up or down and zg pulls towards or away
from the viewer. |
|
|
|
|
float xg; // X Gravity
float yg; // Y Gravity
float zg; // Z Gravity
|
|
|
|
|
particles
is the name of our structure. |
|
|
|
|
}
particles; // Particles Structure
|
|
|
|
|
Next
we create an array called particle. This array will store MAX_PARTICLES.
Translated into english we create storage for 1000 (MAX_PARTICLES)
particles. This storage space will store the information for each individual
particle. |
|
|
|
|
particles particle[MAX_PARTICLES]; // Particle Array (Room For Particle Info)
|
|
|
|
|
We
cut back on the amount of code required for this program by storing our
12 different colors in a color array. For each color from 0 to 11 we store
the red intensity, the green intensity, and finally the blue intensity.
The color table below stores 12 different colors fading from red to violet. |
|
|
|
|
static GLfloat colors[12][3]= // Rainbow Of Colors
{
{1.0f,0.5f,0.5f},{1.0f,0.75f,0.5f},{1.0f,1.0f,0.5f},{0.75f,1.0f,0.5f},
{0.5f,1.0f,0.5f},{0.5f,1.0f,0.75f},{0.5f,1.0f,1.0f},{0.5f,0.75f,1.0f},
{0.5f,0.5f,1.0f},{0.75f,0.5f,1.0f},{1.0f,0.5f,1.0f},{1.0f,0.5f,0.75f}
};
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Declaration For WndProc
|
|
|
|
|
Our
bitmap loading code hasn't changed. |
|
|
|
|
AUX_RGBImageRec *LoadBMP(char *Filename) // Loads A Bitmap Image
{
FILE *File=NULL; // File Handle
if (!Filename) // Make Sure A Filename Was Given
{
return NULL; // If Not Return NULL
}
File=fopen(Filename,"r"); // Check To See If The File Exists
if (File) // Does The File Exist?
{
fclose(File); // Close The Handle
return auxDIBImageLoad(Filename); // Load The Bitmap And Return A Pointer
}
return NULL; // If Load Failed Return NULL
}
|
|
|
|
|
This
is the section of code that loads the bitmap (calling the code above) and
converts it into a textures. Status is used to keep track of whether or
not the texture was loaded and created. |
|
|
|
|
int LoadGLTextures() // Load Bitmaps And Convert To Textures
{
int Status=FALSE; // Status Indicator
AUX_RGBImageRec *TextureImage[1]; // Create Storage Space For The Texture
memset(TextureImage,0,sizeof(void *)*1); // Set The Pointer To NULL
|
|
|
|
|
Our
texture loading code will load in our particle bitmap and convert it to
a linear filtered texture. |
|
|
|
|
if (TextureImage[0]=LoadBMP("Data/Particle.bmp")) // Load Particle Texture
{
Status=TRUE; // Set The Status To TRUE
glGenTextures(1, &texture[0]); // Create One Textures
glBindTexture(GL_TEXTURE_2D, texture[0]);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);
}
if (TextureImage[0]) // If Texture Exists
{
if (TextureImage[0]->data) // If Texture Image Exists
{
free(TextureImage[0]->data); // Free The Texture Image Memory
}
free(TextureImage[0]); // Free The Image Structure
}
return Status; // Return The Status
}
|
|
|
|
|
The
only change I made to the resize code was a deeper viewing distance. Instead
of 100.0f, we can now view particles 200.0f units into the screen. |
|
|
|
|
GLvoid ReSizeGLScene(GLsizei width, GLsizei height) // Resize And Initialize The GL Window
{
if (height==0) // Prevent A Divide By Zero By
{
height=1; // Making Height Equal One
}
glViewport(0, 0, width, height); // Reset The Current Viewport
glMatrixMode(GL_PROJECTION); // Select The Projection Matrix
glLoadIdentity(); // Reset The Projection Matrix
// Calculate The Aspect Ratio Of The Window
gluPerspective(45.0f,(GLfloat)width/(GLfloat)height,0.1f,200.0f); ( MODIFIED )
glMatrixMode(GL_MODELVIEW); // Select The Modelview Matrix
glLoadIdentity(); // Reset The Modelview Matrix
}
|
|
|
|
|
If
you're using the lesson 1 code, replace it with the code below. I've added
code to load in our texture and set up blending for our particles. |
|
|
|
|
int InitGL(GLvoid) // All Setup For OpenGL Goes Here
{
if (!LoadGLTextures()) // Jump To Texture Loading Routine
{
return FALSE; // If Texture Didn't Load Return FALSE
}
|
|
|
|
|
We
enable smooth shading, clear our background to black, disable depth testing,
blending and texture mapping. After enabling texture mapping we select
our particle texture. |
|
|
|
|
glShadeModel(GL_SMOOTH); // Enables Smooth Shading
glClearColor(0.0f,0.0f,0.0f,0.0f); // Black Background
glClearDepth(1.0f); // Depth Buffer Setup
glDisable(GL_DEPTH_TEST); // Disables Depth Testing
glEnable(GL_BLEND); // Enable Blending
glBlendFunc(GL_SRC_ALPHA,GL_ONE); // Type Of Blending To Perform
glHint(GL_PERSPECTIVE_CORRECTION_HINT,GL_NICEST); // Really Nice Perspective Calculations
glHint(GL_POINT_SMOOTH_HINT,GL_NICEST); // Really Nice Point Smoothing
glEnable(GL_TEXTURE_2D); // Enable Texture Mapping
glBindTexture(GL_TEXTURE_2D,texture[0]); // Select Our Texture
|
|
|
|
|
The
code below will initialize each of the particles. We start off by activating
each particle. If a particle is not active, it won't appear on the screen,
no matter how much life it has.
After we've made the particle
active, we give it life. I doubt the way I apply life, and fade the particles
is the best way, but once again, it works good! Full life is 1.0f. This
also gives the particle full brightness. |
|
|
|
|
for (loop=0;loop<MAX_PARTICLES;loop++) // Initials All The Textures
{
particle[loop].active=true; // Make All The Particles Active
particle[loop].life=1.0f; // Give All The Particles Full Life
|
|
|
|
|
We
set how fast the particle fades out by giving fade a random value.
The variable life will be reduced by fade each time the particle
is drawn. The value we end up with will be a random value from 0 to 99.
We then divide it by 1000 so that we get a very tiny floating point value.
Finally we then add .003 to the final result so that the fade speed is
never 0. |
|
|
|
|
particle[loop].fade=float(rand()%100)/1000.0f+0.003f; // Random Fade Speed
|
|
|
|
|
Now
that our particle is active, and we've given it life, it's time to give
it some color. For the initial effect, we want each particle to be a different
color. What I do is make each particle one of the 12 colors that we've
built in our color table at the top of this program. The math is simple.
We take our loop variable and add one to it to prevent a divide
by zero error. Then we divide loop by the number of particles we
plan to create divided by the number of colors in our table +1.
If loop is 0 the result
would be 0+1/(1000/12)=0.012. Because the result is an integer value, that
will be rounded down to 0 (our first color). If loop was 999 (1000 particles),
the result would be 999+1/(1000/12)=12 (which is our last color). |
|
|
|
|
particle[loop].r=colors[(loop+1)/(MAX_PARTICLES/12)][0]; // Select Red Rainbow Color
particle[loop].g=colors[(loop+1)/(MAX_PARTICLES/12)][1]; // Select Green Rainbow Color
particle[loop].b=colors[(loop+1)/(MAX_PARTICLES/12)][2]; // Select Blue Rainbow Color
|
|
|
|
|
Now
we'll set the direction that each particle moves, along with the speed.
We're going to multiply the results by 10.0f to create a spectacular explosion
when the program first starts.
We'll end up with either
a positive or negative random value. This value will be used to move the
particle in a random direction at a random speed. |
|
|
|
|
particle[loop].xi=float((rand()%50)-26.0f)*10.0f; // Random Speed On X Axis
particle[loop].yi=float((rand()%50)-25.0f)*10.0f; // Random Speed On Y Axis
particle[loop].zi=float((rand()%50)-25.0f)*10.0f; // Random Speed On Z Axis
|
|
|
|
|
Finally,
we set the amount of gravity acting on each particle. Unlike regular gravity
that just pulls things down, our gravity can pull up, down, left, right,
forward or backward. To start out we want semi strong gravity pulling downwards.
To do this we set xg to 0.0f. No pull left or right on the x plane.
We set yg to -0.8f. This creates a semi-strong pull downwards. If
the value was positive it would pull upwards. We don't want the particles
pulling towards or away from us so we'll set zg to 0.0f. |
|
|
|
|
particle[loop].xg=0.0f; // Set Horizontal Pull To Zero
particle[loop].yg=-0.8f; // Set Vertical Pull Downward
particle[loop].zg=0.0f; // Set Pull On Z Axis To Zero
}
return TRUE; // Initialization Went OK
}
|
|
|
|
|
Now
for the fun stuff. The next section of code is where we draw the particle,
check for gravity, etc. It's important that you understand what's going
on, so please read carefully :)
We reset the Modelview
Matrix only once. We'll position the particles using the glVertex3f() command
instead of using tranlations, that way we don't alter the modelview matrix
while drawing our particles. |
|
|
|
|
int DrawGLScene(GLvoid) // Where We Do All The Drawing
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear Screen And Depth Buffer
glLoadIdentity(); // Reset The ModelView Matrix
|
|
|
|
|
We
start off by creating a loop. This loop will update each one of our particles. |
|
|
|
|
for (loop=0;loop<MAX_PARTICLES;loop++) // Loop Through All The Particles
{
|
|
|
|
|
First
thing we do is check to see if the particle is active. If it's not active,
it wont be updated. In this program they're all active, all the time. But
in a program of your own, you may want to make certain particles inactive. |
|
|
|
|
if (particle[loop].active) // If The Particle Is Active
{
|
|
|
|
|
The
next three variables x, y and z are temporary variables
that we'll use to hold the particles x, y and z position. Notice we add
zoom to the z position so that our scene is moved into the screen
based on the value stored in zoom. particle[loop].x holds our x
position for whatever particle we are drawing (particle loop).
particle[loop].y
holds our y position for our particle and particle[loop].z holds
our z position. |
|
|
|
|
float x=particle[loop].x; // Grab Our Particle X Position
float y=particle[loop].y; // Grab Our Particle Y Position
float z=particle[loop].z+zoom; // Particle Z Pos + Zoom
|
|
|
|
|
Now
that we have the particle position, we can color the particle. particle[loop].r
holds the red intensity of our particle, particle[loop].g holds
our green intensity, and
particle[loop].b holds our blue intensity.
Notice I use the particles life for the alpha value. As the particle dies,
it becomes more and more transparent, until it eventually doesn't exist.
That's why the particles life should never be more than 1.0f. If you need
the particles to burn longer, try reducing the fade speed so that the particle
doesn't fade out as fast. |
|
|
|
|
// Draw The Particle Using Our RGB Values, Fade The Particle Based On It's Life
glColor4f(particle[loop].r,particle[loop].g,particle[loop].b,particle[loop].life);
|
|
|
|
|
We
have the particle position and the color is set. All that we have to do
now is draw our particle. Instead of using a textured quad, I've decided
to use a textured triangle strip to speed the program up a bit. Most 3D
cards can draw triangles alot faster than they can draw quads. Some 3D
cards will convert the quad to two triangles for you, but some don't. So
we'll do the work ourselves. We start off by telling OpenGL we want to
draw a triangle strip. |
|
|
|
|
glBegin(GL_TRIANGLE_STRIP); // Build Quad From A Triangle Strip
|
|
|
|
|
Quoted
directly from the red book: A triangle strip draws a series of triangles
(three sided polygons) using vertices V0, V1, V2,
then V2, V1, V3 (note the order), then
V2, V3, V4, and so on. The ordering is
to ensure that the triangles are all drawn with the same orientation so
that the strip can correctly form part of a surface. Preserving the orientation
is important for some operations, such as culling. There must be at least
3 points for anything to be drawn.
So the first triangle
is drawn using vertices 0, 1 and 2. If you look at the picture you'll see
that vertex points 0, 1 and 2 do indeed make up the first triangle (top
right, top left, bottom right). The second triangle is drawn using vertices
2, 1 and 3. Again, if you look at the picture, vertices 2, 1 and 3 create
the second triangle (bottom right, top left, bottom left). Notice that
both triangles are drawn with the same winding (counter-clockwise orientation).
I've seen quite a few web sites that claim every second triangle is wound
the opposite direction. This is not the case. OpenGL will rearrange
the vertices to ensure that all of the triangles are wound the same way!
There are two good reasons
to use triangle strips. First, after specifying the first three vertices
for the initial triangle, you only need to specify a single point for each
additional triangle. That point will be combined with 2 previous vertices
to create a triangle. Secondly, by cutting back the amount of data needed
to create a triangle your program will run quicker, and the amount of code
or data required to draw an object is greatly reduced.
Note: The number
of triangles you see on the screen will be the number of vertices you specify
minus 2. In the code below we have 4 vertices and we see two triangles. |
|
|
|
|
glTexCoord2d(1,1); glVertex3f(x+0.5f,y+0.5f,z); // Top Right
glTexCoord2d(0,1); glVertex3f(x-0.5f,y+0.5f,z); // Top Left
glTexCoord2d(1,0); glVertex3f(x+0.5f,y-0.5f,z); // Bottom Right
glTexCoord2d(0,0); glVertex3f(x-0.5f,y-0.5f,z); // Bottom Left
|
|
|
|
|
Finally
we tell OpenGL that we are done drawing our triangle strip. |
|
|
|
|
glEnd(); // Done Building Triangle Strip
|
|
|
|
|
Now
we can move the particle. The math below may look strange, but once again,
it's pretty simple. First we take the current particle x position. Then
we add the x movement value to the particle divided by slowdown
times 1000. So if our particle was in the center of the screen on the x
axis (0), our movement variable (xi) for the x axis was +10 (moving
us to the right) and slowdown was equal to 1, we would be moving
to the right by 10/(1*1000), or 0.01f. If we increase the slowdown to 2
we'll only be moving at 0.005f. Hopefully that helps you understand how
slowdown works.
That's also why multiplying
the start values by 10.0f made the pixels move alot faster, creating an
explosion.
We use the same formula
for the y and z axis to move the particle around on the screen. |
|
|
|
|
particle[loop].x+=particle[loop].xi/(slowdown*1000); // Move On The X Axis By X Speed
particle[loop].y+=particle[loop].yi/(slowdown*1000); // Move On The Y Axis By Y Speed
particle[loop].z+=particle[loop].zi/(slowdown*1000); // Move On The Z Axis By Z Speed
|
|
|
|
|
After
we've calculated where to move the particle to next, we have to apply gravity
or resistance. In the first line below, we do this by adding our resistance
(xg) to the speed we are moving at (xi).
Lets say our moving speed
was 10 and our resistance was 1. Each time our particle was drawn resistance
would act on it. So the second time it was drawn, resistance would act,
and our moving speed would drop from 10 to 9. This causes the particle
to slow down a bit. The third time the particle is drawn, resistance would
act again, and our moving speed would drop to 8. If the particle burns
for more than 10 redraws, it will eventually end up moving the opposite
direction because the moving speed would become a negative value.
The resistance is applied
to the y and z moving speed the same way it's applied to the x moving speed. |
|
|
|
|
particle[loop].xi+=particle[loop].xg; // Take Pull On X Axis Into Account
particle[loop].yi+=particle[loop].yg; // Take Pull On Y Axis Into Account
particle[loop].zi+=particle[loop].zg; // Take Pull On Z Axis Into Account
|
|
|
|
|
The
next line takes some life away from the particle. If we didn't do this,
the particle would never burn out. We take the current life of the particle
and subtract the fade value for that particle. Each particle will have
a different fade value, so they'll all burn out at different speeds. |
|
|
|
|
particle[loop].life-=particle[loop].fade; // Reduce Particles Life By 'Fade'
|
|
|
|
|
Now
we check to see if the particle is still alive after having life taken
from it. |
|
|
|
|
if (particle[loop].life<0.0f) // If Particle Is Burned Out
{
|
|
|
|
|
If
the particle is dead (burnt out), we'll rejuvenate it. We do this by giving
it full life and a new fade speed. |
|
|
|
|
particle[loop].life=1.0f; // Give It New Life
particle[loop].fade=float(rand()%100)/1000.0f+0.003f; // Random Fade Value
|
|
|
|
|
We
also reset the particles position to the center of the screen. We do this
by resetting the x, y and z positions of the particle to zero. |
|
|
|
|
particle[loop].x=0.0f; // Center On X Axis
particle[loop].y=0.0f; // Center On Y Axis
particle[loop].z=0.0f; // Center On Z Axis
|
|
|
|
|
After
the particle has been reset to the center of the screen, we give it a new
moving speed / direction. Notice I've increased the maximum and minimum
speed that the particle can move at from a random value of 50 to a value
of 60, but this time we're not going to multiply the moving speed by 10.
We don't want an explosion this time around, we want slower moving particles.
Also notice that I add
xspeed to the x axis moving speed, and yspeed to the y axis
moving speed. This gives us control over what direction the particles move
later in the program. |
|
|
|
|
particle[loop].xi=xspeed+float((rand()%60)-32.0f); // X Axis Speed And Direction
particle[loop].yi=yspeed+float((rand()%60)-30.0f); // Y Axis Speed And Direction
particle[loop].zi=float((rand()%60)-30.0f); // Z Axis Speed And Direction
|
|
|
|
|
Lastly
we assign the particle a new color. The variable col holds a number
from 0 to 11 (12 colors). We use this variable to look of the red, green
and blue intensities in our color table that we made at the beginning of
the program. The first line below sets the red (r) intensity to
the red value stored in colors[col][0]. So if col was 0, the red
intensity would be 1.0f. The green and blue values are read the same way.
If you don't understand
how I got the value of 1.0f for the red intensity if col is 0, I'll explain
in a bit more detail. Look at the very top of the program. Find the line:
static GLfloat colors[12][3]. Notice there are 12 groups of 3 number. The
first of the three number is the red intensity. The second value is the
green intensity and the third value is the blue intensity. [0], [1] and
[2] below represent the 1st, 2nd and 3rd values I just mentioned. If col
is equal to 0, we want to look at the first group. 11 is the last group
(12th color). |
|
|
|
|
particle[loop].r=colors[col][0]; // Select Red From Color Table
particle[loop].g=colors[col][1]; // Select Green From Color Table
particle[loop].b=colors[col][2]; // Select Blue From Color Table
}
|
|
|
|
|
The
line below controls how much gravity there is pulling upward. By pressing
8 on the number pad, we increase the yg (y gravity) variable. This
causes a pull upwards. This code is located here in the program because
it makes our life easier by applying the gravity to all of our particles
thanks to the loop. If this code was outside the loop we'd have to create
another loop to do the same job, so we might as well do it here. |
|
|
|
|
// If Number Pad 8 And Y Gravity Is Less Than 1.5 Increase Pull Upwards
if (keys[VK_NUMPAD8] && (particle[loop].yg<1.5f)) particle[loop].yg+=0.01f;
|
|
|
|
|
This
line has the exact opposite affect. By pressing 2 on the number pad we
decrease yg creating a stronger pull downwards. |
|
|
|
|
// If Number Pad 2 And Y Gravity Is Greater Than -1.5 Increase Pull Downwards
if (keys[VK_NUMPAD2] && (particle[loop].yg>-1.5f)) particle[loop].yg-=0.01f;
|
|
|
|
|
Now
we modify the pull to the right. If the 6 key on the number pad is pressed,
we increase the pull to the right. |
|
|
|
|
// If Number Pad 6 And X Gravity Is Less Than 1.5 Increase Pull Right
if (keys[VK_NUMPAD6] && (particle[loop].xg<1.5f)) particle[loop].xg+=0.01f;
|
|
|
|
|
Finally,
if the 4 key on the number pad is pressed, our particle will pull more
to the left. These keys give us some really cool results. For example,
you can make a stream of particles shooting straight up in the air. By
adding some gravity pulling downwards you can turn the stream of particles
into a fountain of water! |
|
|
|
|
// If Number Pad 4 And X Gravity Is Greater Than -1.5 Increase Pull Left
if (keys[VK_NUMPAD4] && (particle[loop].xg>-1.5f)) particle[loop].xg-=0.01f;
|
|
|
|
|
I
added this bit of code just for fun. My brother thought the explosion was
a cool effect :) By pressing the tab key all the particles will be reset
back to the center of the screen. The moving speed of the particles will
once again be multiplied by 10, creating a big explosion of particles.
After the particles fade out, your original effect will again reappear. |
|
|
|
|
if (keys[VK_TAB]) // Tab Key Causes A Burst
{
particle[loop].x=0.0f; // Center On X Axis
particle[loop].y=0.0f; // Center On Y Axis
particle[loop].z=0.0f; // Center On Z Axis
particle[loop].xi=float((rand()%50)-26.0f)*10.0f; // Random Speed On X Axis
particle[loop].yi=float((rand()%50)-25.0f)*10.0f; // Random Speed On Y Axis
particle[loop].zi=float((rand()%50)-25.0f)*10.0f; // Random Speed On Z Axis
}
}
}
return TRUE; // Everything Went OK
}
|
|
|
|
|
The
code in KillGLWindow(), CreateGLWindow() and WndProc() hasn't changed,
so we'll skip down to WinMain(). I'll rewrite the entire section of code
to make it easier to follow through the code. |
|
|
|
|
int WINAPI WinMain( HINSTANCE hInstance, // Instance
HINSTANCE hPrevInstance, // Previous Instance
LPSTR lpCmdLine, // Command Line Parameters
int nCmdShow) // Window Show State
{
MSG msg; // Windows Message Structure
BOOL done=FALSE; // Bool Variable To Exit Loop
// Ask The User Which Screen Mode They Prefer
if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?", "Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO)
{
fullscreen=FALSE; // Windowed Mode
}
// Create Our OpenGL Window
if (!CreateGLWindow("NeHe's Particle Tutorial",640,480,16,fullscreen))
{
return 0; // Quit If Window Was Not Created
}
|
|
|
|
|
This
is our first change to WinMain(). I've added some code to check if the
user decide to run in fullscreen mode or windowed mode. If they decide
to use fullscreen mode, I change the variable
slowdown to 1.0f instead
of 2.0f. You can leave this bit code out if you want. I added the code
to speed up fullscreen mode on my 3dfx (runs ALOT slower than windowed
mode for some reason). |
|
|
|
|
if (fullscreen) // Are We In Fullscreen Mode ( ADD )
{
slowdown=1.0f; // Speed Up The Particles (3dfx Issue) ( ADD )
}
while(!done) // Loop That Runs Until done=TRUE
{
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) // Is There A Message Waiting?
{
if (msg.message==WM_QUIT) // Have We Received A Quit Message?
{
done=TRUE; // If So done=TRUE
}
else // If Not, Deal With Window Messages
{
TranslateMessage(&msg); // Translate The Message
DispatchMessage(&msg); // Dispatch The Message
}
}
else // If There Are No Messages
{
if ((active && !DrawGLScene()) || keys[VK_ESCAPE]) // Updating View Only If Active
{
done=TRUE; // ESC or DrawGLScene Signalled A Quit
}
else // Not Time To Quit, Update Screen
{
SwapBuffers(hDC); // Swap Buffers (Double Buffering)
|
|
|
|
|
I
was a little sloppy with the next bit of code. Usually I don't include
everything on one line, but it makes the code look a little cleaner :)
The line below checks
to see if the + key on the number pad is being pressed. If it is and
slowdown
is greater than 1.0f we decrease slowdown by 0.01f. This causes
the particles to move faster. Remember in the code above when I talked
about slowdown and how it affects the speed at which the particles
travel. |
|
|
|
|
if (keys[VK_ADD] && (slowdown>1.0f)) slowdown-=0.01f; // Speed Up Particles
|
|
|
|
|
This
line checks to see if the - key on the number pad is being pressed. If
it is and
slowdown is less than 4.0f we increase the value of slowdown.
This causes our particles to move slower. I put a limit of 4.0f because
I wouldn't want them to move much slower. You can change the minimum and
maximum speeds to whatever you want :) |
|
|
|
|
if (keys[VK_SUBTRACT] && (slowdown<4.0f)) slowdown+=0.01f; // Slow Down Particles
|
|
|
|
|
The
line below check to see if Page Up is being pressed. If it is, the variable
zoom is increased. This causes the particles to move closer to us. |
|
|
|
|
if (keys[VK_PRIOR]) zoom+=0.1f; // Zoom In
|
|
|
|
|
This
line has the opposite effect. By pressing Page Down, zoom is decreased
and the scene moves futher into the screen. This allows us to see more
of the screen, but it makes the particles smaller. |
|
|
|
|
if (keys[VK_NEXT]) zoom-=0.1f; // Zoom Out
|
|
|
|
|
The
next section of code checks to see if the return key has been pressed.
If it has and it's not being 'held' down, we'll let the computer know it's
being pressed by setting rp to true. Then we'll toggle rainbow mode. If
rainbow was true, it will become false. If it was false, it will
become true. The last line checks to see if the return key was released.
If it was,
rp is set to false, telling the computer that the key
is no longer being held down. |
|
|
|
|
if (keys[VK_RETURN] && !rp) // Return Key Pressed
{
rp=true; // Set Flag Telling Us It's Pressed
rainbow=!rainbow; // Toggle Rainbow Mode On / Off
}
if (!keys[VK_RETURN]) rp=false; // If Return Is Released Clear Flag
|
|
|
|
|
The
code below is a little confusing. The first line checks to see if the spacebar
is being pressed and not held down. It also check to see if rainbow mode
is on, and if so, it checks to see if the variable delay is greater
than 25. delay is a counter I use to create the rainbow effect.
If you were to change the color ever frame, the particles would all be
a different color. By creating a delay, a group of particles will become
one color, before the color is changed to something else.
If the spacebar was pressed
or rainbow is on and delay is greater than 25, the color will be
changed! |
|
|
|
|
if ((keys[' '] && !sp) || (rainbow && (delay>25))) // Space Or Rainbow Mode
{
|
|
|
|
|
The
line below was added so that rainbow mode would be turned off if the spacebar
was pressed. If we didn't turn off rainbow mode, the colors would continue
cycling until the return key was pressed again. It makes sense that if
the person is hitting space instead of return that they want to go through
the colors themselves. |
|
|
|
|
if (keys[' ']) rainbow=false; // If Spacebar Is Pressed Disable Rainbow Mode
|
|
|
|
|
If
the spacebar was pressed or rainbow mode is on, and delay is greater
than 25, we'll let the computer know that space has been pressed by making
sp equal true. Then we'll set the delay back to 0 so that it can
start counting back up to 25. Finally we'll increase the variable
col
so that the color will change to the next color in the color table. |
|
|
|
|
sp=true; // Set Flag Telling Us Space Is Pressed
delay=0; // Reset The Rainbow Color Cycling Delay
col++; // Change The Particle Color
|
|
|
|
|
If
the color is greater than 11, we reset it back to zero. If we didn't reset
col to zero, our program would try to find a 13th color. We only
have 12 colors! Trying to get information about a color that doesn't exist
would crash our program. |
|
|
|
|
if (col>11) col=0; // If Color Is To High Reset It
}
|
|
|
|
|
Lastly
if the spacebar is no longer being pressed, we let the computer know by
setting the variable sp to false. |
|
|
|
|
if (!keys[' ']) sp=false; // If Spacebar Is Released Clear Flag
|
|
|
|
|
Now
for some control over the particles. Remember that we created 2 variables
at the beginning of our program? One was called xspeed and one was
called yspeed. Also remember that after the particle burned out,
we gave it a new moving speed and added the new speed to either xspeed
or yspeed. By doing that we can influence what direction the particles
will move when they're first created.
For example. Say our particle
had a moving speed of 5 on the x axis and 0 on the y axis. If we decreased
xspeed until it was -10, we would be moving at a speed of -10 (xspeed)
+ 5 (original moving speed). So instead of moving at a rate of 10 to the
right we'd be moving at a rate of -5 to the left. Make sense?
Anyways. The line below
checks to see if the up arrow is being pressed. If it is, yspeed
will be increased. This will cause our particles to move upwards. The particles
will move at a maximum speed of 200 upwards. Anything faster than that
doesn't look to good. |
|
|
|
|
// If Up Arrow And Y Speed Is Less Than 200 Increase Upward Speed
if (keys[VK_UP] && (yspeed<200)) yspeed+=1.0f;
|
|
|
|
|
This
line checks to see if the down arrow is being pressed. If it is, yspeed
will be decreased. This will cause the particles to move downward. Again,
a maximum downward speed of 200 is enforced. |
|
|
|
|
// If Down Arrow And Y Speed Is Greater Than -200 Increase Downward Speed
if (keys[VK_DOWN] && (yspeed>-200)) yspeed-=1.0f;
|
|
|
|
|
Now
we check to see if the right arrow is being pressed. If it is, xspeed
will be increased. This will cause the particles to move to the right.
A maximum speed of 200 is enforced. |
|
|
|
|
// If Right Arrow And X Speed Is Less Than 200 Increase Speed To The Right
if (keys[VK_RIGHT] && (xspeed<200)) xspeed+=1.0f;
|
|
|
|
|
Finally
we check to see if the left arrow is being pressed. If it is... you guessed
it...
xspeed is decreased, and the particles start to move left.
Maximum speed of 200 enforced. |
|
|
|
|
// If Left Arrow And X Speed Is Greater Than -200 Increase Speed To The Left
if (keys[VK_LEFT] && (xspeed>-200)) xspeed-=1.0f;
|
|
|
|
|
The
last thing we need to do is increase the variable delay. Like I
said above, delay is used to control how fast the colors change
when you're using rainbow mode. |
|
|
|
|
delay++; // Increase Rainbow Mode Color Cycling Delay Counter
|
|
|
|
|
Like
all the previous tutorials, make sure the title at the top of the window
is correct. |
|
|
|
|
if (keys[VK_F1]) // Is F1 Being Pressed?
{
keys[VK_F1]=FALSE; // If So Make Key FALSE
KillGLWindow(); // Kill Our Current Window
fullscreen=!fullscreen; // Toggle Fullscreen / Windowed Mode
// Recreate Our OpenGL Window
if (!CreateGLWindow("NeHe's Particle Tutorial",640,480,16,fullscreen))
{
return 0; // Quit If Window Was Not Created
}
}
}
}
}
// Shutdown
KillGLWindow(); // Kill The Window
return (msg.wParam); // Exit The Program
}
|
|
|
|
|
In
this lesson, I have tried to explain in as much detail, all the steps required
to create a simple but impressive particle system. This particle system
can be used in games of your own to create effects such as Fire, Water,
Snow, Explosions, Falling Stars, and more. The code can easily be modified
to handle more parameters, and new effects (fireworks for example).
Thanks
to Richard Nutman
for suggesting that the particles be positioned with glVertex3f() instead
of resetting the Modelview Matrix and repositioning each particle with
glTranslatef(). Both methods are effective, but his method will reduce
the amount of work the computer has to do before it draws each particle,
causing the program to run even faster.
Thanks
to Antoine Valentim
for suggesting triangle strips to help speed up the program and to introduce
a new command to this tutorial. The feedback on this tutorial has been
great, I appreciate it!
I hope you enjoyed this
tutorial. If you had any problems understanding it, or you've found a mistake
in the tutorial please let me know. I want to make the best tutorials available.
Your feedback is important!
Jeff
Molofee (NeHe)
* DOWNLOAD Visual
C++ Code For This Lesson.
* DOWNLOAD Visual
C++ / OpenIL Code For This Lesson. ( Conversion by Denton
Woods )
* DOWNLOAD Visual
Basic Code For This Lesson. ( Conversion by Edo
)
* DOWNLOAD Cygwin
(FREE Language) Code For This Lesson.
( Conversion by Stephan Ferraro
)
* DOWNLOAD Linux
Code For This Lesson. ( Conversion by Ken
Rockot )
* DOWNLOAD Delphi
Code For This Lesson. ( Conversion by Marc
Aarts )
* DOWNLOAD Mac
OS Code For This Lesson. ( Conversion by Owen
Borstad )
* DOWNLOAD Irix
Code For This Lesson. ( Conversion by Dimitrios
Christopoulos )
* DOWNLOAD MingW32
& Allegro Code For This Lesson. ( Conversion by Peter
Puck )
* DOWNLOAD Borland
C++ Builder 4.0 Code For This Lesson. ( Conversion by Patrick
Salmons ) |
|
|
|
|
|
Back
To NeHe Productions!
|
|
|
|
|