in Maths, Tutorial, Unity

The Secrets of Colour Interpolation

This post discusses about the tricky problem of colour interpolation, and explores possible solutions. Many software and engines offer read-to-use functions to interpolate colours. In Unity, for instance, Color.Lerp is available and does its job pretty nicely. Use the interactive swatch below to see how Color.Lerp works.

There’s nothing wrong in using these functions, as long as you know what the deal with colour interpolation is.

Understanding interpolation

Interpolation is a technique that allows you to “fill a gap” between two numbers. Most APIs expose linear interpolation based on three parameters: the starting point a, the ending point b and a value t between 0 and 1 which moves along the segment that connected them:

    \[c = a + \left(b-a\right)*t\]

When t=0, a is returned. When t=1, a+\left(b-a\right)=b is returned instead. The beauty of this formula is that is easy to understand, efficient to implement, and it works in any dimension. Lerping in two dimension only requires to independently lerp the X and Y components. Lerping always returns points on the line that connects a and b, regardless of the number of dimensions. A standard RGB lerp can be done as such:

public static Color LerpRGB (Color a, Color b, float t)
{
	return new Color
	(
		a.r + (b.r - a.r) * t,
		a.g + (b.g - a.g) * t,
		a.b + (b.b - a.b) * t,
		a.a + (b.a - a.a) * t
	);
}

x

If it’s true that linear interpolation works as expected in three dimensions, the same cannot say for colours. There’s a fundamental difference between the XYZ and RGB spaces: the way the human eye perceive colours. While it make sense to connect two points in a 3D space with a line, the same doesn’t always apply for points in the RGB space. Interpolating the R, G and B components independently offers no guarantee on the hue of the intermediate colours. As Stuart Denman highlights in his Improve Color Blending, the RGB space of cyan and red meet halway in grey. A new hue appears because the RGB space does not capture how Humans perceive colours very well.

Hue Interpolation

A first attempt to compensate for this is to switch to different colour space, such as HSV (also known as HSB). It has been designed to be “artist-friendly”, grouping colours by hue and ignoring how they are created on screen.

x

The result, as it can be seen above, is rather disappointing. The reason is that interpolating the H component cycles through different hues. In this case we don’t have to go through green, but looping over the H space in the opposite direction.

HSVV255To implement an HSV lerping function we need to understand how these components are handled. For this example, we’ll assume all the HSV components range from 0 to 1. The following code is inspired from Improved Color Blending and relies on the ColorHSV Unity extension by C.J. Kimberlin:

public static Color LerpHSV (ColorHSV a, ColorHSV b, float t)
{
	// Hue interpolation
	float h;
	float d = b.h - a.h;
	if (a.h > b.h)
	{
		// Swap (a.h, b.h)
		var h3 = b.h2;
		b.h = a.h;
		a.h = h3;

		d = -d;
		t = 1 - t;
	}

	if (d > 0.5) // 180deg
	{
		a.h = a.h + 1; // 360deg
		h = ( a.h + t * (b.h - a.h) ) % 1; // 360deg
	}
	if (d <= 0.5) // 180deg
	{
		h = a.h + t * d
	}

	// Interpolates the rest
	return new ColorHSV
	(
		h,			// H
		a.s + t * (b.s-a.s),	// S
		a.v + t * (b.v-a.v),	// V
		a.a + t * (b.a-a.a)	// A
	);
}

x

For comparison, the linear lerping through HSV space is also shown together with the corrected lerping (HSV*).

Luminosity Interpolation

Despite all the effort, the transition still doesn’t look good. The reason is that even if we have correctly learped through the Hue component, different colours have different luminosities. As explained by Gregor Aisch in How To Avoid Equidistant HSV Colors, equidistant colours in the HSV space are not perceived as really equidistant. Even HSV colours with the same brightness (V) can differ in their perceived brightness and luminosity. Many aspects are responsible for this. The R, G and B components of a colour contributes in different ways the perceived luminosity, due to the way their respective photoreceptors work. Several attempts have been made to capture the non-linear relationships between R, G and B in a colour model. One of the most successful is the LCH (also known as HCL for Hue, Chroma and Lightness). Equidistant colours in the LCH space are also perceived as equidistant. The swatches below clearly shows how the LCH space provides a more uniform distribution of the colours.

x

The conversion from RGB to LCH is very expensive. This is because colours have to be converted into to intermediate spaces, the XYZ and LAB. A very good library which supports all of these conversions is chroma.js.

Using colours with equidistant perceived luminosity is essential for all these applications in which colours have a precise meaning, such as diagrams and heatmaps. Providing uniform luminosity is also important for colour blind people, as discussed in Accessibility Design: Color Blindness. A starting point to design a safe colour palette is ColorBrewer.

Conclusion

Interpolating colours by lerping their RGB components is the most common and lazy easy approach to tackle a very complex problem. If the interpolated colours need to be visible at the same time (for instance in a chart or a diagram) chances are you might need a more advance technique. Conversion from RGB to HSV are supported by most frameoworks, but if you want to go the extra mile you should adopt the LCH colour space.

x

This post was strongly inspired by the many works of Gregor Aisch.

