cross posted from my blog.
For an embedded project I’m working on, I had to implement an algorithm to convert from HSB (hue/saturation/brightness) to 8-bit RGB color values for PWM’ing LEDs. This is what I came up with. It creates a ‘constant brightness’ RGB value: at full saturation (no ‘whitewash’), only two of the 3 RGB colors are on at a time, and their ‘on’ times are mathematically complimentary (though not phase-complimentary) with respect to the max 255 value.
Increasing saturation will increase the overall brightness of the LEDs, but that is to be expected — for a given maximum magnitude, there is more overall energy in white light than there is in light of a particular color. The saturation calculation works by adding a constant ‘floor’ value to all channels. Individual color values are then placed between this floor and the 255 maximum. A saturation value of 0 results in all channels at 100% duty cycle.
The ‘brightness’ is the last calculation performed. It takes a saturation modified hue value, and simply proportions it to the maximum value. So, a brightness of 197 will output light which is ~197/255 of the maximum output value. Naturally, there are losses inherent to integer arithmetic, but it’s close enough for most uses. Further, the linear nature of the brightness control means it is not ‘gamma corrected’ — that would require logarithmic brightness control which, for what I’m doing, is completely unnecessary.
This algorithm uses no sine tables or floating point math, so it’s pretty fast, though it could probably be optimized to use shifts and adds instead of mults and divides. It’s also relatively small. The code itself is in C, so it can be used on most platforms.
Click ‘more’ for the code snippet and just copy/paste into a text editor —
/****************************************************************************** * accepts hue, saturation and brightness values and outputs three 8-bit color * values in an array (color[]) * * saturation (sat) and brightness (bright) are 8-bit values. * * hue (index) is a value between 0 and 767. hue values out of range are * rendered as 0. * *****************************************************************************/ void hsb2rgb(uint16_t index, uint8_t sat, uint8_t bright, uint8_t color[3]) { uint16_t r_temp, g_temp, b_temp; uint8_t index_mod; uint8_t inverse_sat = (sat ^ 255); index = index % 768; index_mod = index % 256; if (index < 256) { r_temp = index_mod ^ 255; g_temp = index_mod; b_temp = 0; } else if (index < 512) { r_temp = 0; g_temp = index_mod ^ 255; b_temp = index_mod; } else if ( index < 768) { r_temp = index_mod; g_temp = 0; b_temp = index_mod ^ 255; } else { r_temp = 0; g_temp = 0; b_temp = 0; } r_temp = ((r_temp * sat) / 255) + inverse_sat; g_temp = ((g_temp * sat) / 255) + inverse_sat; b_temp = ((b_temp * sat) / 255) + inverse_sat; r_temp = (r_temp * bright) / 255; g_temp = (g_temp * bright) / 255; b_temp = (b_temp * bright) / 255; color[RED] = (uint8_t)r_temp; color[GREEN] = (uint8_t)g_temp; color[BLUE] = (uint8_t)b_temp; }
Lianna has optimized the code (below, in the comments), but some of her parentheses are showing up as smilies. I don’t know how to fix that so I’ll just add it to the post as preformatted text. Thanks Lianna! Great work!
Lianna’s version 1:
void hsb2rgbAN1(uint16_t index, uint8_t sat, uint8_t bright, uint8_t color[3]) {
uint8_t temp[5], n = (index >> 8) % 3;
temp[0] = temp[3] = (uint8_t)(( (sat ^ 255) * bright) / 255);
temp[1] = temp[4] = (uint8_t)((((( (index & 255) * sat) / 255) + (sat ^ 255)) * bright) / 255);
temp[2] = (uint8_t)(((((((index & 255) ^ 255) * sat) / 255) + (sat ^ 255)) * bright) / 255);
color[RED] = temp[n + 2];
color[GREEN] = temp[n + 1];
color[BLUE] = temp[n ];
}
version 2:
void hsb2rgbAN2(uint16_t index, uint8_t sat, uint8_t bright, uint8_t color[3]) { uint8_t temp[5], n = (index >> 8) % 3; // %3 not needed if input is constrained, but may be useful for color cycling and/or if modulo constant is fast uint8_t x = ((((index & 255) * sat) >> 8) * bright) >> 8; // shifts may be added for added speed and precision at the end if fast 32 bit calculation is available uint8_t s = ((256 - sat) * bright) >> 8; temp[0] = temp[3] = s; temp[1] = temp[4] = x + s; temp[2] = bright - x ; color[RED] = temp[n + 2]; color[GREEN] = temp[n + 1]; color[BLUE] = temp[n ]; }
Yeah, I don’t like unoptimized code either, but gcc does wonders with some of these expressions. Still, I would go with something like the following, if only because it’s shorter, so less error-prone but functionally 1:1 equivalent:
void hsb2rgbAN1(uint16_t index, uint8_t sat, uint8_t bright, uint8_t color[3]) {
uint8_t temp[5], n = (index >> 8) % 3;
temp[0] = temp[3] = (uint8_t)(( (sat ^ 255) * bright) / 255);
temp[1] = temp[4] = (uint8_t)((((( (index & 255) * sat) / 255) + (sat ^ 255)) * bright) / 255);
temp[2] = (uint8_t)(((((((index & 255) ^ 255) * sat) / 255) + (sat ^ 255)) * bright) / 255);
color[RED] = temp[n + 2];
color[GREEN] = temp[n + 1];
color[BLUE] = temp[n ];
}
If speed was more critical or not compiling with gcc (or emulated divs were longer than expected), I would settle for something like the following:
void hsb2rgbAN2(uint16_t index, uint8_t sat, uint8_t bright, uint8_t color[3]) {
uint8_t temp[5], n = (index >> 8) % 3;
// %3 not needed if input is constrained, but may be useful for color cycling and/or if modulo constant is fast
uint8_t x = ((((index & 255) * sat) >> 8) * bright) >> 8;
// shifts may be added for added speed and precision at the end if fast 32 bit calculation is available
uint8_t s = ((256 – sat) * bright) >> 8;
temp[0] = temp[3] = s;
temp[1] = temp[4] = x + s;
temp[2] = bright – x ;
color[RED] = temp[n + 2];
color[GREEN] = temp[n + 1];
color[BLUE] = temp[n ];
}
Approximation statistics (compared to output from your code), % of output within +-n:
0:(42.7%) 1:(36.4%) 2:(17.5%) 3:(3.1%) 4:(0.3%)
Looks quite similar 🙂
D*** these emoticons… and whitespace folding…
Testing PRE:
void hsb2rgbAN2(uint16_t index, uint8_t sat, uint8_t bright, uint8_t color[3]) {
uint8_t temp[5], n = (index >> 8) % 3;
// %3 not needed if input is constrained, but may be useful for color cycling and/or if modulo constant is fast
uint8_t x = ((((index & 255) * sat) >> 8) * bright) >> 8;
// shifts may be added for added speed and precision at the end if fast 32 bit calculation is available
uint8_t s = ((256 – sat) * bright) >> 8;
temp[0] = temp[3] = s;
temp[1] = temp[4] = x + s;
temp[2] = bright – x ;
color[RED] = temp[n + 2];
color[GREEN] = temp[n + 1];
color[BLUE] = temp[n ];
}
In any case, substitute “eight” “right parenthesis” in place of every smiley with sunglasses at the above post…