Color, part b

This reading picks up where the previous reading left off, presenting some different color models and talking about the mathematics of interpolation across triangles.

Last time, we learned about RGB. Today, we'll learn about some other important ones.

HSL

Psychologically, humans don't think in terms of light and RGB. For example, it's very weird to think of yellow as red+green. Most people find it more intuitive to think of

These three dimensions can be defined in terms of RGB, and we can convert back and forth with some complicated math.

How do the HSL dimensions relate to the RGB dimensions? First, imagine a line starting at the black vertex of the color cube and going through the center of the cube and ending at the white vertex. That is, the line goes directly from (0,0,0) to (1,1,1) in RGB space.

Now, realize that the coordinates of every point on that line are all equal; that is, every point is (a,a,a) for some value of "a" between 0 and 1. Because the coordinates are all the same, that means every point on the line is gray. The line starts at black, goes through dark grays, becoming lighter, until it ends at white. This axis is lightness. The farther a point is from black, the greater its value. (This is an approximate definition; the math is a bit complicated, and I'm only trying to give some intuition.)

If we choose a particular number for the lightness, say 0.5, we get a two-dimensional surface going through the RGB cube. Let's press that surface out flat on a piece of paper, so we can think in two dimensions instead of three. One way to do so results in a hexagon, like this:

color hexagon color hexagon, annotated

This is a hexagon rather than a wheel, with the six corners being named, well-known colors: red, yellow, green, cyan, blue, magenta and back to red. This is a slice through a 3D figure sometimes called a hexcone. We'll have some visualizations later.

In the center of that region is the place where the gray axis (the axis from pure black to pure white) pierces the region. Let's call that the gray point. In the second figure above, the gray point is where the two black lines meet.

Imagine asking how far a point in the region is from the gray point. If the point is near the gray point, the point is similar to that value of gray, and the color is called unsaturated. If the point is far from the line, the color is called saturated, so distance from the gray point is saturation. (Again, this is an intuitive definition, not a mathematical one.) Looking at the figure above, you'll notice that the colors are more intense (saturated) near the perimeter.

Finally, pick the reddest point on the edge of the region, and connect the gray point in the center with the red point on the edge. Now, any point in the region can be described not only by its distance from the gray point, but also by the angle with respect to this reference line. The hue is defined as this angle. The color with hue of zero (at 3 o'clock if you image the color wheel like a clock) is red. Any saturation of red will have an angle of zero, greens will be at about 120 degrees (11 o'clock), and blues will be about about 240 degrees (7 o'clock). Yellows will be between red and green, so angles of about 60 degrees (2 o'clock), and so forth.

Here's a nice interactive color wheel. It shows you value/lightness (hard to tell which) in a 1D strip and hue and saturation in a 2D circle. You can close the chat window when it pops up.

The Wikipedia article on HSL and HSV is good. I particularly like this picture of the double hexcone model.

Color perception is a fascinating topic that we won't be able to explore entirely. However, just to whet your appetite, here's an amusing optical illusion with color perception: color perception

HSL and HSV both use the same intuition above. The difference is that the breadth of the pyramid is biggest at a value=1 for HSV, and for HSL, the breadth is maximized at lightness=0.5. For lightness from 0.5 to 1, we get a second pyramid, shrinking down to an apex of pure white. So, HSL is like HSV except that it's a double hexcone (two hexcones with their "color wheel" faces stuck together). This reflects the fact that as you get closer to white, there is less "room" for hue and saturation information. The double hexcone is a better approximation for the RGB color cube, but both are non-linear transformations. HSV is more commonly used in computer graphics, while HSL is more commonly used by artists. Nevertheless, THREE.js has support for converting from HSL to RGB and back, so we'll use that.

For more info, consult Wikipedia article on HSL

HSL and Color Pickers

There are a zillion color pickers on the web and in various graphics applications, such as Photoshop. Consider the color picker in dat.GUI(), which you can play with using this page: dat.GUI() color picker. Here's a screenshot:

dat.GUI color picker

The color picker allows you to specify three dimensions of color using a 1D vertical rainbow strip and a 2D square next to it.

  1. Hue is arranged on the right as the vertical rainbow strip, going from red (zero degrees) on the bottom through yellow (at 60 degrees) up to red again (at 360 degrees) at the top.
  2. lightness goes vertically from black (zero) at the bottom edge of the square to the maximum (one) along the top edge of the square.
  3. saturation goes horizontally from gray (zero) anywhere along the left edge to the hue (one) on the right edge.

Since both lightness and saturation are shown in the square, we can look at the edges of the square to get more insight:

HSL in THREE.js

If you want to use HSL in your THREE.js programs, you certainly can. The THREE.Color() object has method to convert from and to HSL values.

For example, suppose you want to use a less-saturated red. Red happens to have a hue angle of zero, and we'll use 0.5 for the lightness (through the fat part of the double hexcone, see below), and then we'll turn down the saturation. Here's some code:

var red = new THREE.Color("red");
alert("red is "+red.getHexString());
var red_hsl = red.getHSL();
alert("red is hue of "+red_hsl.h+", lightness of "+red_hsl.l+" and saturation: "+red_hsl.s);
var less_red = new THREE.Color();
less_red.setHSL(red_hsl.h, red_hsl.l, 0.5 * red_hsl.s );
alert("less_red is "+less_red.getHexString());

The background color of this paragraph is the half-saturated red, or #bf3f3f, that we just computed using THREE.js's Color object

Note that hue is considered an angle, but the API in THREE.js has it go from zero to one, rather than using degrees (zero to 360) or radians (zero to 2*π).

Demo: Color Cone

When we press the region flat into a hexagon, that means that the full 3D space is a pair of hexcones, as demonstrated by the following THREE.js program. The first hexcone has a lightness value that goes from 0 (completely black) at the apex to 0.5. The second hexcode has a lightness value that goes from 0.5 to 1 (completely white) at the apex.

Here's the color cones demo:

Be sure to look at it from the side (+X direction) by using the "x" key or dragging with the mouse.

The demo has some additional keyboard callbacks:

The code for the Color Cone Scene is here. Note how the keyboard callbacks are set up.

  

Finally, if you're curious, here is how the color pyramids are built:

  
  

Understanding polar coordinates like this is important, so take a few minutes to understand this code, particularly lines 27--35. This figure may help:

circle making

The main computations that, for angles counter-clockwise around the origin:

x = radius * cosine( angle )
y = radius * sine( angle )

Keyboard Callbacks

The Color Cone demo allowed you to adjust the number of vertices up and down. That code can be broken down into three simple parts:

This technique will let you dynamically adjust many aspect of your models very easily.

In the color cone demo above, we saw triangles that smoothly changed from one color to another across then. Next, we'll look at how that's done. This builds on what we learned last time about parametric equations of lines, so you should refresh your memory if necessary. A parametric equation for a line from A to B is:

P(t) = A + (B-A) t

Parametric Equation for a Triangle

Since a triangle is a 2D thing, the parametric equation for a triangle will have two parameters. One way to think about these two parameters is that the first parameter, say t, moves you along one side of the triangle, from vertex A to vertex B. Let P(t) be that point along the AB edge of the triangle. The second parameter, say s, is the parameter of a line from vertex C to P(t). That is, the endpoint of the second line is a moving target. The point Q(s,t) is a point in the triangle, on a line between C and P(t).

The following figure sketches these ideas:

lines to set up parametric equation of a triangle

Q(s,t) = C + (P(t) -C)s
Q(s,t) = C + (P(t)s - Cs)
Q(s,t) = [A(1-t)+B(t)]s + C(1-s)
Q(s,t) = A(1-t)s+Bts + C(1-s)

Notice that we have several choices: the line from A to B could instead go from B to A. Similarly, the line from C to P(t) could go from P(t) to C. These yield equivalent equations, just as the equations of a line from A to B is equivalent to a the equation of a line from B to A.

The last line of the math for Q(s,t) above shows a three-way mixture of the vertices. That is, a triangle is all points in the convex sum of the vertices. A convex sum is a weighted sum of N things, where the weights all add up to 1.0:

w1*A+w2*B+w3*C
w1+w2+w3 = 1

Compare this to the weighted sum formula for Q(s,t). The last equation is perhaps a little surprising, but you can check that the weights sum to one:

(1-t)s+ts+(1-s)=1

Incidentally, the center of the triangle is when all the weights are equal: one-third.

Example: Finding the Equation of a Triangle from Three Points

Suppose we have a triangle ABC whose vertices are:

A = (1,2,3)
B = (2,4,1)
C = (3,1,5)

We could write down the following equation for the triangle:

Q(s,t) = A(1-t)s + B(t)s + C(1-s)
Q(s,t) = (1,2,3)(1-t)s + (2,4,1)ts + (3,1,5)(1-s)

So, doing each coordinate separately, we have:

x(s,t) = (1-t)s + 2ts + 3(1-s)
y(s,t) = 2(1-t)s + 4ts + (1-s)
z(s,t) = 3(1-t)s + ts + 5(1-s)

Which we can simplify algebraically to

x(s,t) = (1-t)s + 2ts + 3(1-s)
= s-ts+2ts+3-3s
= ts-2s+3

y(s,t) = 2(1-t)s + 4ts + (1-s)
= 2s -2ts + 4ts + 1-s
= 2ts + s + 1

z(s,t) = 3(1-t)s + ts + 5(1-s)
= 3s -3ts + ts + 5 - 5s
= -2ts -2s + 5

Suppose we have a point whose parameters with respect to that triangle are (0.5,0.5). What does that mean? It means that the point is halfway between C and the midpoint of AB. The coordinates are:

x(0.5,0.5) = (0.5)(0.5)-2(0.5)+3 = 2.25
y(0.5,0.5) = 2(1-0.5)0.5 + 4(0.5)(0.5) + (1-0.5) = 2
z(0.5,0.5) = 3(1-0.5)0.5 + (0.5)(0.5) + 5(1-0.5) = 3.25

So the coordinates of Q(0.5,0.5) are (2.25,2,3.25).

Equivalently, we can think of computing Q(s,t) as a weighted sum of the triangles vertices:

Q(s,t) = A(1-t)s + B(t)s + C(1-s)
Q(0.5,0.5) = A(1-0.5)(0.5) + B(0.5)(0.5) + C(1-0.5)
Q(0.5,0.5) = A(0.25) + B(0.25) + C(0.5)

Then, to find the coordinates of Q(0.5,0.5), we just substitute the coordinates of ABC and calculate the weighted sum.

Color Interpolation in a Triangle

If the colors of the vertices are different, OpenGL interpolates them, using the same equations that we used for calculating coordinates. Suppose A is red (1,0,0), B is magenta (1,0,1) and C is yellow (1,1,0). We can compute the color of the middle point, Q(0.5,0.5), as:

Q(0.5,0.5) = A(0.25) + B(0.25) + C(0.5)
Q(0.5,0.5) = (1,0,0)(0.25) + (1,0,1)(0.25) + (1,1,0)(0.5)
Q(0.5,0.5) = (1,0.5,0.75)

an triangle with smooth
interpolation

The triangle as a whole looks like this.

Bilinear Interpolation for a Quadrilateral

A quad works the same way, except that now the parameter t moves along both sides of the quadrilateral. In the figure below, t might move us from A to B and simultaneously from C to D. Meanwhile, s moves us from P(t) along the upper edge down to Q(t) along the lower edge.

lines to set up parametric equations
   of a quad

Thus,

R(s,t) = (1-s)P(t) + sQ(t)

Color Interpolation in OpenGL

In a hardware implementation, the graphics card figures out the intersection of the projection of two segments with a scan line (one of the lines of the raster, corresponding to a horizontal line of pixels on the monitor). That gives the colors of both endpoints. Then, it fills the pixels in between, stepping the color by the same amount at each step. If there are 100 pixels between the edges of the projection, the color is stepped in 99 equal-size increments.

Don't rely on OpenGL to do bi-linear interpolation on quads or the right thing for general polygons. It's much better to break it down into triangles and use that. This is because graphics cards break larger polygons into triangles for fast processing. THREE.js does this for us.

Suppose you have a quad with three red vertices and one white vertex. You might expect a smooth gradient of pinkness as you approach the white vertex. In fact, it depends entirely on which vertex is the white one. If it's the fourth vertex, the triangulation will leave that vertex entirely out of the first triangle, and so the first triangle will be solid red. The second triangle will have the shading you expect, though starting closer than you'd expect. However, if the white vertex is the first vertex, both triangles will have a white vertex, and so you'll get much more of the shading you'd expect, though, in fact, neither will match the result you'd get with bi-linear interpolation.

The following figure demonstrates several different results of drawing a quad with smooth shading and each vertex having a different color. In every case, vertices are given to OpenGL in clockwise order; in this case, starting at the lower left (the red vertex). It goes Red, Green, Blue, Yellow.

5 ways to do smooth
   shading of a quad

The pictures in the first row are (1) a quad with RGBY vertices with the first vertex being the lower left one and proceding clockwise, (2) two triangles cut along the diagonal from lower-left to upper-right, (3) two triangles cut along the diagonal from upper-left to lower-right. Note that the quad matches the second figure, not the third. This shows that the triangulation is done as you'd expect: the first three vertices are formed into a triangle, then the next three.

The pictures in the second row are (4) the quad cut into four triangles using the middle point, and (5) the quad cut into 400 quads using a mesh.

The main problem is that the two triangulations are different, when you might think it wouldn't matter. (With flat shading, they don't, because the fourth vertex determines the color.) Next, none of the elements on the first row matches the four-part triangulation in the second row. The fifth version is the smoothest yet, but you might think they should all match.

