The Minute Physics video Computer Color is Broken is good at explaining the problem, but it done has some (small) issue.
He notes that RGB operates on a square root compressed version of actual intensity:
| Linear Light |----> √r, √g, √b ---->| RGB |
And so before you try to mix RGB, you need to convert it back to "linear light" by applying the inverse:
| Linear Light |<---- r², g², b² <----| RGB |
But the Internet, and nearly all electronics and consumer devices, have agreed to a standard (called sRGB), where they apply something's not quite a square root.
- What you know as a square root can be expressed as: r1/2
- What sRGB calls for is close, but subtly different: r1/2.4
So it's something that's not quiiiiite a square (and square root to undo it).
sRGB companding is also non-linear
The formula used by sRGB to convert linear light into sRGB encoded light is non-linear:
- for Rlinear > 0.0031308
- Rsrgb = 1.055×Rlinear1/2.4 - 0.055
In other words, it's very close to simply square-rooting:
With the only gotcha of the linear section when you get down really close to zero.
So typically you need a function that applies this nearly-square root sRGB companding function:
r = SrgbCompanding(Rlinear);
g = SrgbCompanding(Glinear);
b = SrgbCompanding(Blinear);
Before you start averaging or mixing colors in RGB, you need to apply the inverse:
Rlinear = SrgbInverseCompanding(r);
Glinear = SrgbInverseCompanding(g);
Blinear = SrgbInverseCompanding(b);
Once your R, G, and B are in linear light, you can average them:
R3 = (SrgbInverseCompanding(r1) + SrgbInverseCompanding(r2)) / 2;
G3 = (SrgbInverseCompanding(g1) + SrgbInverseCompanding(g2)) / 2;
B3 = (SrgbInverseCompanding(b1) + SrgbInverseCompanding(b2)) / 2;
And then you have to reapply the sRGB companding:
R3 = SrgbCompanding(R3);
G3 = SrgbCompanding(G3);
B3 = SrgbCompanding(B3);