RGB LED easy smooth colour transitions

This is a yet another contribution to the already extensive literature that surrounds RGB LEDs and changing colours, but in all my research I have not seen the method I have implemented, so here it is in case someone can use it.

I wanted to build a mood lamp type of device, using a single RGB LED placed in a ping pong ball as a diffuser. I needed to find an algorithm that would allow me to fit the code and data in 2k as I am using an ATTiny 2313 chip, prototyped on an Arduino Uno. Most of the code I found used the random() function, ending up over 3k when compiled, probably due to the libraries, and many of the other methods I uncovered just did not do it for me.

The RGB colour space can be visualised as a cube whose (x, y, z) coordinates range from (0, 0, 0) or black, to (255, 255, 255) or white. More generally the cube is defined in the 3D space (0,0,0) to (1,1,1), scaled by 255. The vertices of this cube define the boundaries of the colour space, and moving along the 3D coordinates from one point to the next will naturally provide smooth colour transitions.

The algorithm I have implemented exploits this and traces the RGB coordinates from one corner to another, 'plotting' the coordinates of the line to the RGB led, which displays different colours. In the code below the path of the transitions is defined as series of vertex numbers for each move, but it can equally be implemened by selecting the next vertex randomly. Defining the path can allow a bias for a particular area of the colour cube (eg, cooler or warmer colours), and the path can be much longer than in the code below with little penalty in memory use.

/*
 RGB LED - Automatic Smooth Color Cycling

 Marco Colli
 April 2012
 
 Uses the properties of the RGB Colour Cube
 The RGB colour space can be viewed as a cube of colour. If we assume a cube of dimension 1, then the 
 coordinates of the vertices for the cubve will range from (0,0,0) to (1,1,1) (all black to all white).
 The transitions between each vertex will be a smooth colour flow and we can exploit this by using the 
 path coordinates as the LED transition effect. 
*/
// Output pins for PWM
#define  R_PIN  3  // Red LED
#define  G_PIN  5  // Green LED
#define  B_PIN  6  // Blue LED

// Constants for readability are better than magic numbers
// Used to adjust the limits for the LED, especially if it has a lower ON threshold
#define  MIN_RGB_VALUE  10   // no smaller than 0. 
#define  MAX_RGB_VALUE  255  // no bigger than 255.

// Slowing things down we need ...
#define  TRANSITION_DELAY  70   // in milliseconds, between individual light changes
#define  WAIT_DELAY        500  // in milliseconds, at the end of each traverse
//
// Total traversal time is ((MAX_RGB_VALUE - MIN_RGB_VALUE) * TRANSITION_DELAY) + WAIT_DELAY
// eg, ((255-0)*70)+500 = 18350ms = 18.35s

// Structure to contain a 3D coordinate
typedef struct
{
  byte  x, y, z;
} coord;

static coord  v; // the current rgb coordinates (colour) being displayed

/*
 Vertices of a cube
      
    C+----------+G
    /|        / |
  B+---------+F |
   | |       |  |    y   
   |D+-------|--+H   ^  7 z
   |/        | /     | /
  A+---------+E      +--->x

*/
const coord vertex[] = 
{
 //x  y  z      name
  {0, 0, 0}, // A or 0
  {0, 1, 0}, // B or 1
  {0, 1, 1}, // C or 2
  {0, 0, 1}, // D or 3
  {1, 0, 0}, // E or 4
  {1, 1, 0}, // F or 5
  {1, 1, 1}, // G or 6
  {1, 0, 1}  // H or 7
};

/*
 A list of vertex numbers encoded 2 per byte.
 Hex digits are used as vertices 0-7 fit nicely (3 bits 000-111) and have the same visual
 representation as decimal, so bytes 0x12, 0x34 ... should be interpreted as vertex 1 to 
 v2 to v3 to v4 (ie, one continuous path B to C to D to E).
*/
const byte path[] =
{
  0x01, 0x23, 0x76, 0x54, 0x03, 0x21, 0x56, 0x74,  // trace the edges
  0x13, 0x64, 0x16, 0x02, 0x75, 0x24, 0x35, 0x17, 0x25, 0x70,  // do the diagonals
};

