There's no simple physical model which will do this, the painter's colors have very elaborate interactions with light. Fortunately we have computers, which are not limited to modelling the physical world - we can make them do any arbitrary thing we'd like!
The first step is to create a color wheel with the hue distribution that we require, with red, yellow, and blue at 120 degree increments. There are many examples on the web. I've created one here that only has fully saturated colors so that it can be used to generate the full RGB gamut. The colors on the wheel are completely arbitrary; I've set Orange (60°) to (255,160,0) because the midpoint between Red and Yellow was too red, and I've moved pure Blue (0,0,255) to 250° instead of 240° so that the 240° Blue would look better.
Remembering the experiments of my childhood, when you mix equal amounts of Red, Yellow, and Blue together you get an indistinct brownish gray. I've chosen a suitable color which you can see at the center of the color wheel; in the code I affectionately call it "mud".
To get every conceivable color you need more than Red, Yellow, and Blue, you also need to mix White and Black. For example you get Pink by mixing Red and White, and you get Brown by mixing Orange (Yellow+Red) with Black.
The conversion works with ratios, not absolute numbers. As with real paint there's no difference between mixing 1 part red and 1 part yellow, versus 100 parts red and 100 parts yellow.
The code is presented in Python but it shouldn't be hard to convert to other languages. The trickiest part is adding the Red, Yellow and Blue to create a hue angle. I use vector addition and convert back to an angle with atan2
. Almost everything else is done with linear interpolation (lerp).
# elementary_colors.py
from math import degrees, radians, atan2, sin, cos
red = (255, 0, 0)
orange = (255, 160, 0)
yellow = (255, 255, 0)
green = (0, 255, 0)
cyan = (0, 255, 255)
blue = (0, 0, 255)
magenta = (255, 0, 255)
white = (255, 255, 255)
black = (0, 0, 0)
mud = (94, 81, 74)
colorwheel = [(0, red), (60, orange), (120, yellow), (180, green),
(215, cyan), (250, blue), (330, magenta), (360, red)]
red_x, red_y = cos(radians(0)), sin(radians(0))
yellow_x, yellow_y = cos(radians(120)), sin(radians(120))
blue_x, blue_y = cos(radians(240)), sin(radians(240))
def lerp(left, right, left_part, total):
if total == 0:
return left
ratio = float(left_part) / total
return [l * ratio + r * (1.0 - ratio) for l,r in zip(left, right)]
def hue_to_rgb(deg):
deg = deg % 360
previous_angle, previous_color = colorwheel[0]
for angle, color in colorwheel:
if deg <= angle:
return lerp(previous_color, color, angle - deg, angle - previous_angle)
previous_angle = angle
previous_color = color
def int_rgb(rgb):
return tuple(int(c * 255.99 / 255) for c in rgb)
def rybwk_to_rgb(r, y, b, w, k):
if r == 0 and y == 0 and b == 0:
rgb = white
else:
hue = degrees(atan2(r * red_y + y * yellow_y + b * blue_y,
r * red_x + y * yellow_x + b * blue_x))
rgb = hue_to_rgb(hue)
rgb = lerp(mud, rgb, min(r, y, b), max(r, y, b))
gray = lerp(white, black, w, w+k)
rgb = lerp(rgb, gray, r+y+b, r+y+b+w+k)
return int_rgb(rgb)