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.
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:
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.
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.
To generate a bloom effect, use the following steps:
LumThresh
. Otherwise, color the pixel black.vec4 val = texture(HdrTex, TexCoord); if( luminance(val.rgb) > LumThresh ) FragColor = val; else FragColor = vec4(0.0);
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;
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.
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.
13.59.100.42