Color Interpolation in Three.js

The following FAQ is a little out of date, but it does explain how to do color interpolation in Three.js.

The key elements are:

  1. The THREE.Geometry() object will have a vertexColors property that is an array of colors.
  2. Each THREE.Face() object (a list of these is in the Geometry object) will have a three-element array of colors, each of which is the color of the corresponding vertex of the face.
  3. Using THREE.MeshBasicMaterial, set the vertexColors property to THREE.VertexColors. That value of that property alerts Three.js that a vertices of a face (a triangle) could have different colors.

Here's a demo that creates an RGB triangle:

triangleInterpolation.shtml

Please take the time to read that code. It's not too long.

There's a function call there to TW.computeFaceColors(triGeom);. This is a function I wrote to iterate over all the triangles of a geometry, setting a property of each that is a three-element array of indices into the array of colors. The function essentially says that vertex i has color i for all i.

Here's an example with two triangles:

triangleInterpolation2.shtml

But what about a slightly different example, where we have two adjoining triangles, both of which have color interpolation, but where the same vertex has a different color. Here's the demo:

triangleInterpolation2b.shtml

Notice that at the lower right we have:

Everything else in the code is the same. That is, don't think of the vertex in the lower corner as a single thing that is green for the purpose of the lower triangle and blue for the purpose of the upper triangle. Instead, simply think of it as two different vertices that happen to have the same spatial coordinates.

Subtractive Color and CMYK

Before we're done, we need to talk briefly about a few other important color models. We will have little practical use for them this semester, but you should have some familiarity with them and the concepts. The most important concept is additive versus subtractive color.

Additive color mixes light: the more, the brighter. As most people remember from kindergarten, mixing fingerpaints makes things darker. That's because paint (and ink, and so forth) works by subtracting light.

Subtractive color is used in the printing industry. The primaries are:

C = 1 - R
M = 1 - G
Y = 1 - B

The 1 in the preceding equations is a vector of all 1s, so cyan = (1,1,1) - (1,0,0) = (0,1,1), which is a mixture of Green and Blue (the other two primaries). These are the additive secondaries

Theoretically, you can mix all three subtractive primaries to get black, but if you do you usually get a dark, muddy brown, so we introduce a fourth primary: black, abbreviated K.

These are collectively called CMYK. Think of the four cartridges on a color ink-jet printer.

YIQ

Color TV is broadcast in YIQ, which has intensity (luminance or brightness) as one signal (Y) and two others encode chromaticity (hue and saturation, but not respectively). So it's similar to HSL and HSV in that the black/white signal is one channel (Y), just like L and V.

Summary