Adding Environmental Effects: Particle Systems


All of the objects that we have added to the scene so far have been static. They do not change at all as time goes by. The workhorse of 3D game special effects is the particle system. Particle systems are composed of two components : a particle generator and particles. A particle generator is an invisible object that creates a given type of particle. The created particles will have an initial position, velocity, size , color , and texture based on settings in the generator. By varying these settings as well as the dynamics of the particle, we can simulate a wide variety of special effects. These effects can include smoke, fire, explosions, water, fireworks, and more. The sample game for this book uses particles to represent the sand kicked up from the tires of the vehicles as they are driving around.

One of the main features of particles is the fact that they are dynamic. They change over time. Possible changes include position, velocity, and color. The particles also cease to exist once certain conditions are met. For the game engine s particle system to be multipurpose, we do not want to hard code a specific set of dynamics for our particles. Instead, we will use the delegate feature of C# to pass an Update method into the particle generator for use by all particles created by that generator. The signature for this delegate is shown here:

 public delegate void ParticleUpdate(ref Particle Obj, float DeltaT); 

The ParticleUpdate delegate accepts a particle reference and a float value as arguments. The particle reference specifies the particle that is going to be affected by the method. The float value represents the fractional number of seconds that have occurred since the last update. This value is typically the inverse of the frame rate. For example, if we were running at 30 Hz ( frames per second) the DeltaT value would be 0.03333333 ( roughly 33 milliseconds ). This value is used to integrate velocities from accelerations and positions from velocities. It can also be used to integrate the amount of time left before the particle is deleted ( assuming that the dynamics terminate the particle based on time). Listing 4 “34 is an example of the update function that we will use with our sand particles. It shows a simple method called Gravity that only applies the effect of gravity on the particles. It does not apply drag or wind effects, but they could be added if we wanted even more fidelity.

Listing 4.34: Example ParticleUpdateMethod
start example
 public void Gravity(ref Particle Obj, float DeltaT)  {     Obj.m_Position += Obj.m_Velocity * DeltaT;     Obj.m_Velocity.Y +=   32.0f * DeltaT;     if (Obj.m_Position.Y < 0.0f) Obj.m_bActive = false;  } 
end example
 

The Gravity method integrates a new position for the particle base on its velocity and the time constant. The acceleration of gravity is then applied by integrating the acceleration due to gravity (32.0 feet/second 2 ) into the Y velocity component. The last thing that each Update method should do is to determine if the particle should become inactive. This could be based on either the amount of time the particle has been active or the particle s current position. We will simply terminate the particle if its vertical position drops below zero. The particle generator maintains the particles based on the state of the active flag.

The particles are maintained using a structure rather than a class. This reduces some of the overhead for the particles, which is important since we may have thousands of them active at a given time within the scene. The definition of the structure is shown in Listing 4 “35. The structure holds all of the information needed to describe a given particle. The only data that is truly required for a particle is the position, color, and active state. Everything else is there to support the dynamics of the particles. The position and color are required for rendering the particle. The active flag is required, of course, so that we will know when the particle should no longer be rendered and is available for reuse.

Listing 4.35: Particle Structure Definition
start example
 public struct Particle  {     public Vector3 m_Position;     public Vector3 m_Velocity;     public float m_fTimeRemaining;     public Vector3 m_InitialPosition;     public Vector3 m_InitialVelocity;     public float m_fCreationTime;     public System.Drawing.Color m_Color;     public bool m_bActive;  } 
end example
 

As I mentioned earlier, the ParticleGenerator is the object that creates and manages a set of particles. The definition for this class is shown in Listing 4 “36. This class inherits from the Object3D class as well as the IDisposable and IDynamic interfaces. Inheriting from the Object3D class gives the particle generator all of the basic abilities and characteristics of any other 3D object. The most important of these is the ability to be rendered if it is in the currently viewable portion of the scene and the ability to be attached as a child of another object. If we were to use the particle generator for a fire or a fountain, it might simply be placed at a location in the scene and not move from that spot.

