## WebGL2 Adaptation Requirements **IMPORTANT: Critical Warning for Standalone HTML Deployment**: Post-processing effects require an input texture to work. When generating standalone HTML, you must: 1. Set `#define USE_DEMO_SCENE 1` to use the built-in demo scene (recommended), or 2. Pass a valid input texture to the `iChannel0` channel, otherwise the screen will be completely black 3. **Critical**: When USE_DEMO_SCENE=1, ensure the #else branch code does not reference non-existent uniforms (e.g., iChannel0) **IMPORTANT: GLSL Type Strictness Rules**: - `vec2 = float` is illegal — must use `vec2(x, x)` or `vec2(x)` - Function parameters must be defined before use; using a variable name in its own initializer is forbidden (e.g., `float w = filmicCurve(w, w)` is an error) - Variables must be declared before use - **#version must be the very first line of shader code**: No characters (including whitespace or comments) may precede `#version 300 es` - **Code in preprocessor branches is still compiled**: Even if `#if USE_DEMO_SCENE` is true, the `#else` branch code is still compiled by the GPU — all branches must be valid GLSL code Code templates in this document use ShaderToy GLSL style. When generating standalone HTML pages, you must adapt to WebGL2: - Use `canvas.getContext("webgl2")` - First line of shader: `#version 300 es`, add `precision highp float;` for fragment shaders - Vertex shader: `attribute` → `in`, `varying` → `out` - Fragment shader: `varying` → `in`, `gl_FragColor` → custom `out vec4 fragColor`, `texture2D()` → `texture()` - ShaderToy's `void mainImage(out vec4 fragColor, in vec2 fragCoord)` must be adapted to standard `void main()` entry - Must create Framebuffers and render to texture before post-processing ### Complete WebGL2 Standalone HTML Template ```html Post-Processing Shader ``` ### Multi-Pass Post-Processing HTML Template (with FBO) Bloom separable blur, TAA, multi-step post-processing pipelines, etc. require rendering to intermediate textures. The following skeleton demonstrates the pattern: render scene to FBO → post-processing reads FBO → output to screen: ```html Multi-Pass Post-Processing ``` # Screen-Space Post-Processing Effects ## Use Cases Screen-space image enhancement on already-rendered scenes: Tone Mapping, Bloom, Vignette, Chromatic Aberration, Motion Blur, DoF, FXAA/TAA, Color Grading, Film Grain, Lens Flare, etc. Typical pipeline order: Scene Rendering → AA → Bloom → Chromatic Aberration → Motion Blur/DoF → Tone Mapping → Color Grading → Contrast → Vignette → Film Grain → Gamma → Dithering. ## Core Principles The essence of post-processing is **per-pixel transformation of an already-rendered image** — input is a framebuffer texture, output is the transformed color value. - **Tone Mapping**: HDR [0, ∞) → LDR [0, 1]. Reinhard `c/(1+c)`, Filmic Reinhard (white point/shoulder parameters), ACES (3×3 matrix + rational polynomial), generic rational polynomial - **Gaussian Blur**: 2D Gaussian kernel is separable into two 1D passes, O(n²) → O(2n) - **Bloom**: Bright-pass extraction → multi-level Gaussian blur → additive blend back to original - **Vignette**: Brightness falloff based on pixel distance to center. Multiplicative or radial - **Chromatic Aberration**: Sample the same texture at different scales for R/G/B channels ## Implementation Steps ### Step 1: Tone Mapping ```glsl // Reinhard vec3 reinhard(vec3 color) { return color / (1.0 + color); } // Filmic Reinhard (W=white point, T2=shoulder parameter) // IMPORTANT: GLSL critical rule: function parameters must be defined before use; using a variable name in its own initializer is forbidden const float W = 1.2, T2 = 7.5; // adjustable float filmic_reinhard_curve(float x) { float q = (T2 * T2 + 1.0) * x * x; return q / (q + x + T2 * T2); } vec3 filmic_reinhard(vec3 x) { float w = filmic_reinhard_curve(W); // compute w using constant W first return vec3(filmic_reinhard_curve(x.r), filmic_reinhard_curve(x.g), filmic_reinhard_curve(x.b)) / w; } // ACES industry standard vec3 aces_tonemap(vec3 color) { mat3 m1 = mat3(0.59719,0.07600,0.02840, 0.35458,0.90834,0.13383, 0.04823,0.01566,0.83777); mat3 m2 = mat3(1.60475,-0.10208,-0.00327, -0.53108,1.10813,-0.07276, -0.07367,-0.00605,1.07602); vec3 v = m1 * color; vec3 a = v * (v + 0.0245786) - 0.000090537; vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081; return clamp(m2 * (a / b), 0.0, 1.0); } // Generic rational polynomial vec3 rational_tonemap(vec3 x) { float a=0.010, b=0.132, c=0.010, d=0.163, e=0.101; // adjustable return (x * (a * x + b)) / (x * (c * x + d) + e); } ``` ### Step 2: Gamma Correction ```glsl color = pow(color, vec3(1.0 / 2.2)); // after tone mapping; ACES already includes gamma, skip this step ``` ### Step 3: Contrast Enhancement (Hermite S-Curve) ```glsl color = clamp(color, 0.0, 1.0); color = color * color * (3.0 - 2.0 * color); // Controllable intensity: color = mix(color, color*color*(3.0-2.0*color), strength); // smoothstep equivalent: color = smoothstep(-0.025, 1.0, color); ``` ### Step 4: Color Grading ```glsl color = color * vec3(1.11, 0.89, 0.79); // per-channel multiply (warm tone), adjustable color = pow(color, vec3(1.3, 1.2, 1.0)); // pow color grading, adjustable // HSV hue shift: hsv.x = fract(hsv.x + 0.05); hsv.y *= 1.1; // Desaturation: color = mix(color, vec3(dot(color, vec3(0.299,0.587,0.114))), 0.2); ``` ### Step 5: Vignette ```glsl // Option A: Multiplicative vec2 q = fragCoord / iResolution.xy; float vignette = pow(16.0 * q.x * q.y * (1.0 - q.x) * (1.0 - q.y), 0.25); color *= 0.5 + 0.5 * vignette; // Option B: Radial distance vec2 centered = (uv - 0.5) * vec2(iResolution.x / iResolution.y, 1.0); float vig = mix(1.0, max(0.0, 1.0 - pow(length(centered)/1.414 * 0.6, 3.0)), 0.5); color *= vig; // Option C: Inverse quadratic falloff vec2 p = 1.0 - 2.0 * fragCoord / iResolution.xy; p.y *= iResolution.y / iResolution.x; float vig2 = 1.25 / (1.1 + 1.1 * dot(p, p)); vig2 *= vig2; color *= mix(1.0, smoothstep(0.1, 1.1, vig2), 0.25); ``` ### Step 6: Gaussian Blur ```glsl float normpdf(float x, float sigma) { return 0.39894 * exp(-0.5 * x * x / (sigma * sigma)) / sigma; } vec3 gaussianBlur(sampler2D tex, vec2 fragCoord, vec2 resolution) { const int KERNEL_SIZE = 11, HALF = 5; // adjustable: KERNEL_SIZE must be odd float sigma = 7.0; // adjustable float kernel[KERNEL_SIZE]; float Z = 0.0; for (int j = 0; j <= HALF; ++j) kernel[HALF + j] = kernel[HALF - j] = normpdf(float(j), sigma); for (int j = 0; j < KERNEL_SIZE; ++j) Z += kernel[j]; vec3 result = vec3(0.0); for (int i = -HALF; i <= HALF; ++i) for (int j = -HALF; j <= HALF; ++j) result += kernel[HALF+j] * kernel[HALF+i] * texture(tex, (fragCoord + vec2(float(i), float(j))) / resolution).rgb; return result / (Z * Z); } ``` ### Step 7: Bloom (Single Pass, Hardware Mipmap) ```glsl vec3 simpleBloom(sampler2D tex, vec2 uv) { vec3 bloom = vec3(0.0); float tw = 0.0; float maxB = 5.0; // adjustable for (int x = -1; x <= 1; x++) for (int y = -1; y <= 1; y++) { vec2 off = vec2(float(x), float(y)) / iResolution.xy; float w = 1.0; bloom += w * min(vec3(maxB), textureLod(tex, uv+off*exp2(5.0), 5.0).rgb); tw += w; bloom += w * min(vec3(maxB), textureLod(tex, uv+off*exp2(6.0), 6.0).rgb); tw += w; bloom += w * min(vec3(maxB), textureLod(tex, uv+off*exp2(7.0), 7.0).rgb); tw += w; } return pow(bloom / tw, vec3(1.5)) * 0.3; // adjustable: gamma and intensity } // Usage: color = color * 0.8 + simpleBloom(iChannel0, uv); ``` ### Step 8: Chromatic Aberration ```glsl #define CA_SAMPLES 8 // adjustable #define CA_STRENGTH 0.003 // adjustable vec3 chromaticAberration(sampler2D tex, vec2 uv) { vec2 center = uv - 0.5; vec3 color = vec3(0.0); float rf = 1.0, gf = 1.0, bf = 1.0, f = 1.0 / float(CA_SAMPLES); for (int i = 0; i < CA_SAMPLES; ++i) { color.r += f * texture(tex, 0.5 - 0.5 * (center * 2.0 * rf)).r; color.g += f * texture(tex, 0.5 - 0.5 * (center * 2.0 * gf)).g; color.b += f * texture(tex, 0.5 - 0.5 * (center * 2.0 * bf)).b; rf *= 1.0 - CA_STRENGTH; gf *= 1.0 - CA_STRENGTH*0.3; bf *= 1.0 + CA_STRENGTH*0.4; } return clamp(color, 0.0, 1.0); } ``` ### Step 9: Film Grain ```glsl float hash(float c) { return fract(sin(dot(c, vec2(12.9898, 78.233))) * 43758.5453); } #define GRAIN_STRENGTH 0.012 // adjustable color += vec3(GRAIN_STRENGTH * hash(length(fragCoord / iResolution.xy) + iTime)); // Bayer matrix ordered dithering (eliminates color banding) const mat4 bayerMatrix = mat4( vec4(0.,8.,2.,10.), vec4(12.,4.,14.,6.), vec4(3.,11.,1.,9.), vec4(15.,7.,13.,5.)); float orderedDither(vec2 fc) { return (bayerMatrix[int(fc.x)&3][int(fc.y)&3] + 1.0) / 17.0; } color += (orderedDither(fragCoord) - 0.5) * 4.0 / 255.0; ``` ### Step 10: Demo Scene (Required for Standalone HTML!) **IMPORTANT: Critical Warning**: Standalone HTML deployment must provide an input texture, otherwise post-processing effects will output solid black. ```glsl // Demo scene fallback: used when no valid input texture is available vec3 demoScene(vec2 uv, float time) { // Dynamic gradient background vec3 col = 0.5 + 0.5 * cos(time + uv.xyx + vec3(0, 2, 4)); // Center glowing sphere (for testing bloom) float d = length(uv - 0.5) - 0.15; col += vec3(2.0) * smoothstep(0.02, 0.0, d); // extremely bright region // Moving highlight bar (for testing bloom bleed) float bar = step(0.48, uv.y) * step(uv.y, 0.52); bar *= step(0.0, sin(uv.x * 10.0 - time * 2.0)); col += vec3(1.5, 0.8, 0.3) * bar; // Colored blocks (for testing chromatic aberration and tone mapping) vec2 id = floor(uv * 4.0); float rand = fract(sin(dot(id, vec2(12.9898, 78.233))) * 43758.5453); vec2 rect = fract(uv * 4.0); float box = step(0.1, rect.x) * step(rect.x, 0.9) * step(0.1, rect.y) * step(rect.y, 0.9); col += vec3(rand, 1.0 - rand, 0.5) * box * 0.5; return col; } ``` ### Step 10: Motion Blur ```glsl #define MB_SAMPLES 32 // adjustable #define MB_STRENGTH 0.25 // adjustable vec3 motionBlur(sampler2D tex, vec2 uv, vec2 velocity) { vec2 dir = velocity * MB_STRENGTH; vec3 color = vec3(0.0); float tw = 0.0; for (int i = 0; i < MB_SAMPLES; i++) { float t = float(i) / float(MB_SAMPLES - 1), w = 1.0 - t; color += w * textureLod(tex, uv + dir * t, 0.0).rgb; tw += w; } return color / tw; } ``` ### Step 11: Depth of Field ```glsl #define DOF_SAMPLES 64 #define DOF_FOCAL_LENGTH 0.03 float getCoC(float depth, float focusDist) { float aperture = min(1.0, focusDist * focusDist * 0.5); return abs(aperture * (DOF_FOCAL_LENGTH * (depth - focusDist)) / (depth * (focusDist - DOF_FOCAL_LENGTH))); } float goldenAngle = 3.14159265 * (3.0 - sqrt(5.0)); vec3 depthOfField(sampler2D tex, vec2 uv, float depth, float focusDist) { float coc = getCoC(depth, focusDist); vec3 result = texture(tex, uv).rgb * max(0.001, coc); float tw = max(0.001, coc); for (int i = 1; i < DOF_SAMPLES; i++) { float fi = float(i); float theta = fi * goldenAngle * float(DOF_SAMPLES); float r = coc * sqrt(fi) / sqrt(float(DOF_SAMPLES)); vec2 tapUV = uv + vec2(sin(theta), cos(theta)) * r; vec4 s = textureLod(tex, tapUV, 0.0); float w = max(0.001, getCoC(s.w, focusDist)); result += s.rgb * w; tw += w; } return result / tw; } ``` ### Step 12: FXAA ```glsl vec3 fxaa(sampler2D tex, vec2 fragCoord, vec2 resolution) { vec2 pp = 1.0 / resolution; vec4 color = texture(tex, fragCoord * pp); vec3 luma = vec3(0.299, 0.587, 0.114); float lumaNW = dot(texture(tex, (fragCoord+vec2(-1.,-1.))*pp).rgb, luma); float lumaNE = dot(texture(tex, (fragCoord+vec2( 1.,-1.))*pp).rgb, luma); float lumaSW = dot(texture(tex, (fragCoord+vec2(-1., 1.))*pp).rgb, luma); float lumaSE = dot(texture(tex, (fragCoord+vec2( 1., 1.))*pp).rgb, luma); float lumaM = dot(color.rgb, luma); float lumaMin = min(lumaM, min(min(lumaNW,lumaNE), min(lumaSW,lumaSE))); float lumaMax = max(lumaM, max(max(lumaNW,lumaNE), max(lumaSW,lumaSE))); vec2 dir = vec2(-((lumaNW+lumaNE)-(lumaSW+lumaSE)), ((lumaNW+lumaSW)-(lumaNE+lumaSE))); float dirReduce = max((lumaNW+lumaNE+lumaSW+lumaSE)*0.03125, 1.0/128.0); dir = clamp(dir * 2.5/(min(abs(dir.x),abs(dir.y))+dirReduce), vec2(-8.0), vec2(8.0)) * pp; vec3 rgbA = 0.5 * (texture(tex, fragCoord*pp+dir*(1./3.-0.5)).rgb + texture(tex, fragCoord*pp+dir*(2./3.-0.5)).rgb); vec3 rgbB = rgbA*0.5 + 0.25*(texture(tex, fragCoord*pp+dir*-0.5).rgb + texture(tex, fragCoord*pp+dir*0.5).rgb); float lumaB = dot(rgbB, luma); return (lumaB < lumaMin || lumaB > lumaMax) ? rgbA : rgbB; } ``` ## Complete Code Template Can be run directly in ShaderToy. `iChannel0` is the scene texture. **IMPORTANT: Important Warning**: For standalone HTML deployment, you must: 1. Pass a valid input texture to iChannel0 (or uChannel0) 2. Or set `#define USE_DEMO_SCENE 1` to use the built-in demo scene ```glsl // Post-Processing Pipeline — ShaderToy Template #define ENABLE_TONEMAP 1 #define ENABLE_BLOOM 1 #define ENABLE_CA 1 #define ENABLE_VIGNETTE 1 #define ENABLE_GRAIN 1 #define ENABLE_CONTRAST 1 #define USE_DEMO_SCENE 1 // set to 1 to use built-in demo scene (required for standalone HTML) #define TONEMAP_MODE 2 // 0=Reinhard, 1=Filmic, 2=ACES #define BRIGHTNESS 1.0 #define WHITE_POINT 1.2 #define SHOULDER 7.5 #define BLOOM_STRENGTH 0.08 #define BLOOM_LOD_START 4.0 #define COLOR_TINT vec3(1.11, 0.89, 0.79) #define CA_SAMPLES 8 #define CA_INTENSITY 0.003 #define VIG_POWER 0.25 #define GRAIN_AMOUNT 0.012 float hash11(float p) { return fract(sin(p * 12.9898) * 43758.5453); } // Demo scene fallback: used when no input texture is available vec3 demoScene(vec2 uv, float time) { // Dynamic gradient background vec3 col = 0.5 + 0.5 * cos(time + uv.xyx + vec3(0, 2, 4)); // Center glowing sphere (for testing bloom) float d = length(uv - 0.5) - 0.15; col += vec3(2.0) * smoothstep(0.02, 0.0, d); // Moving highlight bar (for testing bloom bleed) float bar = step(0.48, uv.y) * step(uv.y, 0.52); bar *= step(0.0, sin(uv.x * 10.0 - time * 2.0)); col += vec3(1.5, 0.8, 0.3) * bar; // Colored blocks (for testing chromatic aberration and tone mapping) vec2 id = floor(uv * 4.0); float rand = fract(sin(dot(id, vec2(12.9898, 78.233))) * 43758.5453); vec2 rect = fract(uv * 4.0); float box = step(0.1, rect.x) * step(rect.x, 0.9) * step(0.1, rect.y) * step(rect.y, 0.9); col += vec3(rand, 1.0 - rand, 0.5) * box * 0.5; return col; } vec3 tonemapReinhard(vec3 c) { return c / (1.0 + c); } // IMPORTANT: Critical: filmicCurve takes only one parameter x; w is computed externally via WHITE_POINT float filmicCurve(float x) { float q = (SHOULDER*SHOULDER+1.0)*x*x; return q/(q+x+SHOULDER*SHOULDER); } vec3 tonemapFilmic(vec3 c) { float w = filmicCurve(WHITE_POINT); // compute w using WHITE_POINT constant first return vec3(filmicCurve(c.r), filmicCurve(c.g), filmicCurve(c.b)) / w; } vec3 tonemapACES(vec3 color) { mat3 m1 = mat3(0.59719,0.07600,0.02840, 0.35458,0.90834,0.13383, 0.04823,0.01566,0.83777); mat3 m2 = mat3(1.60475,-0.10208,-0.00327, -0.53108,1.10813,-0.07276, -0.07367,-0.00605,1.07602); vec3 v = m1*color; vec3 a = v*(v+0.0245786)-0.000090537; vec3 b = v*(0.983729*v+0.4329510)+0.238081; return clamp(m2*(a/b), 0.0, 1.0); } vec3 applyTonemap(vec3 c) { c *= BRIGHTNESS; #if TONEMAP_MODE == 0 return tonemapReinhard(c); #elif TONEMAP_MODE == 1 return tonemapFilmic(c); #else return tonemapACES(c); #endif } vec3 sampleBloom(sampler2D tex, vec2 uv) { vec3 bloom = vec3(0.0); float tw = 0.0; for (int x = -1; x <= 1; x++) for (int y = -1; y <= 1; y++) { vec2 off = vec2(float(x),float(y))/iResolution.xy; float w = 1.0; bloom += w*textureLod(tex, uv+off*exp2(BLOOM_LOD_START), BLOOM_LOD_START).rgb; bloom += w*textureLod(tex, uv+off*exp2(BLOOM_LOD_START+1.0), BLOOM_LOD_START+1.0).rgb; bloom += w*textureLod(tex, uv+off*exp2(BLOOM_LOD_START+2.0), BLOOM_LOD_START+2.0).rgb; tw += w*3.0; } return bloom / tw; } vec3 applyChromaticAberration(sampler2D tex, vec2 uv) { vec2 center = 1.0 - 2.0*uv; vec3 color = vec3(0.0); float rf=1.0, gf=1.0, bf=1.0, f=1.0/float(CA_SAMPLES); for (int i = 0; i < CA_SAMPLES; ++i) { color.r += f*texture(tex, 0.5-0.5*(center*rf)).r; color.g += f*texture(tex, 0.5-0.5*(center*gf)).g; color.b += f*texture(tex, 0.5-0.5*(center*bf)).b; rf *= 1.0-CA_INTENSITY; gf *= 1.0-CA_INTENSITY*0.3; bf *= 1.0+CA_INTENSITY*0.4; } return clamp(color, 0.0, 1.0); } void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = fragCoord / iResolution.xy; // Get input color: demo scene or input texture #if USE_DEMO_SCENE vec3 color = demoScene(uv, iTime); #else #if ENABLE_CA vec3 color = applyChromaticAberration(iChannel0, uv); #else vec3 color = texture(iChannel0, uv).rgb; #endif #endif #if ENABLE_BLOOM && !USE_DEMO_SCENE color += sampleBloom(iChannel0, uv) * BLOOM_STRENGTH; #else // In demo scene mode, use simplified bloom sampling from itself #if ENABLE_BLOOM vec3 bloom = vec3(0.0); float tw = 0.0; for (int x = -1; x <= 1; x++) for (int y = -1; y <= 1; y++) { vec2 off = vec2(float(x),float(y))/iResolution.xy * 0.02; vec3 s = demoScene(uv + off, iTime); float w = 1.0; bloom += w * min(vec3(5.0), s); tw += w; } color += bloom / tw * BLOOM_STRENGTH; #endif #endif color *= COLOR_TINT; #if ENABLE_TONEMAP #if TONEMAP_MODE == 2 color = applyTonemap(color); #else color = applyTonemap(color); color = pow(color, vec3(1.0/2.2)); #endif #else color = pow(color, vec3(1.0/2.2)); #endif #if ENABLE_CONTRAST color = clamp(color, 0.0, 1.0); color = color*color*(3.0-2.0*color); #endif #if ENABLE_VIGNETTE vec2 q = fragCoord/iResolution.xy; color *= 0.5 + 0.5*pow(16.0*q.x*q.y*(1.0-q.x)*(1.0-q.y), VIG_POWER); #endif #if ENABLE_GRAIN color += GRAIN_AMOUNT * hash11(dot(uv, vec2(12.9898,78.233)) + iTime); #endif fragColor = vec4(clamp(color, 0.0, 1.0), 1.0); } ``` ## Common Variants ### Variant 1: Multi-Pass Separable Bloom ```glsl // Buffer A: Horizontal Gaussian blur + bright-pass #define BLOOM_THRESHOLD vec3(0.2) #define BLOOM_DOWNSAMPLE 3 #define BLUR_RADIUS 16 void mainImage(out vec4 fragColor, in vec2 fragCoord) { ivec2 xy = ivec2(fragCoord); if (xy.x >= int(iResolution.x)/BLOOM_DOWNSAMPLE) { fragColor = vec4(0); return; } vec3 sum = vec3(0.0); float tw = 0.0; for (int k = -BLUR_RADIUS; k <= BLUR_RADIUS; ++k) { vec3 texel = max(vec3(0.0), texelFetch(iChannel0, (xy+ivec2(k,0))*BLOOM_DOWNSAMPLE, 0).rgb - BLOOM_THRESHOLD); float w = exp(-8.0 * pow(abs(float(k))/float(BLUR_RADIUS), 2.0)); sum += texel*w; tw += w; } fragColor = vec4(sum/tw, 1.0); } // Buffer B: Vertical blur, same as above but with direction changed to ivec2(0, k) ``` ### Variant 2: ACES + Full Color Pipeline (with Built-in Gamma) ```glsl vec3 aces_tonemap(vec3 color) { mat3 m1 = mat3(0.59719,0.07600,0.02840, 0.35458,0.90834,0.13383, 0.04823,0.01566,0.83777); mat3 m2 = mat3(1.60475,-0.10208,-0.00327, -0.53108,1.10813,-0.07276, -0.07367,-0.00605,1.07602); vec3 v = m1*color; vec3 a = v*(v+0.0245786)-0.000090537; vec3 b = v*(0.983729*v+0.4329510)+0.238081; return pow(clamp(m2*(a/b), 0.0, 1.0), vec3(1.0/2.2)); } ``` ### Variant 3: DoF + Motion Blur Combination ```glsl for (int i = 1; i < BLUR_TAPS; i++) { float t = float(i)/float(BLUR_TAPS); float randomT = hash(iTime + t + uv.x + uv.y*12.345); vec2 tapUV = mix(currentUV, prevFrameUV, (randomT-0.5)*shutterAngle); // motion blur float theta = t*goldenAngle*float(BLUR_TAPS); float r = coc*sqrt(t*float(BLUR_TAPS))/sqrt(float(BLUR_TAPS)); tapUV += vec2(sin(theta), cos(theta))*r; // DoF vec4 tap = textureLod(sceneTex, tapUV, 0.0); float w = max(0.001, getCoC(decodeDepth(tap.w), focusDistance)); result += tap.rgb*w; totalWeight += w; } ``` ### Variant 4: TAA Temporal Anti-Aliasing ```glsl vec4 current = textureLod(currentFrame, uv - jitterOffset/iResolution.xy, 0.0); vec3 vMin = vec3(1e5), vMax = vec3(-1e5); for (int iy = -1; iy <= 1; iy++) for (int ix = -1; ix <= 1; ix++) { vec3 s = texelFetch(currentFrame, ivec2(fragCoord)+ivec2(ix,iy), 0).rgb; vMin = min(vMin, s); vMax = max(vMax, s); } vec4 history = textureLod(historyBuffer, reprojectToPrevFrame(worldPos, prevViewProjMatrix), 0.0); float blend = (all(greaterThanEqual(history.rgb, vMin)) && all(lessThanEqual(history.rgb, vMax))) ? 0.9 : 0.0; color = mix(current.rgb, history.rgb, blend); ``` ### Variant 5: Lens Flare + Starburst ```glsl #define NUM_APERTURE_BLADES 8.0 vec2 toSun = normalize(sunScreenPos - uv); float angle = atan(toSun.y, toSun.x); float starburst = pow(0.5+0.5*cos(1.5*3.14159+angle*NUM_APERTURE_BLADES), max(1.0, 500.0-sunDist*sunDist*501.0)); float ghost = smoothstep(0.015, 0.0, length(ghostCenter-uv)-ghostRadius); totalFlare += wavelengthToRGB(300.0+fract((length(ghostCenter-uv)-ghostRadius)*5.0)*500.0) * ghost * 0.25; ``` ## Performance & Composition **Performance**: Separable blur 121→22 samples | `textureLod` hardware mipmap for free downsampling | Downsample 2-4x before blurring | Sample counts: MB 16-32, DoF 32-64, CA 4-8 | Inter-texel sampling = free bilinear | `#define` switches have zero cost | Use `mix`/`step`/`smoothstep` instead of branches **Composition**: Bloom+ToneMap (compute bloom in HDR space then tonemap, not reversible) | TAA+MB+DoF (shared sampling loop) | CA+Vignette+Grain (lens trio) | ColorGrading+ToneMap+Contrast (grade in linear space → HDR compression → gamma-space S-curve) | Bloom+LensFlare (shared bright-pass) | Multi-pass pipeline: BufA scene → BufB/C Bloom H/V → BufD TAA → Image compositing ## Further Reading For complete step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/post-processing.md)