HDR effects

This week I’ve been working on making explosions and weapon effects cooler. I ended up using some HDR lighting techniques in an interesting non-traditional way so I thought I would write up some of the things I learned. The main message is (1) particles rock (2) use premultiplied alpha and (3) make the brightest, most fully saturated parts of additively blended particle effects white to make them appear brighter. There is an easy way to do this in a post-processing “tonemap” shader.

Screen Shot 2013-12-19 at 2.03.39 PM

Particles

Particle systems are an easy and versatile way to add special effects to your game. Lutz Latta has a good technology overview over on Gamasutra. Relatively high performance particle systems do not have to be complex: My particle system supports a quarter million particles in about 300 lines of code. It is stateless (position is a function of time) and moves the particles in the vertex shader. Particles (vertex attributes) are stored and allocated in a single large circular buffer so I only need to do a single glBufferSubData to send new particles to the GPU every frame and a single draw call to draw it.

Smoke and Fire

Both smoke and fire contribute to that explodey look. Dark smoke in the background provides contrast to make the sparks really stand out. We want areas with a lot of fire particles to appear brighter than areas with only a few fire particles, whereas adding more smoke to a smokey area does not increase brightness.

In graphics parlance the fire particles are “additively blended”, which means that we just directly add the fire color to whatever was underneath the fire. In OpenGL this translates to glBlendFunc(GL_ONE, GL_ONE) (i.e. the final color is 1 * particle color + 1 * original color).

Smoke on the other hand can be modeled as a more typical semi-transparent material. Smoke particles do obscure underneath pixels. In OpenGL this is implemented with glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) (i.e. the final color is alpha * particle color + (1-alpha) * the original color). What if we want to support both types of particles without switching the blending function?

The solution is to use glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA). To support additive blending (fire), we set the alpha to zero. To support transparency (smoke), we set alpha to the transparency we want and then premultiply the RGB components by the alpha value. This gives the same image as the above blending functions but is more flexible. Scaling the RGB components like this is called “premultiplied alpha” and Tom Forsynth has a lucid explanation of its many uses on his blog. With premultiplied alpha you can think of the color components (rgb) as adding light to the image and the alpha component (a) as blocking light.

High Dynamic Range

From wikipedia:

Tone mapping is a technique used in image processing and computer graphics to map one set of colors to another in order to approximate the appearance of high dynamic range images in a medium that has a more limited dynamic range.

What happens when you have a bunch of fire particles on top of each other? Since they are additively blended, if you are using a typical framebuffer (8 bits each of red, green, and blue), the color components are just added together and then clamped. This quickly results in fully saturated colors as used color components (i.e. red and green for orange fire) are clamped to 255 and unused components (blue) remain at zero.

The basic idea in HDR lighting is to first draw everything to a floating point framebuffer (e.g. GL_RGBA16F_ARB, where colors may be brighter than 1.0) to avoid loosing information to clamping, then apply a tonemapping operator to squish the dynamic range back down to 8 bits per channel for display.

Typically in photographic or photorealistic applications this means making the darkest parts of the image brighter and the brightest parts darker so that detail is preserved everywhere. In my case I just wanted to make the brightest parts of the image bright white. This is my tonemapping operator (in GLSL). It just checks if any components are too bright to directly display, and if so blends the other components towards white (remember that each component is clamped to 1.0 when written to a non-floating point framebuffer).

float mx = max(color.r, max(color.g, color.b));
if (mx > 1.0) {
    color.rgb += vec3(mx - 1.0);
}

In the top left image, you can see that the brightest part of the explosion is pure yellow. This is because the orange and red explosion particles contain only red and green channels with no blue channel, so no amount of adding and clamping will result in white. The right images are using my desaturating tone-mapping operator to make the brightest parts white. It looks brighter right?

explosion with clamping explosion with desaturate tonemapping
shooting with clamping shooting with desaturate tonemapping

2 thoughts on “HDR effects

  1. This is very cool. I was so worried about losing saturation and contrast with tone mapping that the ugliness of light stacking behavior hadn’t even occurred to me.

    I found that adding the full (mx – 1.0) to every channel was a bit extreme though, and ended up dividing the value by 3 (the number of channels) which gave a very industrial-light-and-magic kind of look. Considering that without it the look was more ‘disney movie’ that’s an impressive achievement for just a few lines of very simple shader code.

  2. Pingback: How to fix color banding with dithering | REASSEMBLY

Leave a Reply