跳转至

3.1 OpenGL 1.1 中的形状和颜色

本节介绍了OpenGL的一些核心特性。本节讨论的大部分内容都限于二维。目前,你只需要知道关于三维的是,它在二维的基础上增加了第三个方向。按照惯例,第三个方向被称为z。在默认坐标系统中,x和y轴位于图像平面上,而z轴的正方向指向与图像垂直的方向。

在OpenGL的默认坐标系统中,图像显示了一个三维空间的区域,其中x、y和z的取值范围都在-1到1之间。要显示不同的区域,你必须应用一个变换。目前,我们只会使用位于-1和1之间的坐标。

关于编程的一点说明:OpenGL可以用许多不同的编程语言实现,但API规范更多地假设了语言是C。(参见第A.2节中对C的简短介绍。)在很大程度上,C规范直接转换为其他语言。主要的区别在于C语言中数组的特殊特性。我的示例将遵循C语法,并简要说明其他语言中的不同之处。由于我遵循的是C API,我会引用“函数”而不是“子例程”或“方法”。第3.6节详细介绍了如何使用C和Java编写OpenGL程序。在进行实际编程之前,你需要参考该部分。本书的OpenGL 1.1实时演示是使用一个JavaScript模拟器编写的,该模拟器实现了OpenGL 1.1的一个子集。该模拟器在小节3.6.3中有所介绍。

This section introduces some of the core features of OpenGL. Much of the discussion in this section is limited to 2D. For now, all you need to know about 3D is that it adds a third direction to the x and y directions that are used in 2D. By convention, the third direction is called z. In the default coordinate system, the x and y axes lie in the plane of the image, and the positive direction of the z-axis points in a direction perpendicular to the image.

In the default coordinate system for OpenGL, the image shows a region of 3D space in which x, y, and z all range from minus one to one. To show a different region, you have to apply a transform. For now, we will just use coordinates that lie between -1 and 1.

A note about programming: OpenGL can be implemented in many different programming languages, but the API specification more or less assumes that the language is C. (See Section A.2 for a short introduction to C.) For the most part, the C specification translates directly into other languages. The main differences are due to the special characteristics of arrays in the C language. My examples will follow the C syntax, with a few notes about how things can be different in other languages. Since I'm following the C API, I will refer to "functions" rather than "subroutines" or "methods." Section 3.6 explains in detail how to write OpenGL programs in C and in Java. You will need to consult that section before you can do any actual programming. The live OpenGL 1.1 demos for this book are written using a JavaScript simulator that implements a subset of OpenGL 1.1. That simulator is discussed in Subsection 3.6.3.

3.1.1 OpenGL基元

OpenGL Primitives

OpenGL只能绘制一些基本的形状,包括点、线和三角形。它没有内置的曲线或曲面支持;它们必须由简单的形状近似。这些基本形状被称为原始图元。在OpenGL中,原始图元由其顶点定义。顶点简单地是3D空间中的一个点,由其x、y和z坐标给出。让我们直接开始看如何绘制一个三角形。这需要几个步骤:

glBegin(GL_TRIANGLES);
glVertex2f(-0.7, -0.5);
glVertex2f(0.7, -0.5);
glVertex2f(0, 0.7);
glEnd();

三角形的每个顶点都由对glVertex2f函数的调用指定。顶点必须在glBeginglEnd之间指定。glBegin的参数告诉正在绘制哪种类型的原始图元。GL_TRIANGLES原始图元允许您绘制多个三角形:只需为要绘制的每个三角形指定三个顶点即可。请注意,即使在OpenGL 1.1中,使用glBegin/glEnd也不是指定原始图元的首选方法。然而,替代方法,即在第3.4.2小节中介绍的方法,使用起来更复杂。您应该认为glBegin/glEnd是了解顶点及其属性的便捷方法,但不是您在现代图形API中实际操作的方式。

(我应该指出,OpenGL函数实际上只是向GPU发送命令。OpenGL可以保存命令批次以一起传输,并且绘制实际上直到命令被传输才会完成。为了确保这一点发生,必须调用函数glFlush()。在某些情况下,这个函数可能会被OpenGL API自动调用,但您可能会遇到需要自己调用它的情况。)

对于OpenGL,顶点有三个坐标。函数glVertex2f指定了顶点的x和y坐标,而z坐标设置为零。还有一个函数glVertex3f,它指定了所有三个坐标。名称中的“2”或“3”告诉函数传递了多少个参数。“f”在名称末尾表示参数的类型为float。事实上,还有其他的“glVertex”函数,包括以int或double类型的参数为参数的版本,名称类似glVertex2i和glVertex3d。甚至有一些版本接受四个参数,尽管暂时还不清楚为什么它们应该存在。正如我们后面将看到的,还有一些版本接受一个数字数组而不是单独的数字作为参数。整套顶点函数通常被称为“glVertex”,其中“”代表参数规范。(名称的增多是由于C编程语言不支持函数名的重载;也就是说,C只通过函数名而不是通过传递给函数的参数的数量和类型来区分函数。)

OpenGL 1.1有十种原始图元。其中七种仍然存在于现代OpenGL中;另外三种已经被移除。最简单的原始图元是GL_POINTS,它在原始图元的每个顶点处简单地渲染一个点。默认情况下,点被渲染为单个像素。点原始图元的大小可以通过调用

glPointSize(size);

来改变,其中参数size的类型为float,指定了渲染点的直径,以像素为单位。默认情况下,点是正方形的。您可以通过调用

glEnable(GL_POINT_SMOOTH);

