# What colour space or mixing algorithm should I use for “watercolor-like” colour mixing in-game?

I want to make a game where the player creates potions out of coloured ingredients, and I think the RGB or HSV colour spaces would be confusing for players expecting (red + yellow = orange, red + blue = purple).

RYB (red-yellow-blue) seems the closest to what I want:

However, I don't want to hard-code the colours, as some ingredients are randomly generated or have impure colours. Also, I'd like to represent the "locations" of potions of objects on the colour space on a 2D "map". How would I implement this in code? E.g. mix two RED items with one BLUE item and one GREEN item.

To answer your question succinctly, RYB (red, yellow, blue) would be most intuitive and closest to mixing physical colors, aka pigments, which blend subtractively.

To understand the mixing behavior of RYB color-space, see both the color wheel diagram in the question, as well as the color tree below. For your use case it seems to me it would be simplest to implement hard coded blending, roughly following these RYB principles.

However, since you mentioned you are opposed to implementing hard coded blending, if you want this mixing behavior to be dynamic you can use CMYK color space, which is a subtractive color model used primarily for printing. Blending two CMYK colors will behave analogously to pigmentary blending

With this approach however you may be restricted by the CMYK color space, which is rather compressed and can be difficult to convert to and from while preserving color.

For a visualization of the problem of converting between color spaces you could check out this video overview of color spaces, as well as this color space comparison diagram.

For those who care to go a bit deeper, read on.

As the question hints at, there are a variety of color spaces and blending types. To design the gameplay mechanic you have described I believe you are looking for pigment color / subtractive color blending.

The wiki page on pigments has far more details about the physics and theory behind the perception and behavior of these but the birds-eye overview of pigment color can be summarized in a few points:

First, when visible light mixes, it becomes white. This is called additive blending. When pigment mixes, it becomes black-ish. This is called subtractive blending.

Secondly, by defining primary colors (ideally distributed relatively evenly along the emission spectrum, commonly RBY, RGB, CMY etc) these colors can be mixed to create a color space. (You can mix them additively or subtractively but in this case we are looking at subtractive blending) Mixing primaries you can make secondary, tertiary, quaternary, etc... Folks rarely bother beyond quaternary colors because they all devolve into browns and various earth-tones the deeper you go, asymptotically approaching black. This will happen regardless of the primary colors, but choosing more evenly distributed primary colors will reduce the severity of this effect.

Color science and color theory are massive fields and you can go far deeper into the specifics but I think for feeling out the basic mechanics of the game all you need is that primary color based mixing behavior, which is demonstrated both in the diagram you included in your question, as well as in the chart below.

I would say hard coding this blending would be the simplest solution to give you flexibility for making a variety of potion colors, regardless of their distribution along the color space, while avoiding degenerating into muddy tones, but if you really want dynamic blending you can get it through CMYK color representation.

CMYK is primarily used for printers, and is subtractive, as opposed to RGB and most other color repesentations typically used in digital displays, which are additive. The wiki page for CMYK goes into more detail but basically if you add 2 CMYK colors they will give you the pigmentary blending behavior you are looking for.

Note though, if you are currently coloring your potions with RGB, Hex, etc you will need to convert to CMYK, do the blend, then convert back to whatever your normal color representation is to apply it to the new object, but this can be trickier than it sounds.

Blending CMYK colors can be difficult due to CMYK's limited color space, so your colors may get squished when converting back and forth, eg:

RGB(A) != RGB(B) but CMYK(A) == CMYK(B)

You can experiment with the compression of converting to and from CMYK and see the specific formula for implementing the conversion in this RGB->CMYK converter.

To understand the weird behavior here it helps to see a visualization of the color space, check out this video.

You can also look at the difference between CMYK and other color spaces representable colors this color space comparison, as well as this video overlaying several color spaces in 3D, demonstrating the gamut issues with CMYK conversion.

That should be more than enough theory, so for a bit of practical exploration, check out some of the other answers with code samples, or try comparing various colors with the Google color picker.

As pointed out in other answers, the colour wheel most of us are familiar with doesn't match the way that pure light mixes - which is what's easiest to model with our usual representations of colours in a computer program.