Listing 4.36: ParticleGenerator Definition
start example
 public class ParticleGenerator : Object3D, IDisposable, IDynamic  {     #region Attributes     private bool m_bValid = false;     private string m_sTexture;     private Texture m_Texture;     private int m_BaseParticle = 0;     private int m_Flush = 0;     private int m_Discard = 0;     private int m_ParticlesLimit = 2000;     private int m_Particles = 0;     private Color m_Color;     private float m_fTime = 0.0f;     private VertexBuffer m_VB;     private bool m_bActive = false;     private ArrayList m_ActiveParticles = new ArrayList();     private ArrayList m_FreeParticles = new ArrayList();     private ParticleUpdate m_Method = null;     private System.Random rand = new System.Random();     public float m_fRate = 22.0f; // Particles to create per second     public float m_fPartialParticles = 0.0f;     public float m_fEmitVel = 7.5f;     public Attitude m_Attitude;       // Window around the pitch axis for distribution (radians)     public float m_PitchWidth = 1.0f;       // Window around the heading axis for distribution (radians)     public float m_HeadingWidth = 1.0f;     public float m_PointSize = 0.02f;     public float m_PointSizeMin = 0.00f;     public float m_PointScaleA = 0.00f;     public float m_PointScaleB = 0.00f;     public float m_PointScaleC = 1.00f;     public bool Valid { get { return m_bValid; } }     public bool Active { set { m_bActive = value; } }  #endregion 
end example
 

We will be using these generators to throw up sand from the rear tires of each of the vehicles. While we could require the programmers using the game engine to manually move the particle generators around as the vehicles move, this would rapidly become tedious and unmanageable, depending on the number of vehicles. By attaching generators to the vehicle objects and providing positional and rotational offsets from the parent object, they will automatically move correctly with the vehicles.

The IDisposable interface specifies that we need a Dispose method for cleanup purposes. The IDynamic interface indicates that we will need an Update method. The members of the class can be considered as two groups. One group of members is used in managing the particles and their creation. The other group is used in the creation and rendering of the particles. The specific uses of these variables will be covered as we explore each of the methods in the class.

The first ParticleGenerator method is the copy constructor (shown in Listing 4 “37). Copy constructors are a handy way of duplicating a constructor that we have already configured. In the case of our sand generators, we will need two for each of the vehicles. If we have a total of three vehicles, we will then need six sand particle generators that are configured the same way. The copy constructor allows us to set up the first one and then make five copies. The six generators would then be attached to the vehicles. The method uses the private Copy method to copy all of the member data except the vertex buffer. Each particle generator will have its own vertex buffer that is created after the other data has been copied . The vertex buffer is created to hold as many positioned and colored points as the value of the m_Discard variable. By flagging the buffer as dynamic, we indicate to the driver that we will be changing the data. The particle generator is not set as valid unless the vertex buffer was successfully created.

Listing 4.37: ParticleGenerator Copy Constructor
start example
 public ParticleGenerator(string sName, ParticleGenerator other)     : base(sName)  {     m_sName = sName;     Copy(other);     try     {        m_VB = new VertexBuffer(typeof(CustomVertex. PositionColoredTextured),                  m_Discard, CGameEngine.Device3D,                  Usage.Dynamic  Usage.WriteOnly  Usage.Points,                  CustomVertex. PositionColoredTextured.Format, Pool.Default);        m_bValid = true;     }     catch (DirectXException d3de)     {        Console.AddLine("Unable to create vertex buffer for " + m_sTexture);        Console.AddLine(d3de.ErrorString);     }     catch (Exception e)     {        Console.AddLine("Unable to create vertex buffer for " + m_sTexture);        Console.AddLine(e.Message);     }  } 
end example
 

The normal constructor for the class (shown in Listing 4 “38) is a bit more involved. The arguments to the constructor are as follows :

Listing 4.38: ParticleGenerator Constructor
start example
 public ParticleGenerator(string sName, int numFlush, int numDiscard,     Color color, string sTextureName, ParticleUpdate method)     : base(sName)  {     m_sName = sName;     m_Color = color;     m_Flush = numFlush;     m_Discard = numDiscard;     m_sTexture = sTextureName;     m_Method = method;     try     {        m_Texture = GraphicsUtility.CreateTexture(CGameEngine.Device3D,                  m_sTexture, Format.Unknown);        try        {           m_VB = new VertexBuffer(typeof(CustomVertex. PositionColoredTextured),              m_Discard, CGameEngine.Device3D,              Usage.Dynamic  Usage.WriteOnly  Usage.Points,              CustomVertex. PositionColoredTextured.Format, Pool.Default);           m_bValid = true;        }        catch (DirectXException d3de)        {           Console.AddLine("Unable to create vertex buffer for " +              m_sTexture);           Console.AddLine(d3de.ErrorString);        }        catch (Exception e)        {           Console.AddLine("Unable to create vertex buffer for " +              m_sTexture);           Console.AddLine(e.Message);        }     }     catch (DirectXException d3de)     {        Console.AddLine("Unable to create texture " + m_sTexture);        Console.AddLine(d3de.ErrorString);     }     catch (Exception e)     {        Console.AddLine("Unable to create texture " + m_sTexture);        Console.AddLine(e.Message);     }  } 
end example
 
  • The name for the generator

  • The number of particles that are added to the vertex buffer each time prior to flushing the data to the driver

  • The number of particles to put into the vertex buffer before starting over at the beginning of the buffer

  • The color of the particles that will be created

  • The path to the texture image used for the particles

  • The delegate function that will be called to do the particle updates.

The argument values are saved into member variables. The constructor then attempts to load the texture image and create the vertex buffer. If both operations are successful, the generator is considered valid. Notice that many of the variables that configure the generator for the particle characteristics default to the values from the class definition. These members are public and are modified by the controlling program if the default values are not appropriate.

The private Copy method (shown in Listing 4 “39) is used by the copy constructor to copy member variables from the supplied class instance into the calling class. A few of the variables in this method have not been addressed yet. The m_fRate value defines the number of particles that should be created each second. The m_fEmitVel value specifies the speed in meters per second of the particles at the moment of creation. The Attitude value is the angular offset between the parent model s attitude and the angle at which the particles velocity vector will be oriented at creation time. The pitch and heading width variables define an angular cone around the attitude. Random values are calculated within this cone so that all particles created do not follow the exact same path. It provides a more natural look to the flow of particles. The point size and scale variables are used by the DirectX driver to calculate how the size of the particles are scaled with their distance from the viewpoint.

Listing 4.39: ParticleGenerator Copy Method
start example
 private void Copy(ParticleGenerator other)  {     m_sName = other.m_sName;     m_Flush = other.m_Flush;     m_Discard = other.m_Discard;     m_sTexture = other.m_sTexture;     m_Texture = other.m_Texture;     m_Method = other.m_Method;     m_fRate = other.m_fRate;     m_fEmitVel = other.m_fEmitVel;     m_Attitude = other.m_Attitude;     m_PitchWidth = other.m_PitchWidth;     m_HeadingWidth = other.m_HeadingWidth;     m_PointSize = other.m_PointSize;     m_PointSizeMin = other.m_PointSizeMin;     m_PointScaleA = other.m_PointScaleA;     m_PointScaleB = other.m_PointScaleB;     m_PointScaleC = other.m_PointScaleC;     m_bValid = other.m_bValid;  } 
end example
 

The Update method (shown in Listing 4 “40) fulfills the interface specified by IDynamic . The method is called by the GameEngine class to provide the dynamics of the particle generator. This method is responsible for generating any new particles and managing the particles that have already been created. The method integrates a time value that will be used as the creation time ”the time in seconds since game play began ”for each particle. The method then needs to determine how many particles to create during this pass. This is a function of the rate that particles are to be emitted and the amount of time that has passed. Any fractional particles that could not be created last pass are also included. The integer portion of this number is the number of particles to be created during this pass. The remainder is carried forward to the next pass. This allows us to uncouple the particle creation rate from the games frame rate. Otherwise, we would be limited to multiples of the frame rate.

Listing 4.40: ParticleGenerator Update Method
start example
 public override void Update(float DeltaT)  {     m_fTime += DeltaT;     // Emit new particles.     float TotalNewParticles = (DeltaT * m_fRate)+m_fPartialParticles ;     int NumParticlesToEmit = (int)TotalNewParticles;     m_fPartialParticles = TotalNewParticles   NumParticlesToEmit;     int particlesEmit = m_Particles + NumParticlesToEmit;     while(m_Particles < m_Particles Limit && m_Particles < particlesEmit)     {        Particle particle;        if(m_FreeParticles.Count > 0)        {           particle = (Particle)m_FreeParticles[0];           m_FreeParticles.RemoveAt(0);        }        else        {           particle = new Particle();        }       // Emit new particle.        float fRand1 = (float)(rand.NextDouble()   0.5) * m_PitchWidth;        float fRand2 = (float)(rand.NextDouble()   0.5) * m_HeadingWidth;        m_Matrix = Matrix.RotationYawPitchRoll(m_Attitude.Heading+fRand2,                             m_Attitude.Pitch+fRand1, 0.0f);        Matrix TotalMatrix;        if (m_Parent != null)        {           TotalMatrix = Matrix. Multiply(m_Matrix, m_Parent.GetMatrix);        }        else        {           TotalMatrix = m_Matrix;        }       particle.m_InitialVelocity = Vector3.TransformCoordinate(new Vector3(0.0f, 0.0f, m_fEmitVel), TotalMatrix);        particle.m_InitialPosition = Vector3. TransformCoordinate (m_vPosition, TotalMatrix);        particle.m_Position = particle.m_InitialPosition;        particle.m_Velocity = particle.m_InitialVelocity;        particle.m_Color = m_Color;        particle.m_fCreationTime = m_fTime;        particle.m_bActive = true;        m_ActiveParticles.Add(particle);        m_Particles++;     }     for (int i=0; i < m_ActiveParticles.Count; i++)     {        Particle p = (Particle)m_ActiveParticles[i];        m_Method(ref p, DeltaT);        if (p.m_bActive)        {           m_ActiveParticles[i] = p;        }        else        {           m_ActiveParticles.RemoveAt(i);           m_FreeParticles.Add(p);           m_Particles--;        }     }  } 
end example
 

The method loops while we still have new particles to create and we have not reached the limit of active particles. If any old particles are waiting in the free particles list, they are reused prior to creating any new particles. Regardless of a particle s origins, it needs to be initialized before it is released into the scene. A rotation matrix is calculated using a combination of the generator s attitude, random factors, and the parent object s attitude. This matrix is applied against the particle velocity to create a velocity vector as the particle s initial velocity. The position and initial position of the particle is also transformed using the matrix. Once the particle is initialized , it is activated and added to the active particle list.

Now that the new particles have been added, it is time to update all of the particles. We loop through all of the particles and call the Update method for each particle. If a particle is still active, it is updated within the active particle list.

Note

Referencing an object in a list actually gets a copy of the object. In order for the changes to the object to be maintained, we must copy the object back to the list.

If the particle is no longer active, it is removed from the active particle list and added to the free particle list for future reuse. The number of active particles is also decremented.

The particles are drawn by the Render method (shown in Listing 4 “41). Microsoft has provided support for particle systems in point sprites . Point sprites work like small billboards. The advantage is that we do not need to send a strip of two triangles to the video card for each particle. Instead, we just send the position of each particle in the vertex buffer. This is very important because of the large number of particles that will be rendered. Extra render states must be set up when using point sprites. A specific render state flag must be set as well as enabling scaling of the points. If we do not enable scaling, the points will not appear to change size with distance. There are also scaling factors and minimum size values that affect the size of the points.

Listing 4.41: ParticleGenerator Render Method
start example
 public override void Render(Camera cam)  {     try     {     if (m_ActiveParticles.Count > 0)     {        // Set the render states for using point sprites.        CGameEngine.Device3D.RenderState.ZBufferWriteEnable = false;        CGameEngine.Device3D.RenderState.AlphaBlendEnable = true;        CGameEngine.Device3D.RenderState.SourceBlend = Blend.One;        CGameEngine.Device3D.RenderState.DestinationBlend = Blend.One;        CGameEngine.Device3D.SetTexture(0, m_Texture);        CGameEngine.Device3D.RenderState.PointSpriteEnable = true;        CGameEngine.Device3D.RenderState.PointScaleEnable = true ;        CGameEngine.Device3D.RenderState.PointSize = m_PointSize;        CGameEngine.Device3D.RenderState.PointSizeMin = m_PointSizeMin;        CGameEngine.Device3D.RenderState.PointScaleA = m_PointScaleA;        CGameEngine.Device3D.RenderState.PointScaleB = m_PointScaleB;        CGameEngine.Device3D.RenderState.PointScaleC = m_PointScaleC;        CGameEngine.Device3D.VertexFormat =                 CustomVertex.PositionColoredTextured.Format;        // Set up the vertex buffer to be rendered.        CGameEngine.Device3D.SetStreamSource(0, m_VB, 0);        CustomVertex. PositionColoredTextured [] vertices = null;        int numParticlesToRender = 0;        // Lock the vertex buffer. We fill the vertex buffer in small        // chunks, using LockFlags.NoOverWrite. When we are done filling        // each chunk, we call DrawPrim, and lock the next chunk. When        // we run out of space in the vertex buffer, we start over at        // the beginning, using LockFlags.Discard.        m_BaseParticle += m_Flush;        if(m_BaseParticle >= m_Discard)           m_BaseParticle = 0;        int count = 0;        vertices = (CustomVertex. PositionColoredTextured [])            m_VB.Lock(m_BaseParticle *           DXHelp.GetTypeSize(typeof(CustomVertex. PositionColoredTextured)),          typeof(CustomVertex. PositionColoredTextured),          (m_BaseParticle != 0) ? LockFlags.NoOverwrite : LockFlags.Discard,             m_Flush);        foreach(Particle p in m_ActiveParticles)        {          vertices[count].X      = p.m_Position.X;          vertices[count].Y      = p.m_Position.Y;          vertices[count].Z      = p.m_Position.Z;          vertices [count].Color = p.m_Color.ToArgb();          count++;          if(++numParticlesToRender == m_Flush)          {            // Done filling this chunk of the vertex buffer. Let's unlock and            // draw this portion so we can begin filling the next chunk.             m_VB.Unlock();             CGameEngine.Device3D.DrawPrimitives(PrimitiveType. Point List, m_BaseParticle,                   numParticlesToRender) ;             // Lock the next chunk of the vertex buffer. If we are at the             // end of the vertex buffer, LockFlags.Discard the vertex             // buffer and start at the beginning. Otherwise,             // specify LockFlags. NoOverwrite, so we can continue filling             // the VB while the previous chunk is drawing.             m_BaseParticle += m_Flush;             if(m_BaseParticle >= m_Discard)                m_BaseParticle = 0;            vertices = (CustomVertex.PositionColored[])m_VB.Lock(m_BaseParticle *                      DXHelp.GetTypeSize(typeof(CustomVertex. PositionColoredTextured)),                      typeof(CustomVertex. PositionColoredTextured),                       (m_BaseParticle != 0) ?                      LockFlags.NoOverWrite : LockFlags. Discard, m_Flush);             count = 0;             numParticlesToRender = 0;           }        }        // Unlock the vertex buffer.        m_VB.Unlock();        // Render any remaining particles.        if(numParticlesToRender > 0)           CGameEngine.Device3D.DrawPrimitives(PrimitiveType.Point List, m_BaseParticle,                 numParticlesToRender);        // Reset render states.        CGameEngine.Device3D.RenderState.PointSpriteEnable = false;        CGameEngine.Device3D.RenderState.PointScaleEnable = false;        CGameEngine.Device3D.RenderState.ZBufferWriteEnable = true;        CGameEngine.Device3D.RenderState.AlphaBlendEnable = false;     }     }    catch (DirectXException d3de)     {        Console.AddLine("Unable to Render Particles for " + Name);        Console.AddLine(d3de.ErrorString);     }     catch (Exception e)     {        Console.AddLine("Unable to Render Particles for " + Name);        Console.AddLine(e.Message);     }  } 
end example
 

Since we do have so many particles, we will send them to the video card in batches. Each batch will be a portion of the vertex buffer. The flush variable defines how many points will be added to the vertex buffer before flushing them to the card for processing. The size of the buffer is defined by the m_Discard variable. When we reach that point in the buffer, it is time to start over at the beginning of the buffer. This lets us use the vertex buffer like a circular buffer.

Since the particle generator has no size, we will treat it just like the billboards when it comes to determining if it is within a rectangle. The InRect method (shown in Listing 4 “42) works just like the method in the Billboard class. It uses the Rectangle class s Contains method.

Listing 4.42: ParticleGenerator InRect Method
start example
 public override bool InRect(Rectangle rect)  {     return rect.Contains((int)m_vPosition.X, (int)m_vPosition.Z);  } 
end example
 

The last method in the class is the Dispose method (shown in Listing 4 “43). Like the other classes, it must dispose of the texture and vertex buffer.

Listing 4.43: ParticleGenerator Dispose Method
start example
 public override void Dispose()  {     m_Texture.Dispose();     if (m_VB != null)     {        m_VB.Dispose();     }  } 
end example
 

There is no image that can do justice to the sand particle generator. You must see the particles in motion to appreciate the dynamics of particles. When you run the sample game for the book, you will see the sand particles streaming from the rear tires of each vehicle as it drives along.




Introduction to 3D Game Engine Design Using DirectX 9 and C#
Introduction to 3D Game Engine Design Using DirectX 9 and C#
ISBN: 1590590813
EAN: 2147483647
Year: 2005
Pages: 98

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