‘Constant Brightness’ HSB to RGB Algorithm

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 &gt;&gt; 8) % 3;
    temp[0] = temp[3] = (uint8_t)((                                        (sat ^ 255)  * bright) / 255);
    temp[1] = temp[4] = (uint8_t)((((( (index &amp; 255)        * sat) / 255) + (sat ^ 255)) * bright) / 255);
    temp[2] =          (uint8_t)(((((((index &amp; 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 &gt;&gt; 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 &amp; 255) * sat) &gt;&gt; 8) * bright) &gt;&gt; 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) &gt;&gt; 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    ];
}


Adafruit publishes a wide range of writing and video content, including interviews and reporting on the maker market and the wider technology world. Our standards page is intended as a guide to best practices that Adafruit uses, as well as an outline of the ethical standards Adafruit aspires to. While Adafruit is not an independent journalistic institution, Adafruit strives to be a fair, informative, and positive voice within the community – check it out here: adafruit.com/editorialstandards

Join Adafruit on Mastodon

Adafruit is on Mastodon, join in! adafruit.com/mastodon

Stop breadboarding and soldering – start making immediately! Adafruit’s Circuit Playground is jam-packed with LEDs, sensors, buttons, alligator clip pads and more. Build projects with Circuit Playground in a few minutes with the drag-and-drop MakeCode programming site, learn computer science using the CS Discoveries class on code.org, jump into CircuitPython to learn Python and hardware together, TinyGO, or even use the Arduino IDE. Circuit Playground Express is the newest and best Circuit Playground board, with support for CircuitPython, MakeCode, and Arduino. It has a powerful processor, 10 NeoPixels, mini speaker, InfraRed receive and transmit, two buttons, a switch, 14 alligator clip pads, and lots of sensors: capacitive touch, IR proximity, temperature, light, motion and sound. A whole wide world of electronics and coding is waiting for you, and it fits in the palm of your hand.

Have an amazing project to share? The Electronics Show and Tell is every Wednesday at 7pm ET! To join, head over to YouTube and check out the show’s live chat – we’ll post the link there.

Join us every Wednesday night at 8pm ET for Ask an Engineer!

Join over 36,000+ makers on Adafruit’s Discord channels and be part of the community! http://adafru.it/discord

CircuitPython – The easiest way to program microcontrollers – CircuitPython.org


Maker Business — “Packaging” chips in the US

Wearables — Enclosures help fight body humidity in costumes

Electronics — Transformers: More than meets the eye!

Python for Microcontrollers — Python on Microcontrollers Newsletter: Silicon Labs introduces CircuitPython support, and more! #CircuitPython #Python #micropython @ThePSF @Raspberry_Pi

Adafruit IoT Monthly — Guardian Robot, Weather-wise Umbrella Stand, and more!

Microsoft MakeCode — MakeCode Thank You!

EYE on NPI — Maxim’s Himalaya uSLIC Step-Down Power Module #EyeOnNPI @maximintegrated @digikey

New Products – Adafruit Industries – Makers, hackers, artists, designers and engineers! — #NewProds 7/19/23 Feat. Adafruit Matrix Portal S3 CircuitPython Powered Internet Display!

Get the only spam-free daily newsletter about wearables, running a "maker business", electronic tips and more! Subscribe at AdafruitDaily.com !



2 Comments

  1. 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 🙂

  2. 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…

Sorry, the comment form is closed at this time.