With pure light wavelengths, yellow and blue are complimentary in our vision system, and blending them yields black (if subtractively mixing CMYK inks for example) or a neutral grey (if additively mixing emission from computer screen phosphors, or even a rapidly-spinning disc as shown in this example from the Dimensions of Color):

But it's still true that many substances that our players are familiar with in everyday life obey different colour mixing rules - due to more complicated physical & chemical interactions in their constituents. For (some) paint, yellow and blue does indeed make green, as shown in this example from CharM:

Modelling these fine-scale interactions is likely waaaay more complicated than we want to get for most games, but we can hack it a bit to give our players a more familiar experience.

One strategy is to convert the colour to a familiar HSV space, then apply some biases over that space to warp it the way we want. For example, we can shift the hues around so that yellow sits equidistant between red & blue, taking the place of green:

On the left you can see the standard HSV colour wheel, with red, green, and blue equidistant, which places blue and yellow as complementaries, opposite one another on the colour wheel. Taking an average between them gives us a neutral grey, as it does when mixing coloured light.

On the right, we've smushed the hues around, so that blue is opposite orange instead (its conventional complementary in painting colour theory), and yellow takes the place of green. Now when we take the average of yellow and blue, we land not in the grey middle of the disc, but in a decidedly green hue.

Here's how this colour transformation might look in code (using Unity C# as an example):

Vector3 ToPigmentSpace(Color color) {
// Convert RGB color to standard Hue Saturation Value (range 0-1 each).
Color.RGBToHSV(color, out float h, out float s, out float v);

// Change hue to degrees for ease of intuition.
h *= 360f;

// Compute a shift that will stretch the orange range
// until yellow reaches the 120 degree mark,
// and compress the cyan/teal range so blue stays at 240 degrees.
float hueShift = Mathf.Clamp01(h > 90f ? 2.0f - h / 120f : h / 60f);

float shifted = (h + 60f * hueShift) * Mathf.Deg2Rad;

// Model the colour as a point in a spindle, with the bright hues
// around the outer rim, and the greyscale running up the z-axis.
Vector3 pigmentSpace = new Vector3(
s * Mathf.Cos(shifted),
s * Mathf.Sin(shifted),
v - 0.5f * s);

return pigmentSpace;
}

// Convert back from our pseudo-pigment spindle space to an RGB color.
Color FromPigmentSpace(Vector3 pigmentSpace) {
// The hue & saturation are expressed in the XY plane.
var chromaticity = (Vector2)pigmentSpace;

// Saturation is our radius from the grey axis.
float s = chromaticity.magnitude;
// Value is our height along the spindle, treating saturated colours as max.
float v = pigmentSpace.z + 0.5f * s;

// Decode our shifted hue angle into degrees.
float shifted = Mathf.Atan2(chromaticity.y, chromaticity.x) * Mathf.Rad2Deg;
if (shifted < 0f)
shifted += 360f;

// Undo the hue shift to bring us back to standard HSV space.
float hueShift = Mathf.Clamp01(shifted > 150f ? 4.0f - shifted / 60f : shifted / 120f);
float h = shifted - 60f * hueShift;

// Convert from HSV back to RGB.
return Color.HSVToRGB(h/360f, s, v);
}


To mix these, you can do something like this:

Vector3 pigmentSpace = Vector3.zero;

foreach(var ingredient in ingredients) {
float weight = ingredient.weight / totalWeight;
pigmentSpace += ToPigmentSpace(ingredient.rgbColor) * weight;
}

Color result = FromPigmentSpace(pigmentSpace);


Also, I'd like to represent the "locations" of potions of objects on the colour space on a 2D "map".

You can use the 3D "pigment space" defined above as your map coordinate system. Just discard the z axis (roughly corresponding to value), and what remains is a 2D space describing a colour disc, 1 unit in radius, with the pure hues along the outside and neutral grey in the middle.

• Because the hue coordinate is an angle, we don't have a well-defined component-wise average between two colours 180° apart. The same argument used to say it should be green can just as easily be used to argue it should be pink: both options are 90° or halfway between the two input hues. So this adds discontinuities we don't see in real colour mixing. The conversion to an intermediate Cartesian rather than polar colour system also better reflects what happens in real-world colour mixing, where combining two different saturated primaries yields a mixture less saturated than the inputs. – DMGregory Jan 6 '20 at 22:42

