[ Previous Section | Next Section | Chapter Index | Main Index ]

Light Color and Position
Other Properties of Lights
The Global Light Model
The Lighting Equation

Section 4.3

OpenGL Lighting

Our 3D worlds so far have been illuminated by just one light -- a white light shining from the direction of the viewer onto the scene. You get this light just by turning on lighting with gl.glEnable(GL.GL_LIGHTING) and enabling light number zero with gl.glEnable(GL.GL_LIGHT0). This "viewpoint light" shines on everything that the viewer can see, and it is sufficient for some purposes. However, in OpenGL, it is possible to define multiple lights. They can shine from different directions and can have various colors. Every OpenGL implementation is required to support at least eight lights, which are identified by the constants GL_LIGHT0, GL_LIGHT1, ..., GL_LIGHT7. (These constants are consecutive integers.) Each light can be separately enabled and disabled; they are all off by default. Only the lights that are enabled while a vertex is being rendered can have any effect on that vertex.

4.3.1  Light Color and Position

Light number zero, as we have seen, is white by default. All the other lights, however, are black. That is, they provide no illumination at all even if they are enabled. The color and other properties of a light can be set with the glLight* family of commands, most notably

public void glLightfv(int light, int propName, float[] propValue, int offset)

The first parameter, light, specifies the light whose property is being set. It must be one of the constants GL.GL_LIGHT0, GL.GL_LIGHT1, and so on. The second parameter, propName, specifies which property of the light is being set. Commonly used properties are GL.GL_POSITION, GL.GL_AMBIENT, GL.GL_DIFFUSE, and GL.GL_SPECULAR. Some other properties are discussed in the next subsection. The third parameter of glLightfv is an array that contains the new value for the property. The fourth parameter is, as usual, the starting index of that value in the array.

A light can have color. In fact, each light in OpenGL has an ambient, a diffuse, and a specular color, which are set using glLightfv with property names GL.GL_AMBIENT, GL.GL_DIFFUSE, and GL.GL_SPECULAR respectively. Just as the color of a material is more properly referred to as reflectivity, color of a light is more properly referred to as intensity or energy. A light color in OpenGL is specified by four numbers giving the red, green, blue, and alpha intensity values for the light. These values are often between 0 and 1 but are not clamped to that range. (I have not, however, been able to figure out how the alpha component of a light color is used, or even if it is used; it should be set to 1.)

The diffuse intensity of a light is the aspect of the light that interacts with diffuse material color, and the specular intensity of a light is what interacts with specular material color. It is common for the diffuse and specular light intensities to be the same. For example, we could make a somewhat blue light with

float[] bluish = { 0.3f, 0.3f, 0.7f, 1 };
gl.glLightfv(GL.GL_LIGHT1, GL.GL_DIFFUSE, bluish, 0);
gl.glLightfv(GL.GL_LIGHT1, GL.GL_SPECULAR, bluish, 0);

The ambient intensity of a light works a little differently. Recall that ambient light is light that is not directly traceable to any light source. Still, it has to come from somewhere and we can imagine that turning on a light should increase the general level of ambient light in the environment. The ambient intensity of a light in OpenGL is added to the general level of ambient light. This ambient light interacts with the ambient color of a material, and this interaction has no dependence on the position of any light source. So, a light doesn't have to shine on an object for the object's ambient color to be affected by the light source; the light source just has to be turned on. Since ambient light should never be too intense, the ambient intensity of a light source should always be rather small. For example, we might want our blue light to add a slight bluish tint to the ambient light. We could do this by calling

gl.glLightfv(GL.GL_LIGHT1, GL.GL_AMBIENT, new float[] { 0, 0, 0.1f, 0}, 0);

I should emphasize again that this is all just an approximation, and in this case not one that has a basis in the physics of the real world. Real light sources do not have separate ambient, diffuse, and specular colors.

The other major property of a light is its position. There are two types of lights, positional and directional. A positional light represents a light source at some point in 3D space. Light is emitted from that point -- in all directions by default, or, if the light is made into a spotlight, in a cone whose vertex is at that point. A directional light, on the other hand, shines in parallel rays from some set direction. A directional light imitates light from the sun, whose rays are essentially parallel by the time they reach the earth.

