The Developer’s Cry

Yet another blog by a hobbyist programmer

OpenGL color-cycling and HSL fading

Back in the olden days, arcade games would blink and flash colors to get you excited. Like a flipper box flashes its lights, the action popped off the screen. They don’t often make games like that anymore due to a change in style, but it’s also more difficult to do nowadays due to a change in hardware. Modern hardware display colors in RGB coordinates, whereas back in the day hardware used a color palette. Although there were plenty colors to choose from, only a very limited number could be on display at the same time. By rapidly changing the values for a given color index you could draw flashing sprites.

Fast forward to today where the RGB values already are encoded in the sprite and no color palette is being used. We can still change the colors through OpenGL color blending. Typically we render sprites in white: color (1, 1, 1). However, if you choose to draw it in red (for example), then it will blend and show up in red. Now it becomes a game of just animating the color over time.

Things become more interesting when you want to fade between colors. Linear interpolation goes like this:

// fraction is between 0.0 and 1.0
// lerp function
vec3 color = color_a * (1.0f - fraction) + color_b * fraction;

This works, but it is not pretty if you are fading between all three components of red, green, and blue. The cause of this lies in the color distribution of the RGB color space. To fade between arbitrary colors we should choose a different color space, like HSL (hue, saturation, lightness). To do this we have to convert the RGB coordinates to HSL, do the interpolation, and convert back to RGB for use with OpenGL.

vec3 from = rgb_to_hsl(color0);
vec3 to = rgb_to_hsl(color1);

vec3 color = lerp(from, to, fraction);
color = hsl_to_rgb(color);

So far, so good; things have been easy up till now. Converting between RGB and HSL is not so easy and requires some math. Luckily the formulas can be found online (see below for references). What’s a little counterintuitive about these formulas is that you need if statements to obtain the values. The exact code is a bit long to post, therefore I use pseudocode here. The algorithm is as described on Wikipedia.

vec3 rgb_to_hsl(r, g, b) {
    M = max(max(r, g), b)
    m = min(min(r, g), b)
    c = M - m   // chroma value

    h = NaN if c == 0
    h = fmod((g - b) / c, 6.0) if M == r
    h = (b - r) / c + 2.0      if M == g
    h = (r - g) / c + 4.0      if M == b
    h *= 60.0

    l = 0.5 * (M + m)

    s = 0 if l == 1.0
    else s = c / (1.0 - fabs(2.0 * l - 1.0))

    return vec3(h, s, l)
}

Mind that hue is a value between zero and 360 degrees, and may also be undefined: NaN. Saturation and lightness lie between zero and one. Also mind that when calculating saturation there is a division by zero lurking about.

vec3 hsl_to_rgb(h, s, l) {
    c = (1.0 - fabs(2.0 * l - 1.0)) * s     // chroma value

    rgb = vec3(0, 0, 0) if h is NaN
    else {
        h /= 60.0
        x = c * (1.0 - fabs(fmod(h, 2.0) - 1.0))
        rgb = vec3(0, 0, 0) if h < 0.0 || h >= 6.0
        rgb = vec3(c, x, 0) if h <= 1.0
        rgb = vec3(x, c, 0) if h <= 2.0
        rgb = vec3(0, c, x) if h <= 3.0
        rgb = vec3(0, x, c) if h <= 4.0
        rgb = vec3(x, 0, c) if h <= 5.0
        else rgb = vec3(c, 0, x)
    }

    // add m to match lightness
    m = l - 0.5 * c
    rgb.r += m
    rgb.g += m
    rgb.b += m
    return rgb
}

Funnily enough this pseudocode is very different from the solutions posted on StackOverflow. Although it works fine, I did have some difficulty with certain color fades that would appear in gray. This has to do with being close to an epsilon value that is there to prevent the division by zero that I mentioned earlier. As a workaround I now fade to 0.99 instead of the full 1.0. It may not be perfect, but it’s good enough for the moment.

I haven’t tried, but I suppose that both a color-cycle and a color fader could be implemented as OpenGL shaders.

More material: