Rendering Gradients Dithering

Overview

When drawing gradients, it’s possible to construct a gradient that exhibits banding artifacts when displayed. This usually occurs when interpolating between similar colors, resulting in a lack of precision in the texture format. For example, a typical texture has 8 bits per color component - that means that differences below 1/255 cannot be displayed.

Let’s take for example the following page:

<!DOCTYPE html>
<html>
<head>
<style>
#grad1 {
    height: 300px;
    background-image: linear-gradient(to bottom right, #006060, #005050);
}
</style>
</head>
<body>
    <div id="grad1"></div>
</body>
</html>

This results in the following image:

Even though the banding is only slightly visible, the artifact may sometimes be exaggerated by other effects or transformations.

Possible solutions

A possible solution would be to use a larger texture format (e.g. a 16-bit per component one), as well as a 16bpp display format. This would generally mean using an HDR-capable display, which is still too restrictive.

The most widely used solution for mitigating the banding effect is to apply dithering. This is basically adding some slight noise to the image, which tricks the human eye into perceiving the image as if it had slightly different colors.

Here’s the same page as above, rendered using the dithering method:

It looks like the banding is gone, even though we don’t have any more bit depth than before.

Using dithering in your backend

The dithering algorithm is implemented behind a preprocessor macro in the CohRenoirShaderPS HLSL shader (disabled by default). You only need to uncomment the //#define APPLY_DITHER part and it will automatically work.

If you want to apply dithering to another platform you’d need to do the following:

  1. Implement a pseudo-random shader function. An example of one written in HLSL is:
float Random(float2 coords)
{
    return frac(sin(dot(coords.xy, float2(12.9898, 78.233))) * 43758.5453);
}
  1. Define the amount of noise you want to generate. Half a unit of the bit depth is usually a good amount. You can use a definition like this static const float NOISE_GRANULARITY = 0.5 / 255.0;
  2. In RenoirShaderPS, after the gradient color is obtained, apply the dithering amount using the Random function and the texture coordinates as input for the randomness:
colorTemp += lerp(-NOISE_GRANULARITY, NOISE_GRANULARITY, Random(input.VaryingParam0.xy));
  1. You now have dithering!