Post-Processing

Render the scene to an offscreen texture, then draw a fullscreen triangle (§5.4) with a pixel shader — or dispatch a compute shader — that reads it.

22.1 Fullscreen pass structure

Render the scene to an offscreen texture, then draw a fullscreen triangle (§5.4) with a pixel shader — or dispatch a compute shader — that reads it.

Texture2D<float4> gSource : register(t0);
SamplerState      gClamp  : register(s0);
cbuffer Post : register(b0) { float2 gTexelSize; float gTime; float gStrength; };

struct FSIn { float4 pos : SV_Position; float2 uv : TEXCOORD0; };

float4 GrayscalePS(FSIn i) : SV_Target
{
    float3 c = gSource.Sample(gClamp, i.uv).rgb;
    return float4(Luminance(c).xxx, 1);
}

float4 VignettePS(FSIn i) : SV_Target
{
    float3 c = gSource.Sample(gClamp, i.uv).rgb;
    float d = distance(i.uv, 0.5);
    return float4(c * smoothstep(0.8, 0.3, d), 1);
}

float4 ChromaticAberrationPS(FSIn i) : SV_Target
{
    float2 dir = (i.uv - 0.5) * gStrength;
    float r = gSource.Sample(gClamp, i.uv + dir).r;
    float g = gSource.Sample(gClamp, i.uv).g;
    float b = gSource.Sample(gClamp, i.uv - dir).b;
    return float4(r, g, b, 1);
}

float4 SobelEdgePS(FSIn i) : SV_Target
{
    float k[9];
    [unroll] for (int y = -1; y <= 1; ++y)
    [unroll] for (int x = -1; x <= 1; ++x)
        k[(y + 1) * 3 + (x + 1)] = Luminance(gSource.Sample(gClamp, i.uv + float2(x, y) * gTexelSize).rgb);
    float gx = (k[2] + 2 * k[5] + k[8]) - (k[0] + 2 * k[3] + k[6]);
    float gy = (k[6] + 2 * k[7] + k[8]) - (k[0] + 2 * k[1]
…

22.2 Separable Gaussian blur (two passes, bilinear-optimized)

Using linear filtering between texels gives a 9-tap kernel with 5 fetches.

cbuffer Blur : register(b1) { float2 gDirection; };   // (1,0) then (0,1)

static const float kOffsets[3] = { 0.0, 1.3846153846, 3.2307692308 };
static const float kWeights[3] = { 0.2270270270, 0.3162162162, 0.0702702703 };

float4 GaussianPS(FSIn i) : SV_Target
{
    float3 c = gSource.Sample(gClamp, i.uv).rgb * kWeights[0];
    [unroll] for (int k = 1; k < 3; ++k)
    {
        float2 o = gDirection * gTexelSize * kOffsets[k];
        c += gSource.Sample(gClamp, i.uv + o).rgb * kWeights[k];
        c += gSource.Sample(gClamp, i.uv - o).rgb * kWeights[k];
    }
    return float4(c, 1);
}

22.3 Bloom

Bright pass / prefilter: soft threshold on luminance.

Downsample chain (13-tap or 4-tap Karis average on the first mip to avoid fireflies).

Upsample chain with a tent filter, additively blending each level.

Composite: hdr + bloom * intensity before tone mapping.

float3 Prefilter(float3 c, float threshold, float knee)
{
    float br = max(c.r, max(c.g, c.b));
    float rq = clamp(br - threshold + knee, 0, 2 * knee);
    rq = rq * rq / (4 * knee + 1e-5);
    return c * max(rq, br - threshold) / max(br, 1e-5);
}

22.4 Other post effects

edge-detection anti-aliasing on the LDR image jitter the projection each frame, reproject history with motion vectors, clamp neighborhood colors Depth of field circle of confusion from depth; gather blur blur along per-pixel velocity vectors sample depth/normals around the pixel to estimate occlusion

3D LUT lookup: gLUT.SampleLevel(s, color * (31.0/32) + 0.5/32, 0) Film grain, CRT, pixelation procedural noise, scanlines, UV quantization

FSR (open source HLSL), DLSS/XeSS (SDKs)