The type and position or direction of a light are set using glLightfv with property name equal to GL.GL_POSITION. The property value is an array of four numbers (x,y,z,w), of which at least one must be non-zero. When the fourth number, w, is zero, then the light is directional and the point (x,y,z) specifies the direction of the light: The light rays shine in the direction of the line from the point (x,y,z) to the origin. This is related to homogeneous coordinates: The source of the light can be considered to be a point at infinity in the direction of (x,y,z). (See Subsection 3.1.3.) On the other hand, if the fourth number, w, is 1, then the light is positional and is located at the point (x,y,z). Again, this is really homogeneous coordinates: Any non-zero value for w specifies a positional light at the point (x/w,y/w,z/w). The default position for all lights is (0,0,1,0), representing a directional light shining from the positive direction of the z-axis (and towards the negative direction of the z-axis).

The position specified for a light is transformed by the modelview matrix that is in effect at the time the position is set using glLightfv. Thus, lights are treated in the same way as other objects in OpenGL in that they are subject to the same transformations. For example, setting the position of light number 1 with

gl.glLightfv(GL.GL_LIGHT1, GL.GL_POSITION, new float[] { 1,2,3,1 }, 0);

puts the light in the same place as

gl.glLightfv(GL.GL_LIGHT1, GL.GL_POSITION, new float[] { 0,0,0,1 }, 0);

The Camera class in package glutil allows the user to rotate the 3D world using the mouse. If cam is a Camera, then cam.apply(gl) can be called at the beginning of display() to set up the projection and viewing transformations. If a light position is set up after calling cam.apply, then that light will rotate along with the rest of the scene.

Note that the default light position is, in effect, set before any transformation has been applied and is therefore given directly in eye coordinates. That is, when we say that the default light shines towards the negative direction of the z-axis, we mean the z-axis in eye coordinates, in which the viewer is at the origin, looking along the negative z-axis. So, the default position makes a light into a viewpoint light that shines in the direction the viewer is looking.

Here is an applet that demonstrates various aspects of lights and materials. This is an applet version of the sample program LightDemo.java.

In the LightDemo example, a wave-like surface is illuminated by three lights: a red light that shines from above, a blue light that shines from below, and a white light that shines down the x-axis. (In this example, the z-axis is pointing up, and the x-axis is pointing out of the screen.) The surface itself is gray, although it looks colored under the colored lights. Both the surface material and the lights have a non-zero specular component to their color. A set of axes and a grid of lines on the surface are also shown, at the user's option; these features are drawn with lighting turned off.

The user can use the mouse to rotate the scene. A control below the display determines whether the lights are fixed with respect to the viewer or with respect to the world. In the first case, as the user rotates the scene, the surface rotates but the lights don't move; so, for example, the part of the surface that is illuminated by the red light is whatever part happens to be facing upwards in the display. In the second case, the lights rotate along with the surface; so, for example, the red light always illuminates the same part of the surface, no matter how the scene has been rotated. Note that even in the second case, the positions of specular highlights on the surface do change as the scene is rotated, since specular highlights depend on the position of the viewer relative to the surface, as well as on the position of the lights relative to the surface.

The program also has controls that let the user turn the red, blue, and white lights, as well as ambient light, on and off. If all the lights are turned off, the surface disappears entirely. If only ambient light is on, the surface appears a a flat patch of gray. Ambient light by itself does not give any 3D appearance to the surface, since it does not depend in any way on the orientation of the surface or the location of any light source. The user should try other combinations of settings and try to understand what is going on. Also, take a look at the source code.

4.3.2  Other Properties of Lights

In addition to color and position, lights have six other properties that are more rarely used. These properties have to do with spotlights and attenuation. Five of the six properties are numbers rather than arrays and are set using either of the following forms of glLight*

public void glLighti(int light, int propName, int propValue)
public void glLightf(int light, int propName, float propValue)

It is possible to turn a positional light into a spotlight, which emits light in a cone of directions, rather than in all directions. (For directional lights, spotlight settings are ignored.) A positional light is a spotlight if the value of its GL_SPOT_CUTOFF property is set to a number in the range 0 to 90. The value of this property determines the size of the cone of light emitted by the spotlight; it gives the angle, measured in degrees, between the axis of the cone and the side of the cone.

The default value of GL_SPOT_CUTOFF is a special value, 180, that indicates an ordinary light rather than a spotlight. The only legal values for GL_SPOT_CUTOFF are 180 and numbers in the range 0 to 90.

A spotlight is not completely specified until we know what direction it is shining. The direction of a spotlight is given by its GL_SPOT_DIRECTION property, an array of three numbers that can be set using the glLightfv method. Like the light's position, the spotlight direction is subject to the modelview transformation that is in effect at the time when the spotlight direction is set. The default value is (0,0,−1), giving a spotlight that points in the negative direction of the z-axis. Since the default is set before the modelview transformation has been changed from the identity, this means the viewer's z-axis. That is, the default spotlight direction makes it point directly away from the viewer, into the screen.

