Particle System

This is the implementation of a state-of-the-art particle system that can make full use of all CPU cores (if particles are updated on the CPU, not GPU) and work with any kind of particle structure. Particles are useful to simulate many kinds of special effects such as explosions, smoke, rain or water.

Particles.jpg

Features

Here is a list of the features that make this particle system so special
  • Lean and Simple - despite all the magic, this is an efficient and low-overhead particle system
  • Use Multiple CPU Cores - if you're simulating your particles on the CPU, the particle system can update your particles in parallel, making full use of a multi-core CPU. You have full control over the number of threads - you could use all but one CPU core and update the rest of the game on the remaining core while the particle system updates.
  • User-defined Particle Structures - you can use your own structures to store your particle's properties. If your particles only need their position, you don't have to carry the bloat of some full-blown library-provided particle structure around.
  • Designed for testability - the whole codebase is 100% unit tested and has been layed out so you can achieve the same test coverage for your own particle-related classes.
  • Render Multiple Particle Systems in one Batch - Particles are rendered in batches through the PrimitiveBatch class. Multiple particle systems can share a common PrimitiveBatch, thus, saving you video memory and, provided both particle systems use the same effect, even render multiple distinct particle system in a single batch.
  • Abstraction of Particle Influences - any influences to your particles (such as gravity, wind, decay, attraction) are added to a particle system through affectors. Affectors work on entire batches of particles and thus have no per-particle call overhead.
  • Specialized and General-Purpose Affectors - even though user-defined particle structures are supported, the particle system allows you to implement general purpose affectors that manipulate any kind of particle structure. And if the absolute best possible performance is required, you can implement specialized affectors that work directly on your particle structure.
  • Fast Catch-Up - if a particle system has to perform multiple updates at once, it can coalesce multiple updates into a single one if all registered affectors support it. If not, the particle system will transparently do a multi-pass update for you. Even hybrid updates combining both are supported!
  • Use Particle Structure as Vertex - you can either directly use your particle structure as vertex structure for rendering to avoid an additional processing step, or repot particles into a specialized vertex structure before sending them to the GPU if your particles have many fields not used by the shader.

Usage

Here is a short guide that explains how to use the particle system

1. Define a structure to hold the fields of your particles. This structure can double as your particle vertex structure or you can use a different structure for your vertices. If one particle becomes multiple vertices, a different structure will be required.
public struct SimpleParticle {

  /// <summary>Initializes a new simple particle</summary>
  /// <param name="position">Initial position of the particle</param>
  /// <param name="velocity">Velocity the particle is moving at</param>
  public SimpleParticle(Vector3 position, Vector3 velocity) {
    this.Position = position;
    this.Velocity = velocity;
  }

  /// <summary>Current position of the particle in space</summary>
  public Vector3 Position;
    
  /// <summary>Velocity the particle is moving at</summary>
  public Vector3 Velocity;
  
}

2. If your want to apply general-purpose affectors to your particles, you'll have to implement a particle modifier for your structure. This allows the affectors to modify your particles without knowing the structure itself
public class SimpleParticleModifier : IParticleModifier<SimpleParticle> {

  /// <summary>Adds each particle's velocity to its current position</summary>
  /// <param name="particles">Particles that will be modified</param>
  /// <param name="start">Index of the first particle that will be modified</param>
  /// <param name="count">Number of particles that will be modified</param>
  public void AddVelocityToPosition(
    SimpleParticle[] particles, int start, int count
  ) { /* ... */ }

  /// <summary>Obtains the position of an individual particle</summary>
  /// <param name="particle">Particle whose position will be returned</param>
  /// <param name="position">
  ///   Output parameter that will receive the particle's position
  /// </param>
  public void GetPosition(
    ref SimpleParticle particle, out Vector3 position
  ) { /* ... */ }

  /// <summary>Changes the position of an individual particle</summary>
  /// <param name="particle">Particle whose position will be changed</param>
  /// <param name="position">Position the particle will be moved to</param>
  public void SetPosition(
    ref SimpleParticle particle, ref Vector3 position
  ) { /* ... */ }

  // ...

}

3. Create a particle system and add any affectors your wish to influence the particles in it
// Create a new particle system that can hold up to 4096 particles
this.particleSystem = new ParticleSystem<SimpleParticle>(4096);

// Add a gravity affector which accelerates particles downwards
this.particleSystem.Affectors.Add(
  new GravityAffector<SimpleParticle>(SimpleParticleModifier.Default)
);

// Add a movement affector which will move particles by their velocity
this.particleSystem.Affectors.Add(
  new MovementAffector<SimpleParticle>(SimpleParticleModifier.Default)
);

