Creating
the Data Structure
Now that
we know what features we need, it's time to design our classes. Figure
1 shows an overview of the system we're going to build. Notice that
there is a particle
manager, which I will explain more about in a moment.
 |
|
Figure
1. A global overview of the sytem we are going to build
|
|
Table
1. Particle attributes
|
|
Data
type
|
Name
|
Description
|
| Vector3 |
position |
The
position of the particle in world-space |
| Vector3 |
oldPos |
The
previous position of the particle, useful in some systems |
| Vector3 |
velocity |
The
velocity vector (position += velocity) |
| dword |
color |
The
color of the particle (its vertex colors) |
| int |
energy |
The
energy of the particle |
| float |
size |
The size
of the particle |
Let's
use a bottom-up approach to design our classes, beginning with the particle
class.
The
particle class. If you have built a particle system before, you
probably know the types of attributes a particle must have. Table 1
lists of some common attributes.
Note that
the previous position of a particle can also be useful in some systems.
For example, you might want to stretch a particle between its previous
and current positions. Sparks are a good example of particles that benefit
from this feature. You can see some spark effects I've created in Figure
2.
 |
|
Figure
2. Some spark effects you can create using a particle system such
as theone discussed in this article. Note that all particles perform
accurate collision detectino and response in these two screenshots.
|
The color
and energy attributes can be used to create some interesting effects
as well. In a previous particle system I created, I used color within
the smoke system, which let me dynamically light the smoke particles
using lights within the scene.
Energy value is very important as well. Energy is analogous to the age
of the particle - you can use this to determine whether a particle has
died. And because the color or intensity of some particles (such as
sparks) changes over time, you may want to link it to the alpha channel
of the vertex colors.
I strongly recommend that you leave the constructor of your particle
class empty, because you don't want to use default values at construction
time, simply because these values will be different for most particle
systems.
The
particle system class. This class is the heart of the system. Updating
the particle attributes and setting up the shape of the particles takes
place inside this class. My current particle system class uses the node
base class of my 3D engine, which contains data such as a position vector,
a rotation quaternion, and scale values. Because I inherit all members
of this node class, I can link my particle systems within the hierarchy
of the engine, allowing the engine to affect the position of the particle
system as discussed in the above cigarette example. If your engine does
not have hierarchy support, or if you are building a stand-alone particle
system, this is not needed. Table 2 lists the attributes which you need
to have in the particle system base class.
|
Table
2. Particle system base class attributes
|
|
Data
type
|
Name
|
Description
|
| Texture |
*texture |
A
pointer to a texture, which all particles will use. For performance
reasons, we only use one texture for each individual particle system;
all particles within the specific system will have the same texture
assigned. |
| BlendMode |
blendMode |
The
blend mode you want to use for the particles. Smoke will probably
have a different blend mode from blood - that's the reason you also
store the blend mode for each particle system. |
| int |
systemType |
A
unique ID, which represents the type of system (smoke or sparks,
for example). The systemType identifier is also required, since
you may want to check for a specific type of particle system within
the collection of all systems. For example, to remove all smoke
systems, you need to know whether a given system is a smoke system
or not. |
| Array
Particle |
particles |
The
collection of particles within this system. This may also be a linked
list instead of an array. |
| Array
PShape |
shapes |
A
collection of shapes, describing the shapes of the particles. The
shape descriptions of the particles usually consist of four positions
in 3D camera-space. These four positions are used to draw the two
triangles for our particle. As you can see in Table 1, a particle
is only stored as a single position, but it requires four positions
(vertices) to draw the texture-mapped shape of the particle. |
| int |
nrAlive |
Number
of particles in the system which are still alive. If this value
is zero, it means all particles are dead and the system can be removed.
|
| BoundingBox3 |
boundingBox |
boundingBox
The 3D axis-aligned bounding box (AABB), used for visibility determination.
We can use this for frustum, portal, and anti-portal checks. |
Here's
how to calculate the four positions of a normal (not stretched) particle
that always faces the active camera. First, transform your particle
world-space position into camera-space (multiply the world-space position
and your active camera matrix) using the size attribute of the particle
to calculate the four vertices.
The four vertices, which form the shape, are what we use to render the
particle, though a particle has only one position, xyz. In order to
render a particle (such as a spark), we need to set up a shape (created
from four vertices). Two triangles are then rendered between these four
points. Imagine a non-stretched particle always facing the camera in
front of you, as seen in Figure 3.
 |
|
Figure
3. Setting up a shape to render a particle
|
In our
case, the particle is always facing the active camera, so this means
we can simply add and subtract values from the x and y values of the
particle position in camera-space. In other words, leave the z value
as it is and pretend you are working only in 2D. You can see an example
of this calculation in Listing
1.
The functions.
Now that we know what attributes are needed in the particle system base
class, we can start thinking about what functions are needed. Since this
is the base class, most functions are declared as virtual functions. Each
type of particle system updates particle attributes in a different way,
so we need to have a virtual update function. This update function performs
the following tasks:
- Updates
all particle positions and other attributes.
- Updates
the bounding box if we can't precalculate it.
- Counts
the number of alive particles. It returns FALSE if there are no alive
particles, and returns TRUE if particles are still alive. The return
value can be used to determine whether a system is ready to be deleted
or not.
Now our
base class has the ability to update the particles, and we are ready
to set up the shapes which can be constructed using the new (and perhaps
previous) position. This function, SetupShape, needs to be virtual,
because some particle system types will need to have their particles
stretched and some won't. You can see an example of this function in
Listing 1.
To add a particle to a given system, or to respawn it, it's useful to
have a function that takes care of this. Again, it should be another
virtual function, which is declared like this:
virtual void SetParticleDefaults( Particle &p );
As I explained above, this function initializes the attributes for a
given particle. But what if you want to alter the speed of the smoke
or change the wind direction that affects all of your smoke systems?
This brings us to the next subject: the particle system's constructor.
Many particle systems will need their own unique constructors, forcing
us to create a virtual constructor and destructor within the base class.
In the constructor of the base class, you should enter the following
information:
- The
number of particles you initially want to have in this particle system.
- The
position of the particle system itself.
- The
blend mode you want to use for this system.
- The
texture or texture file name you want this system to use.
- The
system type (its ID).
In my
engine, the constructor in the particle system base class looks like
this:
virtual ParticleSystem(int nr, rcVector3 centerPos, BlendMode
blend=Blend_AddAlpha, rcString file name="Effects/Particles/
green_particle", ParticleSystemType type=PS_Manual);
So where do various settings, such as the wind direction for the smoke
system, get addressed? You can either add settings specific to the system
type (such as wind direction) into the constructor, or you can create
a struct called InitInfo inside each class, which contains all of the
appropriate settings. If you use the latter method, make sure to add
a new parameter in the constructor, which is a pointer to the new struct.
If the pointer is NULL, the default settings are used.
As you can imagine, the first solution can result in constructors with
many parameters, and that's not fun to work with as programmer. ("Parameter
number 14
hmmm. What does that value represent again?") That's
the main reason I don't use the first method. It's much easier to use
the second method, and we can create a function in each particle system
class to initialize its struct with default settings. An example of
this code and a demo application can be found on the Game
Developer web site (http://www.gdmag.com) or my own site at http://www.mysticgd.com.
________________________________________________________
The
Particle Manager