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)