#define  MAX_PATH_SIZE  (sizeof(path)/sizeof(path[0]))  // size of the array

void setup()
{
  pinMode(R_PIN, OUTPUT);   // sets the pins as output
  pinMode(G_PIN, OUTPUT);  
  pinMode(B_PIN, OUTPUT);
}

void traverse(int dx, int dy, int dz)
// Move along the colour line from where we are to the next vertex of the cube.
// The transition is achieved by applying the 'delta' value to the coordinate.
// By definition all the coordinates will complete the transition at the same 
// time as we only have one loop index.
{
  if ((dx == 0) && (dy == 0) && (dz == 0))   // no point looping if we are staying in the same spot!
    return;
    
  for (int i = 0; i < MAX_RGB_VALUE-MIN_RGB_VALUE; i++, v.x += dx, v.y += dy, v.z += dz)
  {
    // set the colour in the LED
    analogWrite(R_PIN, v.x);
    analogWrite(G_PIN, v.y);
    analogWrite(B_PIN, v.z);
    
    delay(TRANSITION_DELAY);  // wait fot the transition delay
  }

  delay(WAIT_DELAY);          // give it an extra rest at the end of the traverse
}

void loop()
{
  int    v1, v2=0;    // the new vertex and the previous one

  // initialise the place we start from as the first vertex in the array
  v.x = (vertex[v2].x ? MAX_RGB_VALUE : MIN_RGB_VALUE);
  v.y = (vertex[v2].y ? MAX_RGB_VALUE : MIN_RGB_VALUE);
  v.z = (vertex[v2].z ? MAX_RGB_VALUE : MIN_RGB_VALUE);

  // Now just loop through the path, traversing from one point to the next
  for (int i = 0; i < 2*MAX_PATH_SIZE; i++)
  {
    // !! loop index is double what the path index is as it is a nybble index !!
    v1 = v2;
    if (i&1)  // odd number is the second element and ...
      v2 = path[i>>1] & 0xf;  // ... the bottom nybble (index /2) or ...
    else      // ... even number is the first element and ...
      v2 = path[i>>1] >> 4;  // ... the top nybble
      
    traverse(vertex[v2].x-vertex[v1].x, 
             vertex[v2].y-vertex[v1].y, 
             vertex[v2].z-vertex[v1].z);
  }
}
1 Like

Thanks for the code, this seems to be smoother "on the eye"...

I noticed a quirk using a common anode RGB led (running this code on an ATTiny85 btw) - here there are visible jumps in between the colors. Do you think you could update the code to either accept common cathode or common anode RGB leds?

Thanks,
Christian.

Any jumpyness in color has nothing to do with the LEDs being common and or common cathode but rather with the fact that only 8 bit are used per color and that the eyes response to light intensity is not linear.
@OP I've seen this algorithm referenced in another post fairly recently, but the poster did not quite understand it. If you've reinvented this from scratch kudos to you!

What Headroom said is correct. You wil notice that there are lower and upper thresholds for the LEDs defined in the code. This was to ensure that the really big jumpiness that you get at the lower end (when the LED suddenly turns on) was avoided. In hindsight, it may have been better to have a threshold for each colour (R, G, B) as they seem to respond differently. However, it was good enough for me at the time and I still enjoy my little LED changer on the desk at work :slight_smile:

@Headroom - Thanks, this was the first real project I did and on the Arduino early in 2012, about 2 months after I started with the hardware. Lots of water under the bridge since then and I post most of my reasonable stuff on the site in my signature below.

So i was trying to use this for a lamp i was building my girlfriend for her birthday, in between blue and green it fades out completely, or flickers. I don't want that and was wondering how to make it smoothly loop? (I'm a newbie at any arduino stuff, this in fact, is my first project involving it.)

The only suggestion I have is to experiment with the led thresholds. These determine the lowest level at which the led will be set by the software.

This is a great piece of code. Well done.

Amazing!!!!!! Did not understand a bit of the code. Did only copy paste, and change the pin numbers to 9,10,11. Result..??!!! Amazing!!!!!!!!! Great coder....... my respect to you guru.