Framebuffers & Post-Processing

Optimizations: linear sampling trick (sample between texels to halve fetches), downsample first, Kawase/dual-filter blur.

21.1 Render-to-texture setup (host)

GLuint fbo, colorTex, depthRbo;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glGenTextures(1, &colorTex);
glBindTexture(GL_TEXTURE_2D, colorTex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, w, h, 0, GL_RGBA, GL_FLOAT, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTex, 0);
glGenRenderbuffers(1, &depthRbo);
glBindRenderbuffer(GL_RENDERBUFFER, depthRbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, w, h);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, depthRbo);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { /* error */ }
// Frame: render scene into fbo → bind default framebuffer (0) → draw fullscreen triangle sampling colorTex

21.2 Post-processing effects (fullscreen fragment shaders)

#version 330 core
in vec2 vUV;
out vec4 outColor;
uniform sampler2D uScene;
uniform vec2 uTexelSize;        // 1.0 / resolution

// Grayscale & sepia
vec3 grayscale(vec3 c) { return vec3(dot(c, vec3(0.299, 0.587, 0.114))); }
vec3 sepia(vec3 c) { return vec3(dot(c, vec3(0.393, 0.769, 0.189)), dot(c, vec3(0.349, 0.686, 0.168)), dot(c, vec3(0.272, 0.534, 0.131))); }

// 3×3 convolution kernel (sharpen / edge detect / emboss)
vec3 convolve(mat3 k) {
    vec3 sum = vec3(0.0);
    for (int y = -1; y <= 1; ++y)
        for (int x = -1; x <= 1; ++x)
            sum += k[x + 1][y + 1] * texture(uScene, vUV + vec2(x, y) * uTexelSize).rgb;
    return sum;
}

// Vignette
float vignette(vec2 uv) { uv *= 1.0 - uv.yx; return pow(uv.x * uv.y * 15.0, 0.25); }

// Chromatic aberration
vec3 chromatic(vec2 uv) {
    vec2 dir = (uv - 0.5) * 0.005;
    return vec3(texture(uScene, uv + dir).r, texture(uScene, uv).g, texture(uScene, uv - dir).b);
}

// Pixelation
vec2 pixelate(vec2 uv, float cells) { return floor(uv * cells) / cells; }

void main() {
    vec3 c = chromatic(vUV);
    c *= vignette(vUV);
    outColor = vec4(c, 1.0);
}

21.3 Separable Gaussian blur (two passes)

Optimizations: linear sampling trick (sample between texels to halve fetches), downsample first, Kawase/dual-filter blur.

uniform bool uHorizontal;
const float weights[5] = float[](0.2270270270, 0.1945945946, 0.1216216216, 0.0540540541, 0.0162162162);
void main() {
    vec2 dir = uHorizontal ? vec2(uTexelSize.x, 0.0) : vec2(0.0, uTexelSize.y);
    vec3 result = texture(uScene, vUV).rgb * weights[0];
    for (int i = 1; i < 5; ++i) {
        result += texture(uScene, vUV + dir * float(i)).rgb * weights[i];
        result += texture(uScene, vUV - dir * float(i)).rgb * weights[i];
    }
    outColor = vec4(result, 1.0);
}

21.4 Bloom

Extract bright areas (max(color - threshold, 0) or a soft knee) into a separate texture.

Blur at multiple downsampled resolutions (mip chain).

Upsample and add back to the HDR scene before tone mapping.

21.5 Other post effects

Sobel edge detection & outlines (on depth/normals), FXAA/SMAA anti-aliasing, depth of field (circle of confusion from depth), motion blur (velocity buffer), film grain, color grading (LUTs), lens flares, god rays (radial blur), screen-space reflections, temporal anti-aliasing (TAA with reprojection), CRT/retro effects.

// Sobel edge detection on luminance
float lum(vec2 o) { return dot(texture(uScene, vUV + o * uTexelSize).rgb, vec3(0.299, 0.587, 0.114)); }
float gx = -lum(vec2(-1,-1)) - 2.0*lum(vec2(-1,0)) - lum(vec2(-1,1)) + lum(vec2(1,-1)) + 2.0*lum(vec2(1,0)) + lum(vec2(1,1));
float gy = -lum(vec2(-1,-1)) - 2.0*lum(vec2(0,-1)) - lum(vec2(1,-1)) + lum(vec2(-1,1)) + 2.0*lum(vec2(0,1)) + lum(vec2(1,1));
float edge = length(vec2(gx, gy));