来获取圆形的点。函数glPointSize和glEnable改变了OpenGL的“状态”。状态包括所有影响渲染的设置。我们将遇到许多改变状态的函数。函数glEnable和glDisable可以用来打开和关闭许多功能。通常的规则是,任何需要额外计算的渲染特性默认都是关闭的。如果您想要该功能,则必须通过使用适当的参数调用glEnable来打开它。

有三种用于绘制线段的原始图元:GL_LINES、GL_LINE_STRIP和GL_LINE_LOOPGL_LINES绘制不连接的线段;为要绘制的每个线段指定两个顶点。另外两个原始图元绘制连接的线段序列。唯一的区别在于GL_LINE_LOOP在最后一个顶点和第一个顶点之间添加了额外的线段。如果使用迄今为止我们看到的四种原始图元相同的六个顶点,则会得到以下结果:

123

这些点A、B、C、D、E和F按照这个顺序被指定。在这个示例中,所有点都位于同一个平面上,但请记住,通常情况下,点可以位于3D空间的任何位置。

线段原始图元的宽度可以通过调用glLineWidth(width)来设置。线宽始终以像素为单位指定。它不受变换的缩放影响。

让我们看一个例子。OpenGL没有圆形原始图元,但我们可以通过绘制一个边数很多的多边形来近似圆形。为了绘制多边形的轮廓,我们可以使用GL_LINE_LOOP原始图元:

glBegin(GL_LINE_LOOP);
for (i = 0; i < 64; i++) {
    angle = 6.2832 * i / 64;  // 6.2832代表2*PI
    x = 0.5 * cos(angle);
    y = 0.5 * sin(angle);
    glVertex2f(x, y);
}
glEnd();

这样就绘制了一个半径为0.5、中心在(0,0)的圆的近似轮廓。请记住,要了解如何在一个完整、运行的程序中使用像这样的示例,您将需要阅读第3.6节。此外,根据您使用的OpenGL实现,您可能需要对代码进行一些更改。

下一组原始图元用于绘制三角形。其中有三种:GL_TRIANGLESGL_TRIANGLE_STRIPGL_TRIANGLE_FAN

123

左侧的三个三角形组成了一个GL_TRIANGLES原始图元,共有九个顶点。对于该原始图元,每组三个顶点形成一个单独的三角形。对于GL_TRIANGLE_STRIP原始图元,前三个顶点生成一个三角形。之后,每个新顶点都会向条带添加另一个三角形,连接新顶点到前两个顶点。右侧显示了两个GL_TRIANGLE_FAN原始图元。同样地,对于GL_TRIANGLE_FAN,前三个顶点形成一个三角形,之后的每个顶点都添加另一个三角形,但在这种情况下,新的三角形是通过将新顶点连接到上一个顶点和最初指定的第一个顶点(图片中的顶点“A”)而形成的。请注意,GL_TRIANGLE_FAN可用于绘制填充多边形。在这张图片中,点和线不是原始图元的一部分;OpenGL只会绘制图形的填充部分,即绿色的内部。

剩下的三个原始图元已经从现代OpenGL中删除,它们分别是GL_QUADSGL_QUAD_STRIPGL_POLYGON。名称“quad”是四边形的简称,即四边形。一个四边形由四个顶点确定。为了在OpenGL中正确渲染四边形,四边形的所有顶点必须位于同一个平面上。多边形原始图元也是如此。同样地,为了正确渲染,四边形和多边形必须是凸的(参见第2.2.3小节)。由于OpenGL不检查是否满足这些条件,因此使用四边形和多边形是容易出错的。由于可以很容易地使用三角形原始图元生成相同的形状,它们实际上并不是必需的,但是这里记录一些例子:

123

这些原始图元的顶点按照顺序A、B、C、...指定。注意两个四边形原始图元的顺序不同:对于GL_QUADS,每个单独的四边形的顶点应按照四边形周围的逆时针顺序指定;对于GL_QUAD_STRIP,顶点应该从带的一侧交替到另一侧。

OpenGL can draw only a few basic shapes, including points, lines, and triangles. There is no built-in support for curves or curved surfaces; they must be approximated by simpler shapes. The basic shapes are referred to as primitives. A primitive in OpenGL is defined by its vertices. A vertex is simply a point in 3D, given by its x, y, and z coordinates. Let's jump right in and see how to draw a triangle. It takes a few steps:

glBegin(GL_TRIANGLES);
glVertex2f( -0.7, -0.5 );
glVertex2f( 0.7, -0.5 );
glVertex2f( 0, 0.7 );
glEnd();

Each vertex of the triangle is specified by a call to the function glVertex2f. Vertices must be specified between calls to glBegin and glEnd. The parameter to glBegin tells which type of primitive is being drawn. The GL_TRIANGLES primitive allows you to draw more than one triangle: Just specify three vertices for each triangle that you want to draw. Note that using glBegin/glEnd is not the preferred way to specify primitives, even in OpenGL 1.1. However, the alternative, which is covered in Subsection 3.4.2, is more complicated to use. You should consider glBegin/glEnd to be a convenient way to learn about vertices and their properties, but not the way that you will actually do things in modern graphics APIs.

