GLSL 1D Convolution for Anamorphic Streak Blurs

This article covers the technical pipeline for generating anamorphic lens flare horizontal streaks using 1D convolution passes in GLSL. It details how bright-pass filtering isolates light sources, how horizontal-only discrete kernel sampling produces directional blurring, how hardware-accelerated bilinear filtering optimizes performance, and how multi-iteration downsampling achieves wide horizontal spreads.

1. Bright-Pass Filtering (Thresholding)

Anamorphic streaks are caused by strong light sources reflecting off the cylindrical elements of anamorphic lenses. Before applying horizontal convolution, the scene texture must be filtered to isolate pixels exceeding a specific luminance threshold.

In GLSL, this is evaluated by computing relative luminance (using standard Rec. 709 weights) and subtracting a threshold:

// Bright-pass fragment shader snippet
vec3 color = texture(u_SceneTexture, v_TexCoord).rgb;
float luminance = dot(color, vec3(0.2126, 0.7152, 0.0722));
vec3 brightColor = max(color - vec3(u_Threshold), vec3(0.0));
fragColor = vec4(brightColor * u_Intensity, 1.0);

2. The 1D Horizontal Convolution Kernel

Standard bloom effects typically require two separable 1D passes (one horizontal, one vertical) to approximate a 2D Gaussian blur. Anamorphic flares, however, simulate cylindrical lens distortion that expands almost exclusively along the X-axis. Therefore, the convolution only requires horizontal 1D passes.

The 1D convolution evaluates the weighted sum of neighboring texels along the horizontal axis:

\[I_{\text{out}}(u, v) = \sum_{i=-N}^{N} w_i \cdot I_{\text{in}}\left(u + i \cdot \Delta u, v\right)\]

Where \(w_i\) represents the kernel weight (often an exponential decay or standard Gaussian distribution), and \(\Delta u\) is the reciprocal of the render target's width (1.0 / target_width).

3. Bilinear Hardware Sampling Optimization

To minimize texture fetches, the sampling offsets can be calculated to tap into the midpoint between two adjacent texels. Leveraging GPU bilinear hardware filtering reduces the required texture fetches from \(2N + 1\) to roughly \(N + 1\).

For two adjacent weights \(w_1\) and \(w_2\), the combined weight \(W\) and texture coordinate offset \(D\) are derived as:

\[W = w_1 + w_2\] \[D = \frac{w_2}{w_1 + w_2}\]

In the GLSL shader, evaluating a symmetric 9-tap 1D blur is reduced to five hardware texture fetches:

#version 330 core
out vec4 fragColor;
in vec2 v_TexCoord;

uniform sampler2D u_InputTexture;
uniform float u_TexelOffset; // 1.0 / target_width

// Precalculated bilinear weights and offsets
const float weights[3] = float[](0.227027, 0.3162162, 0.0702702);
const float offsets[3] = float[](0.0, 1.38461538, 3.23076923);

void main() {
    vec3 result = texture(u_InputTexture, v_TexCoord).rgb * weights[0];

    for (int i = 1; i < 3; ++i) {
        vec2 offset = vec2(offsets[i] * u_TexelOffset, 0.0);
        result += texture(u_InputTexture, v_TexCoord + offset).rgb * weights[i];
        result += texture(u_InputTexture, v_TexCoord - offset).rgb * weights[i];
    }

    fragColor = vec4(result, 1.0);
}

4. Achieving Wide Streaks via Multi-Pass Downsampling

A single 1D convolution pass with a modest kernel radius only yields localized horizontal blur. Real-world anamorphic streaks can span the entire screen width. Evaluating this with a single kernel requires hundreds of taps, which is computationally expensive.

To achieve large spreads efficiently:

  1. Horizontal Downsampling: Downsample the isolated bright-pass buffer horizontally (e.g., downscaling width by factors of 2, 4, or 8 while keeping height constant or minimally reduced).
  2. Iterative Ping-Pong Convolution: Run the 1D horizontal blur across several iterations using ping-pong framebuffers, progressively increasing the step size \(\Delta u\).
  3. MIP-Chain Blurring: Create a horizontal mip-chain where each successive level applies the 1D horizontal blur, followed by upsampling and combining the levels additively.

5. Tinting and Composite

Anamorphic flares typically exhibit characteristic chromatic tints, most commonly deep blue or cyan, due to the anti-reflective lens coatings. Once the wide horizontal blur is evaluated, the result is tinted and blended additively over the original scene:

vec3 sceneColor = texture(u_SceneTexture, v_TexCoord).rgb;
vec3 streakColor = texture(u_StreakTexture, v_TexCoord).rgb;

// Apply anamorphic blue tint
vec3 anamorphicTint = vec3(0.1, 0.4, 1.0);
vec3 finalColor = sceneColor + (streakColor * anamorphicTint);

fragColor = vec4(finalColor, 1.0);