Colour Spaces

There are a number of different colour spaces that I use in my art. Let's look at them, how they differ, and where they come into play. Colour spaces are ways of organizing and specifying colours. Different colour spaces have different ranges of representable colours, different numbers of colours that can be represented, and different utilities with respect to the technologies of displaying and manipulating colours.


First up is good old RGB. This is the default that SVG and CSS and HTML play with; all those hex numbers: #CD853E, sometimes written out more fully in the decimal equivalent rgb(205,133,63). The three 8 bit numbers give the amount of red, green, and blue (on a scale from 0 to 255) in the colour. (Hence the name.) Sometimes the values are given as percentages, sometimes as decimal values between 0 and 1. RGB is an additive colour model where you are adding in the red, the blue, and the green to produce the final result. Technically there are a lot of different RGB colour spaces all based on the RGB colour model. The one used for Web technologies is sRGB, and that's the one I mean here.

The problem with RGB is that it doesn't map particularly well to human perception and is non-linear, so manipulating colour values mathematically isn't foing to work very well.

Sure, if you increase the red value and hold everything the same, generally things look redder.


Can you really predict what #0FA485 + #900000 is going to look like? Which of these?


And what if you just want to make things lighter? Well, generally incrementing all the values will do that:


Unless you already maxed out on one of the components, or you hit the max value, so now you have non-linear effects and weird hue changes:


Maybe you just want to blend two colours: 30% this and 70% that. Sunshine on the water? What is 30% #EEE8AA + 70% #6A5ACD. If we convert the hex numbers to fractions (convert to integer and divide by 255), perform the percentage blend component by component and then convert back (multiply by 255, round) we get #9285C3. Is that what you expected? Maybe. It kinda works, except when it doesn't.


A better RGB space to be playing in for these kinds of operations is CIE-XYZ: it is a linear RGB colour space. The definition of the axes is based on a series of experiments with human perception. Converting from RGB and XYZ requires multiplying the appropriate matrix by the scaled (0 to 1) RGB values. Which matrix to use depends on the exact RGB space and reference white value. [1] shows some tables for the conversion. For example, for sRGB with reference white D65 the calculation is:

⎡ X ⎤    ⎡ 0.4124564 0.3575761 0.1804375 ⎤ ⎡ R ⎤
⎢ Y ⎥ =  ⎢ 0.2126729 0.7151522 0.0721750 ⎥ ⎢ G ⎥
⎣ Z ⎦    ⎣ 0.0193339 0.1191920 0.9503041 ⎦ ⎣ B ⎦

Converting back to RGB means multiplying by the inverted matrix, and then rescaling the decimal RGB values back to the 0 to 255 range and rounding.

Now we we can interpolate colours with a little more confidence. The 30% to 70% blend of #EEE8AA and #6A5ACD is #A198C3, quite a bit lighter than the direct RGB blend.

Compare the interpolation using RGB coordinates:                     to the interpolation using XYZ coordinates:                     . The latter shows a smoother gradation and doesn't max out to white so quickly.

We can now apply lighting models with more confidence as well. For example, the Lambert reflection model for diffuse lighting:

Id = L·N Il C

where Id is the surface brightness computed by multiplying the surface colour C by the incoming light intensity Il by the dot product of the light vector L and the normal vector to the surface N.

We can get a little more sophisticated here, taking into account surface albedo (brightness) by multiplying that in as well as the intensity of the source light. Wikipedia includes a nice set of albedos for a variety of surfaces.

Finally, the ambient light colour can be added in to the directional light being applied to the surface colouring.

For example, here we apply this lighting model to some pie slices, where the light source is to the bottom of the octagon. The lighting colour is pale yellow, the base ground colour is tan and the ambient light is either absent (black, if you will), white, or red.

No ambient lighting, no albedo moderation (i.e. albedo=1.0):

White ambient lighting, no albedo moderation:

Red ambient lighting, no albedo modification:

Red ambient lighting, albedo of snow (0.5):

Red ambient lighting, albedo of sand (0.25):


HSL is a colour space defined in terms of hue, saturation, and luminance. Hue is given as an angle (0 to 360); saturation and luminance are percentage ranges. The idea is that it is more intuitive to work with: want to darken a colour? Just adjust the luminance coordinate. Want a range of colours with the same lightness? Just turn the hue knob but keep the rest the same. The problem with it (see [2] for more details) is that it doesn't actually match human colour perception with respect to lightness, so I use a variation called HSLuv, which is something of a cross between HSL and CIELUV LCh. The colour range is a little murkier, but you can bank on the perceptual uniformity of a given lightness value across different hues.

HSLuv can be handy when you just want to add some shadowing: just tweak the luminance coordinate, or when you want to make a palette of colours with the same brightness or make a colour gradient that reduces in brightness.

A palette of random colours, shifted to all have the same luminance (50%):

A palette based on a random colour, with luminance values evenly divided:

Using HSL luminance tweaks can be a quick way of getting some shading messing about with lighting models: here I just adjust the luminance based on the angle bisecting the first point of the triangle, for a nice textured effect.