Fix Color Banding with DirectX Dithering

Color banding occurs when smooth gradients are displayed with insufficient bit depth, resulting in visible, stepped lines across subtle color transitions. This article examines why low-contrast gradients are uniquely vulnerable to quantization errors and explains how implementing dithering algorithms in DirectX pixel shaders eliminates these visual artifacts by distributing high-frequency noise across color boundaries.

The Cause of Color Banding in Low-Contrast Gradients

Color banding is fundamentally a quantization problem driven by the limitations of standard display pipelines. Most modern rendering engines output to 8-bit-per-channel (R8G8B8A8) render targets, providing only 256 discrete values per channel (0 to 255).

While 256 levels are often sufficient for high-contrast scenes with broad dynamic ranges, low-contrast gradients occupy only a narrow slice of this palette. For instance, a subtle shadow transition or a dark sky might shift across a range of just five discrete values—such as from red value 10 to red value 15—over a spatial span of 500 pixels.

Because the GPU cannot represent fractional values in an integer render target, it truncates or rounds the floating-point values computed during shading. This forces hundreds of adjacent pixels to snap to the exact same integer value. The result is distinct, uniform bands of solid color separated by sharp edges. This artifact is further amplified by the human visual system, which naturally emphasizes contrast boundaries through an optical phenomenon known as Mach banding.

How Dithering Solves Banding

Dithering prevents hard quantization steps by trading spatial resolution for perceived color resolution. Instead of snapping every pixel to the nearest discrete integer, dithering introduces a sub-least-significant-bit (LSB) perturbation to the pixel values before quantization occurs.

At the threshold between color level \(N\) and level \(N+1\), dithering intersperses pixels of both values in a ratio proportional to the true floating-point shade. For example, a target value of 10.25 will be rendered as a field where approximately 75% of the pixels are value 10 and 25% are value 11. The human eye acts as a low-pass filter, blending these microscopic variations into the perception of a smooth, continuous shade that does not physically exist in the display format.

Implementing Dithering in DirectX

Modern DirectX pipelines (Direct3D 11 and Direct3D 12) do not rely on fixed-function hardware dithering. Instead, dithering is implemented directly in the High-Level Shading Language (HLSL) within post-processing or final composition pixel shaders.

The standard dithering workflow in a DirectX pixel shader involves three steps:

  1. Maintain High Precision Upstream: Calculations must be performed in full precision (such as float4 or half-precision min16float) to preserve fractional color data throughout lighting and post-processing passes.
  2. Generate or Sample Noise: A pseudo-random or structured pattern is calculated using screen-space coordinates (SV_Position.xy). The ideal noise patterns are:
    • Bayer Matrices (Ordered Dithering): Computationally cheap and deterministic, though they can introduce faint cross-hatch patterning.
    • Interleaved Gradient Noise (IGN): A lightweight procedural function designed by Jorge Jimenez that produces high-frequency, non-repeating noise with minimal computation.
    • Blue Noise Textures: The industry standard for visual quality. Blue noise concentrates energy in high frequencies where human visual sensitivity is lowest, creating smooth, grain-free results without visible clumps or patterns.
  3. Apply the Offset Before Quantization: The sampled noise value—scaled to the range of a single 8-bit step (approximately \(1.0 / 255.0\))—is added to the floating-point color value immediately before writing to the 8-bit render target.

When the GPU writes the final color to an 8-bit buffer, the injected high-frequency variance breaks up the continuous, sharp quantization thresholds. The wide, visible bands are replaced by a uniform distribution of alternating discrete values, rendering low-contrast gradients completely smooth to the human observer.