Brendan Keesing   

FOMOGRAPHY     Projects     Blog     About


Blending Colors

November 27, 2020

How do you blend between two colors? Chances are, you’re just doing a standard lerp (linear interpolation) between RGB values, right? If we were to blend between red and green, we should see something like this:

RGB

Result = (ColorB - ColorA) * t + ColorA

We love this method because computers love this method, and when computers love something, they do it fast. But, have you ever noticed the weird dull splotch in the center? It’s kind of a weird dark desaturated yellow.

RGB Center

We really lose the vibrancy of the red and green. But shouldn’t halfway between two vibrant colors be another vibrant color? So what happened here?

RGB

The problem lies in the RGB color space. Let’s visualise what RGB looks like by plotting the red, green and blue values on different axes.

RGB Color Space

RGB Color Space

A cube is a great way to represent a color space because we can easily see what the stored values (RGB) are just by looking at the axes (XYZ).

So what is happening when we do a lerp? We are just drawing a straight line from one point on the cube to another.

Linear Interpolation between red and green in RGB

Notice how it pierces through the cube to create a straight line. That yucky murky yellow color must be somewhere between those two points. But if you just look at the cube, you can kind of see where you would rather it to go; we want a nice vibrant yellow! This means that we want to bend the line so that it goes along the surface of the cube instead.

How do we achieve this?

Okay, so what crazy geometry math hack are we going to need to pull this off? Well, I’d rather stay away from that sort of crazy math. Instead, let’s find an easier way to think about the problem. What if, instead of changing the line to suit the cube, we change the cube to suit the line? In other words, we need to change the color space?

HSV

HSV

HSV Color Space

Introducing HSV (Hue Saturation Value)! It’s a lot more intuitive, isn’t it? The vertical axis increases the lightness, outward for vibrancy, and rotation to pick a hue. Easy! That’s why artists love it.

But there’s something odd with this. If you put hue, saturation and value on different axes, you get this instead.

HSV Cube

HSV Color Space?

Kinda ugly isn’t it? Yet we get a good glimpse of what our 3 values are actually storing. So what happens now if we do our lerp between our red and green?

It goes through the vibrant yellow which looks great. Problem solved!

But hang on, what about if we try some other colors. What about magenta to yellow?

Ouch, that’s not right. Kinda cool, but really not right. Maybe a better way to think about this is to just look at how artists solve this. How does a painter, with full creative liberty over their palette, blend between two colors? If we were to lay it out on the HSV cylinder, you can see how simple it really is.

Artist Blending Colors, simplified

See how it’s just a straight line across the cylinder? It’s so simple! Yet we haven’t been able to achieve this with these damn cubes. What if we ditched the cubes and just got the XYZ coordinates on the HSV cylinder. Then it would just be a normal lerp between the coordinates. Another way of thinking about it is putting our color space cube around the cylinder.

HSVxyz

HSV Cylinder in XYZ Coordinates (HSVxyz)

Now you might be asking, “What about the empty space outside the cylinder (yet still within the cube)? What if you have values in that empty space? What color will it be?” A color in this empty space is invalid. If you want, you can clamp it to the cylinder, but it ultimately serves no purpose (which means we’re technically wasting data).

Now, let’s do the magic. The steps to pull this off is mostly just a series of conversions between color space.

RGB -> HSV -> XYZ -> Interpolate -> HSV -> RGB

The code to do this can be a bit intimidating. I won’t go into detail how it all works, but I’ll have it here for completeness.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
float3 rgb_to_hsvxyz(float3 rgb)
{
	float3 hsv = rgb_to_hsv(rgb);
	float r = rotate_around_origin(float2(hsv.y, 0), hsv.x * pi * 2);
	return float3(r.x, hsv.z, r.y);
}

float3 hsvxyz_to_rgb(float3 xyz)
{
	float hue = arctan2(xyz.z, xyz.x) / (pi * -2);
	float sat = length(float2(xyz.x, xyz.z)) * 2;
	return hsv_to_rgb(float3(hue, sat, xyz.y));
}

So now you can use it like this:

1
2
3
4
5
6
7
float3 hsvxyz_lerp(float3 colorA, float3 colorB)
{
	xyzA = rgb_to_hsvxyz(colorA);
	xyzB= rgb_to_hsvxyz(colorB);
	xyz = lerp(xyzA, xyzB, t);
	return hsvxyz_to_rgb(xyz);
}

Obviously this is going to be a lot more computationally expensive than the traditional lerp but screw computers, we want nice looking mid-tones! So, brace yourself. Now we get to see the result of all this hard work!

RGB² (XYZ)

Okay, there’s one more strategy for lerping color that I should really mention. We’ve mostly been talking about what looks good artistically, but we should also talk about what is physically correct. You see, the way computer colors work is kind of fudged. Light works on a logarithmic scale when compared to what we see from our human eyes. For example, if you wanted to double the brightness of something, you would actually need four times the amount of light. It’s important to note that this is a simplification of the XYZ color space (which is kind of the holy grail of color spaces).

Alright, so how can we harness this to improve our lerping? We just need to convert our RGB color into RGB².

RGB²

RGB²

If you compare this with the original RGB cube, you’ll see that the colors are more vibrant in the RGB² cube. So, what if we do our lerp here?

RGB² Gradient

RGB²

It looks a lot closer to RGB than HSVxyz, but it’s significantly brighter in the center. But the greatest part of RGB² is how computationally simpler it is.

1
2
3
4
float3 lerp_sqr(float3 colorA, float3 colorB, float t)
{
	return sqrt(lerp(colorA, colorB, t));
}

Comparison

Finally, here’s a comparison with the different types of blending from red to green:

RGB RGB
HSV HSV
HSVxyz HSVxyz
RGB² RGB²

But what about that horrible magenta to yellow?

RGB RGB
HSV HSV
HSVxyz HSVxyz
RGB² RGB²

Conclusion

So what is the best way to blend your colors? It really depends on what you want, but I would summarize with:

RGB Super fast, can have dull mid-tones
HSV Slow and has all sorts of weird rainbow effects
HSVxyz Super slow by gives vibrant mid-tones
RGB² Physically accurate, still quite fast

Further Reading



Twitter YouTube GitHub Email RSS