Skip to content

Instantly share code, notes, and snippets.

@postspectacular
Last active June 18, 2024 22:02
Show Gist options
  • Save postspectacular/2a4a8db092011c6743a7 to your computer and use it in GitHub Desktop.
Save postspectacular/2a4a8db092011c6743a7 to your computer and use it in GitHub Desktop.
Super compact HSV/RGB conversions for Arduino/C
int redPin = 6;
int greenPin = 5;
int bluePin = 9;
float col[3];
float hue = 0.0;
void setup() {
pinMode(redPin, OUTPUT);
pinMode(greenPin, OUTPUT);
pinMode(bluePin, OUTPUT);
}
void loop() {
setColor(hsv2rgb(hue, 1.0, 1.0, col));
delay(50);
hue += 0.01;
if (hue >= 1.0) hue = 0.0;
}
void setColor(float *rgb) {
analogWrite(redPin, (int)((1.0 - rgb[0]) * 255));
analogWrite(greenPin, (int)((1.0 - rgb[1]) * 255));
analogWrite(bluePin, (int)((1.0 - rgb[2]) * 255));
}
// HSV->RGB conversion based on GLSL version
// expects hsv channels defined in 0.0 .. 1.0 interval
float fract(float x) { return x - int(x); }
float mix(float a, float b, float t) { return a + (b - a) * t; }
float step(float e, float x) { return x < e ? 0.0 : 1.0; }
float* hsv2rgb(float h, float s, float b, float* rgb) {
rgb[0] = b * mix(1.0, constrain(abs(fract(h + 1.0) * 6.0 - 3.0) - 1.0, 0.0, 1.0), s);
rgb[1] = b * mix(1.0, constrain(abs(fract(h + 0.6666666) * 6.0 - 3.0) - 1.0, 0.0, 1.0), s);
rgb[2] = b * mix(1.0, constrain(abs(fract(h + 0.3333333) * 6.0 - 3.0) - 1.0, 0.0, 1.0), s);
return rgb;
}
float* rgb2hsv(float r, float g, float b, float* hsv) {
float s = step(b, g);
float px = mix(b, g, s);
float py = mix(g, b, s);
float pz = mix(-1.0, 0.0, s);
float pw = mix(0.6666666, -0.3333333, s);
s = step(px, r);
float qx = mix(px, r, s);
float qz = mix(pw, pz, s);
float qw = mix(r, px, s);
float d = qx - min(qw, py);
hsv[0] = abs(qz + (qw - py) / (6.0 * d + 1e-10));
hsv[1] = d / (qx + 1e-10);
hsv[2] = qx;
return hsv;
}
@Andrea77S
Copy link

Andrea77S commented Apr 28, 2020

Hi. I think that in the function hsv2rgb, the parameters s and b are inverted.
If I understand correctly, h should stand for hue, s for saturation and b for brightness, but you have to invert the parameters to get it right.

So, the correct code should be:

float* hsv2rgb(float h, float s, float b, float* rgb) {
  // h = hue, s = saturation, b = brightness
  rgb[0] = s * mix(1.0, constrain(abs(fract(h + 1.0) * 6.0 - 3.0) - 1.0, 0.0, 1.0), b);
  rgb[1] = s * mix(1.0, constrain(abs(fract(h + 0.6666666) * 6.0 - 3.0) - 1.0, 0.0, 1.0), b);
  rgb[2] = s * mix(1.0, constrain(abs(fract(h + 0.3333333) * 6.0 - 3.0) - 1.0, 0.0, 1.0), b);
  return rgb;
}

@postspectacular
Copy link
Author

@Andrea77S I don't think you're right here. For one that code was extracted from a working project and I also just tested once more:

// pure yellow
hsv2rgb(1/6.0, 1.0, 1.0)
// [ 1, 0.9999995827674866, 0 ]

// yellow @ 50% brightness
hsv2rgb(1/6.0, 1.0, 0.5)
// [ 0.5, 0.4999997913837433, 0 ]

// yellow @ 50% saturation (correctly increases blue chan)
hsv2rgb(1/6.0, 0.5, 1.0)
// [ 1, 0.9999998211860657, 0.5 ]

@Andrea77S
Copy link

Hi postspectacuar
Thank you for your sketch and for your comment.
My consideration comes from my test setup with Arduino and an RGB led.
I have setup two 10k ohm potentiometers on my breadboard and connected to Arduino analog inputs and used to change the s and b parameters.

When using my version, i can see that changing the b parameter, the hue is kept, and the brightness of the color changes from full-color to black. When changing the s parameter, it changes from full-color to white.
This is why I think the two parameters are inversed in the original sketch.
Of course using the original code, the behaviour is opposite.

BTW, using 1/6.0 as hue value, i don't get a yellow, but a wonderful blue... (don't know why)

Here is my full sketch for example:

#define pinRED 6
#define pinGRN 5
#define pinBLU 3
#define pinSAT A0
#define pinVAL A5

float col[3];
float hue = 0.0;  //automatic, rolling             from 0.0 to 1.0
float sat = 0.0;  //manual, with potentiometer 10k from 0.0 to 1.0
float bri = 0.0;  //manual, with potentiometer 10k from 0.0 to 1.0

unsigned long msecs;

void setup() {
  pinMode(pinRED, OUTPUT);
  pinMode(pinGRN, OUTPUT);
  pinMode(pinBLU, OUTPUT);

  Serial.begin(9600);
  
}

