Building a Direct3D Particle Engine

by Michael Fötsch
August 16, 2000 (Last update: December 18, 2000)

This article was originally published at Gary Simmons’ Mr GameMaker.

Particle  Demo Screenshot

A screenshot of the downloadable particle demo. There are four particle systems: snow, smoke, a fountain, and (believe it or not) a lawn sprinkler. As is always the case with particle engines: “This looks much better when animated…”

Table of Contents

  1. Introduction
  2. The Basics
  3. Making particles face the camera
  4. Alpha-blending
  5. Engine architecture
  6. The first particle system
  7. Let the Fun begin!
  8. References
  9. Code archive

Introduction

There seems to be a law that all particle engine tutorials have to begin with “Imagine the following scene…”. Let’s break this rule! Just take a look at the screenshot above and you’ll immediately know what particle engines are, how they look like, and what they can be used for. This tutorial will introduce you to the concepts behind particle engines, and the downloadable source code will get you started immediately. Believe me, it’s not even complicated!

The Basics

Once you know how to do billboards, a particle engine is not that far away. What looks like a fountain, a fire, or snowfall is actually nothing but a bunch of translucent textured rectangles moving through the scene. The trick is to make them behave like water, fire, snow, or whatever. Using your common sense is usually enough to be able to create a simulation of the real world: Snow falls, doesn’t it; a fountain throws water droplets into the air, which are affected by gravity and thus will come down again; smoke rises but is also affected by wind before it dissolves after some time…

As billboards are that important for particle engines, let’s summarize what we already know about them:

  • In 3D graphics, billboards are not “signs along the road” with pictures of the M…boro Man on them…
  • Billboards are rectangular shapes made up of two triangles each.
  • They are textured.
  • They always face the camera.

Screenshot of Billboard DirectX SDK sample
These trees are billboards. (Screenshot of Billboard DirectX SDK sample)

In addition to the characteristics of billboards, particles are almost always translucent, i.e. D3DRENDERSTATE_ALPHABLENDENABLE is set to TRUE.

Making particles face the camera

We’ll get back to alpha-blending soon. But first, let’s solve another problem: How does one ensure that a particle always faces the camera? As you know, vertices are affected by three matrices: The world, the view, and the projection matrix. Obviously, what we have to do is revert the rotation applied by the view matrix (our representation of the camera). We still want our particles to be translated and scaled–but not rotated, because that would immediately reveal that they are in fact flat.
What we have to do is apply the view matrix to the position of the particle by hand. We then set up a rectangle by positioning its four vertices around the (translated) center. The z-coordinate will be the same (0.0f) for all vertices. While rendering the particle, the view transformation must be disabled, so that the vertices aren’t transformed twice. Here’s the code that creates a particle:

    // Get the current view matrix:
    D3DMATRIX matView;
    lpDevice->GetTransform(D3DTRANSFORMSTATE_VIEW, &matView);

    D3DVECTOR Pos = Particles[i].Position;
    D3DVECTOR TransPos;     // Transformed position
    // Apply the view matrix to the position vector:
    TransPos.x = Pos.x * matView(0, 0) + Pos.y * matView(1, 0)
        + Pos.z * matView(2, 0) + matView(3, 0);
    TransPos.y = Pos.x * matView(0, 1) + Pos.y * matView(1, 1)
        + Pos.z * matView(2, 1) + matView(3, 1);
    TransPos.z = Pos.x * matView(0, 2) + Pos.y * matView(1, 2)
        + Pos.z * matView(2, 2) + matView(3, 2);

    D3DLVERTEX Shape[4];
    // upper left:
    Shape[0] = D3DLVERTEX(
        TransPos + D3DVECTOR(
            -0.5f*Particles[i].Size, 0.5f*Particles[i].Size, 0.0f),
        ParticleColor, 0xffffffff,      // color (with alpha) and specular color
        0.0f, 0.0f);                    // texture coords
    // upper right:
    Shape[1] = D3DLVERTEX(
        TransPos + D3DVECTOR(
            0.5f*Particles[i].Size, 0.5f*Particles[i].Size, 0.0f),
        ParticleColor, 0xffffffff, 1.0f, 0.0f);
    // lower left:
    Shape[2] = D3DLVERTEX(
        TransPos + D3DVECTOR(
            -0.5f*Particles[i].Size, -0.5f*Particles[i].Size, 0.0f),
        ParticleColor, 0xffffffff, 0.0f, 1.0f);
    // lower right:
    Shape[3] = D3DLVERTEX(
        TransPos + D3DVECTOR(
            0.5f*Particles[i].Size, -0.5f*Particles[i].Size, 0.0f),
        ParticleColor, 0xffffffff, 1.0f, 1.0f);

