2D Particles in XNA – Part 3 (of 3)

This article is a followup of 2D Particles in XNA – Part 2 (of 3). It assumes you’ve read the article and understood it.

In part 2, we created a working particle effect, but it looked more like smoke than fire. Also, it had several other shortcomings, you might have noticed them if you’ve been playing around with the code.

There were two shortcomings that i’m going to discuss. There probably are more ways to make the particle effect even better, but fixing these shortcomings is a start.

First shortcoming – Intensity:

Suppose you want to create a fire effect. In a fire effect, it’s nice to make the core of the fire a bit white, this makes the core look warmer. Making this effect with the code we have now will make it look more like smoke than fire, because the color of the core isn’t really different from the outer parts.

This shortcoming can be fixed very easily: It has something to do with the alpha blending. Blending a procedure to follow when one sprite overlaps another sprite to determine the final color of one pixel.

For example: By default, if you have two sprites (without transparency) and draw sprite one over sprite two, then a part or the whole of sprite two will be invisible, because it (the GPU) draws the first sprite on it.

If these images do have transparency, it takes the color of the texture in the back and front and combines them using the alpha value of the color. This happens the way you would imagine it to happen, heres the formula to calculate the color of the pixel.

FinalColor = (1.0f-FrontAlpha)*BackColor+FrontAlpha*FrontColor

An example will probably help understanding this if you don’t understand the formula.

Example: Say we have a backcolor (something that was already drawn on the screen) and that color is Red RGB(1.0f, 0, 0) and we want to draw another color that is a bit transparent. This color is Blue RGB(0, 0, 1.0f) with an alpha value of 0.3f. You should know that an alpha value of 0 means it is completely transparent and an alpha value of 1 is nontransparent. When we put these values in the formula, we get:

(1-0.3)*RGB(1.0f, 0, 0)+0.3*RGB(0, 0, 1.0f) gives

RGB(0.7*1.0, 0.7*0, 0.7*0) + RGB(0.3*0, 0.3*0, 0.3*1.0) gives

RGB(0.7f, 0, 0) + RGB(0, 0, 0.3f) gives

RGB(0.7f, 0, 0.3f)

If you don’t believe it, open up your favorite painting program (that supports alpha values) and draw a red background, make a new layer, make its opacity 30%, fill it with blue. Merge the layers and see what color comes out by using a color picker tool. However, many painting programs show the RGB and A (Alpha) values as integer numbers between 0 and 255. However, the idea is exactly the same.

Now that we’ve examined how the default settings work, we now need to determine what that formula should be to be suitable for our particle system.

Remember that we wanted the core of the fire effect to be bright, kind of white. What determines if a pixel on the screen is in the core of the particle effect? Well, there are many particle sprites overlapping each other at that spot. So what if we would just add all those overlapping colors together? That would give a white core, since the color values are capped at RGB(1f, 1f, 1f), and that is white. So of you add lots of say RGB(0.17f, 0,35f, 0.6f) values together, you would eventually get the color white. Off course, if you would like to make a smoke particle system, you should stick with the old method.

The formula would be: FinalColor = BackColor + FrontColor

Luckily for us, we don’t have to implement these formulas ourselves, since XNA (and for the C++ developers, also their APIs) has several different ways to change this procedure. The easiest one, which we’re going to use is an overload of the SpriteBatch.Begin() method.

If you want to use this blending type (which is called Additive Blending by the way)  you can call a method spriteBatch.Begin(SpriteBlendMode.Additive) and everything you draw within the begin and close call of the spritebatch will use Additive blending to calculate the final color.

The following example uses the code found in Part 2

For example, change the CMain.Draw method to this:

protected override void Draw(GameTime gameTime)
{
    this.GraphicsDevice.Clear(Color.Black);
    spriteBatch.Begin(SpriteBlendMode.Additive);   // We only changed this line
    particleSystem.Draw(spriteBatch, 1, Vector2.Zero);
    spriteBatch.End();
}

This will make the fire look a lot more like a fire.

That was quite a bit of explanation to prepare you to modify 1 line of code, but it’s needed, because although you don’t have to know how it works you still need to know what it does. (And understand why Additive blending isn’t that good for a smoke effect)

Now let’s look at the next shortcoming of our old code.

Second shortcoming – Particle spawn time:

The method to check whether you need to spawn particles. You might think this is fine, but if the spawn rate of your particles is faster than your frame rate, then multiple particles will be spawned at exactly the same time and position.

You can see this for yourself by making a particle system follow your mouse and moving the mouse really fast. Usually. You would expect to get a nice trail of particles behind the mouse, but you get a dotted line.

The frame rate is 60fps, but the particle spawn rate can exceed 1000 particles per second. One possibility is to call the update method more often, for example in a different thread, but this might waste CPU cycles because it runs to fast and you’ll have to introduce a new thread to your program which will make it more complicated while there are other ways to solve this.

The “other” way is to remember the old position of the emitter (which is the old position of the particle system), calculate the new position and spawn the particles between those two positions.

I’ll show you an example:

Let’s say the frame rate is 60fps and our particle spawn rate is 600 particles per second.

Let’s say at frame 0 the particle system position is (0, 0) and at frame 1 the position is (1000, 0)

With the old code, the particle system would spawn 10 particles at (0,0) and 10 particles at (1000, 0) and leave a gap between.

With the new method, it should spawn one at (100, 0) one at (200, 0), (300, 0), (400, 0), (500, 0) etc. We could achieve this by linearly interpolating the position between the two points with a t set on which particle we are spawning devided by the total number of particles being spawned. So for the second particle, the position would be LinearInterpolate(new Vector2(0, 0), new Vector2(1000, 0), 2/10.0))

However, we’re not done yet. Now we have spawned the particles at different positions, but we ignored the fact that the particle closest to the “previous” position lived a little longer than the one closest to the “new” position. So we’ll need to update every particle separately before we’re done.

Since every particle has its own update call, we could just call its update method (with the right delta time off course) right after it has been created.

As the parameter to the update method, we could again use a linear interpolation. LinearInterpolate(secondsPerFrame, 0, particleNumber/totalParticles)

Well, that’s it. The particle system should work fine now.

Here’s the code that implements this:

In order to remember the last position of the particle system, we the following code to the particle system class:

Vector2 position;
public Vector2 Position
{
    get { return position; }
    set { LastPos = position; position = value; }
}
public Vector2 LastPos;

Also add this to the constructor.

this.LastPos = Position;

In Emitter.cs at the creation of the new particle, change

RelPosition + Parent.Position,

to

RelPosition + MathLib.LinearInterpolate(Parent.LastPos, Parent.Position, SecPassed / dt),

And add

ActiveParticles.Last.Value.Update(SecPassed);

after the creation of the particle.

Well, that’s all folks.
You can still add extra functionality to it like a bool that controls whether particles should be spawned or not.

This concludes the first tutorial i’ve ever written. Please post comments or questions, it’s always fun to read those.

Heres the source code: Project Files!