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:
- HSL and HSV on Wikipedia
- HSL, HSB and HSV color: differences and conversion on codeitdown.com