This shape can be rendered as a triangle strip:

    lpDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, D3DFVF_LVERTEX,
        Shape, 4, NULL);

Note: Instead of writing your own code for applying a matrix to a vector, you can use D3DMath_VectorMatrixMultiply from the DirectX SDK D3DFrame.

Note: You do not have to use lit vertices (D3DLVERTEX). You can also have Direct3D do the lighting for you. With lit vertices it is just easier to apply new alpha and color values wihout having to set a material. Furthermore, many particle systems should not be influenced by light sources anyway (fire, for example, emits its own light, as well as explosions).

Alpha-blending

For particle engines, alpha-blending (along with a good choice of blend factors) is vital:

Particles rendered without alpha-blending Particles rendered with alpha-blending

In the left screenshot, you can see the particles rendered without alpha-blending. You can see that they are simply textured rectangles and how a typical particle texture looks like. In the right screenshot, alpha-blending is enabled and source and destination blend factor are both set to D3DBLEND_ONE.

There is nothing special about alpha-blending when working with particles, which means the formula for calculating the final color is still the same (FinalColor = DestColor × DestBlendFactor + SrcColor × SrcBlendFactor). In fact, you are free to use any combination of blend modes (and texture stage states) that looks good. For a typical particle system, however, some blend modes are more useful than others.

For example, the more particles are on top of each other, the brighter the color in that area should be. A single white particle with an opacity of 0.5f would change a black background to a 50% gray background. A second particle at the same location (or partially overlapping the first particle), would then brighten up the background even further, either to 75% (averaging) or to 100% (adding):

This figure demonstrates how averaging would look like. One particle makes the background 50% gray, two particles make it 75% gray, three particles make it 87.5% gray.

Averaging can be achieved with a source blend factor of D3DBLEND_SRCALPHA and a destination blend factor of D3DBLEND_INVSRCALPHA:

    lpDevice->SetRenderState(D3DRENDERSTATE_SRCBLEND, D3DBLEND_SRCALPHA);
    lpDevice->SetRenderState(D3DRENDERSTATE_DESTBLEND, D3DBLEND_INVSRCALPHA);

Note: These blend modes require that the texture has an alpha channel. (DDS files support this. They can be created using dxtex.exe from the DirectX SDK.) Otherwise, it would not look correct, because the entire rectangle would be dimmed but only the non-black parts of the texture would be lit up again. I will later present a way around this.

Figure  demonstrating the (probably undesired) results of color addition Figure  demonstrating the workaround for the color addition issue

Simply adding the color, as with source and destination blend modes set to D3DBLEND_ONE, does not always give the desired results. Take, for example, a red particle (1, 0, 0) on a blue background (0, 0, 1). A single particle would change the destination color to magenta (1, 0, 1). Rendering a second particle at the same location would not change the destination pixels any further. The red and blue components are already “full on”, and the green component is not affected by the particles at all.
What you could do is setting the other color components of your red particle to some small value, e.g. 1.0f, 0.1f, 0.1f. This would ensure that additional particles have additional influence on the destination color. However, this does not make the particles more red (ultimately, the result will be white).
To summarize the whole thing, it is not possible to darken the destination pixels or single color channels. They will be at least as bright as they were before. The only thing that would help here is a texture with an alpha-channel…

Note: At least for black particles (for smoke effects), there is a way around alpha-channels: Set the vertex color to white and use a source blend factor of D3DBLEND_ZERO and a destination blend factor of D3DBLEND_INVSRCCOLOR. The level of white (of the vertices) gives the opacity of the particle, i.e. white is fully opaque, black is fully transparent. Of course, darker areas in the particle texture will still be more transparent than bright areas.
However, there is no good reason why you should not use alpha-channels. I just wanted to find out whether it is even possible to do without them. As it turns out, alpha-channels sometimes make your life a lot easier!

Note: If you are new to alpha-blending and are looking for a tutorial, see the References section for links to alpha-blending tutorials on this site.

Engine architecture

Let’s finally get down to actually writing such a particle engine. I chose an object-oriented architecture, because it’s easy to work with and easily extensible. The engine will consist of at least two classes: TParticleSystem and TParticle.