(I should note that OpenGL functions actually just send commands to the GPU. OpenGL can save up batches of commands to transmit together, and the drawing won't actually be done until the commands are transmitted. To ensure that that happens, the function glFlush() must be called. In some cases, this function might be called automatically by an OpenGL API, but you might well run into times when you have to call it yourself.)

For OpenGL, vertices have three coordinates. The function glVertex2f specifies the x and y coordinates of the vertex, and the z coordinate is set to zero. There is also a function glVertex3f that specifies all three coordinates. The "2" or "3" in the name tells how many parameters are passed to the function. The "f" at the end of the name indicates that the parameters are of type float. In fact, there are other "glVertex" functions, including versions that take parameters of type int or double, with named like glVertex2i and glVertex3d. There are even versions that take four parameters, although it won't be clear for a while why they should exist. And, as we will see later, there are versions that take an array of numbers instead of individual numbers as parameters. The entire set of vertex functions is often referred to as "glVertex", with the "" standing in for the parameter specification. (The proliferation of names is due to the fact that the C programming language doesn't support overloading of function names; that is, C distinguishes functions only by their names and not by the number and type of parameters that are passed to the function.)

OpenGL 1.1 has ten kinds of primitive. Seven of them still exist in modern OpenGL; the other three have been dropped. The simplest primitive is GL_POINTS, which simply renders a point at each vertex of the primitive. By default, a point is rendered as a single pixel. The size of point primitives can be changed by calling

glPointSize(size);

where the parameter, size, is of type float and specifies the diameter of the rendered point, in pixels. By default, points are squares. You can get circular points by calling

glEnable(GL_POINT_SMOOTH);

The functions glPointSize and glEnable change the OpenGL "state." The state includes all the settings that affect rendering. We will encounter many state-changing functions. The functions glEnable and glDisable can be used to turn many features on and off. In general, the rule is that any rendering feature that requires extra computation is turned off by default. If you want that feature, you have to turn it on by calling glEnable with the appropriate parameter.

There are three primitives for drawing line segments: GL_LINES, GL_LINE_STRIP, and GL_LINE_LOOP. GL_LINES draws disconnected line segments; specify two vertices for each segment that you want to draw. The other two primitives draw connected sequences of line segments. The only difference is that GL_LINE_LOOP adds an extra line segment from the final vertex back to the first vertex. Here is what you get if use the same six vertices with the four primitives we have seen so far:

123

The points A, B, C, D, E, and F were specified in that order. In this illustration, all the points lie in the same plane, but keep in mind that in general, points can be anywhere in 3D space.

The width for line primitives can be set by calling glLineWidth(width). The line width is always specified in pixels. It is not subject to scaling by transformations.

Let's look at an example. OpenGL does not have a circle primitive, but we can approximate a circle by drawing a polygon with a large number of sides. To draw an outline of the polygon, we can use a GL_LINE_LOOP primitive:

glBegin( GL_LINE_LOOP );
for (i = 0; i < 64; i++) {
    angle = 6.2832 * i / 64;  // 6.2832 represents 2*PI
    x = 0.5 * cos(angle);
    y = 0.5 * sin(angle);
    glVertex2f( x, y );
}
glEnd();

This draws an approximation for the circumference of a circle of radius 0.5 with center at (0,0). Remember that to learn how to use examples like this one in a complete, running program, you will have to read Section 3.6. Also, you might have to make some changes to the code, depending on which OpenGL implementation you are using.

The next set of primitives is for drawing triangles. There are three of them: GL_TRIANGLES, GL_TRIANGLE_STRIP, and GL_TRIANGLE_FAN.

123

The three triangles on the left make up one GL_TRIANGLES primitive, with nine vertices. With that primitive, every set of three vertices makes a separate triangle. For a GL_TRIANGLE_STRIP primitive, the first three vertices produce a triangle. After that, every new vertex adds another triangle to the strip, connecting the new vertex to the two previous vertices. Two GL_TRIANGLE_FAN primitives are shown on the right. Again for a GL_TRIANGLE_FAN, the first three vertices make a triangle, and every vertex after that adds anther triangle, but in this case, the new triangle is made by connecting the new vertex to the previous vertex and to the very first vertex that was specified (vertex "A" in the picture). Note that Gl_TRIANGLE_FAN can be used for drawing filled-in polygons. In this picture, by the way, the dots and lines are not part of the primitive; OpenGL would only draw the filled-in, green interiors of the figures.

The three remaining primitives, which have been removed from modern OpenGL, are GL_QUADS, GL_QUAD_STRIP, and GL_POLYGON. The name "quad" is short for quadrilateral, that is, a four-sided polygon. A quad is determined by four vertices. In order for a quad to be rendered correctly in OpenGL, all vertices of the quad must lie in the same plane. The same is true for polygon primitives. Similarly, to be rendered correctly, quads and polygons must be convex (see Subsection 2.2.3). Since OpenGL doesn't check whether these conditions are satisfied, the use of quads and polygons is error-prone. Since the same shapes can easily be produced with the triangle primitives, they are not really necessary, but here for the record are some examples:

123

The vertices for these primitives are specified in the order A, B, C, .... Note how the order differs for the two quad primitives: For GL_QUADS, the vertices for each individual quad should be specified in counterclockwise order around the quad; for GL_QUAD_STRIP, the vertices should alternate from one side of the strip to the other.

3.1.2 OpenGL颜色

OpenGL Color

OpenGL有一大堆函数,可以用来为我们绘制的几何图形指定颜色。这些函数的名称形式为glColor,其中“”代表一个后缀,用于指定参数的数量和类型。我现在应该警告你,对于逼真的3D图形,OpenGL有一个更复杂的颜色概念,使用了一组不同的函数。你将在下一章中学习到这一点,但现在我们将专注于glColor*。

例如,函数glColor3f有三个类型为float的参数。这些参数以0.0到1.0的范围内的数字给出了颜色的红色、绿色和蓝色分量。(实际上,允许超出这个范围的值,甚至是负值。当颜色值用于计算时,超出范围的值将被视为给定值。当颜色实际上出现在屏幕上时,其分量值将被夹紧到0到1的范围内。也就是说,小于零的值将被更改为零,大于一的值将被更改为一。)