The red-yellow-blue color circle invented by Le Blon in the 1720s is incorrect. You can't create perfect green by mixing equal parts blue and yellow. Creating a perfect magenta with red and blue is even harder - you usually end up with a muddy purple. And if you mix all three, you don't get grey, you get brown.

People already noticed that in the 18th century when it was introduced. They attributed that to the limitations of the pigments they had back then. But nowadays we know that it wasn't the pigments which were wrong, it was the theory. Unfortunately it refuses to disappear from the literature and is even still taught in some art schools.

The physically correct color model for subtractive color mixing is actually Cyan - Magenta - Yellow:

But how do we represent this color model in software when our monitors work by additive color mixing of Red, Green and Blue?

When you want to mix two CMY colors, you can actually do the same as with RGB. You just calculate the sums of all three color channels:

c = color1.c + color2.c
m = color1.m + color2.m
y = color1.y + color2.y


Contrary to RGB, these channels do not represent light, they represent darkness. The default state in the CMY color model is white, and adding color makes the image darker. Add enough color to reach maximum saturation on all 3 channels, and you get black.

You can then convert the final CMY colors to an RGB color using the following algorithm:

r = 255 - c
g = 255 - m
b = 255 - y


By the way, some graphic APIs support subtractive color mixing out-of-the box. When they do, you can usually input your source colors in RGB.

• Can you provide some source or explanation for the statement "You can't create green by mixing blue and yellow" in your opening paragraph? You mention that this RYB color space "refuses to disappear from literature", yet the wiki page for RYB has no mention of being debunked or updated. Additionally this real life video of mixing blue and yellow dyed water clearly shows the resulting mix is green. I think there may still be some confusion about pigments vs visible light behavior here. – disc_code22 Nov 21 '19 at 17:13
• @disc_code22 The video actually shows perfectly how the RYB color model is off. The performer mixes 2 parts yellow and 1 part blue to get green. If the color model were correct, they would need 1 part each. Also note how the result is a very dark green. That's because there is the magenta part from the blue which doesn't belong in there. – Philipp Nov 21 '19 at 17:17
• While that explanation is correct in the perfect, theorized situation, the real life behavior of actual pigments is rarely so. Mixing equal proportions would result in a muddy greenish tone because neither the yellow nor blue in the video is a totally pure pigment, one seems to contain some amount of red. Red is the compliment of green, and mixing any complimentary colors will always result in a brown (eg yellow + purple = brown, green+red = brown, orange+blue = brown). Unless you have very pure pigments mixing in perfect proportions will always result in browning. (Continued in next comment) – disc_code22 Nov 21 '19 at 17:33
• If you watch an artist mixing paints they will usually mix in a small dollop of one paint into a large glob of another and then slowly mix them to avoid descending into browns. In the video they stop short so the color is brighter, but the result is still clear that mixing the two creates a green, as predicted by RYB, which is still the standard used for artists and other people working with pigments as far as I can tell. – disc_code22 Nov 21 '19 at 17:33
• @disc_code22 Here is a video I found on YouTube. – Philipp Nov 21 '19 at 19:00

In order to have both intuitive and minimal effort coding this, I would go for blending in a linear RGB color space, by means of multiplication. I'd expect this to behave very intuitively.

I would suggest trying this approach (pseudo code):

vec3 mix_potions(vec3 i1, vec3 i2, float amount1, float amount2) {
i1 = pow(i1, 2.2); // raise each R,G,B component to power 2.2
i2 = pow(i2, 2.2); // to linearize the values.
float mix_amount = min(amount1, amount2); // how much will mix?
amount1 -= mix_amount;
amount2 -= mix_amount;
vec3 mix = mix_amount * i1 * i2 + amount1 * i1 + amount2 * i2;
mix /= max(max(mix.r, mix.g), mix.b); // renormalize one component to 1
mix = pow(i2, 1.0 / 2.2); // apply gamma companding again
}


The renomelization is will make your mixed colors bright, which is of course optional.

Alternatively try additive blending for experimentation:

vec3 mix = i1 * amount1 + i2 * amount2; // additive mixing (non-watercolor like)