A problem that’s plagued SilverLining since its inception is color banding. In twilight conditions, there is a very narrow range of colors in the sky from the rising or setting sun, and 8 bits of color per channel just isn’t enough to represent it. This results in banding artifacts that are fundamentally a limitation of today’s display devices. The same issue can arise within the clouds, which also take up a narrow slice of colors, or surrounding the moon at night.

Version 6.15 of SilverLining finally fixes this for OpenGL users through dithering; we finally got the math for this just right. The implementation of this resides within our “user functions” for extending our shaders, specifically in the writeFragmentData function of Resources/Shaders/UserFunctions-frag.glsl:


void writeFragmentData(in vec4 finalColor)
{
#ifdef DITHER
    // We add a pseudo-random value (tied to screen space to avoid motion aliasing) mapped to +/- 0.5/255 to the final color.
    // This reduces banding artifacts in low light conditions, where 8 bit color isn't enough to smoothly represent
    // the resulting gradients in the sky and clouds.

    float noise = mix(-0.5/255.0, 0.5/255.0, fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453));
    gl_FragColor.xyz = finalColor.xyz + noise;
    gl_FragColor.w = finalColor.w;
#else
    gl_FragColor = finalColor;
#endif
}

The results speak for themselves! Un-dithered, banded image on the left, dithered image on the right (click for full resolution):

OpenGL dithering

At the heart of the GLSL code above is a mathematical psuedo-random-number function that’s nice and fast – even faster than looking up a value from a 2D noise texture. Since it is a function of the screen position of the fragment (gl_FragCoord), it does not result in any motion or temporal aliasing artifacts – you don’t even notice it’s there. The noise is scaled to +/- 0.5/255, which is just enough to randomize the final colors by no more than a single gradation in what 8-bit color can represent.

Since it’s applied in our writeFragmentData hook, it conveniently applies to everything drawn by SilverLining – not just the sky, but the clouds too.

However, as it is part of our default user shaders, you will need to copy this into your own user shaders if you are maintaining your own copy of UserFunctions-frag.glsl15.

I hope this increases the immersion of your simulations!