你可以通过使用glColor4f()来添加第四个分量到颜色中。第四个分量,称为alpha,不会在默认绘制模式下使用,但可以配置OpenGL将其用作颜色的透明度程度,类似于我们所看到的2D图形API中的alpha分量的使用。你需要两条命令来启用透明度:

glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

第一条命令启用了alpha分量的使用。它可以通过调用glDisable(GL_BLEND)来禁用。当GL_BLEND选项被禁用时,alpha会被简单地忽略。第二条命令告诉颜色的alpha分量将如何使用。这里显示的参数是最常见的;它们以通常的方式实现了透明度。我应该指出的是,虽然在2D中透明度运作良好,但在3D中正确使用透明度要困难得多。

如果您想要使用范围在0到255之间的整数颜色值,您可以使用glColor3ub()或glColor4ub来设置颜色。在这些函数名称中,“ub”代表“无符号字节”。无符号字节是一个八位数据类型,其值在0到255范围内。下面是一些在OpenGL中设置绘图颜色的命令示例:

glColor3f(0,0,0);         // 绘制黑色。

glColor3f(1,1,1);         // 绘制白色。

glColor3f(1,0,0);         // 绘制全强度的红色。

glColor3ub(1,0,0);        // 绘制一个与黑色略有不同的颜色。 (后缀“ub”或“f”很重要!)

glColor3ub(255,0,0);      // 绘制全强度的红色。

glColor4f(1, 0, 0, 0.5);  // 绘制半透明的红色,但只有在OpenGL被配置为支持透明度时才会生效。默认情况下,这与绘制纯红色相同。

使用任何这些函数都会设置“当前颜色”的值,该值是OpenGL状态的一部分。当您使用glVertex*函数之一生成顶点时,当前颜色将与顶点坐标一起保存,作为顶点的一个属性。我们将看到,顶点除了颜色之外还可以具有其他类型的属性。关于OpenGL的一个有趣的地方是,颜色与单个顶点相关联,而不是与完整的形状相关联。通过在glBegin()和glEnd()之间调用时更改当前颜色,您可以获得具有不同颜色属性的形状,不同的顶点。当您这样做时,OpenGL将通过对顶点颜色进行插值来计算形状内部的像素颜色。(再次说明,由于OpenGL非常可配置,我必须指出,颜色插值只是默认行为。)例如,下面是一个三角形,其中三个顶点分别被赋予红色、绿色和蓝色:

123

这个图像经常被用作OpenGL的一种“Hello World”示例。可以用以下命令绘制三角形:

glBegin(GL_TRIANGLES);
glColor3f(1, 0, 0); // 红色
glVertex2f(-0.8, -0.8);
glColor3f(0, 1, 0); // 绿色
glVertex2f(0.8, -0.8);
glColor3f(0, 0, 1); // 蓝色
glVertex2f(0, 0.9);
glEnd();

请注意,在绘制原始图元时,不需要像这里那样为每个顶点显式设置颜色。如果您想要一个完全相同颜色的形状,您只需要在绘制形状之前(或者在glBegin()调用之后)设置当前颜色一次即可。例如,我们可以用以下命令绘制一个实心的黄色三角形:

glColor3ub(255,255,0);  // 黄色
glBegin(GL_TRIANGLES);
glVertex2f(-0.5, -0.5);
glVertex2f(0.5, -0.5);
glVertex2f(0, 0.5);
glEnd();

同时请记住,顶点的颜色在生成顶点的glVertex*调用之前指定。

这是一个交互式演示,绘制了基本的OpenGL三角形,具有不同颜色的顶点。您可以控制顶点的颜色,以查看三角形内部的插值颜色受到的影响。这是我们的第一个OpenGL示例。实际上,这个演示使用了WebGL,因此您可以将其用作测试,以检查您的Web浏览器是否支持WebGL。

示例程序 jogl/FirstTriangle.java 使用Java绘制了基本的OpenGL三角形。程序 glut/first-triangle.c 使用C语言完成了相同的任务。而glsim/first-triangle.html 是一个使用了我的JavaScript模拟器的版本,该模拟器仅实现了本书中涵盖的OpenGL 1.1的部分。这些程序中的任何一个都可以用来在OpenGL中进行2D绘图的实验。您也可以用它们来测试您的OpenGL编程环境。


一个常见的操作是通过用某种背景颜色填充来清除绘图区域。可以通过绘制一个大的彩色矩形来实现,但是OpenGL有一种可能更有效的方法。该函数

glClearColor(r,g,b,a);

设置要用于清除绘图区域的颜色。(这只是设置颜色;颜色直到您实际给出清除绘图区域的命令时才会被使用。)参数是范围在0到1之间的浮点值。这个函数没有变体;您必须提供所有四个颜色分量,它们必须在0到1的范围内。默认的清除颜色是全零,即,颜色为黑色,alpha分量也等于零。执行实际清除的命令是:

glClear(GL_COLOR_BUFFER_BIT);

我一直称之为绘图区域的正确术语是颜色缓冲区,其中“缓冲区”是指内存中的一个区域的通用术语。除了颜色缓冲区之外,OpenGL还使用了几个缓冲区。我们将在稍后遇到“深度缓冲区”。glClear命令可以用于一次清除多个不同的缓冲区,这可能比分开清除它们更有效,因为清除可以并行进行。glClear的参数告诉它要清除哪个缓冲区或哪些缓冲区。要一次清除多个缓冲区,请使用代表它们的常量与算术OR操作结合。例如,

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

