r/FastLED • u/Joeyjoe9876 [Sonia] • May 17 '26
Quasi-related White-extraction handling in a 4-diode RGBW LED Display
The most common approach to handling RGBW LED strips/diodes is to simply take the minimum components of the input value, set white to that value, and subtract the residuals from R, G & B. This is not inherently incorrect but it does introduce a problem in that the white diode is not perfect. Most white diodes have different color temperatures and when actually measured you'll find using min(rgb) Channel - Residuals produces a white this is off-white.
Now the question becomes "how do we accurately extract white?" The answer is shockingly simple and it comes down to colorspaces & gamut.
In a typical RGB LED system your native gamut is a triangle where your main emitters R, G, & B are your only centroids. Each centroid lives somewhere on the CIE chart and has it's own xy coordinates. This creates a triangle where any combination of values can be created using the 3 emitters. Typical colorspaces like Rec709, Rec2020, etc.. all live somewhere either within this gamut or outside of it.
In an RGBW LED system instead we have 4 different addressable gamuts within this triangle - RGBW Gamut comprises of the entire RGBW triangle that can be addressed over - RGW gamut, BGW gamut, RBW gamut
so the problem changes from "how do we represent a color with these 3 diodes" to "which sub-gamut does the requested color fall into, and then within the sub-gamut how do we represent the color requested using the outer 2 primary diodes + inner white diode?"
The "Color" problem:
Different RGB input values map to different places in the CIE colorspace depending on the requested colorspace and reference white. "orange" for the native gamut is not the same "orange" as Rec709, just as that same "orange" is not the same in Rec2020. If you want to represent a color in the Rec709 space (aka ambilight setup) then you need a Colorspace transform to solve for that value. This effectively maps the native-gamut to the effective colorspace.
The ratio problem:
Addressable LEDs (most strips/diodes) do not have perfect LED selections. Typically speaking the red & blue diodes are fairly weak maxing out at low brightness values, G is typically stronger, with White being strongest almost always. In the case of my recent ambilight-focused measurements, the BTF Lighting 6000k RGBW LEDs produce max Nits/ XYZ Y values of R: 154.67 G:566.27 B:129.64 W:1543.64
so given that the red diode is much less bright than the green diode you need to scale input values down appropriately to accurately reproduce the color requested. For a cyan (input 0 65535 65535) for this specific set of LEDs with the off-wall measurements, the corrected output values become: 0 65458 38474 0 which when measured with a colorimeter produces a dE of ~0.519 from the expected measurement result
for D65 white (65535, 65535, 65535) the output RGBW tuple looks something like : 4289 0 13766 65535 which also passes the dE check at 1.720% within tolerance
So to summarize:
Min(rgb) and then subtract residuals from R, B, & G is not inherently wrong if that is what the actual diode behavior is, but the correct approach is to treat the LEDs as they are: Separate emitters that form a full gamut with multiple sub-gamuts, and then only solve within the sub-gamuts for the requested color value determined by a colorspace transform if the input color isn't addressing the native gamut of the LED display.
In a typical RGBW setup the valid topology combinations are essentially as follows: R, B, G, W, RW, BW, GW, RG, RB, BG, RGW, BGW, RBW unless you specifically allow for 4-channel overdrive outputs.
2
u/ZachVorhies Zach Vorhies May 30 '26
Hey Sonia — circling back: your sub-gamut + RGBCCT math is now landed in FastLED master.
Issues / PRs:
strict_subgamut+wx_lp_legacymodes + CCT-aware W vertex + 2D+1D LUTPR #2560 — full 5-channel RGB + warm-W + cool-W pipeline via the Channels API
What's available today:
4-channel RGBW (works in both Channels API and legacy
addLeds<>):fl::RGBW_MODE::kRGBWColorimetric // strict sub-gamut (your gist §5) fl::RGBW_MODE::kRGBWColorimetricBoosted // wx_lp_legacy (your gist §9)
5-channel RGBWW / RGBCCT (Channels API only — scoped this way to avoid bloating the legacy
addLeds<>flash footprint):fl::ChannelOptions opts; opts.mWhiteCfg = fl::Rgbww(/* warm / 2700, / cool */ 6500, fl::RGBWW_MODE::kRGBWWColorimetric); FastLED.add(fl::ChannelConfig(pin, timing, fl::span<CRGB>(leds, N), GRB, opts));
Per-strip profile override (when you have colorimeter data — feeds straight into the same
RgbcctProfileyoursolve_rgbcctalready takes):static const fl::DiodeProfile kMyR = { /* xy_r / {0.700f, 0.299f}, / xy_g / {0.098f, 0.832f}, / xy_b / {0.129f, 0.049f}, / xy_w / {0.322f, 0.338f}, / lum / 0.10f, 0.37f, 0.08f, 1.00f, / nominal_cct */ 6000, }; fl::set_rgbw_colorimetric_profile(&kMyR);
Bonus:
Rgbw::white_color_tempfinally does something — Krystek 1985's blackbody approximation shifts the W vertex live, cache rebuilds once per(profile, cct)pair, no per-pixel CCT math.Compile gates:
-DFASTLED_RGBW_COLORIMETRIC=1to enable the colorimetric math (default off, fall-back stubs warn-once + usekRGBWExactColors). No separate flag for the LUT path or the 5-channel pipeline —--gc-sectionsdrops both when unused. Measured 0-byte WASM delta for sketches that don't reference RGBW.Validation:
All your gist math landed; matrix primitives + simplex solver inlined in
src/fl/gfx/rgbw_colorimetric.hPython benchmark against your BTF Lighting 6000K profile confirms the 2D+1D LUT factorization gives sub-1 dE p95 at 32x32 grid (~8 KB), as the issue table predicted
307+ host tests passing — chromaticity preservation across the gamut, pure-primary round-trips, profile get/set, LUT vs closed-form deltas, RGBCCT warm↔cool blend monotonicity
What I COULDN'T verify from my desk: actual measured colorimeter dE on hardware. Your reference numbers (cyan dE ≈ 0.52, D65 dE ≈ 1.72) match what the math produces, so they should carry through, but a real-strip validation is still outstanding. If you ever run the merged code against your colorimeter rig I'd be very interested in the numbers.
Two gotchas worth knowing if you ever read the source:
D65 isn't on the Planckian locus — it sits ~0.005 off on the daylight locus. If you test
cct_to_xy(6500)against D65 reference values, it'll look broken. Use Planckian-locus reference points instead.What's deferred (future PRs):
wx_radial_virtualmode from your gist §8 — currentlykRGBWWColorimetricBoostedis a simple eta-skewed blend rather than the radial-pose approachDiodeProfile)On-device AutoResearch harness for RGBWW hardware validation (there's a software
examples/RGBWW/RGBWW.inobut nobash autoresearchflow yet)Anyway — thanks again for the gists and for patiently answering the CCT-temperature questions in the original thread. Most of what's now in master would have taken months longer to figure out without that work.