Files
WLED_MM_Infinity/wled00/colors.cpp
Frank a956b22a16 unGamma8() accuracy improvement
based on upstream changes by @DedeHai
2025-11-05 22:01:07 +01:00

504 lines
19 KiB
C++

#include "wled.h"
#if !defined(ARDUINO_ARCH_ESP32) || !defined(WLEDMM_FASTPATH) || defined(WLEDMM_SAVE_FLASH) // WLEDMM: color utils moved into colorTools.hpp, so comiler can inline calls (up to 12% faster)
/*
* Color conversion & utility methods
*/
/*
* color blend function
*/
uint32_t WLED_O3_ATTR IRAM_ATTR_YN __attribute__((hot)) color_blend(uint32_t color1, uint32_t color2, uint_fast16_t blend, bool b16) {
// min / max blend checking is omitted: calls with 0 or 255 are rare, checking lowers overall performance
//if ((color1 == color2) || (blend == 0)) return color1; // WLEDMM
const uint_fast16_t blendmax = b16 ? 0xFFFF : 0xFF;
if(blend >= blendmax) return color2;
const uint_fast8_t shift = b16 ? 16 : 8;
uint16_t w1 = W(color1); // WLEDMM 16bit to make sure the compiler uses 32bit (not 64bit) for the math
uint16_t r1 = R(color1);
uint16_t g1 = G(color1);
uint16_t b1 = B(color1);
uint16_t w2 = W(color2);
uint16_t r2 = R(color2);
uint16_t g2 = G(color2);
uint16_t b2 = B(color2);
if (b16 == false) {
// WLEDMM based on fastled blend8() - better accuracy for 8bit
uint8_t w3 = (w1+w2 == 0) ? 0 : (((w1 << 8)|w2) + (w2 * blend) - (w1*blend) ) >> 8;
uint8_t r3 = (((r1 << 8)|r2) + (r2 * blend) - (r1*blend) ) >> 8;
uint8_t g3 = (((g1 << 8)|g2) + (g2 * blend) - (g1*blend) ) >> 8;
uint8_t b3 = (((b1 << 8)|b2) + (b2 * blend) - (b1*blend) ) >> 8;
return RGBW32(r3, g3, b3, w3);
} else {
// old code has lots of "jumps" due to roundding errors
const uint_fast16_t blend2 = blendmax - blend; // WLEDMM pre-calculate value
uint32_t w3 = ((w2 * blend) + (w1 * blend2)) >> shift;
uint32_t r3 = ((r2 * blend) + (r1 * blend2)) >> shift;
uint32_t g3 = ((g2 * blend) + (g1 * blend2)) >> shift;
uint32_t b3 = ((b2 * blend) + (b1 * blend2)) >> shift;
return RGBW32(r3, g3, b3, w3);
}
}
/*
* color add function that preserves ratio
* idea: https://github.com/Aircoookie/WLED/pull/2465 by https://github.com/Proto-molecule
*/
uint32_t WLED_O2_ATTR IRAM_ATTR_YN color_add(uint32_t c1, uint32_t c2, bool fast) // WLEDMM added IRAM_ATTR_YN
{
if (c2 == 0) return c1; // WLEDMM shortcut
if (c1 == 0) return c2; // WLEDMM shortcut
if (fast) {
uint8_t r = R(c1);
uint8_t g = G(c1);
uint8_t b = B(c1);
uint8_t w = W(c1);
r = qadd8(r, R(c2));
g = qadd8(g, G(c2));
b = qadd8(b, B(c2));
w = qadd8(w, W(c2));
return RGBW32(r,g,b,w);
} else {
uint32_t r = R(c1) + R(c2);
uint32_t g = G(c1) + G(c2);
uint32_t b = B(c1) + B(c2);
uint32_t w = W(c1) + W(c2);
uint32_t max = r;
if (g > max) max = g;
if (b > max) max = b;
if (w > max) max = w;
if (max < 256) return RGBW32(r, g, b, w);
else return RGBW32(r * 255 / max, g * 255 / max, b * 255 / max, w * 255 / max);
}
}
/*
* fades color toward black
* if using "video" method the resulting color will never become black unless it is already black
*/
uint32_t WLED_O2_ATTR IRAM_ATTR_YN __attribute__((hot)) color_fade(uint32_t c1, uint8_t amount, bool video)
{
if (c1 == 0 || amount == 0) return 0; // black or no change
if (amount == 255) return c1;
uint32_t addRemains = 0;
if (!video) amount++; // add one for correct scaling using bitshifts
else {
// video scaling: make sure colors do not dim to zero if they started non-zero unless they distort the hue
uint8_t r = byte(c1>>16), g = byte(c1>>8), b = byte(c1), w = byte(c1>>24); // extract r, g, b, w channels
uint8_t maxc = (r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b); // determine dominant channel for hue preservation
addRemains = r && (r<<5) > maxc ? 0x00010000 : 0; // note: setting color preservation threshold too high results in flickering and
addRemains |= g && (g<<5) > maxc ? 0x00000100 : 0; // jumping colors in low brightness gradients. Multiplying the color preserves
addRemains |= b && (b<<5) > maxc ? 0x00000001 : 0; // better accuracy than dividing the maxc. Shifting by 5 is a good compromise
addRemains |= w ? 0x01000000 : 0; // i.e. remove color channel if <13% of max
}
const uint32_t TWO_CHANNEL_MASK = 0x00FF00FF;
uint32_t rb = (((c1 & TWO_CHANNEL_MASK) * amount) >> 8) & TWO_CHANNEL_MASK; // scale red and blue
uint32_t wg = (((c1 >> 8) & TWO_CHANNEL_MASK) * amount) & ~TWO_CHANNEL_MASK; // scale white and green
return (rb | wg) + addRemains;
}
//
// overwrite FastLed colorFromPalette with an optimized version created by dedehai (https://github.com/Aircoookie/WLED/pull/4138)
//
// 1:1 replacement of fastled function optimized for ESP, slightly faster, more accurate and uses less flash (~ -200bytes)
CRGB IRAM_ATTR_YN __attribute__((hot)) ColorFromPaletteWLED(const CRGBPalette16& pal, unsigned index, uint8_t brightness, TBlendType blendType)
{
if (blendType == LINEARBLEND_NOWRAP) {
index = (index*240) >> 8; // Blend range is affected by lo4 blend of values, remap to avoid wrapping
}
unsigned hi4 = byte(index) >> 4;
const CRGB* entry = (CRGB*)((uint8_t*)(&(pal[0])) + (hi4 * sizeof(CRGB)));
unsigned red1 = entry->r;
unsigned green1 = entry->g;
unsigned blue1 = entry->b;
if (blendType != NOBLEND) {
if (hi4 == 15) entry = &(pal[0]);
else ++entry;
unsigned f2 = ((index & 0x0F) << 4) + 1; // +1 so we scale by 256 as a max value, then result can just be shifted by 8
unsigned f1 = (257 - f2); // f2 is 1 minimum, so this is 256 max
red1 = (red1 * f1 + (unsigned)entry->r * f2) >> 8;
green1 = (green1 * f1 + (unsigned)entry->g * f2) >> 8;
blue1 = (blue1 * f1 + (unsigned)entry->b * f2) >> 8;
}
if (brightness < 255) { // note: zero checking could be done to return black but that is hardly ever used so it is omitted
uint32_t scale = brightness + 1; // adjust for rounding (bitshift)
red1 = (red1 * scale) >> 8;
green1 = (green1 * scale) >> 8;
blue1 = (blue1 * scale) >> 8;
}
return RGBW32(red1,green1,blue1,0);
//return CRGB(red1,green1,blue1);
}
#endif
void setRandomColor(byte* rgb)
{
lastRandomIndex = strip.getMainSegment().get_random_wheel_index(lastRandomIndex);
colorHStoRGB(lastRandomIndex*256,255,rgb);
}
void colorHStoRGB(uint16_t hue, byte sat, byte* rgb) //hue, sat to rgb
{
float h = ((float)hue)/65535.0f;
float s = ((float)sat)/255.0f;
int i = floorf(h*6);
float f = h * 6.0f - i;
int p = int(255.0f * (1.0f-s));
int q = int(255.0f * (1.0f-f*s));
int t = int(255.0f * (1.0f-(1.0f-f)*s));
p = constrain(p, 0, 255);
q = constrain(q, 0, 255);
t = constrain(t, 0, 255);
switch (i%6) {
case 0: rgb[0]=255,rgb[1]=t, rgb[2]=p; break;
case 1: rgb[0]=q, rgb[1]=255,rgb[2]=p; break;
case 2: rgb[0]=p, rgb[1]=255,rgb[2]=t; break;
case 3: rgb[0]=p, rgb[1]=q, rgb[2]=255;break;
case 4: rgb[0]=t, rgb[1]=p, rgb[2]=255;break;
case 5: rgb[0]=255,rgb[1]=p, rgb[2]=q; break;
}
}
//get RGB values from color temperature in K (https://tannerhelland.com/2012/09/18/convert-temperature-rgb-algorithm-code.html)
void colorKtoRGB(uint16_t kelvin, byte* rgb) //white spectrum to rgb, calc
{
// WLEDMM safe exit (do nothing) to avoid logf domain errors. argument to logf must be >= 1.0f to avoid bad result 0 or -inf;
// kelvin >65k might be a signed/unsigned conversion error
if ((kelvin < 1200) || (kelvin > 65000)) {
rgb[0] = 255;
rgb[1] = 255;
rgb[2] = 255;
rgb[3] = 0;
return;
}
int r = 0, g = 0, b = 0;
float temp = float(kelvin) / 100.0f; // WLEDMM "float()" added - to make sure its done in float, not in double or int
if (temp <= 66.0f) {
r = 255;
g = roundf(99.4708025861f * logf(temp) - 161.1195681661f);
if (temp <= 19.0f) {
b = 0;
} else {
b = roundf(138.5177312231f * logf((temp - 10.0f)) - 305.0447927307f); // safe because temp > 19.0f
}
} else {
// temp-60.0f is always > 0 here (since temp>66)
r = roundf(329.698727446f * powf((temp - 60.0f), -0.1332047592f));
g = roundf(288.1221695283f * powf((temp - 60.0f), -0.0755148492f));
b = 255;
}
//g += 12; //mod by Aircoookie, a bit less accurate but visibly less pinkish
// WLEDMM min(max()) is faster than constrain()
rgb[0] = (uint8_t) min(max(r, 0), 255);
rgb[1] = (uint8_t) min(max(g, 0), 255);
rgb[2] = (uint8_t) min(max(b, 0), 255);
rgb[3] = 0;
}
void colorCTtoRGB(uint16_t mired, byte* rgb) //white spectrum to rgb, bins
{
//this is only an approximation using WS2812B with gamma correction enabled
if (mired > 475) {
rgb[0]=255;rgb[1]=199;rgb[2]=92;//500
} else if (mired > 425) {
rgb[0]=255;rgb[1]=213;rgb[2]=118;//450
} else if (mired > 375) {
rgb[0]=255;rgb[1]=216;rgb[2]=118;//400
} else if (mired > 325) {
rgb[0]=255;rgb[1]=234;rgb[2]=140;//350
} else if (mired > 275) {
rgb[0]=255;rgb[1]=243;rgb[2]=160;//300
} else if (mired > 225) {
rgb[0]=250;rgb[1]=255;rgb[2]=188;//250
} else if (mired > 175) {
rgb[0]=247;rgb[1]=255;rgb[2]=215;//200
} else {
rgb[0]=237;rgb[1]=255;rgb[2]=239;//150
}
}
#ifndef WLED_DISABLE_HUESYNC
void colorXYtoRGB(float x, float y, byte* rgb) //coordinates to rgb (https://www.developers.meethue.com/documentation/color-conversions-rgb-xy)
{
float z = 1.0f - x - y;
float X = (1.0f / y) * x;
float Z = (1.0f / y) * z;
float r = (int)255*(X * 1.656492f - 0.354851f - Z * 0.255038f);
float g = (int)255*(-X * 0.707196f + 1.655397f + Z * 0.036152f);
float b = (int)255*(X * 0.051713f - 0.121364f + Z * 1.011530f);
if (r > b && r > g && r > 1.0f) {
// red is too big
g = g / r;
b = b / r;
r = 1.0f;
} else if (g > b && g > r && g > 1.0f) {
// green is too big
r = r / g;
b = b / g;
g = 1.0f;
} else if (b > r && b > g && b > 1.0f) {
// blue is too big
r = r / b;
g = g / b;
b = 1.0f;
}
// Apply gamma correction
r = r <= 0.0031308f ? 12.92f * r : (1.0f + 0.055f) * powf(r, (1.0f / 2.4f)) - 0.055f;
g = g <= 0.0031308f ? 12.92f * g : (1.0f + 0.055f) * powf(g, (1.0f / 2.4f)) - 0.055f;
b = b <= 0.0031308f ? 12.92f * b : (1.0f + 0.055f) * powf(b, (1.0f / 2.4f)) - 0.055f;
if (r > b && r > g) {
// red is biggest
if (r > 1.0f) {
g = g / r;
b = b / r;
r = 1.0f;
}
} else if (g > b && g > r) {
// green is biggest
if (g > 1.0f) {
r = r / g;
b = b / g;
g = 1.0f;
}
} else if (b > r && b > g) {
// blue is biggest
if (b > 1.0f) {
r = r / b;
g = g / b;
b = 1.0f;
}
}
rgb[0] = byte(255.0f*r);
rgb[1] = byte(255.0f*g);
rgb[2] = byte(255.0f*b);
}
void colorRGBtoXY(byte* rgb, float* xy) //rgb to coordinates (https://www.developers.meethue.com/documentation/color-conversions-rgb-xy)
{
float X = rgb[0] * 0.664511f + rgb[1] * 0.154324f + rgb[2] * 0.162028f;
float Y = rgb[0] * 0.283881f + rgb[1] * 0.668433f + rgb[2] * 0.047685f;
float Z = rgb[0] * 0.000088f + rgb[1] * 0.072310f + rgb[2] * 0.986039f;
xy[0] = X / (X + Y + Z);
xy[1] = Y / (X + Y + Z);
}
#endif // WLED_DISABLE_HUESYNC
//RRGGBB / WWRRGGBB order for hex
void colorFromDecOrHexString(byte* rgb, char* in)
{
if (in[0] == 0) return;
char first = in[0];
uint32_t c = 0;
if (first == '#' || first == 'h' || first == 'H') //is HEX encoded
{
c = strtoul(in +1, NULL, 16);
} else
{
c = strtoul(in, NULL, 10);
}
rgb[0] = R(c);
rgb[1] = G(c);
rgb[2] = B(c);
rgb[3] = W(c);
}
//contrary to the colorFromDecOrHexString() function, this uses the more standard RRGGBB / RRGGBBWW order
bool colorFromHexString(byte* rgb, const char* in) {
if (in == nullptr) return false;
size_t inputSize = strnlen(in, 9);
if (inputSize != 6 && inputSize != 8) return false;
uint32_t c = strtoul(in, NULL, 16);
if (inputSize == 6) {
rgb[0] = (c >> 16);
rgb[1] = (c >> 8);
rgb[2] = c ;
} else {
rgb[0] = (c >> 24);
rgb[1] = (c >> 16);
rgb[2] = (c >> 8);
rgb[3] = c ;
}
return true;
}
#if 0 // WLEDMM minf/maxf are defined in libm already
static float minf (float v, float w) // WLEDMM better use standard library fminf()
{
if (w > v) return v;
return w;
}
static float maxf (float v, float w) // WLEDMM better use standard library fmaxf()
{
if (w > v) return w;
return v;
}
#endif
// WLEDMM colorBalanceFromKelvin moved into bus_manager.cpp for better optimization
//approximates a Kelvin color temperature from an RGB color.
//this does no check for the "whiteness" of the color,
//so should be used combined with a saturation check (as done by auto-white)
//values from http://www.vendian.org/mncharity/dir3/blackbody/UnstableURLs/bbr_color.html (10deg)
//equation spreadsheet at https://bit.ly/30RkHaN
//accuracy +-50K from 1900K up to 8000K
//minimum returned: 1900K, maximum returned: 10091K (range of 8192)
uint16_t approximateKelvinFromRGB(uint32_t rgb) {
//if not either red or blue is 255, color is dimmed. Scale up
uint8_t r = R(rgb), b = B(rgb);
if (r == b) return 6550; //red == blue at about 6600K (also can't go further if both R and B are 0)
if (r > b) {
//scale blue up as if red was at 255
uint16_t scale = 0xFFFF / r; //get scale factor (range 257-65535)
b = ((uint16_t)b * scale) >> 8;
//For all temps K<6600 R is bigger than B (for full bri colors R=255)
//-> Use 9 linear approximations for blackbody radiation blue values from 2000-6600K (blue is always 0 below 2000K)
if (b < 33) return 1900 + b *6;
if (b < 72) return 2100 + (b-33) *10;
if (b < 101) return 2492 + (b-72) *14;
if (b < 132) return 2900 + (b-101) *16;
if (b < 159) return 3398 + (b-132) *19;
if (b < 186) return 3906 + (b-159) *22;
if (b < 210) return 4500 + (b-186) *25;
if (b < 230) return 5100 + (b-210) *30;
return 5700 + (b-230) *34;
} else {
//scale red up as if blue was at 255
uint16_t scale = 0xFFFF / b; //get scale factor (range 257-65535)
r = ((uint16_t)r * scale) >> 8;
//For all temps K>6600 B is bigger than R (for full bri colors B=255)
//-> Use 2 linear approximations for blackbody radiation red values from 6600-10091K (blue is always 0 below 2000K)
if (r > 225) return 6600 + (254-r) *50;
uint16_t k = 8080 + (225-r) *86;
return (k > 10091) ? 10091 : k;
}
}
#if !defined(WLED_USE_CIE_BRIGHTNESS_TABLE)
//gamma 2.8 lookup table used for color correction
static byte gammaT[256] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2,
2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5,
5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10,
10, 10, 11, 11, 11, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16,
17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 24, 24, 25,
25, 26, 27, 27, 28, 29, 29, 30, 31, 32, 32, 33, 34, 35, 35, 36,
37, 38, 39, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 50,
51, 52, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 66, 67, 68,
69, 70, 72, 73, 74, 75, 77, 78, 79, 81, 82, 83, 85, 86, 87, 89,
90, 92, 93, 95, 96, 98, 99,101,102,104,105,107,109,110,112,114,
115,117,119,120,122,124,126,127,129,131,133,135,137,138,140,142,
144,146,148,150,152,154,156,158,160,162,164,167,169,171,173,175,
177,180,182,184,186,189,191,193,196,198,200,203,205,208,210,213,
215,218,220,223,225,228,231,233,236,239,241,244,247,249,252,255 };
#else
// experimental
// CIE 1931 lookup table (8bit->8bit) that was proposed during discussion of issue #2767
// https://github.com/Aircoookie/WLED/issues/2767#issuecomment-1310961308
// unfortunately NeoPixelBus has its own internal table, that kills low brightness values similar to the original WLED table.
// see https://github.com/Makuna/NeoPixelBus/blob/master/src/internal/NeoGamma.h
static const byte gammaT[256] = {
0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4,
4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6,
7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 10, 10, 10, 10, 11,
11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 15, 15, 15, 16, 16, 17,
17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 23, 24, 24,
25, 25, 26, 27, 27, 28, 28, 29, 30, 30, 31, 31, 32, 33, 33, 34,
35, 35, 36, 37, 38, 38, 39, 40, 41, 41, 42, 43, 44, 44, 45, 46,
47, 48, 49, 50, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61,
62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 74, 75, 76, 77, 78,
79, 81, 82, 83, 84, 85, 87, 88, 89, 91, 92, 93, 94, 96, 97, 99,
100, 101, 103, 104, 106, 107, 109, 110, 111, 113, 115, 116, 118, 119, 121,
122, 124, 126, 127, 129, 130, 132, 134, 135, 137, 139, 141, 142, 144, 146,
148, 150, 151, 153, 155, 157, 159, 161, 163, 165, 167, 169, 170, 172, 174,
177, 179, 181, 183, 185, 187, 189, 191, 193, 195, 198, 200, 202, 204, 207,
209, 211, 213, 216, 218, 220, 223, 225, 227, 230, 232, 235, 237, 240, 242,
245, 247, 250, 252, 255 };
#endif
// WLEDMM begin
static uint8_t gammaTinv[256] = { 0 };
static void calcInvGammaTable(float gamma)
{
float gammaInv = 1.0f / 2.4f; // surprise surprise: WLED palettes use a fixed gamma of 2.4 !!!
//float gammaInv = 1.0f / gamma; // if we go by the book, 1.0/gamma will revert gamma corrections
for (size_t i = 1; i < 256; i++) {
gammaTinv[i] = (int)(powf(((float)i - 0.5f) / 255.0f, gammaInv) * 255.0f + 0.5f); // improved by @dedehai
}
gammaTinv[0]=0;
gammaTinv[255]=255;
}
IRAM_ATTR_YN uint8_t __attribute__((hot)) unGamma8(uint8_t value) {
//if (!gammaCorrectCol || (value == 0) || (value == 255)) return value;
if ((gammaCorrectVal < 0.999f) || (gammaCorrectVal > 3.0f)) return value;
if (gammaTinv[255] == 0) calcInvGammaTable(gammaCorrectVal);
return gammaTinv[value];
}
IRAM_ATTR_YN uint32_t __attribute__((hot)) unGamma24(uint32_t c) {
if ((gammaCorrectVal < 0.999f) || (gammaCorrectVal > 3.0f)) return c;
if (gammaTinv[255] == 0) calcInvGammaTable(gammaCorrectVal);
return RGBW32(gammaTinv[R(c)], gammaTinv[G(c)], gammaTinv[B(c)], W(c));
}
// wleDMM end
uint8_t gamma8_cal(uint8_t b, float gamma)
{
if (b==0) return 0;
if (b==255) return 255;
return (int)(powf((float)b / 255.0f, gamma) * 255.0f + 0.5f);
}
// re-calculates & fills gamma table
void calcGammaTable(float gamma)
{
#if !defined(WLED_USE_CIE_BRIGHTNESS_TABLE) // WLEDMM not possible when using the CIE table
for (uint16_t i = 1; i < 256; i++) {
gammaT[i] = gamma8_cal(i, gamma);
}
gammaT[0]=0;
gammaT[255]=255;
#endif
calcInvGammaTable(gamma); // WLEDMM
}
// used for individual channel or brightness gamma correction
IRAM_ATTR_YN __attribute__((hot)) uint8_t gamma8(uint8_t b) // WLEDMM added IRAM_ATTR_YN
{
return gammaT[b];
}
// used for color gamma correction
uint32_t __attribute__((hot)) gamma32(uint32_t color)
{
if (!gammaCorrectCol) return color;
uint8_t w = W(color);
uint8_t r = R(color);
uint8_t g = G(color);
uint8_t b = B(color);
w = gammaT[w];
r = gammaT[r];
g = gammaT[g];
b = gammaT[b];
return RGBW32(r, g, b, w);
}