tl;dr: myPaint smudge mixes color in RGB, which makes “blue + yellow = green” impossible to mix.
I modified the smudging to use HCY instead, so it blends along the Hue circle:
Disclaimer: I suck at maths and programming, so there might be errors. Feel free to correct me.
The issue
We all learn in school that the primaries are red, yellow and blue (the color circle by Johannes Itten).
And while this is grossly inaccurate in theory, it still holds some practical thruth:
Blue and Yellow pigments really blend to green (not neutral grey). As far as I know ArtRage is the only
application that tries to emulate this “real” color blending. So, when looking at the HCY picker I suddenly came up with an idea that shouldn’t be hard to implement.
The plan
My first thought was to interpolate on straight lines (Like MixerHCYDesaturate, line 18) through HCY space. The benefit are mixed colors that are dull and not so saturated (= more realistic). The downsides however are that B+Y != G but neutral grey again and that it’s quite complicated. So instead I opted for h, c, y separately.
Step number one was to pull over the hcy-to-rgb-to-hcy functions from mypaint/lib/color.py to brushlib/helpers.c.
The next big, buggy hurdle was to jump over 0 to 360 degrees (red). In the end I came up with my own formula, but there are some in the web and even in color.py. And last I had to deal with the greys and the transparent BG, which has hcy 0-0-0 by default. We don’t want to mix red in those…
Edit: See post 12 & 13 for newer code with fixed errors.
replaces line 748 to 776 in brushlib\mypaint-brush.c
// color part
float color_h = mapping_get_base_value(self->settings[MYPAINT_BRUSH_SETTING_COLOR_H]);
float color_s = mapping_get_base_value(self->settings[MYPAINT_BRUSH_SETTING_COLOR_S]);
float color_v = mapping_get_base_value(self->settings[MYPAINT_BRUSH_SETTING_COLOR_V]);
float eraser_target_alpha = 1.0;
if (self->settings_value[MYPAINT_BRUSH_SETTING_SMUDGE] > 0.0) {
float a_Src = self->states[MYPAINT_BRUSH_STATE_SMUDGE_A ]; // alpha of canvas
// mix (in HCY) the smudge color with the brush color
float fac = self->settings_value[MYPAINT_BRUSH_SETTING_SMUDGE];
if (fac > 1.0) fac = 1.0;
// If the smudge color somewhat transparent, then the resulting
// dab will do erasing towards that transparency level.
// see also ../doc/smudge_math.png
eraser_target_alpha = (1-fac)*1.0 + fac*a_Src;
// fix rounding errors (they really seem to happen in the previous line)
eraser_target_alpha = CLAMP(eraser_target_alpha, 0.0, 1.0);
if (eraser_target_alpha > 0.0 && a_Src > 0.0) {
// get and convert canvas
float r_Src = self->states[MYPAINT_BRUSH_STATE_SMUDGE_RA] / a_Src;
float g_Src = self->states[MYPAINT_BRUSH_STATE_SMUDGE_GA] / a_Src;
float b_Src = self->states[MYPAINT_BRUSH_STATE_SMUDGE_BA] / a_Src;
rgb_to_hcy_float (&r_Src, &g_Src, &b_Src);
// convert brush
hsv_to_rgb_float (&color_h, &color_s, &color_v);
rgb_to_hcy_float (&color_h, &color_s, &color_v);
float h_Brush = color_h; // brush
float h_Src; //canvas
float q_fac = fac; // 0.0 = 100% brush, 1.0 = 100% canvas
float dist; // delta between hues
float h_Tmp;
if (g_Src <= 0.0 && color_s <= 0.0) { // both greyscale
color_v = (fac*b_Src + (1-fac)*color_v); // luminance only
} else { // one or none greyscale
h_Src = r_Src;
// one grey, replace default 0.0 red hue
if (g_Src <= 0.0) {
h_Src = h_Brush;
} else if (color_s <= 0.0) {
h_Brush = h_Src;
}
dist = h_Src - h_Brush;
// CW/CCW turn at over 180deg
if (fabs(dist) > 0.5) {
if (h_Brush < h_Src) {
h_Brush += 1.0;
} else {
h_Src += 1.0;
}
dist = h_Src - h_Brush;
}
h_Tmp = h_Brush + (q_fac * dist); // interpolate hue
// Wrap around 360deg
if (h_Tmp >= 1.0){
h_Tmp -= 1.0;
} else if (h_Tmp < 0.0) {
h_Tmp = 1.0 - h_Tmp;
}
color_h = h_Tmp; // Hue
color_s = (fac*g_Src + (1-fac)*color_s); // Chroma;
color_v = (fac*b_Src + (1-fac)*color_v); // Luminance;
}
hcy_to_rgb_float (&color_h, &color_s, &color_v);
rgb_to_hsv_float (&color_h, &color_s, &color_v);
} // else: We erase with the original hsv brush colors
}`
Issues & Improvements
The performance is probably not as good as rgb, because I do some more conversions.
It bothers me a bit that the saturation in the mix is so high. Real pigments dull a bit. However, I can’t really change this. As far as I understand the smudge does not know if it is “halfway there”. Every dab is a new “start”.
The biggest problem however is the 180 degree turn CW/CCW at 0.5 distance (shortest angular dist). You just need to be 1 degree off from blue to violet and you wont get green when mixing with yellow. If you look carefully at the image above you even notice how cyan-red-cyan and green-magenta-green switch direction around the circle…
I already have an idea how to prevent this a bit manually. This will also fix some of the “odd” mixes like red+cyan to green - I’d say this should go through magenta…
Also: In my local code I just replace the rgb smudge. Implementing it in mypaint for real needs a bit more consideration. e.g. ArtRage enables/disables it’s real color blending in the options, however it’s off by default and especially beginners may never see it.
It could be added as a second smudge brush setting, this ensures that existing brushes behave the same as before (may or may not be a good thing, though). Downside: Overkill.
Another thought just hit me: maybe hcy is not even necessary and hsv works as well o_O I choose hcy in the beginning because of beautiful straight triangulation lines through hcy space…
mixing from “primaries”, pure red, blue, yellow plus black&white…