Note: What I refer to as a “particle system” is a single occurance of a specific special effect, e.g. a single fountain, a single explosion, etc. The “particle engine” is the code module that updates and renders the particle systems.

TParticle is the class that encapsulates the properties of a single particle within a particle system. It’s members are Position, Velocity, Acceleration, Age, etc. My sample implementation looks like this:

    class TParticle
    {
    public:
        D3DVECTOR Position;
        D3DVECTOR OldPos;
        D3DVECTOR Velocity;
        D3DVECTOR Acceleration;
        DWORD Age;
        D3DVALUE Size;
        bool Alive;
    };

The purpose of the class members will be explained shortly. Most of them are self-explanatory anyway.

TParticleSystem, on the other hand, is an abstract base class that the actual particle systems will inherit from. It declares a number of virtual methods and common properties:

    class TParticleSystem
    {
    protected:
        D3DLVERTEX Shape[4];
        TParticle *Particles;
        int NumParts;

    public:
        TParticleSystem(int numparts, D3DVECTOR origin);
        virtual ~TParticleSystem();

        D3DVECTOR SystemOrigin;

        virtual void ResetSystem();
        virtual void SetParticleDefaults(int i) = 0;
        virtual void UpdateSystem(DWORD TimePassed) = 0;
        virtual HRESULT RenderSystem(LPDIRECT3DDEVICE7 lpDevice) = 0;
    };

Let’s take a closer look at TParticleSystem and how it works:

In its constructor, TParticleSystem allocates memory for an array of NumParts TParticle structures (pointed to by TParticle *Particles). Furthermore, it stores its parameters in the appropriate class members. Derived classes will have additional parameters, but should still call the base class constructor to have the particles array allocated.

The particle system is initialized by calling ResetSystem, which is the only non-abstract member function of TParticleSystem. ResetSystem calls SetParticlesDefaults for every particle in the array. Dervied classes will usually not have to overwrite ResetSystem.

UpdateSystem will usually be called from the UpdateScene function of your application. It calculates the new particle properties for the current frame and applies them to the TParticle structures. If after these changes the particle has reached its final state (snow hits the ground, smoke has dissolved, etc.), SetParticleDefaults is called, which sets the particle properties to their initial states. Usually, “initial state” means a position near the system origin and a new (random) speed and direction.

RenderSystem draws the particle system to the screen. As this method is abstract, it must be overwritten by derived classes.

The first particle system

Talking about base classes is way too…abstract, isn’t it. It’s time to get something onto our screens! And there could be no better time for “class TSnow” than this!

Note: The following source code is taken from ParticleEng.cpp and .h. You can download these files as part of a ZIP file from the code archive.

We start off by deriving a new class from TParticleSystem:

    class TSnowfall : public TParticleSystem
    {

Next, we declare a new member that will store the name of the particle texture:

    protected:
        char TexFile[MAX_PATH];

Note: In the sample application accompanying this tutorial, I am using D3DTextr.cpp from the DirectX SDK “D3DFrame” to manage textures. Using D3DTextr, textures are referenced through their filename. In your own implementation of the particle engine, you might want to store a pointer to a DirectDraw surface instead.

The constructor is declared as follows:

    TSnowfall(LPDIRECT3DDEVICE7 lpDevice, int numparts, D3DVECTOR origin,
        float width, float depth, float ground, char *texfile);

The paramters lpDevice and texfile are both required to load the particle texture. Snowflakes are released from within the rectangle (origin.x-width/2, origin.y, origin.z+depth/2)-(origin.x+width/2, origin.y, origin.z-depth/2). Once their height is <= ground, they are reset.

Furthermore, we need to overwrite the abstract methods and declare a new destructor (where the texture will be released):

    virtual ~TSnowfall();

        float Width, Depth, Ground;

        virtual void SetParticleDefaults(int i);
        virtual void UpdateSystem(DWORD TimePassed);
        virtual HRESULT RenderSystem(LPDIRECT3DDEVICE7 lpDevice);
};