void loop() {

  sat = analogRead(A0)/1024.0;
  bri = analogRead(A5)/1024.0;

  if (millis() - msecs > 250){
    Serial.print("hue: ");
    Serial.print(hue);
    Serial.print(" saturation: ");
    Serial.print(sat);
    Serial.print(" brightness: ");
    Serial.print(bri);
    Serial.println("");
    msecs = millis();
  }  
  // hue, saturation, brightness
  setColor(hsv2rgb(hue, sat, bri, col));
  delay(100);
  hue += 0.01;
  if (hue >= 1.0) hue = 0.0;

}

void setColor(float *rgb) {
  analogWrite(pinRED, (int)((1.0 - rgb[0]) * 255));
  analogWrite(pinGRN, (int)((1.0 - rgb[1]) * 255));
  analogWrite(pinBLU, (int)((1.0 - rgb[2]) * 255));  
}

// HSV->RGB conversion based on GLSL version
// expects hsv channels defined in 0.0 .. 1.0 interval
float fract(float x) { return x - int(x); }
float mix(float a, float b, float t) { return a + (b - a) * t; }
float step(float e, float x) { return x < e ? 0.0 : 1.0; }

float* hsv2rgb(float h, float s, float b, float* rgb) {
  // h = hue; s = saturation; b = brightness
  rgb[0] = s * mix(1.0, constrain(abs(fract(h + 1.0) * 6.0 - 3.0) - 1.0, 0.0, 1.0), b);
  rgb[1] = s * mix(1.0, constrain(abs(fract(h + 0.6666666) * 6.0 - 3.0) - 1.0, 0.0, 1.0), b);
  rgb[2] = s * mix(1.0, constrain(abs(fract(h + 0.3333333) * 6.0 - 3.0) - 1.0, 0.0, 1.0), b);
  return rgb;
}

float* rgb2hsv(float r, float g, float b, float* hsv) {
  float s = step(b, g);
  float px = mix(b, g, s);
  float py = mix(g, b, s);
  float pz = mix(-1.0, 0.0, s);
  float pw = mix(0.6666666, -0.3333333, s);
  s = step(px, r);
  float qx = mix(px, r, s);
  float qz = mix(pw, pz, s);
  float qw = mix(r, px, s);
  float d = qx - min(qw, py);
  hsv[0] = abs(qz + (qw - py) / (6.0 * d + 1e-10));
  hsv[1] = d / (qx + 1e-10);
  hsv[2] = qx;
  return hsv;
}

@postspectacular
Copy link
Author

I think that confusion might come from how these values are used in setColor():

void setColor(float *rgb) {
  analogWrite(redPin, (int)((1.0 - rgb[0]) * 255));
  analogWrite(greenPin, (int)((1.0 - rgb[1]) * 255));
  analogWrite(bluePin, (int)((1.0 - rgb[2]) * 255));  
}

I can't remember why anymore (project was from 2015), but you can see I've been using the resulting RGB values in an inverted manner (e.g. 1.0 - rbg[0])... that would explain the cyan vs yellow and maybe the other issue too. Just try to replace with:

void setColor(float *rgb) {
  analogWrite(redPin, (int)(rgb[0] * 255));
  analogWrite(greenPin, (int)(rgb[1] * 255));
  analogWrite(bluePin, (int)(rgb[2] * 255));  
}

@Andrea77S
Copy link

Yes I saw it .
I think this is causing the problem with brightness and saturation also:
If you change this:

void setColor(float *rgb) {
  analogWrite(pinRED, (int)((1.0 - rgb[0]) * 255));
  analogWrite(pinGRN, (int)((1.0 - rgb[1]) * 255));
  analogWrite(pinBLU, (int)((1.0 - rgb[2]) * 255));  
}

to this (removing the inversion):

void setColorNEW(float *rgb) {
  analogWrite(pinRED, (int)(rgb[0] * 255));
  analogWrite(pinGRN, (int)(rgb[1] * 255));
  analogWrite(pinBLU, (int)(rgb[2] * 255));
}

You get the right colours, but you also get the opposite behaviour with Saturation and Brightness (which is the correct behaviour BTW!)

So, it seems to me that to correct your code, you have to remove the inversion in the setColor function.

@R3tr0BoiDX
Copy link

Hello, thanks for your code! I get weird RGB-values on an ESP32:
https://pastebin.com/zs44RqPb

Calculations don't yield fractional results, only one and zero. What could be the problem?

I have the exact same behavior with the ESP8266. I used your code for quite a few projects with several types of Arduino, which always worked for me, but not with any ESP microprocessor.

@Andrea77S
Copy link

Ensure that all variables are float types, maybe you are writing a value into an int...

@faizalalirozan
Copy link

faizalalirozan commented Nov 19, 2021

what kinda sensor u use?

@postspectacular
Copy link
Author

There's no sensor at all, it's to control an RGB LED...

@AJ-Smoothie
Copy link

You are absolutely beautiful :)! Is there anywhere you give an in-depth explanation of how all of your code works? (the important stuff like where is the 0.666666 number coming from, not how passing a array by reference works). I would very much appreciate that! Thank you again

@baloghr
Copy link

baloghr commented May 8, 2023

Problems with inverted / non-inverted math in SetColor() function are related to the RGB LED diode you are using. The inverted version is for the common anode (CA) diode, while the non-inverted is for the common cathode (CA) RGB diode.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment