[This article was first featured in Gamasutra]
In 3D and 2D, particle systems are used extensively to simulate a variety of physical phenomena and special effects, such as: Fire, smoke, blood, magic, sparks, rain, water, grass and many more.
A particle system is essentially an emitter object, which has shape and size, which creates particles according to certain rules. Each particle that's created receives a variety of attributes form the emitter and environment, such as speed, color progression, direction, rotation and time-to-live.
The idea is to combine and blend many of these basic particles in a way that create pretty and complex effects
(The above is a single emitter which emits about 40 particles a second, each with a random fade duration, grow, speed and angular speed. Particles fade from opaque-red to transparent-black as their end-of-life approaches. The smoke textures are grayscale, so any smoke color can be generated with the same assets as we MULTIPLY the texture's color).
Illumination and Occlusion
If we keep it simple, a pixel blended to the pixel buffer can either occlude the existing pixel, or illuminate it.
Smoke or blood occludes the scene, while sparks, flames and flares illuminate it. However, a particle system isn't limited to one of these -- consider a torch particle system -- A particle starts as an illuminating particle (flame) but as time progresses it transforms to an occluding particle (smoke). Therefore, we need to support both effects, sometimes in the same particle at the same time.
For our effects to look right, we need to use the appropriate blend mode. I'll focus on OpenGL, but everything stated here holds for DirectX or any other low-level 3D API.
The last stage of the OpenGL 1 pipeline is to combine the current color at the pixel buffer with the color of the incoming fragment. The examples for glBlendFunc in OpenGL ES 1.1 docs states that:
Transparency is best implemented using glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) with primitives sorted from farthest to nearest.
This is unfortunate, as composing images with this blend mode is limited and error prone. (Working naïvely with this blend mode might cause dark halos around objects)
Consider a pixel represented by: (Rs,Gs,Bs,As) which we would like to blend with the existing pixel: (Rd,Gd,Bd,Ad) where each pixel component is in the [0,1] range.
OpenGL suggests the following blending: (using the red channel as an example)
Rout = As*Rs + (1-As)*Rd
Notice that all components are always lower than or equals to one -- We cannot lighten the scene, only darken it. This blending mode is good for occluding (see the world through pink glasses), but cannot be used to lighten a scene.
To illuminate (use a flashlight over something for example), we might have chosen to do:
That blends as follows:
Rout = As*Rs + Rd
Where we only add light to scene and control the amount of light added with the source's alpha channel. However, again, as all components are always non-negative we can only lighten the scene, disallowing occlusion.
To accommodate for both needs, Porter & Duff in a 1984 paper (Computer Graphics Volume 18, Number 3 July 1984 pp 253-259) suggest using:
Rout = Rs + (1-As)*Rd
That allows us achieve both occlusion and illumination:
- Occlusion is achieved by premultiplying the RGB channels with the alpha channel: Rs=Rs*As for every pixel in the original texture. This makes the above method identical to the method suggested in the OpenGL docs, and saves a runtime multiplication.
- Illumination is achieved by not premultiplying the texture, and using the alpha channel as the amount of light to add to the scene
A picture is worth a thousand words. In the following example both particles are rendered with the same alpha channel (shown on the right) in the same blend mode (mentioned at the bottom). Notice how we're able to achieve smooth occlusion on the right particle, while being able to lighten the background on the circumference of the left particle:
If I would have had to guess why OpenGL authors chose the less useful blending equation as their example, I would guess that it's because most (if not all) graphic editors don't premultiply alpha, and this is the correct way to achieve the expected particle (on the right).
In the popular editing programs premultiplying a texture is not straightforward. For example, this is what you'd have to do to convert a non-multiplied image to premultiplied image in The GIMP:
- Open the image you wish to premultiply (make sure it is flattened)
- Right-click drawing layer -> Add Layer Mask…
- Choose: Transfer layer's alpha channel
- Click the layer mask
- Select all and copy
- Choose the drawing layer and paste
- Click the New Layer button to make the floating layer a new layer. You should now see the alpha channel on top of the drawing
- Change the alpha layer mode to Multiply
- Merge the alpha layer into the drawing layer (Right-click -> Merge down)
- You now have a premultiplied image. Export and use
At this stage, it is important to note two issues we've encountered specifically on Android:
- BitmapFactory.decodeBitmap does not handle PNG transparency well enough for our needs. Transparent pixel's RGB is overridden with black, and low alpha values aren't perfectly loaded. To utilize to the full extent the method suggested here, you will have to use a better decoder (a few are available in appropriate licensing and quality)
- As I've mentioned before, graphic editing application do not usually premultiply alpha. I assume that for that reason the implementers of GLUtils.texImage2D have chosen to premultiply all textures loaded using this function before passing it to OpenGL. Just remember that if your texture is already premultiplied, you need to use glTexImage2D directly. This is unfortunate, as combining this behavior with the one recommended by the OpenGL docs causes double-alpha multiplication, which is characterized by ugly dark halos around objects.
Let's combine everything for an effect. We will create a very basic, simple and quick fire-burst effect, like the one you'll expect to see in the rocket's exhaust or an oil rig. Our fire will have three stages: Ignition, flame and smoke. The ignition happens at the base of the fire, and it's very bright -- it illuminates its surrounding and it's almost white in color. The flame is the fire after it's detached from the fuel, and is yellow-orange in color. It still provides some illumination, but not like the ignition. The last part is the smoke and soot -- It is grey or black and blocks what's behind it until it disappears.
Here are the three stages in our particle's lifecycle: (zoom set to 300%).
(Please note that for demonstration purpose I only listed the key frames in our particle's animation. These key frames should be interpolated to create smoother particle animation.)
The alpha channel tells the story. The first stage's RGB is not premultiplied, and the alpha channel set to black -- making the particle a complete addition mask. Note that I've chosen the particle's color to be a shade of orange that still has some color in the blue channel -- otherwise adding these particles on top of each other will give pure yellow, instead of the white we're aiming at. The particle's ADD mask is built to soften the edge of the flame in the ignition stage.
The second stage is similar to the first stage, except the add mask is softer -- the color added is darker, and the add factor is lower. This serves as a good transition between the bright ignition and the dark soot. The texture is not premultiplied, although it may seem that way at first glance.
The smoke is very simple -- it is a gray, alpha premultiplied particle, which only occludes the scene. We use a gray that is lighter than the shade of smoke we want, since we'll also be fading the particle to transparent black as its end-of-life approaches.
The emitter is a simple point emitter that emits 40-50 particles per second. It sets the particles lifetime and speed (height of the flame), its speed direction (flame spread), and its grow rate (flame's "body"). In addition, each particle is created with a slightly different semitransparent white "tint" (flame intensity) and finally, as mentioned before, each particle is tinted to transparent black toward the end of its life.
With a little trial-and-error we calibrate the emitter to achieve the final result:
There is plenty of room for modifications -- for example, by changing the time each particle spends in each stage we can get more intense ignition, or a longer smoke trail. We could also apply physical modifiers to each particle, adding wind or gravity for instance.