4. There are two paths you can take to render and update your particle systems. One is to directly work with the ParticleSystem instance and call its Update() and Prune() methods, then render it by accessing the particles directly through its Particles property, outlined in step 5. The other path is to involve the high level particle system manager, which helps you batch particles across multiple particle systems and saves you from having to manage vertex declarations and calling Update() and Prune() on all particle systems yourself, but takes a bit more work to set up first. To take this approach, skip to step 6.

5. If you want to take the do-it-yourself route, you'll have to invoke the particle system's Update() and Prune() methods in your Game's Update() method. You can access the particles for rendering (or further processed) through the Particles property of the ParticleSystem class. If your particle structure doubles as vertex structure and you're rendering particles as point sprites, rendering is just a matter of calling DrawUserPrimitives() with this array (despite its stigma, DUP is the fastest way to render dynamic vertices, as long as you're rendering in batches)

/// <summary>
///   Allows the game to run logic such as updating the world,
///   checking for collisions, gathering input and playing audio.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Update(GameTime gameTime) {

  // Perform a multi-threaded particle system update
  IAsyncResult asyncResult = this.particleSystem.BeginUpdate(
    1, 4, null, null // run 1 update cycle with 4 threads
  );

  // TODO: Do some other things here while the particle system is updating
  //       But do not touch the particle system!

  this.particleSystem.EndUpdate(asyncResult);

  // Prune dead particles from the system. This can also be done
  // asynchronously if you still have other work to do in your main thread
  this.particleSystem.Prune(isParticleAlive);

}

/// <summary>This is called when the game should draw itself.</summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Draw(GameTime gameTime) {

  this.effect.Begin(SaveStateMode.SaveState);
  try {
    EffectTechnique technique = this.effect.CurrentTechnique;
    for(int pass = 0; pass < technique.Passes.Count; ++pass) {

      technique.Passes[pass].Begin();
      try {
        ArraySegment<SimpleParticle> particles = this.particleSystem.Particles;
        GraphicsDevice.DrawUserPrimitives<SimpleParticle>(
          PrimitiveType.PointList,
          particles.Array, particles.Offset, particles.Count
        );
      }
      finally {
        technique.Passes[pass].End();
      }

    } // for
  }
  finally {
    this.Effect.End();
  }

}

6. If you followed step 5, you're finished now. On the other hand, if in step 4 you decided to use the ParticleSystemManager, you will need to create an instance of the ParticleSystemManager in your game. The ParticleSystemManager will automatically created vertex declarations for you (however, you can also provide them yourself if you wish to) and supports batched rendering of particles across multiple particle systems.
/// <summary>
///   Allows the game to perform any initialization it needs to before starting to run.
///   This is where it can query for any required services and load any non-graphic
///   related content. Calling base.Initialize will enumerate through any components
///   and initialize them as well.
/// </summary>
protected override void Initialize() {

  // ...initialization code from previous examples...

  // Create a new particle system manager
  this.particleSystemManager = new ParticleSystemManager(this.graphicsDeviceService);

  // For vertex structures equipped with [VertexElement] attributes, the particle
  // system manager can automatically create the vertex declaration. For other
  // vertex structures, you can tell it which vertex declaration to use like this:
  this.particleSystemManager.RegisterVertex<VertexPositionColor>(
    new VertexDeclaration(graphicsDevice, VertexPositionColor.VertexElements),
    VertexPositionColor.SizeInBytes,
    true // take ownership of the vertex declaration, handles disposal
  );

  // Now register our particle system with the particle system manager. We're
  // assuming that you chose to have the particle structure double as vertex
  // structure, but through the IParticleRenderer interface, the particle system
  // manager can handle anything, including particles which generate complex
  // geometry (think flying shrapnel)
  this.particleSystemManager.AddParticleSystem<SimpleParticle>(
    this.particleSystem, isParticleAlive,
    new PointParticleRenderer<SimpleParticle>(this.effect)
  );

}

/// <summary>
///   Allows the game to run logic such as updating the world,
///   checking for collisions, gathering input and playing audio.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Update(GameTime gameTime) {

  // Perform a multi-threaded particle system update
  IAsyncResult asyncResult = this.particleSystemManager.BeginUpdate(
    1, 4, null, null // run 1 update cycle with 4 threads
  );

  // TODO: Do some other things here while the particle system is updating
  //       But do not touch the particle system!

  this.particleSystemManager.EndUpdate(asyncResult);

  // Prune dead particles from the system. This can also be done
  // asynchronously if you still have other work to do in your main thread
  this.particleSystemManager.Prune(isParticleAlive);

}

/// <summary>This is called when the game should draw itself.</summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Draw(GameTime gameTime) {
  this.particleSystemManager.Draw(gameTime);
}

/// <summary>Automatically manages particle systems for us</summary>
private ParticleSystemManager particleSystemManager;

Last edited Oct 2, 2009 at 8:58 AM by Cygon, version 13

Comments

No comments yet.