NeHe Productions: OpenGL Lesson #19
Date de publication : 31/03/2006 , Date de mise à jour : 31/03/2006
Lesson: 19 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 we're 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.
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.
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!
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.
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.
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.
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.
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.
particles is the name of our 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.
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.
Our bitmap loading code hasn't changed.
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.
Our texture loading code will load in our particle bitmap and convert it to a linear filtered texture.
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.
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.
We make sure smooth shading is enabled, set the background clear color to black, disable depth testing and enable blending and texture mapping. After enabling texture mapping we select our particle 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.
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.
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 multiply it by the number of colors in our color table divided by the maximum number of particles (MAX_PARTICLES). This prevents the final color value from being higher than our max number of colors (12).
Some quick examples: 900*(12/900)=12. 1000*(12/1000)=12, etc.
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.
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.
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 translations, that way we don't alter the modelview matrix while drawing our particles.
We start off by creating a loop. This loop will update each one of our 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.
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.
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.
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.
Quoted directly from the red book: A triangle strip draws a series of triangles (three sided polygons) using vertices V 0 , V 1 , V 2 , then V 2 , V 1 , V 3 (note the order), then V 2 , V 3 , V 4 , 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.
Finally we tell OpenGL that we are done drawing our 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.
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.
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.
Now we check to see if the particle is still alive after having life taken from it.
If the particle is dead (burnt out), we'll rejuvenate it. We do this by giving it full life and a new fade speed.
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.
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.
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]. 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. 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. ,  and  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).
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.
This line has the exact opposite affect. By pressing 2 on the number pad we decrease yg creating a stronger pull downwards.
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.
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!
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.
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.
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).
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.
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 :)
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.
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.
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.
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!
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 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.
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.
Lastly if the spacebar is no longer being pressed, we let the computer know by setting the variable sp to false.
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.
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.
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.
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.
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.
Like all the previous tutorials, make sure the title at the top of the window is correct.
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 )
Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2006 Nehe Gamedev.net. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.