123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528 |
- #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
- {
- /// <summary>
- /// The main component in charge of displaying particles.
- /// </summary>
- 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
- /// <summary>
- /// Constructor.
- /// </summary>
- public ParticleSystem (Game game,ContentManager content,string settingsName)
- : base(game)
- {
- this.content = content;
- this.settingsName = settingsName;
- }
-
- /// <summary>
- /// Loads graphics for the particle system.
- /// </summary>
- protected override void LoadContent ()
- {
- //return;
- settings = content.Load<ParticleSettings> (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);
- }
- /// <summary>
- /// Helper for loading and initializing the particle effect.
- /// </summary>
- void LoadParticleEffect ()
- {
- Effect effect = content.Load<Effect> ("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<Texture2D> (settings.TextureName);
- parameters ["Texture"].SetValue (texture);
- }
- #endregion
- #region Update and Draw
- /// <summary>
- /// Updates the particle system.
- /// </summary>
- 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;
- }
- /// <summary>
- /// 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.
- /// </summary>
- 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;
- }
- }
- /// <summary>
- /// 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.
- /// </summary>
- 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;
- }
- }
- /// <summary>
- /// Draws the particle system.
- /// </summary>
- 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++;
- }
- /// <summary>
- /// Helper for uploading new particles from our managed
- /// array to the GPU vertex buffer.
- /// </summary>
- 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
- /// <summary>
- /// Sets the camera view and projection matrices
- /// that will be used to draw this particle system.
- /// </summary>
- public void SetCamera (Matrix view, Matrix projection)
- {
- effectViewParameter.SetValue (view);
- effectProjectionParameter.SetValue (projection);
- }
- /// <summary>
- /// Adds a new particle to the system.
- /// </summary>
- 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
- }
- }
|