https://vis4.net/blog/posts/avoid-equidistant-hsv-colors/

Related posts

External resources

💖 Support this blog

This websites exists thanks to the contribution of patrons on Patreon. If you think these posts have either helped or inspired you, please consider supporting this blog.

Twitter_logo

YouTube_logo
📧 Stay updated

You will be notified when a new tutorial is relesed!

📝 Licensing

You are free to use, adapt and build upon this tutorial for your own projects (even commercially) as long as you credit me.

You are not allowed to redistribute the content of this tutorial on other platforms. Especially the parts that are only available on Patreon.

If the knowledge you have gained had a significant impact on your project, a mention in the credit would be very appreciated. ❤️🧔🏻

Write a Comment

Comment

14 Comments

  1. Is there something for Unity (or better some kind of algorithm) to correctly lerp colours in the LCH space? Ideas? I’d like to implement colour transition into an app I’m making and that would be really precious!

  2. Something is wrong with the HSV* function. On the conclusion page the interpolation using that algorithm looks rubbish, even compared to RGB, unless the input colour has a value of 100%.

      • Hey Alan,

        I think I’ve noticed the issue – instead of just swapping the hue on swap, you can swap the whole color (swap variable a with variable b).

        Unless I’m mistaken, I don’t think it makes sense to combine the H of one color with the SV of another – you’ll get colors that aren’t between the two origins at all (but rather on a line diagonally across) and the only reason it works for 100% value colors is it’s a special case where the S,V of the ends are the same.

  3. I have implemented your HSV interpolation algorithm, but one of my users spotted a problem with it.
    Consider interpolation from full blue RGB (0, 0, 255 ) to full white RGB (255, 255, 255) – in HSV this is creating a problem that pink color is inserted in between. That’s something very unwanted.

    And even worse depending on slight change of white channel e.g. (255, 255, 254) white it goes through different colours.

    On your interactive test, it doesn’t even provide interpolation from blue to white – it’s black. I’m not quite sure how to fix this behaviour as it should go straight from white to blue.

    • Hi Pavel!
      Thank you for the message!
      I have spotted a typo in the HSV function, although that is not the cause of the issue you’re experiencing.

      Which software are you using? If you have a native way to convert from RGB to HSV, can you lerp between these 3 numbers?

      Perhaps there is an issue with the HSV conversion code.
      I’ll look into it!

      • I have finally answer for my concerns:
        The HSV interpolation code is correct (once you fix your bugs in it).
        The thing is as it’s cone, the interpolation is basically on disc. So depending which hue you assign to white colour, it will go from e.g. blue to white through different colours.

        If this is unwanted:
        The trick is to use HSL color scheme and interpolate on sphere (Hue from 0 to 360 degrees, Lightness from 0 to 180 degrees, saturation as sphere radius). Use spherical linear interpolation.

        This will solve the problem with white going to blue through different colours. It will still retain property of hue interpolation (e.g. from red to green through yellow).

        However for my purposes after some more thoughts even the property to go from red to green through yellow is unwanted. So I’ll revert back to standard RGB interpolation in my case.

  4. Great post and explanation, thanks for sharing!

    I have a question about interpolating between hues:
    This tool is great for interpolating between colours in the LCH colour space (http://tristen.ca/hcl-picker/#/hlc/13/1.1/FE876B/FF8570). When I use it to create a colour wheel with equal brightness and saturation, but stepping through hues, it seems like the hue steps are not consistent. E.g. the differences between red to orange, or orange to yellow seems much larger than the differences between blues. To better understand what I mean, check this imgur link: https://imgur.com/a/461cY

    If LCH is perceptually uniform, shouldn’t all equal distance hue steps look the same? Is this a shortcoming of LCH when interpolating between hue? Is there a better way to create a perceptually uniform colour wheel? Thanks!

    • Hey! Lerping through colours is never an easy thing.
      I am currently working on a tool that allows to have colours of uniform luminosity. It works by sampling the luminosity distribution of the colours, and then re-sampling the distribution itself to keep the luminosity changes constant.

      While this works nicely, it is not perfect.
      For a better approach I am running gradient descent to find the colour with the closest hue to the one you want, but same luminosity.

      Hopefully I’ll be able to post them sooner or later! 😀

  5. Interesting write up!
    I had a hunch that one could do cheap with “patching” the saturation and/or luminosity with an extra channel, making it simpler than converting the color space. I was also wondering about lerping in linear space. I did a quick test in Photoshop. patching the saturation does weird sharp lines instead of smooth transitions, and luminosity has no result in Photoshop. So they don’t work. But here’s the base, “normal”, linear and LAB result. https://imgur.com/3aYb52h
    As usual, linear wins!

Webmentions

  • Linear Interpolation - Alan Zucconi July 5, 2020

    […] how to properly interpolate between colours is a dark art, which is heavily discussed in The Secrets of Colour Interpolation. You can play with the swatches below to see for yourself what difference it makes to lerp between […]

  • Onsen UI for Vue の スワイプ対応タブバーを使用して、UXを改良しよう! | モナカプレス July 5, 2020

    […] こちら […]

  • Tutorial Series - Alan Zucconi July 5, 2020

    […] The Secrets of Colour Interpolation […]