Skip to content

Post-Processing Effects — Bloom, HDR, Tone Mapping and Depth of Field

DodaTech Updated 2026-06-21 6 min read

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

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

Do post-processing effects cost performance?

Yes, each full-screen pass reads and writes every pixel. Bloom with two blur passes costs 3 extra passes per frame. On modern GPUs, this is typically 1-3ms per pass at 1080p.

Can I do post-processing in WebGL?

Yes, WebGL 2.0 supports framebuffer objects and float textures. Three.js provides EffectComposer and UnrealBloomPass for implementing these effects in the browser.

What is the difference between SDR and HDR rendering?

SDR (Standard Dynamic Range) clamps colors to 0-1. HDR rendering preserves values above 1.0, enabling realistic bright lights, bloom, and tone mapping without clipping.

What is chromatic aberration?

Chromatic aberration is a lens distortion that shifts different color channels by different amounts. It creates red/blue fringing at high-contrast edges, often used as a cinematic effect.


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