这是在3D图形中通常使用的glClear形式,其中深度缓冲区起着至关重要的作用。对于2D图形,深度缓冲区通常不使用,因此glClear的适当参数只是GL_COLOR_BUFFER_BIT

OpenGL has a large collection of functions that can be used to specify colors for the geometry that we draw. These functions have names of the form glColor, where the "" stands for a suffix that gives the number and type of the parameters. I should warn you now that for realistic 3D graphics, OpenGL has a more complicated notion of color that uses a different set of functions. You will learn about that in the next chapter, but for now we will stick to glColor*.

For example, the function glColor3f has three parameters of type float. The parameters 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 the 2D graphics APIs that we have looked at. You need two commands to turn on transparency:

glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

The first command enables use of the alpha component. It can be disabled by calling glDisable(GL_BLEND). When the GL_BLEND option is disabled, alpha is simply ignored. The second command tells how the alpha component of a color will be used. The parameters shown here are the most common; they implement transparency in the usual way. I should note that while transparency works fine in 2D, it is much more difficult to use transparency correctly in 3D.

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. Here are some examples of commands for setting drawing colors in OpenGL:

glColor3f(0,0,0);         // Draw in black.

glColor3f(1,1,1);         // Draw in white.

glColor3f(1,0,0);         // Draw in full-intensity red.

glColor3ub(1,0,0);        // Draw in a color just a tiny bit different from
                        // black.  (The suffix, "ub" or "f", is important!)

glColor3ub(255,0,0);      // Draw in full-intensity 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.

Using any of these functions sets the value of a "current color," which is part of the OpenGL state. 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 will 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 interpolating the colors of the vertices. (Again, since OpenGL is extremely configurable, I have to note that interpolation of colors is just the default behavior.) For example, here is a triangle in which the three vertices are assigned the colors red, green, and blue:

123

This image is often used as a kind of "Hello World" example for OpenGL. The triangle can be drawn with the commands

glBegin(GL_TRIANGLES);
glColor3f( 1, 0, 0 ); // red
glVertex2f( -0.8, -0.8 );
glColor3f( 0, 1, 0 ); // green
glVertex2f( 0.8, -0.8 );
glColor3f( 0, 0, 1 ); // blue
glVertex2f( 0, 0.9 );
glEnd();

Note that when drawing a primitive, you do not need 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(). For example, we can draw a solid yellow triangle with

glColor3ub(255,255,0);  // yellow
glBegin(GL_TRIANGLES);
glVertex2f( -0.5, -0.5 );
glVertex2f( 0.5, -0.5 );
glVertex2f( 0, 0.5 );
glEnd();

Also remember that the color for a vertex is specified before the call to glVertex* that generates the vertex.

Here is an interactive demo that draws the basic OpenGL triangle, with different colored vertices. You can control the colors of the vertices to see how the interpolated colors in the interior of the triangle are affected. This is our first OpenGL example. The demo actually uses WebGL, so you can use it as a test to check whether your web browser supports WebGL.

The sample program jogl/FirstTriangle.java draws the basic OpenGL triangle using Java. The program glut/first-triangle.c does the same using the C programming language. And glsim/first-triangle.html is a version that uses my JavaScript simulator, which implements just the parts of OpenGL 1.1 that are covered in this book. Any of those programs could be used to experiment with 2D drawing in OpenGL. And you can use them to test your OpenGL programming environment.


A common operation is to clear the drawing area by filling it with some background color. It is be possible to do that by drawing a big colored rectangle, but OpenGL has a potentially more efficient way to do it. The function

glClearColor(r,g,b,a);

