GLSL Temporal Color Jittering to Reduce Banding

Temporal color jittering, or temporal dithering, suppresses visible color banding on low-bitrate displays by introducing subtle, time-varying noise into pixel values before quantization. In GLSL, this technique modulates fragment color calculations across consecutive frames, utilizing the human visual system's temporal integration to blend intermediate shades that the display hardware cannot natively represent. Evaluating this approach involves assessing its shader implementation efficiency, measuring flicker perception thresholds, and analyzing objective metrics to ensure quantization steps are smoothed without creating crawling or buzzing artifacts.

The Mechanism in GLSL

Color banding occurs when smooth color gradients are quantized to bit depths such as 6-bit or 8-bit channels, creating visible steps between discrete values. In a GLSL fragment shader, temporal jittering adds a small, frame-dependent offset to the final linear color before it is converted to the target framebuffer format.

The shader commonly takes a frame counter uniform (uniform int u_Frame;) and the pixel’s screen-space coordinates (gl_FragCoord.xy). The temporal noise function combines these inputs to produce an offset scaled to the magnitude of the least significant bit (LSB):

// Typical sub-LSB jitter evaluation in GLSL
float lsb = 1.0 / 255.0; // For an 8-bit target
float noise = get_temporal_noise(gl_FragCoord.xy, u_Frame);
vec3 finalColor = rawColor + (noise - 0.5) * lsb;

Effective implementations typically rely on spatio-temporal blue noise textures or low-discrepancy sequences like the golden ratio or Roberts R2 sequence. These distributions ensure that noise varies rapidly across both space and consecutive frames, avoiding spatially coherent clumps or periodic temporal patterns.

Perceptual Evaluation and Flicker Fusion

The primary evaluation criterion for temporal jittering is its behavior under the Human Visual System (HVS), specifically regarding the flicker fusion threshold. At high refresh rates (60Hz, 120Hz, or higher), the human retina and visual cortex integrate photons over brief temporal windows, perceiving two rapidly alternating quantized values as their continuous mathematical average.

Evaluating perceived suppression requires inspecting:

  • Flicker Detectability: If the jitter amplitude exceeds 1 LSB, or if the temporal frequency spectrum contains strong low-frequency components, the gradient will appear to "shimmer," "crawl," or flicker. This is particularly noticeable in high-contrast or dark regions where the eye's contrast sensitivity is elevated.
  • Motion Interaction: When the camera moves or objects translate, temporal jitter can cause ghosting or stroboscopic patterns if screen-space jitter interacts poorly with temporal anti-aliasing (TAA) or display overdrive circuits. Evaluating the technique across dynamic scenes is necessary to verify that screen-space noise does not turn into stationary spatial noise or motion-trailing artifacts.

Quantitative and Analytical Metrics

Beyond visual inspection, temporal jittering in GLSL is evaluated using objective image quality metrics adjusted for temporal domains:

  1. Temporal Contrast Sensitivity Functions (T-CSF): T-CSF models calculate whether the energy of the noise falls outside the visible spatio-temporal passband of human vision. A successful implementation concentrates its energy in high temporal and spatial frequencies.
  2. Mean Squared Error (MSE) Over Time: By averaging several consecutive rendered frames, the resulting composite image is compared against a high-bit-depth ground truth (e.g., 16-bit float). A successful jitter function will show that the temporally accumulated MSE drops significantly compared to static truncation.
  3. Frequency Analysis (Fourier Transforms): Taking the 3D Fast Fourier Transform (2D space + 1D time) of the noise signal reveals whether any temporal DC biases or low-frequency spikes exist that could manifest as perceptible pulse artifacts.

Computational Performance in the Pipeline

Evaluating the shader's computational footprint is crucial for real-time applications. Complex procedural noise algorithms can become ALU-bound, negating the benefits of the technique. High-efficiency GLSL implementations often use a tiny 2D blue noise array (e.g., \(64 \times 64\) texels) and apply an analytical temporal phase shift per frame:

// Temporal animation via phase shift
vec2 uv = gl_FragCoord.xy / 64.0;
float blueNoise = texture(u_BlueNoiseTex, uv).r;
float temporalOffset = fract(blueNoise + float(u_Frame % 16) * 0.61803398875);

Shader profiling tools evaluate this approach based on texture fetch latency, register pressure, and uniform overhead to ensure the dithering pass runs in a fraction of a millisecond on target GPUs.