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.
hueis like the color wheel that we're familiar with from elementary school art. The color wheel is a 2D cross-section of a cone, and the color wheel gives us hue (angle around the wheel, such as the difference between red and 180 degrees from red, which is green) and saturation (which is the distance from the center, which is white or, more precisely, a shade of gray). The third dimension is value, which is how light/dark the colors are. More on this below.
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
Roy G. Biv?
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:
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
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:
The color picker allows you to specify three dimensions of color using a 1D vertical rainbow strip and a 2D square next to it.
Since both lightness and saturation are shown in the square, we can look at the edges of the square to get more insight:
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*π).
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:
The main computations that, for angles counter-clockwise around the origin:
x = radius * cosine( angle )
y = radius * sine( angle )
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:
TW.setKeyboardCallback()
after TW.cameraSetup()
, which is when the built-in key
callbacks are set up.
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
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:
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.
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:
So the coordinates of Q(0.5,0.5) are (2.25,2,3.25).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
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.
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)
![]()
The triangle as a whole looks like this.
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.
Thus,
R(s,t) = (1-s)P(t) + sQ(t)
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.
![]()
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.
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:
THREE.Geometry()
object will have
a vertexColors
property that is an array of
colors.
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.
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:
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:
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:
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.
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.
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.