sets up a color to be used for clearing the drawing area. (This only sets the color; the color isn't used until you actually give the command to clear the drawing area.) The parameters are floating point values in the range 0 to 1. There are no variants of this function; you must provide all four color components, and they must be in the range 0 to 1. The default clear color is all zeros, that is, black with an alpha component also equal to zero. The command to do the actual clearing is:

glClear( GL_COLOR_BUFFER_BIT );

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" in just a moment. The glClear command can be used to clear several different buffers at the same time, which can be more efficient than clearing them separately since the clearing can be done in parallel. The parameter to glClear tells it which buffer or buffers to clear. To clear several buffers at once, combine the constants that represent them with an arithmetic OR operation. For example,

glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

This is the form of glClear that is generally used in 3D graphics, where the depth buffer plays an essential role. For 2D graphics, the depth buffer is generally not used, and the appropriate parameter for glClear is just GL_COLOR_BUFFER_BIT.

3.1.3 glColor 和 glVertex 与数组

glColor and glVertex with Arrays

我们已经看到了glColorglVertex的版本,它们接受不同数量和类型的参数。还有一些版本,让您将命令的所有数据放在单个数组参数中。这些版本的名称以“v”结尾。例如:glColor3fvglVertex2ivglColor4ubvglVertex3dv。实际上,“v”代表“向量”,基本上意味着一维数组。例如,在函数调用glVertex3fv(coords)中,coords将是一个包含至少三个浮点数的数组。

在OpenGL中存在数组参数强制要求在不同编程语言的OpenGL实现之间存在一些差异。Java中的数组与C中的数组不同,JavaScript中的数组也与两者都不同。让我们首先看看C语言中的情况,因为那是原始OpenGL API的语言。

在C语言中,数组变量是指针变量的一种变体,数组和指针可以在许多情况下互换使用。实际上,在C API中,数组参数实际上被指定为指针。例如,glVertex3fv的参数的类型是“指向浮点数的指针”。在对glVertex3fv的调用中的实际参数可以是数组变量,但也可以是指向三个浮点数序列开头的任何指针。例如,假设我们要绘制一个正方形。我们需要每个顶点的两个坐标。在C语言中,我们可以将所有8个坐标放入一个数组中,并使用glVertex2fv来提取我们需要的坐标:

float coords[] = { -0.5, -0.5,  0.5, -0.5,  0.5, 0.5,  -0.5, 0.5 };

glBegin(GL_TRIANGLE_FAN);
glVertex2fv(coords);      // 使用 coords[0] 和 coords[1]。
glVertex2fv(coords + 2);  // 使用 coords[2] 和 coords[3]。
glVertex2fv(coords + 4);  // 使用 coords[4] 和 coords[5]。
glVertex2fv(coords + 6);  // 使用 coords[6] 和 coords[7]。
glEnd();

这个例子使用了“指针算术”,其中 coords + N 表示数组的第N个元素的指针。另一种表示法是 &coords[N],其中“&”是地址运算符,&coords[N]表示“指向coords[N]的指针”。这对于只熟悉Java或JavaScript的人来说可能会感到非常陌生。在我的示例中,我将避免使用指针算术,但偶尔会使用地址运算符。

至于Java,设计JOGL的人想要保留从数组中提取数据的能力。但是,在Java中无法使用指针。解决方案是在JOGL API中用一对参数替换C API中的指针参数——一个参数用于指定包含数据的数组,另一个参数用于指定数组中数据的起始索引。例如,这是正方形绘制代码如何转换为Java:

float[] coords = { -0.5F, -0.5F,  0.5F, -0.5F,  0.5F, 0.5F,  -0.5F, 0.5F };

gl2.glBegin(GL2.GL_TRIANGLES);
gl2.glVertex2fv(coords, 0);  // 使用 coords[0] 和 coords[1]。
gl2.glVertex2fv(coords, 2);  // 使用 coords[2] 和 coords[3]。
gl2.glVertex2fv(coords, 4);  // 使用 coords[4] 和 coords[5]。
gl2.glVertex2fv(coords, 6);  // 使用 coords[6] 和 coords[7]。
gl2.glEnd();

参数方面真的没有太大的区别,尽管第一个glVertex2fv中的零有点让人讨厌。主要区别是前缀“gl2”和“GL2”,这是由JOGL API的面向对象特性所要求的。在这里我不会再多说JOGL的内容,但是如果您需要将我的示例转换为JOGL,您应该记住在处理数组时需要的额外参数。

作为记录,这里是我在本书中将要使用的glVertex*glColor*函数。这不是OpenGL中可用的完整集合:

glVertex2f( x, y );             glVertex2fv( xyArray );
glVertex2d( x, y );             glVertex2dv( xyArray );
glVertex2i( x, y );             glVertex2iv( xyArray );
glVertex3f( x, y, z );          glVertex3fv( xyzArray );
glVertex3d( x, y, z );          glVertex3dv( xyzArray );
glVertex3i( x, y, z );          glVertex3iv( xyzArray );

glColor3f( r, g, b );           glColor3f( rgbArray );
glColor3d( r, g, b );           glColor3d( rgbArray );
glColor3ub( r, g, b );          glColor3ub( rgbArray );
glColor4f( r, g, b, a);         glColor4f( rgbaArray );
glColor4d( r, g, b, a);         glColor4d( rgbaArray );
glColor4ub( r, g, b, a);        glColor4ub( rgbaArray );

对于glColor*,请记住,“ub”变体需要在0到255范围内的整数,而“f”和“d”变体需要在0.0到1.0范围内的浮点数。

We have see that there are versions of glColor and glVertex that take different numbers and types of parameters. There are also versions that let you place all the data for the command in a single array parameter. The names for such versions end with "v". For example: glColor3fv, glVertex2iv, glColor4ubv, and glVertex3dv. The "v" actually stands for "vector," meaning essentially a one-dimensional array of numbers. For example, in the function call glVertex3fv(coords), coords would be an array containing at least three floating point numbers.

The existence of array parameters in OpenGL forces some differences between OpenGL implementations in different programming languages. Arrays in Java are different from arrays in C, and arrays in JavaScript are different from both. Let's look at the situation in C first, since that's the language of the original OpenGL API.

In C, array variables are a sort of variation on pointer variables, and arrays and pointers can be used interchangeably in many circumstances. In fact, in the C API, array parameters are actually specified as pointers. For example, the parameter for glVertex3fv is of type "pointer to float." The actual parameter in a call to glVertex3fv can be an array variable, but it can also be any pointer that points to the beginning of a sequence of three floats. As an example, suppose that we want to draw a square. We need two coordinates for each vertex of the square. In C, we can put all 8 coordinates into one array and use glVertex2fv to pull out the coordinates that we need:

float coords[] = { -0.5, -0.5,  0.5, -0.5,  0.5, 0.5,  -0.5, 0.5 };

glBegin(GL_TRIANGLE_FAN);
glVertex2fv(coords);      // Uses coords[0] and coords[1].
glVertex2fv(coords + 2);  // Uses coords[2] and coords[3].
glVertex2fv(coords + 4);  // Uses coords[4] and coords[5].
glVertex2fv(coords + 6);  // Uses coords[6] and coords[7].
glEnd();

This example uses "pointer arithmetic," in which coords + N represents a pointer to the N-th element of the array. An alternative notation would be &coords[N], where "&" is the address operator, and &coords[N] means "a pointer to coords[N]". This will all seem very alien to people who are only familiar with Java or JavaScript. In my examples, I will avoid using pointer arithmetic, but I will occasionally use address operators.

As for Java, the people who designed JOGL wanted to preserve the ability to pull data out of the middle of an array. However, it's not possible to work with pointers in Java. The solution was to replace a pointer parameter in the C API with a pair of parameters in the JOGL API—one parameter to specify the array that contains the data and one to specify the starting index of the data in the array. For example, here is how the square-drawing code translates into Java:

float[] coords = { -0.5F, -0.5F,  0.5F, -0.5F,  0.5F, 0.5F,  -0.5F, 0.5F };

gl2.glBegin(GL2.GL_TRIANGLES);
gl2.glVertex2fv(coords, 0);  // Uses coords[0] and coords[1].
gl2.glVertex2fv(coords, 2);  // Uses coords[2] and coords[3].
gl2.glVertex2fv(coords, 4);  // Uses coords[4] and coords[5].
gl2.glVertex2fv(coords, 6);  // Uses coords[6] and coords[7].
gl2.glEnd();

There is really not much difference in the parameters, although the zero in the first glVertex2fv is a little annoying. The main difference is the prefixes "gl2" and "GL2", which are required by the object-oriented nature of the JOGL API. I won't say more about JOGL here, but if you need to translate my examples into JOGL, you should keep in mind the extra parameter that is required when working with arrays.

For the record, here are the glVertex* and glColor* functions that I will use in this book. This is not the complete set that is available in OpenGL:

glVertex2f( x, y );             glVertex2fv( xyArray );
glVertex2d( x, y );             glVertex2dv( xyArray );
glVertex2i( x, y );             glVertex2iv( xyArray );
glVertex3f( x, y, z );          glVertex3fv( xyzArray );
glVertex3d( x, y, z );          glVertex3dv( xyzArray );
glVertex3i( x, y, z );          glVertex3iv( xyzArray );

glColor3f( r, g, b );           glColor3f( rgbArray );
glColor3d( r, g, b );           glColor3d( rgbArray );
glColor3ub( r, g, b );          glColor3ub( rgbArray );
glColor4f( r, g, b, a);         glColor4f( rgbaArray );
glColor4d( r, g, b, a);         glColor4d( rgbaArray );
glColor4ub( r, g, b, a);        glColor4ub( rgbaArray );

For glColor*, keep in mind that the "ub" variations require integers in the range 0 to 255, while the "f" and "d" variations require floating-point numbers in the range 0.0 to 1.0.

3.1.4 深度测试

The Depth Test

在3D视图中一个显而易见的问题是,一个物体可以位于另一个物体的后面。当这种情况发生时,背后的物体会被前面的物体遮挡,从而对观察者隐藏。当我们创建一个3D世界的图像时,我们必须确保被其他物体遮挡的物体实际上在图像中不可见。这就是隐藏表面问题

解决方案似乎很简单:只需按从后到前的顺序绘制物体。如果一个物体在另一个物体的后面,那么当绘制前面的物体时,后面的物体将会被遮盖。这被称为画家算法。这本质上就是您在2D中所习惯做的事情。不幸的是,实现起来并不那么容易。首先,您可能会遇到相交的物体,这样每个物体的一部分就会被另一个物体遮挡。无论您以什么顺序绘制这些物体,都会有一些点显示出错误的物体。要解决这个问题,您需要沿着交叉点将物体切割成片,并将这些片段视为单独的物体。事实上,即使没有相交的物体,也可能会出现问题:可能会有三个不相交的物体,其中第一个物体遮挡了第二个的一部分,第二个物体遮挡了第三个的一部分,第三个物体又遮挡了第一个的一部分。无论以哪种顺序绘制这三个物体,画家算法都会失败。解决方案是再次将物体切割成片,但是现在不那么明显在哪里切割。

尽管这些问题可以解决,但还有另一个问题。当改变视点或应用几何变换时,正确的绘制顺序可能会发生变化,这意味着每次发生这种情况时都必须重新计算正确的绘制顺序。在动画中,这意味着对于每一帧都要这样做。

因此,OpenGL 不使用画家算法。相反,它使用一种称为深度测试的技术。深度测试解决了隐藏表面问题,无论以何种顺序绘制物体,因此您可以按任意顺序绘制它们!这里的“深度”一词与观察者到物体的距离有关。深度较大的对象距离观察者更远。具有较小深度的对象将隐藏具有较大深度的对象。为了实现深度测试算法,OpenGL 在图像的每个像素中存储一个深度值。用于存储这些深度值的额外内存组成了我之前提到的深度缓冲区。在绘制过程中,深度缓冲区用于跟踪每个像素当前可见的内容。当第二个物体绘制在该像素时,深度缓冲区中的信息可用于决定新物体是在当前可见的物体前面还是后面。如果新物体在前面,则像素的颜色将被更改以显示新物体,并且深度缓冲区也将被更新。如果新物体在当前物体后面,则新物体的数据将被丢弃,并且颜色和深度缓冲区保持不变。

默认情况下,深度测试是关闭的,这可能会导致在3D绘制时出现非常糟糕的结果。您可以通过调用以下命令来启用深度测试:

glEnable( GL_DEPTH_TEST );

它可以通过调用 glDisable(GL_DEPTH_TEST) 来关闭。如果在绘制3D时忘记启用深度测试,则得到的图像可能会令人困惑,并且在物理上毫无意义。如果在清除颜色缓冲区的同时忘记清除深度缓冲区,也会产生混乱,这使用了本节中早期显示的 glClear 命令。

这里有一个演示,让您可以尝试深度测试。它还让您看到当您的几何图形的一部分超出了可见的 z 值范围时会发生什么。

以下是有关深度测试实现的一些细节:对于每个像素,深度缓冲区存储了从观察者到当前在该像素处可见点的距离表示。这个值本质上是该点的 z 坐标,在应用任何变换后。 (事实上,深度缓冲区通常称为“z 缓冲区”。)可能 z-坐标的范围被缩放到 0 到 1 的范围。深度缓冲区值的有限范围意味着 OpenGL 只能在有限的距离范围内显示对象。深度值 0 对应于最小距离;深度值 1 对应于最大距离。当您清除深度缓冲区时,每个深度值都设置为 1,这可以被认为是表示图像的背景。

通过应用的变换,您可以选择图像中可见的 z-值范围。在没有任何变换的情况下,默认范围是 -1 到 1。z-值超出范围的点在图像中不可见。使用过小的 z-值范围是一个常见问题,因此物体会在场景中消失,或者由于位于可见范围之外而使其前端或后端被截断。您可能会被诱惑使用一个巨大的范围,以确保您希望包含在图像中的对象包含在范围内。但是,这并不是一个好主意:深度缓冲区每个像素的位数有限,因此精度有限。它必须表示的值范围越大,区分几乎相同深度的对象就越困难。(想象一下如果您场景中的所有对象深度值介于 0.499999 和 0.500001 之间会发生什么—深度缓冲区可能将它们全部视为完全相同的深度!)

深度缓冲区算法还存在另一个问题。当两个对象具有完全相同的深度值时,它可能会产生一些奇怪的结果。从逻辑上讲,甚至不清楚哪个对象应该是可见的,但深度测试的真正问题在于,它可能在某些点显示一个对象,在其他一些点显示第二个对象。这是因为数值计算并不完全准确。以下是一个实际的例子:

123

在这里显示的两幅图片中,先绘制了一个灰色的正方形,然后是一个白色的正方形,最后是一个黑色的正方形。这些正方形都位于同一平面上。在绘制对象之前施加了非常小的旋转,以便让计算机在绘制对象之前进行一些计算。左边的图片是在禁用深度测试的情况下绘制的,因此,例如,当绘制白色正方形的像素时,计算机不会尝试确定它是在灰色正方形的前面还是后面;它只是将像素着色为白色。右边,则启用了深度测试,您可以看到奇怪的结果。

最后,顺便说一句,注意这里的讨论假设没有透明的对象。不幸的是,深度测试不能正确处理透明度,因为透明度意味着两个或多个对象可以对像素的颜色产生贡献,但深度测试假设像素的颜色是在该点最靠近观察者的对象的颜色。要在OpenGL中正确处理 3D 透明度,您几乎必须手动实现画家算法,至少对于场景中的透明对象是这样。

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 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 third, and the third hides part of the first. The painter's algorithm will fail regardless of the order in which the three objects are drawn. The solution again is to cut the objects into pieces, but now it's not so obvious where to cut.

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 term "depth" here has to do with the distance from the viewer to the object. Objects at greater depth are farther from the viewer. An object with smaller depth will hide an object with greater depth. To implement the depth test algorithm, OpenGL stores a depth value for each pixel in the image. The extra memory that is used to store these depth values 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). If you forget to enable the depth test when drawing in 3D, the image that you get will likely be confusing and will make no sense physically. You can also get quite a mess if you forget to clear the depth buffer, using the glClear command shown earlier in this section, at the same time that you clear the color buffer.

