DEV Community

Cover image for Post-Processing Effects with Shaders: Enhancing Rendered Scenes with Post-Processing Effects
Haider Aftab
Haider Aftab

Posted on

Post-Processing Effects with Shaders: Enhancing Rendered Scenes with Post-Processing Effects

Post-processing effects are a crucial aspect of modern computer graphics, adding polish and realism to rendered scenes. By applying these effects after the initial rendering pass, developers can achieve a wide range of visual enhancements, from subtle color corrections to dramatic lighting effects. In this blog, we’ll explore the fundamentals of post-processing effects using shaders, providing key concepts, practical examples, and tips to optimize their performance.

What Are Post-Processing Effects?

Post-processing effects are applied after the initial rendering of a scene, hence the term "post-processing." These effects are executed in screen space, meaning they operate on the final image rather than individual 3D objects within the scene. This approach allows for a wide range of visual enhancements without the need for complex modifications to the scene itself.

Common post-processing effects include:

  • Bloom: Adds a glowing effect to bright areas, simulating how bright lights bleed over into surrounding areas.
  • Motion Blur: Blurs moving objects to give a sense of speed and motion.
  • Depth of Field: Blurs distant objects while keeping the focal point sharp, mimicking camera lens behavior.
  • Color Grading: Adjusts colors to achieve a specific look or mood.
  • Screen Space Ambient Occlusion (SSAO): Simulates subtle shadows in crevices and corners to add depth. ## Implementing Post-Processing Effects Post-processing effects are typically implemented using fragment shaders. The rendered scene is first captured into a texture, which is then processed by the shader to apply the desired effect. Below, we will go through examples of implementing some common post-processing effects using GLSL (OpenGL Shading Language).

Example 1: Bloom Effect

The bloom effect makes bright areas of the scene appear to glow. This is achieved by blurring bright areas and then adding them back to the original image.

// Bloom Fragment Shader
uniform sampler2D scene;
uniform float threshold;
uniform float intensity;

void main() {
    vec2 uv = gl_FragCoord.xy / resolution.xy;
    vec3 color = texture(scene, uv).rgb;
    vec3 bloom = vec3(0.0);

    // Extract bright areas
    if (dot(color, vec3(0.299, 0.587, 0.114)) > threshold) {
        bloom = color;
    }

    // Blur bright areas (simple box blur for demonstration)
    vec3 blurred = vec3(0.0);
    float blurRadius = 1.0 / resolution.xy;
    for (float x = -1.0; x <= 1.0; x += 1.0) {
        for (float y = -1.0; y <= 1.0; y += 1.0) {
            blurred += texture(scene, uv + vec2(x, y) * blurRadius).rgb;
        }
    }
    blurred /= 9.0;

    // Combine original scene with blurred bloom
    vec3 finalColor = color + blurred * intensity;
    gl_FragColor = vec4(finalColor, 1.0);
}

Enter fullscreen mode Exit fullscreen mode

Example 2: Motion Blur

Motion blur simulates the blurring of moving objects. This effect requires velocity information for each pixel, which can be precomputed and passed to the shader.

// Motion Blur Fragment Shader
uniform sampler2D scene;
uniform sampler2D velocity;
uniform float blurAmount;

void main() {
    vec2 uv = gl_FragCoord.xy / resolution.xy;
    vec3 color = texture(scene, uv).rgb;
    vec2 vel = texture(velocity, uv).xy * blurAmount;

    // Accumulate colors along the motion vector
    vec3 blurredColor = vec3(0.0);
    int samples = 10;
    for (int i = 0; i < samples; ++i) {
        blurredColor += texture(scene, uv + vel * (i / float(samples - 1))).rgb;
    }
    blurredColor /= samples;

    gl_FragColor = vec4(blurredColor, 1.0);
}

Enter fullscreen mode Exit fullscreen mode

Example 3: Depth of Field

Depth of field simulates the blurring of objects based on their distance from the camera, focusing on a specific depth range.

// Depth of Field Fragment Shader
uniform sampler2D scene;
uniform sampler2D depth;
uniform float focalDepth;
uniform float focalRange;
uniform float blurAmount;

void main() {
    vec2 uv = gl_FragCoord.xy / resolution.xy;
    float depthValue = texture(depth, uv).r;
    float blur = clamp(abs(depthValue - focalDepth) / focalRange, 0.0, 1.0) * blurAmount;

    // Accumulate blurred colors
    vec3 color = vec3(0.0);
    int samples = 16;
    for (int i = 0; i < samples; ++i) {
        float angle = float(i) * 3.14159265 * 2.0 / float(samples);
        vec2 offset = vec2(cos(angle), sin(angle)) * blur;
        color += texture(scene, uv + offset).rgb;
    }
    color /= samples;

    gl_FragColor = vec4(color, 1.0);
}

Enter fullscreen mode Exit fullscreen mode

Read Complete Post Here

Top comments (0)