Vote Charlie!

Improved color wheel function

Posted at age 30.

For years I have generated rainbows using the Wheel() function in Adafruit’s NeoPixels library. While convenient, the linear transition between the primary colors resulted in significantly dimmer colors in between the primaries. Playing around today, I improved the result by using a quadratic equation to accelerate each color component’s journey from 0 to 255.

This video demonstrates rainbow patterns generated first with the linear wheel function and then with the quadratic one, twice each. The video does makes it appear the new function also suffers from some brightness variation but to a lesser extent; I cannot see that at all looking right at the lights versus watching the video.

The original function was like this:

// Input a value 0 to 255 to get a color value.
// The colours are a transition r - g - b - back to r.
uint32_t Wheel(byte WheelPos) {
  WheelPos = 255 - WheelPos;
  if(WheelPos < 85) {
    return strip.Color(255 - WheelPos * 3, 0, WheelPos * 3);
  }
  if(WheelPos < 170) {
    WheelPos -= 85;
    return strip.Color(0, WheelPos * 3, 255 - WheelPos * 3);
  }
  WheelPos -= 170;
  return strip.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
}

The quadratic function is like this:

// Input values 0 to 255 to get color values that transition R->G->B. 0 and 255
// are the same color. This is based on Adafruit's Wheel() function, which used
// a linear map that resulted in brightness peaks at 0, 85 and 170. This version
// uses a quadratic map to make values approach 255 faster while leaving full
// red or green or blue untouched. For example, Wheel(42) is halfway between
// red and green. The linear function yielded (126, 129, 0), but this one yields
// (219, 221, 0). This function is based on the equation the circle centered at
// (255,0) with radius 255:  (x-255)^2 + (y-0)^2 = r^2
unsigned long Wheel(byte position) {
  byte R = 0, G = 0, B = 0;
  if (position < 85) {
    R = sqrt32((1530 - 9 * position) * position);
    G = sqrt32(65025 - 9 * position * position);
  } else if (position < 170) {
    position -= 85;
    R = sqrt32(65025 - 9 * position * position);
    B = sqrt32((1530 - 9 * position) * position);
  } else {
    position -= 170;
    G = sqrt32((1530 - 9 * position) * position);
    B = sqrt32(65025 - 9 * position * position);
  }
  return strip.Color(R, G, B);
}

It uses the following helper function, which I slightly adjusted after finding it in Fast Square root function for integers. on the Arduino for STM32 forum:

// Adapted from https://www.stm32duino.com/viewtopic.php?t=56#p8160
unsigned int sqrt32(unsigned long n) {
  unsigned int c = 0x8000;
  unsigned int g = 0x8000;
  while(true) {
    if(g*g > n) {
      g ^= c;
    }
    c >>= 1;
    if(c == 0) {
      return g;
    }
    g |= c;
  }
}

Initially I wrote a simpler function I would call from within the old Wheel() function instead of it calling Adafruit_NeoPixel::Color() directly. This still uses the same circle equation:

// Use a quadratic map to make values approach 255 faster while
// leaving full red or green or blue untouched.
unsigned long color(byte r, byte g, byte b) {
  return Adafruit_NeoMatrix::Color(
    sqrt32((510 - r) * r),
    sqrt32((510 - g) * g),
    sqrt32((510 - b) * b)
  );
}

I suspected I could optimize a bit by solving the equations as much as possible, hence I ended up with the rewritten Wheel() function above.

I wrote a test program to display a moving rainbow across a 32x8 pixel matrix using the old and new color wheel functions so I could see the performance difference. I feared I had slowed down the program tremendously by introducing square roots. For the test, I used a Flexible 8x32 NeoPixel RGB LED Matrix and an Adafruit Feather M0 Basic Proto board built around the ATSAMD21 Cortex M0 chip.

To my delight, the new function is only 6.3% slower! I executed the test functions 117 times each. The linear version’s duration in milliseconds ranged from 1193 to 1199 with median 1195 and average 1195.38. The quadratic version’s duration ranged from 1265 to 1277 with median 1270 and average 1270.59.

Join the movement!

Show your support by signing up to be among the lucky few notified when I manage to blog. This could be as often as once a day or as infrequently as yearly!

Help get me elected by purchasing products mentioned in this entry!

blog comments powered by Disqus