Here is a demo that lets you experiment with the depth test. It also lets you see what happens when part of your geometry extends outside the visible range of z-values.

Here are a few details about the implementation of the depth test: For each pixel, the depth buffer stores a representation of the distance from the viewer to the point that is currently visible at that pixel. This value is essentially the z-coordinate of the point, after any transformations have been applied. (In fact, the depth buffer is often called the "z-buffer".) The range of possible z-coordinates is scaled to the range 0 to 1. The fact that there is only a limited range of depth buffer values means that OpenGL can only display objects in a limited range of distances from the viewer. A depth value of 0 corresponds to the minimal distance; a depth value of 1 corresponds to the maximal distance. When you clear the depth buffer, every depth value is set to 1, which can be thought of as representing the background of the image.

You get to choose the range of z-values that is visible in the image, by the transformations that you apply. The default range, in the absence of any transformations, is -1 to 1. Points with z-values outside the range are not visible in the image. It is a common problem to use too small a range of z-values, so that objects are missing from the scene, or have their fronts or backs cut off, because they lie outside of the visible range. You might be tempted to use a huge range, to make sure that the objects that you want to include in the image are included within the range. However, that's not a good idea: The depth buffer has a limited number of bits per pixel and therefore a limited amount of accuracy. The larger the range of values that it must represent, the harder it is to distinguish between objects that are almost at the same depth. (Think about what would happen if all objects in your scene have depth values between 0.499999 and 0.500001—the depth buffer might see them all as being at exactly the same depth!)

There is another issue with the depth buffer algorithm. It can give some strange results when two objects have exactly the same depth value. 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:

123

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 force the computer do some calculations before drawing the objects. The picture on the left was drawn with the depth test disabled, so that, for example, when a pixel of the white square was drawn, the computer didn't try to figure out whether it lies in front of or behind the gray square; it simply colored the pixel white. On the right, the depth test was enabled, and you can see the strange result.

Finally, by the way, note that the discussion here assumes that there are no transparent objects. Unfortunately, the depth test does not handle transparency correctly, since transparency means that two or more objects can contribute to the color of the pixel, but the depth test assumes that the pixel color is the color of the object nearest to the viewer at that point. To handle 3D transparency correctly in OpenGL, you pretty much have to resort to implementing the painter's algorithm by hand, at least for the transparent objects in the scene.