#region File Description
//-----------------------------------------------------------------------------
// ParticleSystem.cs
//
// Microsoft XNA Community Game Platform
// Copyright (C) Microsoft Corporation. All rights reserved.
//-----------------------------------------------------------------------------
#endregion
#region Using Statements
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Graphics.PackedVector;
#endregion
namespace Particle3DSample
{
///
/// The main component in charge of displaying particles.
///
public class ParticleSystem : DrawableGameComponent
{
#region Fields
// Name of the XML settings file describing this particle system.
string settingsName;
// Settings class controls the appearance and animation of this particle system.
ParticleSettings settings;
// For loading the effect and particle texture.
ContentManager content;
// Custom effect for drawing particles. This computes the particle
// animation entirely in the vertex shader: no per-particle CPU work required!
Effect particleEffect;
// Shortcuts for accessing frequently changed effect parameters.
EffectParameter effectViewParameter;
EffectParameter effectProjectionParameter;
EffectParameter effectViewportScaleParameter;
EffectParameter effectTimeParameter;
// An array of particles, treated as a circular queue.
ParticleVertex[] particles;
// A vertex buffer holding our particles. This contains the same data as
// the particles array, but copied across to where the GPU can access it.
DynamicVertexBuffer vertexBuffer;
// Index buffer turns sets of four vertices into particle quads (pairs of triangles).
IndexBuffer indexBuffer;
// The particles array and vertex buffer are treated as a circular queue.
// Initially, the entire contents of the array are free, because no particles
// are in use. When a new particle is created, this is allocated from the
// beginning of the array. If more than one particle is created, these will
// always be stored in a consecutive block of array elements. Because all
// particles last for the same amount of time, old particles will always be
// removed in order from the start of this active particle region, so the
// active and free regions will never be intermingled. Because the queue is
// circular, there can be times when the active particle region wraps from the
// end of the array back to the start. The queue uses modulo arithmetic to
// handle these cases. For instance with a four entry queue we could have:
//
// 0
// 1 - first active particle
// 2
// 3 - first free particle
//
// In this case, particles 1 and 2 are active, while 3 and 4 are free.
// Using modulo arithmetic we could also have:
//
// 0
// 1 - first free particle
// 2
// 3 - first active particle
//
// Here, 3 and 0 are active, while 1 and 2 are free.
//
// But wait! The full story is even more complex.
//
// When we create a new particle, we add them to our managed particles array.
// We also need to copy this new data into the GPU vertex buffer, but we don't
// want to do that straight away, because setting new data into a vertex buffer
// can be an expensive operation. If we are going to be adding several particles
// in a single frame, it is faster to initially just store them in our managed
// array, and then later upload them all to the GPU in one single call. So our
// queue also needs a region for storing new particles that have been added to
// the managed array but not yet uploaded to the vertex buffer.
//
// Another issue occurs when old particles are retired. The CPU and GPU run
// asynchronously, so the GPU will often still be busy drawing the previous
// frame while the CPU is working on the next frame. This can cause a
// synchronization problem if an old particle is retired, and then immediately
// overwritten by a new one, because the CPU might try to change the contents
// of the vertex buffer while the GPU is still busy drawing the old data from
// it. Normally the graphics driver will take care of this by waiting until
// the GPU has finished drawing inside the VertexBuffer.SetData call, but we
// don't want to waste time waiting around every time we try to add a new
// particle! To avoid this delay, we can specify the SetDataOptions.NoOverwrite
// flag when we write to the vertex buffer. This basically means "I promise I
// will never try to overwrite any data that the GPU might still be using, so
// you can just go ahead and update the buffer straight away". To keep this
// promise, we must avoid reusing vertices immediately after they are drawn.
//
// So in total, our queue contains four different regions:
//
// Vertices between firstActiveParticle and firstNewParticle are actively
// being drawn, and exist in both the managed particles array and the GPU
// vertex buffer.
//
// Vertices between firstNewParticle and firstFreeParticle are newly created,
// and exist only in the managed particles array. These need to be uploaded
// to the GPU at the start of the next draw call.
//
// Vertices between firstFreeParticle and firstRetiredParticle are free and
// waiting to be allocated.
//
// Vertices between firstRetiredParticle and firstActiveParticle are no longer
// being drawn, but were drawn recently enough that the GPU could still be
// using them. These need to be kept around for a few more frames before they
// can be reallocated.
int firstActiveParticle;
int firstNewParticle;
int firstFreeParticle;
int firstRetiredParticle;
// Store the current time, in seconds.
float currentTime;
// Count how many times Draw has been called. This is used to know
// when it is safe to retire old particles back into the free list.
int drawCounter;
// Shared random number generator.
static Random random = new Random ();
#endregion
#region Initialization
///
/// Constructor.
///
public ParticleSystem (Game game,ContentManager content,string settingsName)
: base(game)
{
this.content = content;
this.settingsName = settingsName;
}
///
/// Loads graphics for the particle system.
///
protected override void LoadContent ()
{
//return;
settings = content.Load (settingsName);
// Allocate the particle array, and fill in the corner fields (which never change).
particles = new ParticleVertex[settings.MaxParticles * 4];
for (int i = 0; i < settings.MaxParticles; i++) {
particles [i * 4 + 0].Corner = new Short2 (-1, -1);
particles [i * 4 + 1].Corner = new Short2 (1, -1);
particles [i * 4 + 2].Corner = new Short2 (1, 1);
particles [i * 4 + 3].Corner = new Short2 (-1, 1);
}
LoadParticleEffect ();
// Create a dynamic vertex buffer.
vertexBuffer = new DynamicVertexBuffer (GraphicsDevice, ParticleVertex.VertexDeclaration,
settings.MaxParticles * 4, BufferUsage.WriteOnly);
// Create and populate the index buffer.
ushort[] indices = new ushort[settings.MaxParticles * 6];
for (int i = 0; i < settings.MaxParticles; i++) {
indices [i * 6 + 0] = (ushort)(i * 4 + 0);
indices [i * 6 + 1] = (ushort)(i * 4 + 1);
indices [i * 6 + 2] = (ushort)(i * 4 + 2);
indices [i * 6 + 3] = (ushort)(i * 4 + 0);
indices [i * 6 + 4] = (ushort)(i * 4 + 2);
indices [i * 6 + 5] = (ushort)(i * 4 + 3);
}
indexBuffer = new IndexBuffer (GraphicsDevice, typeof(ushort), indices.Length, BufferUsage.WriteOnly);
indexBuffer.SetData (indices);
}
///
/// Helper for loading and initializing the particle effect.
///
void LoadParticleEffect ()
{
Effect effect = content.Load ("ParticleEffect");
// If we have several particle systems, the content manager will return
// a single shared effect instance to them all. But we want to preconfigure
// the effect with parameters that are specific to this particular
// particle system. By cloning the effect, we prevent one particle system
// from stomping over the parameter settings of another.
//particleEffect = effect.Clone ();
// No cloning for now so we will just create a new effect for now
particleEffect = effect;
EffectParameterCollection parameters = particleEffect.Parameters;
// Look up shortcuts for parameters that change every frame.
effectViewParameter = parameters ["View"];
effectProjectionParameter = parameters ["Projection"];
effectViewportScaleParameter = parameters ["ViewportScale"];
effectTimeParameter = parameters ["CurrentTime"];
// Set the values of parameters that do not change.
parameters ["Duration"].SetValue ((float)settings.Duration.TotalSeconds);
parameters ["DurationRandomness"].SetValue (settings.DurationRandomness);
parameters ["Gravity"].SetValue (settings.Gravity);
parameters ["EndVelocity"].SetValue (settings.EndVelocity);
parameters ["MinColor"].SetValue (settings.MinColor.ToVector4 ());
parameters ["MaxColor"].SetValue (settings.MaxColor.ToVector4 ());
parameters ["RotateSpeed"].SetValue (
new Vector2 (settings.MinRotateSpeed, settings.MaxRotateSpeed));
parameters ["StartSize"].SetValue (
new Vector2 (settings.MinStartSize, settings.MaxStartSize));
parameters ["EndSize"].SetValue (
new Vector2 (settings.MinEndSize, settings.MaxEndSize));
// Load the particle texture, and set it onto the effect.
Texture2D texture = content.Load (settings.TextureName);
parameters ["Texture"].SetValue (texture);
}
#endregion
#region Update and Draw
///
/// Updates the particle system.
///
public override void Update (GameTime gameTime)
{
if (gameTime == null)
throw new ArgumentNullException ("gameTime");
currentTime += (float)gameTime.ElapsedGameTime.TotalSeconds;
RetireActiveParticles ();
FreeRetiredParticles ();
// If we let our timer go on increasing for ever, it would eventually
// run out of floating point precision, at which point the particles
// would render incorrectly. An easy way to prevent this is to notice
// that the time value doesn't matter when no particles are being drawn,
// so we can reset it back to zero any time the active queue is empty.
if (firstActiveParticle == firstFreeParticle)
currentTime = 0;
if (firstRetiredParticle == firstActiveParticle)
drawCounter = 0;
}
///
/// Helper for checking when active particles have reached the end of
/// their life. It moves old particles from the active area of the queue
/// to the retired section.
///
void RetireActiveParticles ()
{
float particleDuration = (float)settings.Duration.TotalSeconds;
while (firstActiveParticle != firstNewParticle) {
// Is this particle old enough to retire?
// We multiply the active particle index by four, because each
// particle consists of a quad that is made up of four vertices.
float particleAge = currentTime - particles [firstActiveParticle * 4].Time;
if (particleAge < particleDuration)
break;
// Remember the time at which we retired this particle.
particles [firstActiveParticle * 4].Time = drawCounter;
// Move the particle from the active to the retired queue.
firstActiveParticle++;
if (firstActiveParticle >= settings.MaxParticles)
firstActiveParticle = 0;
}
}
///
/// Helper for checking when retired particles have been kept around long
/// enough that we can be sure the GPU is no longer using them. It moves
/// old particles from the retired area of the queue to the free section.
///
void FreeRetiredParticles ()
{
while (firstRetiredParticle != firstActiveParticle) {
// Has this particle been unused long enough that
// the GPU is sure to be finished with it?
// We multiply the retired particle index by four, because each
// particle consists of a quad that is made up of four vertices.
int age = drawCounter - (int)particles [firstRetiredParticle * 4].Time;
// The GPU is never supposed to get more than 2 frames behind the CPU.
// We add 1 to that, just to be safe in case of buggy drivers that
// might bend the rules and let the GPU get further behind.
if (age < 3)
break;
// Move the particle from the retired to the free queue.
firstRetiredParticle++;
if (firstRetiredParticle >= settings.MaxParticles)
firstRetiredParticle = 0;
}
}
///
/// Draws the particle system.
///
public override void Draw (GameTime gameTime)
{
GraphicsDevice device = GraphicsDevice;
// Restore the vertex buffer contents if the graphics device was lost.
if (vertexBuffer.IsContentLost) {
vertexBuffer.SetData (particles);
}
// If there are any particles waiting in the newly added queue,
// we'd better upload them to the GPU ready for drawing.
if (firstNewParticle != firstFreeParticle) {
AddNewParticlesToVertexBuffer ();
}
// If there are any active particles, draw them now!
if (firstActiveParticle != firstFreeParticle) {
device.BlendState = settings.BlendState;
device.DepthStencilState = DepthStencilState.DepthRead;
// Set an effect parameter describing the viewport size. This is
// needed to convert particle sizes into screen space point sizes.
effectViewportScaleParameter.SetValue (new Vector2 (0.5f / device.Viewport.AspectRatio, -0.5f));
// Set an effect parameter describing the current time. All the vertex
// shader particle animation is keyed off this value.
effectTimeParameter.SetValue (currentTime);
// Set the particle vertex and index buffer.
device.SetVertexBuffer (vertexBuffer);
device.Indices = indexBuffer;
// Activate the particle effect.
foreach (EffectPass pass in particleEffect.CurrentTechnique.Passes) {
pass.Apply ();
if (firstActiveParticle < firstFreeParticle) {
// If the active particles are all in one consecutive range,
// we can draw them all in a single call.
device.DrawIndexedPrimitives (PrimitiveType.TriangleList, 0,
firstActiveParticle * 4, (firstFreeParticle - firstActiveParticle) * 4,
firstActiveParticle * 6, (firstFreeParticle - firstActiveParticle) * 2);
} else {
// If the active particle range wraps past the end of the queue
// back to the start, we must split them over two draw calls.
device.DrawIndexedPrimitives (PrimitiveType.TriangleList, 0,
firstActiveParticle * 4, (settings.MaxParticles - firstActiveParticle) * 4,
firstActiveParticle * 6, (settings.MaxParticles - firstActiveParticle) * 2);
if (firstFreeParticle > 0) {
device.DrawIndexedPrimitives (PrimitiveType.TriangleList, 0,
0, firstFreeParticle * 4,
0, firstFreeParticle * 2);
}
}
}
// Reset some of the renderstates that we changed,
// so as not to mess up any other subsequent drawing.
device.DepthStencilState = DepthStencilState.Default;
}
drawCounter++;
}
///
/// Helper for uploading new particles from our managed
/// array to the GPU vertex buffer.
///
void AddNewParticlesToVertexBuffer ()
{
int stride = ParticleVertex.SizeInBytes;
if (firstNewParticle < firstFreeParticle) {
// If the new particles are all in one consecutive range,
// we can upload them all in a single call.
// vertexBuffer.SetData (firstNewParticle * stride * 4, particles,
// firstNewParticle * 4,
// (firstFreeParticle - firstNewParticle) * 4,
// stride, SetDataOptions.NoOverwrite); } else {
// If the new particle range wraps past the end of the queue
// back to the start, we must split them over two upload calls.
// vertexBuffer.SetData (firstNewParticle * stride * 4, particles,
// firstNewParticle * 4,
// (settings.MaxParticles - firstNewParticle) * 4,
// stride, SetDataOptions.NoOverwrite);
if (firstFreeParticle > 0) {
// vertexBuffer.SetData (0, particles,
// 0, firstFreeParticle * 4,
// stride, SetDataOptions.NoOverwrite);
}
}
// Move the particles we just uploaded from the new to the active queue.
firstNewParticle = firstFreeParticle;
}
#endregion
#region Public Methods
///
/// Sets the camera view and projection matrices
/// that will be used to draw this particle system.
///
public void SetCamera (Matrix view, Matrix projection)
{
effectViewParameter.SetValue (view);
effectProjectionParameter.SetValue (projection);
}
///
/// Adds a new particle to the system.
///
public void AddParticle (Vector3 position, Vector3 velocity)
{
// Figure out where in the circular queue to allocate the new particle.
int nextFreeParticle = firstFreeParticle + 1;
if (nextFreeParticle >= settings.MaxParticles)
nextFreeParticle = 0;
// If there are no free particles, we just have to give up.
if (nextFreeParticle == firstRetiredParticle)
return;
// Adjust the input velocity based on how much
// this particle system wants to be affected by it.
velocity *= settings.EmitterVelocitySensitivity;
// Add in some random amount of horizontal velocity.
float horizontalVelocity = MathHelper.Lerp (settings.MinHorizontalVelocity,
settings.MaxHorizontalVelocity,
(float)random.NextDouble ());
double horizontalAngle = random.NextDouble () * MathHelper.TwoPi;
velocity.X += horizontalVelocity * (float)Math.Cos (horizontalAngle);
velocity.Z += horizontalVelocity * (float)Math.Sin (horizontalAngle);
// Add in some random amount of vertical velocity.
velocity.Y += MathHelper.Lerp (settings.MinVerticalVelocity,
settings.MaxVerticalVelocity,
(float)random.NextDouble ());
// Choose four random control values. These will be used by the vertex
// shader to give each particle a different size, rotation, and color.
Color randomValues = new Color ((byte)random.Next (255),
(byte)random.Next (255),
(byte)random.Next (255),
(byte)random.Next (255));
// Fill in the particle vertex structure.
for (int i = 0; i < 4; i++) {
particles [firstFreeParticle * 4 + i].Position = position;
particles [firstFreeParticle * 4 + i].Velocity = velocity;
particles [firstFreeParticle * 4 + i].Random = randomValues;
particles [firstFreeParticle * 4 + i].Time = currentTime;
}
firstFreeParticle = nextFreeParticle;
}
#endregion
}
}