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 colorTex21.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));