It's time to move on to 3D. We will do this by looking at OpenGL, a common and widely supported 3D graphics API. OpenGL was introduced in 1992 and has undergone a lot of changes recently. In fact, we will work for now with the original version, OpenGL 1.0, much of which is removed from the most modern versions, except as an optional module in some versions. However, the newer OpenGL standard is a difficult entry into 3D for beginners. And OpentGL 1.0 is still widely supported, so it makes sense to begin with that.
Currently, OpenGL is implemented in the graphics hardware of most desktop and laptop computers. In practice, they all support OpenGL 1.0 as well as newer versions. (Graphics cards can support other graphics APIs at the same time as OpenGL, including DirectX, the standard graphics library for Windows.) Handheld computing devices such as smart phones and tablets often implement OpenGL ES, a version for "embedded systems." OpenGL ES 1.0 was based on OpenGL 1.3 and includes most of OpenGL 1.0. OpenGL ES 2.0 is not compatible with OpengGL 1.0. Newer Android and iOS devices support both OpenGL ES 1.0 and 2.0, while older devices only supported 1.0. There is also WebGL, which is a technology for 3D graphics on Web pages. It is pretty much the same as OpenGL ES 2.0, with some adaptations to the Web environment. It is now supported pretty well in Firefox, Chrome, and Safari. Although it is not yet in Internet Explorer, it should be included in IE 11.
OpenGL is a very low-level graphics API, similar to the 2D APIs we have covered. That is, it has only very basic drawing operations and no built-in support for scene graphs or object oriented graphics. The plan for the rest of the course is to use old-fashioned OpenGL to learn about 3D graphics fundamentals. Then, we'll spend time on some higher-level 3D graphics systems, more similar to what most graphics programmers and designers will use for their work. After that, we'll come back to OpenGL to cover its more modern features.
Remember that when I say "OpenGL" for the time being, I mean "OpenGL 1.0." OpenGL 1.0 is a large API, and I will only cover a small part of it. The goal is to introduce 3D graphics concepts, not to fully cover the API.
OpenGL consists of a large number of subroutines. It also comes with a large number of named constants for use in subroutine calls. The API is defined in the C programming language, but there are "bindings" for many other languages that make it possible to use OpenGL from those languages. For example, the OpenGL binding for Java is called JOGL. In this section, I will use the C API; I will introduce JOGL in the next section. I also note here that OpenGL is purely an API for drawing. It includes nothing about creating windows in which to draw or interacting with the user; again, that is something we will deal with in the next section. For now, we limit discussion to drawing with the OpenGL 1.0 API.
In a two dimensional coordinate system, points are given by a pair of coordinates, conventionally called x and y, and there are two coordinate axes, the x-axis and the y-axis. In three dimensions, you need three coordinates to specify a point. The third coordinate is usually called z, and the third dimension is represented by a third axis, the z-axis, which is perpendicular to both the x-axis and the y-axis. All three axes intersect at the point (0,0,0), which is called the origin. In the usual OpenGL setup, the x and y-axes lie on the viewing surface (such as your computer screen). The positive direction of the x-axis points to the right, and the positive direction of the y-axis points upward (not down, as in the Java and Canvas APIs). The z-axis is perpendicular to the screen. The positive direction of the z-axis points outward from the screen, towards the viewer, and the negative direction extends behind the screen.
In the default coordinate system, x, y, and z are all limited to the range −1 to 1. Anything drawn inside this cube is projected onto the two-dimensional drawing surface for viewing. Anything drawn outside the cube is clipped and does not appear in the displayed 2D image. The projection is done, by default, by simply discarding the z-coordinate. Of course, all of these defaults can be changed, as we will see. (Saying that the z-axis points out of the screen by default is a little misleading. The truth, which you should probably ignore, is that the actual default coordinate system—the one that you get if you make no changes at all—is a left-handed system with positive z pointing into the screen. However, the conventional way to set up a non-default projection includes a z-axis flip that changes the coordinate system to right-handed, and OpenGL programmers think in terms of a right-handed system.)
OpenGL can draw only points, lines, and triangles. Anything else has to built up out of these very simple shapes. (Actually, OpenGL 1.0 can also draw quadrilaterals and polygons, but they are easy to construct from triangles and have been dropped from modern OpenGL, so I ignore them here.) Let's jump right in and see how to draw a triangle:
glBegin(GL_TRIANGLES); glVertex3f(1,0,0); glVertex3f(0,1,0); glVertex3f(0,0,1); glEnd();
This draws the triangle with vertices at the 3D points (1,0,0), (0,1,0), and (0,0,1). There is already a lot here to talk about. A shape is drawn by specifying a sequence of vertices between calls to glBegin() and glEnd(). The parameter to glBegin() is a constant that tell what kind of shape to draw. In OpenGL terms, the parameter specifies which primitive to draw. Note that GL_TRIANGLES is plural. It can be used to draw several triangles with one glBegin/glEnd. With three vertices, it produces one triangle; with six vertices, it produces two; and so on. There are seven primitives for drawing points, lines, and triangles:
In this illustration, the vertices are labeled in the order in which they would be specified between glBegin() and glEnd().
The GL_TRIANGLES primitive, as I've said, produces separate triangles. GL_TRINAGLE_STRIP produces a strip of connected triangles. After the first vertex, every pair of additional vertices adds another triangle. Note the order of the vertices: Odd numbered vertices are on one side of the strip; even number vertices are on the other side. In a GL_TRIANGLE_FAN, the first vertex becomes the basis of the fan and is connected to all the other vertices. After the first two vertices, each additional vertex will add another triangle to the fan. Note that triangle fans can be used to draw polygons.
Points are easy. The GL_POINTS primitive produces one point for each vertex that is specified. By default, the size of a point is one pixel. With GL_LINES, each pair of vertices produces a line segment joining those points. GL_LINE_STRIP produces a series of line segments joining all the vertices. And GL_LINE_LOOP is similar to GL_LINE_STRIP except that one additional line segment is drawn connecting the last vertex to the first vertex. The default width for lines is one pixel.
In the above example, each vertex is specified by calling the function glVertex3f(). There are actually quite a few functions for specifying vertices. The function names all start with "glVertex". The suffix, such as "3f", gives information about the parameters that are supplied to the function. This is a very common pattern in OpenGL, so it's worth paying attention to how it works. In "glVertex3f", the "3" means that 3 numbers will be provided in the parameter list, and the "f" means that the numbers are of type float. The "f" could be replaced by "d" for numbers of type double or by "i" for numbers of type int. The "3" can be replaced by "2", meaning that just two coordinates will be provided. In that case, the two coordinates are x and y, and the z coordinate is set to 0, so you are effectively drawing in 2D on the xy-plane. For example, to draw a 1-by-1 square with vertices at 0 and 1, one could use:
glBegin(GL_TRIANGLE_FAN); glVertex2i(0,0); glVertex2i(1,0); glVertex2i(1,1); glVertex2i(0,1); glEnd();
There is one more important variation: It is possible to add a "v" to the end of the name. This means that there will be only one parameter, which will be an array that contains the coordinates. The number of coordinates is still given by a number in the function name, not by the length of the array. In fact, in the C API, the parameter is actually a pointer to the data. In C, arrays and pointers are in many ways interchangeable, and the use of a pointer as the parameter adds flexibility to the way the coordinate data can be represented. For example, it is common to put all the coordinate data into one big array and to specify individual vertices using pointers that point into the middle of the array. Here's an example for people who know C:
float coords[] = { 0,0.5, -0.5,-0.5, 0.5,-0.5 }; glBegin(GL_TRIANGLES); glVertex2fv(coords); // array name is a pointer to the start of the array glVertex2fv(&coords[2]); // &coords[2] is a pointer to second element in array glVertex2fv(&coords[4]); // &coords[4] is a pointer to fourth element in array glEnd();
The family of functions with names consisting of "glVertex" with all its possible suffixes is often referred to as glVertex*.
Functions from the family glColor* can be used to specify colors for the geometry that we draw. For example, glColor3f has as parameters three floating points numbers that give the red, green, and blue components of the color as numbers in the range 0.0 to 1.0. (In fact, values outside this range are allowed, even negative values. When color values are used in computations, out-of-range values will be used as given. When a color actually appears on the screen, its component values are clamped to the range 0 to 1. That is, values less than zero are changed to zero, and values greater than one are changed to one.)
You can add a fourth component to the color by using glColor4f(). The fourth component, known as alpha, is not used in the default drawing mode, but it is possible to configure OpenGL to use it as the degree of transparency of the color, similarly to the use of the alpha component in Java graphics.
If you would like to use integer color values in the range 0 to 255, you can use glColor3ub() or glColor4ub to set the color. In these function names, "ub" stands for "unsigned byte." Unsigned byte is an eight-bit data type with values in the range 0 to 255.
There are also versions of these functions with names ending in "v": glColor3fv(), glColor4fv(), glColor3ubv(), glColor4ubv(). These functions have one parameter that is a pointer to an array that contains the color component values. In fact, there are may other variations in the glColor* family of functions, but the one's I have mentioned are probably the most common. Here are some examples of commands for setting drawing colors in OpenGL:
glColor3f(0,0,0); // Draw in black. glColor3f(1,0,0); // Draw in red. glColor3ub(1,0,0); // Draw in a color just a tiny bit different from // black. (The suffix, "ub" or "f", is significant!) glColor3ub(255,0,0); // Draw in red. glColor4f(1, 0, 0, 0.5); // Draw in transparent red, but only if OpenGL // has been configured to do transparency. By // default this is the same as drawing in plain red. float darkCyan[] = { 0.0, 0.5, 0.5 }; glColor3fv(darkCyan); // Draw in a dark blue/green color.
Using any of these functions sets the value of a "current color." When you generate a vertex with one of the glVertex* functions, the current color is saved along with the vertex coordinates as an attribute of the vertex. We'll see that vertices can have other kinds of attribute as well as color. One interesting point about OpenGL is that colors are associated with individual vertices, not with complete shapes. By changing the current color between calls to glBegin() and glEnd(), you can get a shape in which different vertices have different color attributes. When you do this, OpenGL will compute the colors of pixels inside the shape by blending the colors of the vertices. (Again, since OpenGL is extremely configurable, I have to note that blending is just the default behavior.) For example, here is a triangle in which the three vertices are assigned the colors red, green, and blue:
This image is often used as a kind of "Hello World" example for OpenGL. Here's the C code that was used to draw it:
glBegin(GL_TRIANGLES); glColor3f( 1, 0, 0 ); // red glVertex2f( -0.5, -0.5 ); glColor3f( 0, 1, 0 ); // green glVertex2f( 0.5, -0.5 ); glColor3f( 0, 0, 1 ); // blue glVertex2f( 0, 0.5 ); glEnd();
Note that you do not have to explicitly set a color for each vertex, as was done here. If you want a shape that is all one color, you just have to set the current color once, before drawing the shape (or just after the call to glBegin().)
It would be possible to clear the drawing area to a background color by drawing a colored rectangle, but OpenGL has a potentially more efficient way to do it. The function
glClearColor(r,g,b,a);
sets up the color to be used for clearing. The parameters are floating point values in the range 0 to 1, and there are no variants of this function. The default clear color is black, with 0 for all the color components. To fill the drawing area with the clear color, call
glClear( GL_COLOR_BUFFER_BIT );
The parameter is a constant integer that says that it is the drawing area that is being cleared. The correct term for what I have been calling the drawing area is the color buffer, where buffer is a general term referring to a region in memory. OpenGL uses several buffers in addition to the color buffer. We will encounter the "depth buffer" very soon. It is possible to use glClear() to clear several buffers at the same time by combining the constants that represent them with a logical OR operation. For example,
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
We will generally use glClear() in this form.
An obvious point about viewing in 3D is that one object can be behind another object. When this happens, the back object is hidden from the viewer by the front object. When we create an image of a 3D world, we have to make sure that objects that are supposed to be hidden behind other objects are in fact not visible in the image. This is the hidden surface problem.
The solution might seem simple enough: Just draw the objects in order from back to front. If one object is behind another, the back object will be covered up later when the front object is drawn. This is called the painter's algorithm. It's essentially what you are used to doing in 2D. Unfortunately, it's not so easy to implement. First of all, you can have objects that intersect, so that part of each object is hidden by the other. Whatever order you draw the objects in, there will be some points where the wrong object is visible. To fix this, you would have to cut the objects into pieces, along the intersection, and treat the pieces as as separate objects. In fact, there can be problems even if there are no intersecting objects: It's possible to have three non-intersecting objects where the first object hides part of the second, the second hides part of the first, and the third hides part of the first. The painter's algorithm will fail no matter which order you draw the three objects. The solution again is to cut the objects into pieces, but now it's not so obvious where to cut. Consider the famous example of the Borromean rings (public domain image from wikimedia):
Even though these problems can be solved, there is another issue. The correct drawing order can change when the point of view is changed or when a geometric transformation is applied, which means that the correct drawing order has to be recomputed every time that happens. In an animation, that would mean for every frame.
So, OpenGL does not use the painter's algorithm. Instead, it uses a technique called the depth test. The depth test solves the hidden surface problem no matter what order the objects are drawn in, so you can draw them in any order you want! The depth test requires that an extra number be stored for each pixel in the image. This extra memory makes up the depth buffer that I mentioned earlier. During the drawing process, the depth buffer is used to keep track of what is currently visible at each pixel. When a second object is drawn at that pixel, the information in the depth buffer can be used to decide whether the new object is in front of or behind the object that is currently visible there. If the new object is in front, then the color of the pixel is changed to show the new object, and the depth buffer is also updated. If the new object is behind the current object, then the data for the new object is discarded and the color and depth buffers are left unchanged.
By default, the depth test is not turned on, which can lead to very bad results when drawing in 3D. You can enable the depth test by calling
glEnable( GL_DEPTH_TEST );
It can be turned off by calling glDisable(GL_DEPTH_TEST). (There are many OpenGL settings that are turned on and off using glEnable/glDisable.)
You don't necessarily need to know any more about how the depth test actually works, but here are some details: For each pixel, the depth buffer stores a representation of the distance from the user of the point that is currently visible at that pixel. This value is essentially the z-coordinate of the point, after transformations have been applied. (In fact, the depth buffer is often called the z-buffer.) The possible z-coordinates are scaled to the range 0 to 1. The value 1 in the depth buffer represents the background, which is behind any object that can be drawn. When you clear the depth buffer, it is filled with a 1 at each pixel. In OpenGL, the range of z-coordinates can be shown must be limited; the use of the depth buffer is the reason for this limitation.
The depth buffer algorithm can give some strange results when two objects lie at exactly the same distance from the user. Logically, it's not even clear which object should be visible, but the real problem with the depth test is that it might show one object at some points and the second object at some other points. This is possible because numerical calculations are not perfectly accurate. Here an actual example:
In the two pictures shown here, a gray square was drawn, followed by a white square, followed by a black square. The squares all lie in the same plane. A very small rotation was applied, to make the computer do some calculations. The picture on the right was drawn with the depth test disabled. On the left, the depth test was enabled, and you can see the strange result.
By the way, the discussion here assumes that there are no transparent objects. Unfortunately, the depth test does not handle transparency correctly.
We've seen that geometric transformations are an important part of 2D computer graphics. In three dimensions, they become even more essential. The three basic types of transformation—scaling, rotation, and translation—extend to three dimensions with just a few changes. Translation is easiest. In 2D, a translation adds some number onto each coordinate. The same is true in 3D; we just need three numbers, to specify the amour of motion in the direction of each of the coordinate axes. A translation by (tx,ty,tz) transforms the point (x,y,z) to (x+tx, y+ty, z+tz).
Similarly, a 3D scaling transformation requires three scale factors, one to specify the amount of scaling in each of the coordinate directions. A scaling by (sx,sy,sz) transforms the point (x,y,z) to (x*sx, y*sy, z*sz).
Rotation is harder. First, you need to understand a little more about three-dimensional coordinate systems. The standard OpenGL coordinate system is a right-handed coordinate system. The term comes from the fact that if you curl the fingers of your right hand in the direction from the positive x-axis to the positive y-axis, and extend your thumb out from your fist, then your thumb will point in the direction of the positive z-axis. If you do this for the x and y-axes in on the screen in OpenGL's standard orientation, then your thumb will point towards your face. There are also left-handed coordinate systems, where curling the fingers of your left hand from the positive x-axis to the positive y-axis will point your left thumb in the direction of the positive z-axis. Note that the left/right handed distinction is not a property of the world, just of the way that one chooses to lay out coordinates on the world. You can transform a right-handed system to a left-handed system with the right kind of geometric transformation. For example, scaling by (1,1,−1) would do so. That transformation represents a reflection through the xy-plane, and it reverses the orientation of the z-axis. We'll see in a minute how the left-handed/right-handed distinction applies to rotations.
In 2D, rotation is rotation about a point, which we usually select to be the origin. In 3D, rotation is rotation about a line, which is called the axis of rotation. Think of the Earth rotating about its axis. The axis of rotation stays fixed, and points that are not on the axis move in circles about the axis. Any line can be an axis of rotation, but we generally use an axis that passes through the origin. The most common rotations are rotation about the x-axis, rotation about the y-axis, and rotation about the z-axis, but it's convenient to be able to use any line through the origin as the axis. There is an easy way to specify the line: Just specify one other point that is on the line, in addition to the origin. That's how things are done in OpenGL: An axis of rotation is specified by three numbers, (dx,dy,dz), which are not all zero. The axis is the line through (0,0,0) and (dx,dy,dz). To specify a rotation transformation in 3D, you have to specify an axis and the angle of rotation about that axis.
We still have to account for the difference between positive and negative angles. We can't just say clockwise or counterclockwise. If you look down on the rotating Earth from above the North pole, you see a clockwise rotation; if you look down on it from about the South pole, you see a counterclockwise rotation. So, the difference between the two is not well-defined. To define the direction of rotation in 3D, we use the right-hand rule: Point the extended thumb of your right hand in the direction from (0,0,0) to (dx,dy,dz), where (dx,dy,dz) is the point that specifies the axis of rotation. Then the direction of rotation for positive angles is given by the direction in which your fingers curl. However, I need to emphasize that the right-hand rule only works if you are using a right-handed coordinate system. If you have switched to a left-handed coordinate system, then you need to use a left-hand rule to determine the direction of rotation for positive angles.
The functions for doing geometric transformations in OpenGL are glScalef(), glRotatef(), and glTranslatef(). The "f" indicates that the parameters are of type float. There are also versions ending in "d" to indicate parameters of type double. As you would expect, 3D scaling and translation require three parameters. Rotation requires four parameters: The first is the angle of rotation measured in degrees (not radians), and the other three parameters specify the axis of rotation. In the command glRotatef(angle,dx,dy,dz), the rotation axis is the line through the points (0,0,0) and (dx,dy,dz). At least one of dx, dy, and dz should be non-zero. Here are a few examples:
glScalef(2,2,2); // Uniform scaling by a factor of 2. glScalef(0.5,1,1); // Shrink by half in the x-direction only. glScalef(0,0,-1); // Reflect through the xy-plane. glTranslatef(1,0,0); // Move 1 unit in the positive x-direction. glTranslatef(3,5,-7.5); // Move (x,y,z) to (x+3, y+5, z-7.5) glRotatef(90,1,0,0); // Rotate 90 degrees about the x-axis. // Moves the +y axis onto the +z axis // and the +z axis onto the -y axis. glRotatef(-90,-1,0,0); // Has exactly the same effect as the previous rotation. glRotatef(90,0,1,0); // Rotate 90 degrees about the y-axis. // Moves the +z axis onto the +x axis // and the +x axis onto the -z axis. glRotatef(90,0,0,1); // Rotate 90 degrees about the z-axis. // Moves the +x axis onto the +y axis // and the +y axis onto the -x axis. glRotatef(30,1.5,2,-3); // Rotate 30 degrees about the line through // the points (0,0,0) and (1.5,2,-3).
The function glLoadIdentity() can be called to discard any transformations that have been applied. It takes no parameters. The identity transformation is one that has no effect; that is, it leaves every point fixed. Calling glLoadIdentity() gives you a known starting point for setting up transformations.
You already know that when doing hierarchical modeling, you need to be able to save the current transformation and restore it later. And you know that one way to do that is with a matrix stack. OpenGL has direct support for matrix stacks. Calling glPushMatrix() saves a copy of the current transformation on the matrix stack. Calling glPopMatrix() restores a transformation that was saved by glPushMatrix() by removing the top transformation from the stack and installing it as the current transformation.
As a simple example, we will draw a 1-by-1-by-1 cube centered at the origin, where each face of the cube is a different color. For convenience, we use a short function to draw a square in a specified color. The square is drawn with its center at the point (0,0,0.5), so that it is in position to be the front face of the cube.
void square(float r, float g, float b) { glColor3f(r,g,b); // The color for the square. glTranslatef(0,0,0.5); // Move square 0.5 units forward. glBegin(GL_TRIANGLE_FAN); glVertex2f(-0.5,-0.5); // Draw the square (before the glVertex2f(0.5,-0.5); // the translation is applied) glVertex2f(0.5,0.5); // on the xy-plane, with its glVertex2f(-0.5,0.5); // at (0,0,0). glEnd(); }
Given this function, we can draw the cube as follows. In this code, the cube is rotated so that several faces are visible; without the rotation, only the front face would be visible. The square() function can be used to draw the front face directly, but the call to that function is still surrounded by glPushMatrix/glPopMatrix to prevent the translation inside the function from affecting the rest of the drawing. We can get the other faces of the cube by rotating the square from its original position to make it coincide with one of the other faces of the cube. For example, we get the back face of the cube by rotating 180 degrees about the y-axis, and the top face by rotating −90 degrees about the x-axis. Each face uses glPushMatrix/glPopMatrix to make sure that the transformations that are applied to the face affect only that face.
glLoadIdentity(); // start with no transformation glRotatef(15,0,1,0); // rotate the cube 15 degrees about y-axis glRotatef(15,1,0,0); // rotate the cube 15 degrees about x-axis // Now, draw the six faces of the cube by drawing its six faces. glPushMatrix(); square(1,0,0); // front face is red glPopMatrix(); glPushMatrix(); glRotatef(180,0,1,0); // rotate square to back face square(0,1,1); // back face is cyan glPopMatrix(); glPushMatrix(); glRotatef(-90,0,1,0); // rotate square to left face square(0,1,0); // left face is green glPopMatrix(); glPushMatrix(); glRotatef(90,0,1,0); // rotate square to right face square(1,0,1); // right face is magenta glPopMatrix(); glPushMatrix(); glRotatef(-90,1,0,0); // rotate square to top face square(0,0,1); // top face is blue glPopMatrix(); glPushMatrix(); glRotatef(90,1,0,0); // rotate square to bottom face square(1,1,0); // bottom face is yellow glPopMatrix();
Here's an image that was produced from this code:
The coordinate transformations that I have been discussing here operate on 3D space. They make up what is called the modelview transformation, since it is used for geometric modeling and for selecting the point of view. But we also need another type of transformation: the projection transformation that maps the 3D world onto a 2D viewing surface. I will come back to the whole topic of transforms later, but for now, there are a few things you need to know.
The first point is that OpenGL keeps the projection transformation separate from the modelview transformation. The projection transformation is always applied last, no matter when you specify it. Unfortunately, the same set of functions can be used to manipulate both kinds of transform, so you have to tell OpenGL which transformation you want to work on. This is done with the glMatrixMode() function. To start working on the projection transformation, call
glMatrixMode( GL_PROJECTION );
To go back to working on the modelview transformation, call
glMatrixMode( GL_MODELVIEW );
It's common to leave the matrix mode set to GL_MODELVIEW, except when you are actually working in another mode. Here is the code that might be used to set up the projection:
glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho( xmin, xmax, ymin, ymax, zmin, zmax ); glMatrixMode(GL_MODELVIEW);
The glOrtho command does the work. The parameters specify the ranges of x, y, and z that will be visible in the drawing area, and the projection is done by discarding the z-coordinate. This type of projection is not very realistic, and we will look at alternatives later.
Another aspect of 3D graphics that I will mostly defer for now is the effect of lighting on a scene. To get a realistic scene, you have to simulate light sources and the interaction of light with objects in the scene. OpenGL uses a very simplified lighting model, but it can still add significantly to the realism of an image. However, by default, lighting calculations are disabled. To enable them, you have to call
glEnable( GL_LIGHTING ); // enable lighting calculations glEnable( GL_LIGHT0 ); // turn on light source number 0
This turns on very basic lighting, with a single light source of white light that shines from the direction of the viewer onto the scene. However, when you do this, everything in the scene will be a dull gray color. The problem is that the lighting calculation uses something called material instead of simple color. A surface's material properties determine how it interacts with light. In addition to simple color, it can include properties such as texture and reflectivity. Fortunately, it's possible to tell OpenGL to treat the color as specified by glColor* as the basic material color. You can do this by calling
glEnable( GL_COLOR_MATERIAL ); // use glColor* values as material color
Another factor that enters into the lighting calculation is the angle at which light hits a surface. Light that shines directly on a surface illuminates it more brightly than light that hits the surface at a glancing angle. However, to determine this angle, OpenGL needs to know what direction the surface is facing. This direction is determined by the normal vector to the surface. A normal vector indicates the direction that is perpendicular to the surface.
We will return to the topic of lighting, material, and normal vectors in much more detail in a later section.