I’ll skip the implementation of the constructor and destructor here. (Both can be found in the downloadable ZIP file, of course.) Instead, we’ll go right to the next method, SetParticleDefaults. As I told you, that method is responsible for putting the particles at their initial position and giving them an initial speed and velocity. Our particles will all have the same y-position, but will be equally spread along the x- and z-axes. This can be calculated as follows:

    void TSnowfall::SetParticleDefaults(int i)
    {
        Particles[i].Position = D3DVECTOR(
            Width * (RandFloat - 0.5f),
            0,
            Depth * (RandFloat - 0.5f)) + SystemOrigin;

Note: RandFloat returns a float in the range between 0.0f and 1.0f. It is implemented as a macro:

    #define RandFloat ((float)(rand()%1001)/1000.0f)

The particle will also need to move. Velocity is a vector that specifies the particle speed along the three axes. When updating the system, Velocity is multiplied by the number of milliseconds passed, and the result is added to the vector Position. Our snowflakes will slowly fall downwards and will also move slightly sidewards:

    Particles[i].Velocity = D3DVECTOR(
        RandFloat*0.001f-0.0005f,
        -(RandFloat*0.0049+0.0049f),
        RandFloat*0.001f-0.0005f);

Note that the y-velocity is negative. It will be somewhere between 4.9 and 9.8 units per second. This is important for every kind of particle system: Particles should have different lifetimes. (In this case, lieftime is dictated by speed.) Otherwise, the system would just look strange…

Note: This system does not make use of acceleration, i.e. it is set to D3DVECTOR(0, 0, 0). Typically, a derived UpdateSystem will multiply Acceleration by the number of milliseconds and add the result to Velocity. The most common acceleration is gravity, which is an acceleration of 9.8 m/s² (a bit less near the equator, a bit more near the poles) along the negative y-axis.

UpdateSystem will look similar for all particle systems. The particles are moved and, if necessary, reset:

    for (int i=0; i < NumParts; i++)
    {
        Particles[i].Position += Particles[i].Velocity * TimePassed;
        Particles[i].Velocity += Particles[i].Acceleration;
        if (Particles[i].Position.y < Ground) SetParticleDefaults(i);
    }

Other systems might also update Age (Age += TimePassed) and modify Size accordingly. They might also set the alpha value of the particle vertices. (For an example of this, see the “TSmoke” system.)

Finally, the particles are rendered. The sample engine assembles a billboard using the code from above and calls DrawPrimitive for every particle. This is straight-forward but not efficient. You could also add the vertices to a larger buffer and call DrawPrimitive once the buffer is filled. To take advantage of TnL hardware, you’d have to use a vertex buffer.

Prior to rendering the particles, the function sets the appropriate render states:

    lpDevice->SetRenderState(D3DRENDERSTATE_ALPHABLENDENABLE, true);
    lpDevice->SetRenderState(D3DRENDERSTATE_SRCBLEND, D3DBLEND_ONE);
    lpDevice->SetRenderState(D3DRENDERSTATE_DESTBLEND, D3DBLEND_ONE);
    lpDevice->SetRenderState(D3DRENDERSTATE_LIGHTING, false);
    lpDevice->SetRenderState(D3DRENDERSTATE_ZWRITEENABLE, false);

Alpha-blending is enabled, the blend factors are set, lighting is disabled (we’re using previously lit vertices), and the z-buffer will not be updated by the particles. Note that we still want the z-buffer to be read. That way, particles will correctly disappear behind objects. As they are translucent themselves, it is unnecessary (and would give incorrect results) to update the z-buffer.
After rendering is complete, the render states are restored.

Using the particle system

Here is how you would call this and other particle systems from your application:

    TDerivedParticleSystem *ParticleSystem=NULL;

    void InitDeviceObjects()
    {
        ...
        ParticleSystem = new TDerivedParticleSystem(...);
        ParticleSystem->ResetSystem();
        ...
    }

    void UpdateScene()
    {
        DWORD TimePassed = timeGetTime() - LastTime;
        ...
        ParticleSystem->UpdateSystem(TimePassed);
        ...
    }

    void RenderScene()
    {
        RenderNonTransparentObjects();
        ParticleSystem->RenderSystem(lpD3DDevice);
    }

    void DestroyDeviceObjects()
    {
        SAFEDELETE(ParticleSystem);
    }

Let the Fun begin!

You will probably have to modify the particle engine to be able to seemlessly integrate it into your own code. There is also plenty of room for optimizations, and dozens of special effects are waiting on the to-do list…

However, this tutorial was meant as a starting point for you to experiment with particle engines. Take the demo application, modify some variables, add a “*” here and remove a “+” there… If you come up with anything interesting, don’t hesitate to send it to us! We’ll publish the best particle systems in a follow-up article. (With your name in big letters, of course.)

To be continued…


Code archive

particles.zip: Particle engine and demo application source code, compiled demo.

Google+