Post-Processing Effects — Bloom, HDR, Tone Mapping and Depth of Field
In this tutorial, you'll learn about Post. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Post-processing applies full-screen image filters after the main scene is rendered, enabling effects like bloom, HDR tone mapping, depth of field, and color grading that dramatically improve visual quality.
What You'll Learn & Why It Matters
In this tutorial, you will learn how to render to framebuffer objects, implement HDR rendering with tone mapping, create bloom glow effects, simulate depth of field, and apply color grading LUTs.
Real-world use: Every modern game uses post-processing. Durga Antivirus Pro uses bloom and color grading in its security dashboard for visual polish. Understanding post-processing is essential for Game Development and visual effects.
Prerequisites
- OpenGL Basics (previous)
- Shader Programming (previous)
- Framebuffer concepts
Learning Path
flowchart LR A[Shader Programming] --> B[Post-Processing] B --> C[Anti-Aliasing] B --> D[Compute Shaders] B --> E[Real-Time GI] C --> F[GPU Architecture] B:::current classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px
Framebuffer Objects (FBOs)
Post-processing requires rendering the scene to a texture instead of the screen, then applying effects in a second pass.
unsigned int fbo;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
// Color attachment
unsigned int texColorBuffer;
glGenTextures(1, &texColorBuffer);
glBindTexture(GL_TEXTURE_2D, texColorBuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, 800, 600, 0, GL_RGB, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texColorBuffer, 0);
// Depth attachment
unsigned int rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
HDR Rendering
HDR (High Dynamic Range) stores colors as floating-point values, preserving detail in bright and dark areas:
// Fragment shader outputting HDR values
void main()
{
vec3 hdrColor = calculateLighting();
// Unclamped values can exceed 1.0 (e.g., 5.0 for bright lights)
FragColor = vec4(hdrColor, 1.0);
}
Tone Mapping
Tone mapping converts HDR colors to the LDR range (0-1) for display:
// Reinhard tone mapping
uniform float exposure;
void main()
{
vec3 hdrColor = texture(screenTexture, TexCoords).rgb;
vec3 mapped = vec3(1.0) - exp(-hdrColor * exposure);
FragColor = vec4(mapped, 1.0);
}
// ACES filmic tone mapping (higher quality)
vec3 aces_tonemap(vec3 color)
{
float a = 2.51;
float b = 0.03;
float c = 2.43;
float d = 0.59;
float e = 0.14;
return clamp((color * (a * color + b)) / (color * (c * color + d) + e), 0.0, 1.0);
}
Bloom Effect
Bloom simulates how bright areas bleed into surrounding pixels, creating a glow effect:
flowchart TD A[HDR Scene Color] --> B[Extract Bright Regions] B --> C1[Blur Horizontal] C1 --> C2[Blur Vertical] C2 --> D[Gaussian Blurred Bright Regions] A --> E[Original Scene] D --> F[Combine: Original + Blur] F --> G[Tone Map] G --> H[Output]
Step 1: Extract Bright Colors
// Fragment shader for bright extraction
uniform sampler2D hdrBuffer;
uniform float threshold;
void main()
{
vec3 color = texture(hdrBuffer, TexCoords).rgb;
float brightness = dot(color, vec3(0.2126, 0.7152, 0.0722));
vec3 bloom = brightness > threshold ? color : vec3(0.0);
FragColor = vec4(bloom, 1.0);
}
Step 2: Gaussian Blur
// Two-pass Gaussian blur (horizontal pass)
uniform sampler2D inputTexture;
uniform bool horizontal;
void main()
{
vec2 texOffset = 1.0 / textureSize(inputTexture, 0);
vec3 result = texture(inputTexture, TexCoords).rgb * 0.227;
if (horizontal)
{
for (int i = 1; i < 5; i++)
{
vec2 offset = vec2(texOffset.x * i, 0.0);
result += texture(inputTexture, TexCoords + offset).rgb * weights[i];
result += texture(inputTexture, TexCoords - offset).rgb * weights[i];
}
}
else
{
for (int i = 1; i < 5; i++)
{
vec2 offset = vec2(0.0, texOffset.y * i);
result += texture(inputTexture, TexCoords + offset).rgb * weights[i];
result += texture(inputTexture, TexCoords - offset).rgb * weights[i];
}
}
FragColor = vec4(result, 1.0);
}
Step 3: Combine
uniform sampler2D sceneTexture;
uniform sampler2D bloomTexture;
void main()
{
vec3 sceneColor = texture(sceneTexture, TexCoords).rgb;
vec3 bloomColor = texture(bloomTexture, TexCoords).rgb;
vec3 result = sceneColor + bloomColor * 0.04;
FragColor = vec4(aces_tonemap(result), 1.0);
}
Depth of Field
Depth of field blurs objects outside the focal plane, simulating camera lens behavior:
uniform sampler2D colorTexture;
uniform sampler2D depthTexture;
uniform float focalDistance;
uniform float focalRange;
void main()
{
float depth = texture(depthTexture, TexCoords).r;
float blurAmount = abs(depth - focalDistance) / focalRange;
blurAmount = clamp(blurAmount, 0.0, 1.0);
vec3 color = vec3(0.0);
int samples = 0;
for (int x = -4; x <= 4; x++)
{
for (int y = -4; y <= 4; y++)
{
vec2 offset = vec2(x, y) * blurAmount * 0.001;
color += texture(colorTexture, TexCoords + offset).rgb;
samples++;
}
}
FragColor = vec4(color / samples, 1.0);
}
Gamma Correction
void main()
{
vec3 color = texture(screenTexture, TexCoords).rgb;
color = pow(color, vec3(1.0 / 2.2));
FragColor = vec4(color, 1.0);
}
Common Errors & Mistakes
1. FBO Incomplete
Mistake: Creating an FBO without all required attachments, getting an incomplete framebuffer.
Fix: Check completeness with glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE.
2. HDR Clipping
Mistake: Using GL_RGB instead of GL_RGB16F for the HDR framebuffer, clamping values above 1.0.
Fix: Use GL_RGB16F (half-float) or GL_RGBA32F (full float) for HDR color attachments.
3. Incorrect Texture Coordinates
Mistake: Using the wrong coordinate system for the full-screen quad, inverting the image vertically.
Fix: Ensure the full-screen quad uses (0,0) to (1,1) UV coordinates matching the framebuffer texture orientation.
4. Bloom Threshold Too High/Low
Mistake: Bloom threshold set too high (no bloom) or too low (everything blooms, washing out the image).
Fix: Start with a threshold of 1.0 and adjust. The ideal threshold extracts only the brightest scene elements (lights, reflections).
Practice Questions
Question 1
What is the purpose of tone mapping?
Show answer
Tone mapping converts HDR color values (unbounded floating point) to LDR values (0-1 range) suitable for display, preserving detail in both bright and dark regions.Question 2
Why use a two-pass Gaussian blur instead of a single-pass blur?
Show answer
A two-pass blur (horizontal then vertical) is O(2 * n) samples per pixel instead of O(n^2) for a single 2D pass. For a 9x9 blur, this is 18 vs 81 samples.Question 3
How does the bloom threshold parameter affect the result?
Show answer
The threshold determines which pixels are bright enough to glow. A low threshold (0.5) causes most bright surfaces to bloom. A high threshold (2.0) limits bloom to only the brightest light sources.Question 4
What is the difference between Reinhard and ACES tone mapping?
Show answer
Reinhard is simpler and preserves overall brightness but can wash out colors. ACES filmic tone mapping produces more saturated, film-like results with better highlight roll-off.Challenge
Build a complete post-processing pipeline with: HDR rendering, bloom with adjustable threshold and blur radius, ACES tone mapping, and gamma correction. Add a UI to toggle each effect independently.
FAQ
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Author: DodaTech | Last updated: June 21, 2026
DodaTech tutorials are built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro — security tools used by millions worldwide.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro