I converted its code to Objective-C and verified it works as described.
ArtColors should provide a simple function call that subtractively mixes two colors in a realistic way with a minimum of code, taking only two RGB inputs and a blending ratio, like this:
Return Color=SubtractiveMix(Color a, Color b, percentage)
ArtColors uses an algorithm which (I think) gives pretty good results with a fraction of the code of the other methods, and no need for calculating or storing reflectance data or computationally complex formulas. The goal is 80% realism with only 20% of the code.
The basic approach was inspired by considering how paints actually mix. Examine this close-up of paints mixing:
If you look carefully, you can see that in some areas, the two paints are completely blended, and the result is subtractive: yellow and blue are making a much darker green. Red and blue are making a very dark purple. Yet in other areas, where the blending is not so thorough, fine lines of yellow and blue exist side-by-side. These paints are reflecting yellow and blue light. At a distance, these colors are additively blended by the eye when the distict swirls are too small to be seen.
Consider further that mixing paints is a mixture in the Chemistry sense: no chemical change happens. The red and blue molecules are still there in the thorough blend, doing exactly what they were doing when separate: reflecting red and blue light. There's just a lot of subsurface physical effects going on as light bounces around in the medium. Incident light is absorbed and reflected by one molecule, and then by another, and eventually the result reflects out to the eye.
How does this help solve our problem?
Strictly subtractive approaches start with White, and then subtract the RGB values of Color A and Color B from White, and return what is left. This approach is often too dark. Why? Some of each pigment is still reflecting its distinctive color on a tiny scale. If we take an approach that is part additive, part subtractive, we get a more realistic result!
Moreover, if Color A = Color B, our function should return that same color. Mixing the same color with the same color should equal the same color! Using a strictly subtractive algorithm, the result is a darker version of the original hue (because the input color values are subtracted from White twice). The closer the two input colors, the less change should be seen in the blend.
The ArtColor code for subtractive mixing is:
Color ColorMixSub(Color a, Color b, float blend) {
Color out;
Color c,d,f;
c=ColorInv(a);
d=ColorInv(b);
f.r=max(0,255-c.r-d.r);
f.g=max(0,255-c.g-d.g);
f.b=max(0,255-c.b-d.b);
float cd=ColorDistance(a,b);
cd=4.0*blend*(1.0-blend)*cd;
out=ColorMixLin(ColorMixLin(a,b,blend),f,cd);
out.a=255;
return out;
}
Explanation of Code:
Color a
and Color b
are the input colors. blend
specifies how much of each color to blend, from 0 to 1.0, like a linear interpolation (LERP). 0.0 = All color A, 1.0 = All color B. 0.5 = 50%-50% mix of A and B.
First we find the RGB inverses of Color a and b, and assign them to new colors c and d.
c=ColorInv(a);
d=ColorInv(b);
Then we subtract both c and d from pure RGB White, clamping the result to zero, and assign the result to color f.
f.r=max(0,255-c.r-d.r);
f.g=max(0,255-c.g-d.g);
f.b=max(0,255-c.b-d.b);
So far, f is the purely subtractive result, which suffers from the problems mentioned above.
Next, we calculate the "Color Distance" between Color a and Color b, which is just the vector distance between them in RGB space, normalized between 0.0 (identical colors) and 1.0 (completely opposite, like white and black).
float cd=ColorDistance(a,b);
This value will help solve the problem that mixing two similar hues should not change the result very much. The color distance factor cd
is then tranformed by a quadratic transfer function, which regulates how much subtractive and additive mixing we do:
cd=4.0*blend*(1.0-blend)*cd;
The endpoints ensure that blend percentages near 0% or 100% look very close to the original input colors. The quadratic curve gives a good color gamut for the mix that comes next. The peak of the curve is determined by color distance. The output of this function determines the amount of additive vs. subtractive blending in our result. More distant colors will blend with a more subtractive dynamic (fully subtractive at y=1.0). Similar colors blend with a more additive dynamic (a flatter curve) that still has a subtractive factor. The maximum of the quadratic transfer function is the normalized color distance, so colors diametrically opposed in the color space will blend fully subtractively.
The last line does all the work:
out=ColorMixLin(ColorMixLin(a,b,blend),f,cd);`
First, we additively mix Color A and Color B in the specified blend
ratio, which is accomplished by ColorMixLin(a,b,blend)
. This represents the additive blending effect of those fine swirls of color in the image above and subsurface interaction. Absence of this factor may be where a strictly subtractive approach yields odd results. This additive result is then blended with our purely subtractive result color f
, according to the transfer function mentioned above, which is based on the color distance between Color a
and Color b
.
Voila! A pretty good result occurs for a wide range of input colors.