Introduction
Bloom is a post-processing effect which produces fringes on light sources giving a glow effect. Bloom gives all brightly lit regions of a scene a glow-like effect.
- Bloom is most effective when HDR is also used.
The basic steps to implement bloom are:
- Render the scene to a framebuffer with HDR color buffer.
- Extract the HDR color buffer and save it in a texture.
- Extract brightly lit regions from the color buffer i.e. regions which exceed a certain brightness threshold and save them in a seperate ’threshold brightness’ texture.
- Blur the ’threshold brightness’ texture using blurring techniques in the shader. Save the result in a seperate color texture.
- Add the blurred texture to the original HDR scene. The resultant scene will have a glow-effect in the brightly lit regions.
Extracting Bright Colors
Multiple Render Targets
Render to multiple texture targets in a single render pass.
#version 330 core
layout (location = 0) out vec4 FragColor;
layout (location = 1) out vec4 BrightColor;
Brightness Calculation
unsigned int attachments[2] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
glDrawBuffers(2, attachments); // render to both framebuffers
We write the scene colors to FragColor
output (GL_COLOR_ATTACHMENT0
) and the bright colors to BrightColor
(GL_COLOR_ATTACHMENT1
).
FragColor = [...]
// transform FragColor to grayscale
float brightness = dot(FragColor.rgb, vec3(0.2126, 0.7152, 0.0722));
if(brightness > 1.0)
BrightColor = vec4(FragColor.rgb, 1.0);
else
BrightColor = vec4(0.0, 0.0, 0.0, 1.0)
- Using HDR gives more control over which regions are considered bright. With LDR, we would have an undesirable dominant glow effect.
Blur Techniques
Box Blur
Box blur will use an average color from all the surrounding fragments to the central fragment. This is equivalent to using a kernel with equal weights.
float kernel[9] = float[] (
1.0 / 9, 1.0 / 9, 1.0 / 9,
1.0 / 9, 1.0 / 9, 1.0 / 9,
1.0 / 9, 1.0 / 9, 1.0 / 9
);
Gaussian Method
A Gaussian blur is based on the Gaussian curve which is commonly described as a bell-shaped curve giving high values close to its center that gradually wear off over distance.
- For 32 by 32 blur kernel, we would need a 2D set of gaussian weights and would have to sample the texture 1024 times per fragment with Normal Gausian Blur.
Normal Gaussian Blur
For a 3x3 kernel, we would sample around each fragment with the following weights.
float kernel[9] = float[] (
1.0 / 16, 2.0 / 16, 1.0 / 16,
2.0 / 16, 4.0 / 16, 2.0 / 16,
1.0 / 16, 2.0 / 16, 1.0 / 16
);
FragColor = kernel_compute_color(image, TexCoords, kernel);
Two Pass Gaussian Blur
OpenGL Code:
bool horizontal = true, first_iteration = true;
int amount = 10;
shaderBlur.use();
for (unsigned int i = 0; i < amount; i++)
{
glBindFramebuffer(GL_FRAMEBUFFER, pingpongFBO[horizontal]);
shaderBlur.setInt("horizontal", horizontal);
glBindTexture(GL_TEXTURE_2D, first_iteration ? colorBuffers[1] : pingpongBuffers[!horizontal]);
RenderQuad();
horizontal = !horizontal;
if (first_iteration)
first_iteration = false;
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
By running the loop 10 times, we completed the two-pass gaussian blur 5 times.
Shader Code:
uniform float weight[5] = float[] (0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216); // gaussian weights
// we sample the texture 9 times for each fragment
// add a blur effect to the fragment color
vec2 tex_offset = 1.0 / textureSize(image, 0); // size of single texel
vec3 result = texture(image, TexCoords).rgb * weight[0]; // sample this fragment
if(horizontal) // passed by uniform
{
// sample 4 pixels to left and 4 pixels to the right
// add their weighted color to our final fragment color
// Note: the pingpong buffer textures use CLAMP_TO_EDGE, so we don't need to worry about sampling outside texture range
for(int i = 1; i < blur_distance; ++i)
{
result += texture(image, TexCoords + vec2(tex_offset.x * i, 0.0)).rgb * weight[i];
result += texture(image, TexCoords - vec2(tex_offset.x * i, 0.0)).rgb * weight[i];
}
}
else
{
// sample 4 pixels to the top and 4 pixels to the bottom
// add their weighted color to our final fragment color
for(int i = 1; i < blur_distance; ++i)
{
result += texture(image, TexCoords + vec2(0.0, tex_offset.y * i)).rgb * weight[i];
result += texture(image, TexCoords - vec2(0.0, tex_offset.y * i)).rgb * weight[i];
}
}
FragColor = vec4(result, 1.0);
Result
Demo
Click to play the video