FXAA Edge Detection Implementation in GLSL
This article provides a practical guide to implementing the edge detection stage of Fast Approximate Anti-Aliasing (FXAA) in OpenGL Shading Language (GLSL). FXAA is a screen-space technique that smooths jagged silhouettes by analyzing local color contrast rather than evaluating geometric primitives. Readers will learn how FXAA converts color data to luminance, samples orthogonal neighbor pixels, evaluates local contrast against relative and absolute thresholds, and determines whether an edge is oriented horizontally or vertically.
The Principles of FXAA Edge Detection
FXAA detects edges based on human perceptual brightness, known as luminance, rather than raw RGB differences or depth buffer discontinuities. By processing luminance values of a target pixel alongside its immediate neighbors, the shader calculates the local contrast range. If this contrast exceeds predetermined thresholds, the pixel is flagged as an edge that requires blending.
1. Calculating Perceived Luminance
Luminance (\(L\)) is typically derived using standard colorimetric conversion coefficients that reflect human eye sensitivity to green wavelengths:
\[L = 0.299 \cdot R + 0.587 \cdot G + 0.114 \cdot B\]
In optimized rendering pipelines, luminance can be pre-calculated and stored inside the alpha channel of the primary render target to avoid redundant computations during the post-processing pass.
2. Gathering Local Neighborhood Samples
To detect an edge, the fragment shader samples the center pixel and its four direct orthogonal neighbors: North, South, East, and West.
vec2 texCoord = ...; // Normalized screen coordinates [0, 1]
vec2 inverseScreenSize = vec2(1.0 / screenWidth, 1.0 / screenHeight);
// Sample center and cardinal directions
float lumaCenter = rgb2luma(texture(screenTexture, texCoord).rgb);
float lumaDown = rgb2luma(texture(screenTexture, texCoord + vec2(0.0, -1.0) * inverseScreenSize).rgb);
float lumaUp = rgb2luma(texture(screenTexture, texCoord + vec2(0.0, 1.0) * inverseScreenSize).rgb);
float lumaLeft = rgb2luma(texture(screenTexture, texCoord + vec2(-1.0, 0.0) * inverseScreenSize).rgb);
float lumaRight = rgb2luma(texture(screenTexture, texCoord + vec2( 1.0, 0.0) * inverseScreenSize).rgb);3. Contrast Evaluation and Thresholding
The algorithm determines the minimum and maximum luminance values among the sampled pixels to calculate the local contrast range:
float lumaMin = min(lumaCenter, min(min(lumaDown, lumaUp), min(lumaLeft, lumaRight)));
float lumaMax = max(lumaCenter, max(max(lumaDown, lumaUp), max(lumaLeft, lumaRight)));
float lumaRange = lumaMax - lumaMin;To avoid processing flat surfaces or areas hidden in deep shadows, the local range is evaluated against two thresholds:
- Relative Threshold (
EDGE_THRESHOLD): Trims edges inside low-contrast areas (typically tuned between0.125and0.250). - Absolute Threshold
(
EDGE_THRESHOLD_MIN): Prevents edge detection in near-black regions where aliasing is imperceptible (typically tuned between0.0312and0.0833).
#define FXAA_EDGE_THRESHOLD 0.166
#define FXAA_EDGE_THRESHOLD_MIN 0.0833
if (lumaRange < max(FXAA_EDGE_THRESHOLD_MIN, lumaMax * FXAA_EDGE_THRESHOLD)) {
// No anti-aliasing needed; return center color directly
discard;
}Complete GLSL Edge Detection and Direction Pass
Once a pixel passes the threshold check, the algorithm samples diagonal neighbors (North-West, North-East, South-West, South-East) to compute directional gradients. This step establishes whether the edge runs predominantly along the horizontal or vertical axis.
#version 330 core
in vec2 vTexCoord;
out vec4 FragColor;
uniform sampler2D screenTexture;
uniform vec2 uInverseScreenSize;
#define FXAA_EDGE_THRESHOLD 0.166
#define FXAA_EDGE_THRESHOLD_MIN 0.0625
float rgb2luma(vec3 rgb) {
return dot(rgb, vec3(0.299, 0.587, 0.114));
}
void main() {
// Center luminance
vec3 colorCenter = texture(screenTexture, vTexCoord).rgb;
float lumaCenter = rgb2luma(colorCenter);
// Cardinal neighbor luminance
float lumaDown = rgb2luma(texture(screenTexture, vTexCoord + vec2(0.0, -1.0) * uInverseScreenSize).rgb);
float lumaUp = rgb2luma(texture(screenTexture, vTexCoord + vec2(0.0, 1.0) * uInverseScreenSize).rgb);
float lumaLeft = rgb2luma(texture(screenTexture, vTexCoord + vec2(-1.0, 0.0) * uInverseScreenSize).rgb);
float lumaRight = rgb2luma(texture(screenTexture, vTexCoord + vec2( 1.0, 0.0) * uInverseScreenSize).rgb);
// Compute range and evaluate thresholds
float lumaMin = min(lumaCenter, min(min(lumaDown, lumaUp), min(lumaLeft, lumaRight)));
float lumaMax = max(lumaCenter, max(max(lumaDown, lumaUp), max(lumaLeft, lumaRight)));
float lumaRange = lumaMax - lumaMin;
if (lumaRange < max(FXAA_EDGE_THRESHOLD_MIN, lumaMax * FXAA_EDGE_THRESHOLD)) {
FragColor = vec4(colorCenter, 1.0);
return;
}
// Corner luminance for orientation determination
float lumaDownLeft = rgb2luma(texture(screenTexture, vTexCoord + vec2(-1.0, -1.0) * uInverseScreenSize).rgb);
float lumaUpRight = rgb2luma(texture(screenTexture, vTexCoord + vec2( 1.0, 1.0) * uInverseScreenSize).rgb);
float lumaUpLeft = rgb2luma(texture(screenTexture, vTexCoord + vec2(-1.0, 1.0) * uInverseScreenSize).rgb);
float lumaDownRight = rgb2luma(texture(screenTexture, vTexCoord + vec2( 1.0, -1.0) * uInverseScreenSize).rgb);
// Combine cardinal and diagonal deltas to measure edge orientation
float lumaDownUp = lumaDown + lumaUp;
float lumaLeftRight = lumaLeft + lumaRight;
float lumaLeftCorners = lumaDownLeft + lumaUpLeft;
float lumaDownCorners = lumaDownLeft + lumaDownRight;
float lumaRightCorners = lumaDownRight + lumaUpRight;
float lumaUpCorners = lumaUpRight + lumaUpLeft;
// Gradient calculations
float edgeHorizontal = abs(-2.0 * lumaLeft + lumaLeftCorners) +
abs(-2.0 * lumaCenter + lumaDownUp) * 2.0 +
abs(-2.0 * lumaRight + lumaRightCorners);
float edgeVertical = abs(-2.0 * lumaUp + lumaUpCorners) +
abs(-2.0 * lumaCenter + lumaLeftRight) * 2.0 +
abs(-2.0 * lumaDown + lumaDownCorners);
bool isHorizontal = edgeHorizontal >= edgeVertical;
// Edge detected: output debug visualizer or proceed to the blending stage
FragColor = isHorizontal ? vec4(1.0, 0.0, 0.0, 1.0) : vec4(0.0, 0.0, 1.0, 1.0);
}The resulting boolean isHorizontal identifies the normal
direction required for subsequent sub-pixel edge tracing and texture
filtering passes across the detected boundary.