For example, to turn light number 1 into a spotlight with a cone angle of 30 degrees, positioned at the point (10,15,5) and pointing toward the origin, you can use these commands:

gl.glLightfv(GL.GL_LIGHT1, GL.GL_POSITION, new float[] {10,15,5}, 0);
gl.glLighti(GL.GL_LIGHT1, GL.GL_SPOT_CUTOFF, 30);
gl.glLightfv(GL.GL_LIGHT1, GL.GL_SPOT_DIRECTION, new float[] {-10,-15,-5}, 0);

By default, everything in the cone of a spotlight is illuminated evenly. It is also possible to make the illumination decrease as the angle away from the axis of the cone increases. The value of the GL_SPOT_EXPONENT property of a light determines the rate of falloff. This property must have a value in the range 0 to 128. The default value, 0, gives even illumination. Other values cause the illumination to fall off with increasing angle, with larger values giving a more rapid falloff.

Since lighting calculations are only done at vertices, spotlights really only work well when the polygons that they illuminate are very small. When using larger polygons, don't expect to see a nice circle of illumination.

In real-world physics, the level of illumination from a light source is proportional to the reciprocal of the square of the distance from the light source. We say that the light "attenuates" with distance. OpenGL lights do not follow this model because it does not, in practice, produce nice pictures. By default in OpenGL, the level of illumination from a light does not depend at all on distance from the light. However, it is possible to turn on attenuation for a positional light source. For an vertex at distance r from a light source, the intensity of the light on that vertex is computed as

 I * ( 1 / (a + b*r + c*r2) )

where I is the intrinsic intensity of the light (that is, its color level -- this calculation is done for each of the red, green, and blue components of the color). The numbers a, b, and c are values of the properties GL_CONSTANT_ATTENUATION, GL_LINEAR_ATTENUATION, and GL_QUADRATIC_ATTENUATION. By default, a is 1 while b and c are 0. With these default values, the intensity of the light at the vertex is equal to I, the intrinsic intensity of the light, and there is no attenuation.

Attenuation can sometimes be useful to localize the effect of a light, especially when there are several lights in a scene. Note that attenuation applies only to positional lights. The attenuation properties are ignored for directional lights.

4.3.3  The Global Light Model

There are a few properties of the OpenGL lighting system that are "global" in the sense that they are not properties of individual lights. These properties can be set using the glLightModeli and glLightModelfv methods:

public void glLightModelfv(int propertyName, float[] propertyValue, int offset)
public void glLightModeli(int propertyName, int propertyValue)

There is only one property that can be set with glLightModelfv, the global ambient light intensity, using property name GL.GL_LIGHT_MODEL_AMBIENT. Global ambient light is ambient light that is present in the environment even when no light sources are turned on. The default value is (0.2,0.2,0.2,1). For a yellowish ambient light, for example, you could say

                                    new float[] { 0.2f, 0.2f, 0, 1 }, 0);

There are three properties that can be set using glLightModeli. We have already encountered one of them. The command


turns on two-sided lighting, which tells OpenGL to compute normal vectors for the back faces of polygons by reversing the normal vectors that were specified for the front faces. This also enables the front and the back faces of polygons to have different material properties. The second parameter can be GL.GL_TRUE or GL.GL_FALSE, but these are actually just symbolic constants for the numbers 1 and 0.

By default, when computing specular highlights, OpenGL assumes that the direction to the viewer is in the positive direction of the z-axis. Essentially, this assumes, for the purpose of specular light calculations, that the viewer is "at infinity." This makes the computation easier but does not produce completely accurate results. To make OpenGL do the correct calculation, you can call


In practice, this rarely makes a significant difference, but it can be noticeable if the viewer is fairly close to the illuminated objects.

Finally, there is an option that is available only if the OpenGL version is 1.2 or higher, GL.GL_LIGHT_MODEL_COLOR_CONTROL. The value for this property must be either GL.GL_SEPARATE_SPECULAR_COLOR or GL_GL_SINGLE_COLOR. The latter is the default. This default yields poor specular highlights on objects to which a texture has been applied. The alternative will produce better specular highlight on such surfaces by applying the specular highlight after the texture has been applied. To make sure that the option is actually available, you can use the following code:

if (gl.isExtensionAvailable("GL_VERSION_1_2") {

4.3.4  The Lighting Equation

What does it actually mean to say that OpenGL performs "lighting calculations"? The goal of the calculation is produce a color, (r,g,b,a), for a vertex. The alpha component, a, is easy -- it's simply the alpha component of the diffuse material color at that vertex. But the calculations of r, g, and b are fairly complex.

Ignoring alpha components, let's assume that the ambient, diffuse, specular, and emission colors of the material have RGB components (mar,mag,mab), (mdr,mdg,mdb), (msr,msg,msb), and (mer,meg,meb), respectively. Suppose that the global ambient intensity is (gar,gag,gab). Then the red component of the vertex color will be

r = mer + gar*mar + I0r + I1r + I2r + ...

where Iir is the contribution to the color that comes from the i-th light. A similar equation holds for the green and blue components of the color. This equation says that the emission color is simply added to any other contributions to the color. The contribution of global ambient light is obtained by multiplying the global ambient intensity by the material ambient color. This is the mathematical way of saying that the material ambient color is the proportion of the ambient light that is reflected by the surface.

The contributions from the light sources are much more complicated. Note first of all that if a light source is disabled, then the contribution from that light source is zero. For an enabled light source, we have to look at the geometry as well as the colors:

In this illustration, N is the normal vector at the point whose color we want to compute. L is a vector that points towards the light source, and V is a vector that points towards the viewer. (Both the viewer and the light source can be "at infinity", but the direction is still well-defined.) R is the direction of the reflected ray, that is, the direction in which a light ray from the source will be reflected when it strikes the surface at the point in question. The angle between N and L is the same as the angle between N and R. All of the vectors are unit vectors, with length 1. Recall that for unit vectors A and B, the inner product A · B is equal to the cosine of the angle between the two vectors. Inner products occur at several points in the lighting equation.

Now, let's say that the light has ambient, diffuse, and specular color components (lar,lag,lab), (ldr,ldg,ldb), and (lsr,lsg,lsb). Also. let mh be the value of the shininess property (GL_SHININESS) of the material. Then the contribution of this light source to the red component of the vertex color can be computed as

Ir = lar*mar + f*att*spot*( ldr*mdr*(L·N) + lsr*msr*max(0,V·R)mh )

with similar equations for the green and blue components. Here, f is 0 if the surface is facing away from the light and is 1 otherwise. f is 1 when L·N is greater than 0, that is, when the angle between L and N is less than 90 degrees. When f is zero, there is no diffuse or specular contribution from the light to the color of the vertex. Note that even when f is 0, the ambient component of the light can still affect the vertex color.

In the equation, att is the attenuation factor, which represents attenuation of the light intensity due to distance from the light. The value of att is 1 if the light source is directional. If the light is positional, then att is computed as 1/(a+b*r+c*r2), where a, b, and c are the attenuation constants for the light and r is the distance from the light source to the vertex. And spot accounts for spotlights. For directional lights and regular positional lights, spot is 1. For a spotlight, spot is zero when the angle between N and L exceeds the cutoff angle of the spotlight. Otherwise, spot is given by (N·L)e, where e is the value of the falloff property (GL_SPOT_EXPONENT) of the light.

The diffuse component of the color, before adjustment by f, att, and spot, is given by ldr*mdr*(L·N) This represents the diffuse intensity of the light times the diffuse reflectivity of the material, multiplied by the cosine of the angle between L and N. The angle is involved because for a larger angle, the same amount of energy from the light is spread out over a greater area. As the angle increases from 0 to 90 degrees, the cosine of the angle decreases from 1 to 0, so the larger the angle, the smaller the diffuse color contribution. The specular component, lsr*msr*max(0,V·R)mh, is similar, but here the angle involved is the angle between the reflected ray and the viewer, and the cosine of this angle is raised to the exponent mh. The exponent is the material's shininess property. When this property is 0, there is no dependence on the angle (as long as the angle is greater than 0), and the result is the sort of huge and undesirable specular highlight that we have seen in this case. For positive values of shininess, the specular contribution is maximal when this angle is zero and it decreases as the angle increases. The larger the shininess value, the faster the rate of decrease. The result is that larger shininess values give smaller, sharper specular highlights.

Remember that the same calculation is repeated for every enabled light and that the results are combined to give the final vertex color. It's easy, especially when using several lights, to end up with color components larger than one. In the end, before the color is used to color a pixel on the screen, the color components must be clamped to the range zero to one. Values greater than one are replaced by one. It's easy, when using a lot of light, to produce ugly pictures in which large areas are a uniform white because all the color values in those areas exceeded one. All the information that was supposed to be conveyed by the lighting has been lost. The effect is similar to an over-exposed photograph. It can take some work to find appropriate lighting levels to avoid this kind of over-exposure.

[ Previous Section | Next Section | Chapter Index | Main Index ]