Implementing a Basic Particle System


You can think of a particle system as a system that generates small pieces of materials which are responding to some sort of environmental change. For example, a particle system could generate snowflakes, fog, sparks, or dust. In Tankers, you have a tank firing a projectile out of its barrel. Adding a small bit of spark particles to this firing should be easy to do.

Direct3D uses point sprites to render these particles. In the context of Direct3D, they are essentially generalizations of points in space that are rendered using textures. Adding them is also quite simple. In your project file, add a new file called specialeffects.cs and add the code from Listing 18.1 to the code file.

Listing 18.1. Starting the Particle System
 using System; using System.Windows.Forms; using Microsoft.DirectX; using Microsoft.DirectX.Direct3D; using Microsoft.Samples.DirectX.UtilityToolkit; namespace Tankers {     /// <summary>     /// Information about a particle     /// </summary>     public struct Particle     {         public Vector3 positionVector;       // Current position         public Vector3 velocityVector;       // Current velocity         public Vector3 initialPosition;      // Initial position         public Vector3 initialVelocity;      // Initial velocity         public float creationTime;     // Time of creation         public ColorValue diffuseColor; // Initial diffuse color         public ColorValue fadeColor;    // Faded diffuse color         public float fadeProgression;      // Fade progression     }; } 

Before you actually add the particle system into the application, you need to know a few things about the particles you'll be designing for the game. This structure you've just declared will maintain all this information, and as you can see, it stores four different vectors. There is one each for the current position and velocity of the given particle, along with one each for the initial position and velocity for the particle.

You also want to know when this particular particle was created because in this scenario (sparks), the particles do not last forever; they fade away. Speaking of fading away, the next two items are the initial color of the spark and the color the spark should fade into. A fade progression member will determine how to interpolate between the two colors you have.

Construction Cue

Reusing objects that have already been created is almost always a good performance win. It is rarely better performancewise to create a new object rather than simply use one over again. It's important to remember this point.


Now that you have the particle structure declared, you can start implementing the special effect class. In the same code file, take the code in Listing 18.2 and implement it in your code.

Listing 18.2. The Special Effects Class
 /// <summary> /// Our special effects class will use a particle system /// </summary> public class SpecialEffects : IDisposable {     #region Constant Data     private static readonly ColorValue WhiteSmoke = new ColorValue(         (float)System.Drawing.Color.WhiteSmoke.R / 255.0f,         (float)System.Drawing.Color.WhiteSmoke.G / 255.0f,         (float)System.Drawing.Color.WhiteSmoke.B / 255.0f,         1.0f);     private static readonly ColorValue Black = new ColorValue(         0.0f, 0.0f, 0.0f, 0.0f);     private const int MaximumParticles = 1024;     // This seems like a nice round number     private static readonly int VertexBufferSize =       CustomVertex.PositionColored.StrideSize * MaximumParticles;     private static readonly int FlushSize = MaximumParticles / 4;     private static readonly int FlushLockSize =       CustomVertex.PositionColored.StrideSize * FlushSize;     #endregion } 

The first two constants you've declared are the starting color and the faded color of the particle. You want the particle color to start like the white smoke color; notice how you've calculated the color into a ColorValue structure. In the System.Drawing.Color structure where WhiteSmoke is a member, there is a single integer (4 bytes), where each byte represents one of the basic colors, red (R), green (G), and blue (B), and an extra byte represents the alpha (A). Each of the color components range from 0 (no color of this component) to 255 (full color of this component), so to calculate the floating-point color value, you take the color component value and divide it by 255.0f. Because the fade color is black, this calculation isn't necessary. All components (other than alpha) are 0.

The next few constants determine the rendering behavior of the particle system. The first controls the maximum number of particles available in the system at any given time, and the next is the size of the vertex buffer that needs to be created to handle these particles.

Up until this point, all the rendering was done with meshes, and you haven't had to deal with a VertexBuffer outside of briefly calculating a bounding sphere or handle level collisions. A vertex buffer is essentially just a place on your Direct3D device where you can store vertices for later use (namely, rendering). In all actuality, the meshes that you've been using to render with so far are all using these vertex buffers in their private implementations anyway; you just might not have been aware of it at the time. The rest of the constants deal with when a vertex buffer is "flushed" (more on that when you get there) and the size of the lock when the flush happens.

Now, the first code you write for the particle system is the constructor in Listing 18.3.

Listing 18.3. Creating the Special Effects Class
 /// <summary> /// Constructor for creating the special effects object /// </summary> public SpecialEffects(Device device) {     System.Diagnostics.Debug.Assert(device != null,         "You must pass in a valid device to create this class.");     // The texture needs to be created once     particleTexture = TexturePool.CreateTexture(device, "particle.bmp");     // Hook the device events since the vertex buffer will need to be destroyed     // and re-created when the device is lost or reset     device.DeviceLost += new EventHandler(OnDeviceLost);     device.DeviceReset += new EventHandler(OnDeviceReset);     // Fire the OnDeviceRest once     OnDeviceReset(device, EventArgs.Empty); } 

You'll first notice that you create a texture that hasn't been declared yet. You'll declare the instance variables for the class in just a moment. This texture is the same texture used for every particle in the system, and if you look at it from the media folder, you notice it looks similar to a small round circle. This causes the particles to take this shape as well. Because you deal directly with device resources, you need to hook the lost and reset events yourself to handle those situations, and because you can't be sure when the device reset will occur, you call that method directly the first time.

Before you go on, you need to declare the following instance variables for this class:

 // We will store one particle texture private Texture particleTexture = null; // A vertex buffer will be needed to render the particles private VertexBuffer vertexBuffer = null; private float time = 0.0f; private int baseParticle = MaximumParticles; private int particles = 0; private System.Collections.ArrayList particlesList =  new System.Collections.ArrayList(); private System.Collections.ArrayList freeParticles =  new System.Collections.ArrayList(); private System.Random rand = new System.Random(); 

Naturally, the vertex buffer and texture are needed because I already discussed them. You also want to maintain the current application time, as well as the base particle you are currently working on. Aside from the total number of particles in the system, you need two collections of particles, one containing the particles currently being used and the other containing the currently free particles. Last, because you don't want each of the particles to be fired from the exact same location with the exact same trajectory, you supply a little randomness.

You still need to implement the IDisposable interface as well as the device event handlers you declared earlier, so add the code from Listing 18.4 to the class now.

Listing 18.4. Event Handlers and Cleanup
 /// <summary> /// Clean up the vertex buffer used by the special effects /// </summary> public void Dispose() {     // The OnDeviceLost method does what we need, use it     OnDeviceLost(null, EventArgs.Empty); } /// <summary> /// Fired when the device is about to be lost /// </summary> private void OnDeviceLost(object sender, EventArgs e) {     // The vertex buffer simply needs to be destroyed here     if (vertexBuffer != null)     {         vertexBuffer.Dispose();     }     vertexBuffer = null; } /// <summary> /// Fired after the device has been reset /// </summary> private unsafe void OnDeviceReset(object sender, EventArgs e) {     Device device = sender as Device;     System.Diagnostics.Debug.Assert(device != null,         "For some reason the event handler has no device.");     // Create a vertex buffer for the particle system. The size of this buffer     // does not relate to the number of particles that exist. Rather, the     // buffer is used as a communication channel with the device. We fill in     // a bit, and tell the device to draw. While the device is drawing, we     // fill in the next bit using NoOverwrite. We continue doing this until     // we run out of vertex buffer space, and are forced to discard the buffer     // and start over at the beginning.     vertexBuffer = new VertexBuffer(device, VertexBufferSize,         Usage.Dynamic | Usage.WriteOnly | Usage.Points,         CustomVertex.PositionColored.Format, Pool.Default); } 

Because the cleanup is the same for either the device lost or the special effects object being disposed, you could have either method call the other. In this case, the Dispose method is calling the OnDeviceLost method. In that method, the only thing required is the cleanup of the vertex buffer. Because the texture is created as a part of the texture pool, it isn't required to be cleaned up by this object because it will be destroyed when the texture pool is.

When the device is reset, on the other hand, the vertex buffer needs to be re-created because the plan is to put the vertex buffer in the default memory pool (which is video memory). That's also the reason why the vertex buffer needs to be destroyed when a device is lost. All resources that live in the default memory pool must be destroyed before a device is allowed to be reset. Look at the constructor for the vertex buffer that is being used:

 public VertexBuffer ( Microsoft.DirectX.Direct3D.Device device ,    System.Int32 sizeOfBufferInBytes ,    Microsoft.DirectX.Direct3D.Usage usage ,    Microsoft.DirectX.Direct3D.VertexFormats vertexFormat ,    Microsoft.DirectX.Direct3D.Pool pool ) 

The device is always needed for any resources that are created, so that's not a big surprise. The next parameter is the size of the vertex buffer in bytes, and as you can see, you've used the constant that was declared earlier for this value. The usage parameter tells Direct3D how you plan to use this resource; in this case, you plan to use it for point sprites, as a dynamic vertex buffer, and you never read from it. This set is the most efficient set of parameters you can pass to this method for this scenario.

Next, you need to tell Direct3D the format of the data you will be filling the vertex buffer with, and in your case, it contains the position of the vertex along with its color. (Texture coordinates are implied with point sprites.) Finally, you specify the memory pool that this resource should occupy, in this case, the default pool.

Now that you have the class created and the resources behaving correctly, you need a way to create the particles when the tank fires its projectile. Take the method in Listing 18.5 and add it to the class.

Listing 18.5. Adding Sparks
 /// <summary> /// Add new 'firing' effect /// </summary> public void AddFiringEffect(int numberParticlesAdding, Vector3 currentPosition,                              Vector3 vel) {     // Add some new firing particles     int particlesEmit = particles + numberParticlesAdding;     while((particles < MaximumParticles) && (particles < particlesEmit))     {         Particle particle;         if (freeParticles.Count > 0)         {             particle = (Particle)freeParticles[0];             freeParticles.RemoveAt(0);         }         else         {             particle = new Particle();         }         // Emit new particle         particle.initialPosition = currentPosition;         particle.initialVelocity = Vector3.Normalize(vel);         particle.initialVelocity.X += ((float)rand.NextDouble() > 0.5f)              ? ((float)rand.NextDouble() * 0.1f) :              (((float)rand.NextDouble() * 0.1f) * -1);         particle.initialVelocity.Y += ((float)rand.NextDouble() > 0.5f)              ? ((float)rand.NextDouble() * 0.1f) :              (((float)rand.NextDouble() * 0.1f) * -1);         particle.initialVelocity.Z += ((float)rand.NextDouble() > 0.5f)              ? ((float)rand.NextDouble() * 0.1f) :              (((float)rand.NextDouble() * 0.1f) * -1);         particle.initialVelocity.Normalize();         particle.initialVelocity.Scale(vel.Length() * (float)rand.NextDouble());         particle.positionVector = particle.initialPosition;         particle.velocityVector = particle.initialVelocity;         particle.diffuseColor = WhiteSmoke;         particle.fadeColor = Black;         particle.fadeProgression = (float)rand.NextDouble();         particle.creationTime = time;         particlesList.Add(particle);         particles++;     } } 

This method might appear a bit scary at first glance, but once you break it down, it really is quite simple. The goal of this method is to add a group of particles to the system any time it is called. As you can imagine, a single spark particle generated during firing wouldn't seem overly realistic (or exciting), so when this method is called, you pass in a number of particles to add. The two vectors that are passed in are the ones you calculated earlier for the bullet's position and velocity because that's where the sparks will be shooting from.

Now you need to calculate the number of particles that will be emitted once this method is complete, which is the sum of the current number of particles in the system and the number of particles you are trying to add now. You then begin adding particles in a loop until you've added the correct number or until the number of particles in the system has reached the maximum number you specified in the constant. You could simply increase the constant if you wanted to add more particles, but with the gun cool-down rate and the current constant value, you wouldn't be able to get to the maximum number anyway.

The first thing you do in the loop is determine whether any particles are in the free array list you have stored. If so, you retrieve that particle and remove it from the free list because you'll be adding it to the active list in a few moments. Next, you store the initial position and velocity using the parameters passed in, and then, you take that initial velocity and randomize it so that each particle does not take off in the exact same direction. After you calculate a new random velocity (based on the initial velocity), you store the current position and velocity based on the initial values (because this is the initial point in the particle). You set the diffuse and fade colors to the constants defined earlier, set the creation time to the current time, and add the particle to your list. Finally, you update the particle count and continue with the loop until all the particles are added.

You might have noticed that you were taking particles out of the free list during that last method, but you haven't defined any code where you're adding any dead particles to that list yet! You add any particles that have "died" into that list, so you need a way to determine when a particle has died and to update all the particles in the system. Add the method from Listing 18.6 to your class to do so.

Listing 18.6. Updating the Particle System
 /// <summary> /// Updates the particles in the scene /// </summary> public void Update(float elapsedTime) {     time += elapsedTime;     for (int ii = particlesList.Count-1; ii >= 0; ii--)     {         Particle p = (Particle)particlesList[ii];         // Calculate new position         float fT = time - p.creationTime;         float fGravity;         fGravity = -290.8f;         p.fadeProgression -= elapsedTime * 0.25f;         p.positionVector    = p.initialVelocity * fT + p.initialPosition;         p.positionVector.Y += (0.5f * fGravity) * (fT * fT);         p.velocityVector.Y  = p.initialVelocity.Y + fGravity * fT;         if (p.fadeProgression < 0.0f)             p.fadeProgression = 0.0f;         // Kill old particles         if (p.fadeProgression <= 0.0f)         {             // Kill particle             freeParticles.Add(p);             particlesList.RemoveAt(ii);             particles--;         }         else             particlesList[ii] = p;     } } 

This method is eventually called every frame, so everything that needs to happen to update the particles should happen here. Notice first that the total time is constantly being updated by adding the elapsed time since the last frame. This step ensures that any particles are allowed to die quickly enough. Once done, you start looping through each particle starting at the last one and moving to the first and perform a series of operations on each. First you calculate how long the particle has been alive, and then you update the fade progression. You update the position of the particle based on the time it's been alive and then update the y axis once more to account for gravity. Finally, you determine whether the particle has died by checking the fade progression, and if it's now below zero, you add the particle to the free list and remove it from the active list. Otherwise, you store it in the active list.

Construction Cue

You start at the end of the array list and move to the beginning because you might be removing members of the list. If you start at the beginning and remove a member, you just reorder the items you haven't had the chance to look at yet. By moving in reverse, any removal reorders only the items you've already processed.




Beginning 3D Game Programming
Beginning 3D Game Programming
ISBN: 0672326612
EAN: 2147483647
Year: 2003
Pages: 191
Authors: Tom Miller

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net