Creating a bloom effect

A bloom is a visual effect where the bright parts of an image seem to have fringes that extend beyond the boundaries into the darker parts of the image. This effect has its basis in the way that cameras and the human visual system perceive areas of high contrast. Sources of bright light "bleed" into other areas of the image due to the so-called Airy disc, which is a diffraction pattern produced by light that passes through an aperture.

The following image shows a bloom effect in the animated film Elephant's Dream (© 2006, Blender Foundation / Netherlands Media Art Institute / www.elephantsdream.org). The bright white color from the light behind the door "bleeds" into the darker parts of the image.

Creating a bloom effect

Producing such an effect within an artificial CG rendering requires determining which parts of the image are bright enough, extracting those parts, blurring, and re-combining with the original image. Typically, the bloom effect is associated with HDR (High Dynamic Range) rendering. With HDR rendering, we can represent a larger range of intensities for each pixel (without quantizing artifacts). The bloom effect is more accurate when used in conjunction with HDR rendering due to the fact that a wider range of brightness values can be represented.

Despite the fact that HDR produces higher quality results, it is still possible to produce a bloom effect when using standard (non-HDR) color values. The result may not be as effective, but the principles involved are similar for either situation.

In the following example, we'll implement a bloom effect using five passes, consisting of four major steps:

  1. In the first pass, we will render the scene to an HDR texture.
  2. The second pass will extract the parts of the image that are brighter than a certain threshold value. We'll refer to this as the bright-pass filter. We'll also downsample to a lower resolution buffer when applying this filter. We do so because we will gain additional blurring of the image when we read back from this buffer using a linear sampler.
  3. The third and fourth passes will apply the Gaussian blur to the bright parts (refer to the Applying a Gaussian blur filter recipe in this chapter).
  4. In the fifth pass, we'll apply tone mapping and add the tone-mapped result to the blurred bright-pass filter results.

The following diagram summarizes the process. The upper-left shows the scene rendered to an HDR buffer, with some of the colors out of gamut, causing much of the image to be "blown-out". The bright-pass filter produces a smaller (about a quarter or an eighth of the original size) image with only pixels that correspond to a luminance that is above a threshold. The pixels are shown as white because they have values that are greater than one in this example. A two-pass Gaussian blur is applied to the downsampled image, and tone mapping is applied to the original image. The final image is produced by combining the tone-mapped image with the blurred bright-pass filter image. When sampling the latter, we use a linear filter to get additional blurring. The final result is shown at the bottom. Note the bloom on the bright highlights on the sphere and the back wall.

Creating a bloom effect

Getting ready

For this recipe, we'll need two framebuffer objects, each associated with a texture. The first will be used for the original HDR render, the second will be used for the two passes of the Gaussian blur operation. In the fragment shader, we'll access the original render via the variable HdrTex, and the two stages of the Gaussian blur will be accessed via BlurTex.

The uniform variable LumThresh is the minimum luminance value used in the second pass. Any pixels greater than that value will be extracted and blurred in the following passes.

Use a vertex shader that passes through the position and normal in eye coordinates.

How to do it...

To generate a bloom effect, use the following steps:

  1. In the first pass, render the scene to the framebuffer with a high-res backing texture.
  2. In the second pass, switch to a framebuffer containing a high-res texture that is smaller than the size of the full render. In the example code, we use a texture that is one-eighth the size. Draw a full screen quad to initiate the fragement shader for each pixel, and in the fragment shader sample from the high-res texture, and write only those values that are larger than LumThresh. Otherwise, color the pixel black.
    vec4 val = texture(HdrTex, TexCoord);
    if( luminance(val.rgb) > LumThresh )
        FragColor = val;
    else
        FragColor = vec4(0.0);
  3. In the third and fourth passes, apply the Gaussian blur to the results of the second pass. This can be done with a single framebuffer and two textures. "Ping-pong" between them, reading from one and writing to the other. For details, refer to the Applying a Gaussian blur filter recipe in this chapter.
  4. In the fifth and final pass, switch to linear filtering from the texture that was produced in the fourth pass. Switch to the default frame buffer (the screen). Apply the tone-mapping operator from the Implementing HDR lighting with tone mapping recipe to the original image texture (HdrTex), and combine the results with the blurred texture from step 3. The linear filtering and magnification should provide an additional blur.
    // Retrieve high-res color from texture
    vec4 color = texture( HdrTex, TexCoord );
    
    // Apply tone mapping to color, result is toneMapColor
    …
    
    ///////// Combine with blurred texture //////////
    vec4 blurTex = texture(BlurTex1, TexCoord);
    
    FragColor = toneMapColor + blurTex;

How it works...

Due to space constraints, I haven't show the entire fragment shader code here. The code is available from the GitHub repository. The fragment shader is implemented with five subroutine methods, one for each pass. The first pass renders the scene normally to the HDR texture. During this pass, the active framebuffer object (FBO) is the one associated with the texture corresponding to HdrTex, so output is sent directly to that texture.

The second pass reads from HdrTex, and writes out only pixels that have a luminance above the threshold value LumThresh. The value is (0,0,0,0) for pixels that have a brightness (luma) value below LumThresh. The output goes to the second framebuffer, which contains a much smaller texture (one-eighth the size of the original).

The third and fourth passes apply the basic Gaussian blur operation (refer to the Applying a Gaussian blur filter recipe in this chapter). In these passes, we "ping-pong" between BlurTex1 and BlurTex2, so we must be careful to swap the appropriate texture into the framebuffer.

In the fifth pass, we switch back to the default framebuffer, and read from HdrTex and BlurTex1. BlurTex1 contains the final blurred result from step four, and HdrTex contains the original render. We apply tone mapping to the results of HdrTex and add to BlurTex1. When pulling from BlurTex1, we are applying a linear filter, gaining additional blurring.

There's more...

Note that we applied the tone-mapping operator to the original rendered image, but not to the blurred bright-pass filter image. One could choose apply the TMO to the blurred image as well, but in practice, it is often not necessary.

We should keep in mind that the bloom effect can also be visually distracting if it is overused. A little goes a long way.

See also

  • HDR meets Black&White 2 by Francesco Caruzzi in Shader X6
  • The Rendering to a texture in recipe in Chapter 4, Using Textures
  • The Applying an edge detection filter recipe
  • The Using subroutines to select shader functionality recipe in Chapter 2, The Basics of GLSL Shaders
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.59.100.42