跳转至

3.4 多边形网格和 glDrawArrays

Polygonal Meshes and glDrawArrays

我们只用OpenGL绘制了非常简单的形状。在这一部分中,我们将探讨如何以OpenGL渲染为方便的方式来表示更复杂的形状,并介绍一种新的、更高效的绘制OpenGL基元的方法。

OpenGL只能直接渲染点、线和多边形。(事实上,在现代OpenGL中,使用的唯一多边形是三角形。)多面体,多边形的三维模拟,可以被精确地表示,因为多面体的面是多边形。另一方面,如果只有多边形可用,那么曲面,比如球面的表面,只能被近似。一个多面体可以被表示,或者一个曲面可以被近似,作为多边形网格,即一组沿其边连接的多边形。如果多边形很小,这个近似看起来就像是一个曲面。(我们将在下一章节中看到,如何使用光照效果使多边形网格更像曲面,而不像多面体。)

因此,我们的问题是如何表示一组多边形——通常是一组三角形。我们首先定义一种方便的方式来将这样的一组多边形表示为一个数据结构。

We have drawn only very simple shapes with OpenGL. In this section, we look at how more complex shapes can be represented in a way that is convenient for rendering in OpenGL, and we introduce a new, more efficient way to draw OpenGL primitives.

OpenGL can only directly render points, lines, and polygons. (In fact, in modern OpenGL, the only polygons that are used are triangles.) A polyhedron, the 3D analog of a polygon, can be represented exactly, since a polyhedron has faces that are polygons. On the other hand, if only polygons are available, then a curved surface, such as the surface of a sphere, can only be approximated. A polyhedron can be represented, or a curved surface can be approximated, as a polygonal mesh, that is, a set of polygons that are connected along their edges. If the polygons are small, the approximation can look like a curved surface. (We will see in the next chapter how lighting effects can be used to make a polygonal mesh look more like a curved surface and less like a polyhedron.)

So, our problem is to represent a set of polygons—most often a set of triangles. We start by defining a convenient way to represent such a set as a data structure.

3.4.1 索引面集

Indexed Face Sets

多边形网格中的多边形也被称为 "面"(如多面体的面),表示多边形网格的主要手段之一是索引面集或IFS。

IFS 的数据包括出现在网格中的所有顶点的列表,给出每个顶点的坐标。然后,可以通过指定其索引或位置在列表中的整数来标识顶点。例如,考虑这个 "房子",一个有10个顶点和9个面的多面体:

Vertex #0.  (2, -1, 2)
Vertex #1.  (2, -1, -2)
Vertex #2.  (2, 1, -2)
Vertex #3.  (2, 1, 2)
Vertex #4.  (1.5, 1.5, 0)
Vertex #5.  (-1.5, 1.5, 0)
Vertex #6.  (-2, -1, 2)
Vertex #7.  (-2, 1, 2)
Vertex #8.  (-2, 1, -2)
Vertex #9.  (-2, -1, -2)

顶点的顺序是完全任意的。目的只是为了让每个顶点能够通过一个整数进行标识。

要描述网格的一个多边形面,我们只需列出其顶点,按照多边形周围的顺序。对于一个 IFS,我们可以通过给出其在列表中的索引来指定一个顶点。例如,我们可以说一个金字塔的三角形面之一是由顶点 #3、顶点 #2 和顶点 #4 组成的多边形。因此,我们可以通过给出每个面的顶点索引列表来完整地描述网格的数据。这是房子的面数据。请记住,括号中的数字是顶点列表中的索引:

Face #0:  (0, 1, 2, 3)
Face #1:  (3, 2, 4)
Face #2:  (7, 3, 4, 5)
Face #3:  (2, 8, 5, 4)
Face #4:  (5, 8, 7)
Face #5:  (0, 3, 7, 6)
Face #6:  (0, 6, 9, 1)
Face #7:  (2, 1, 9, 8)
Face #8:  (6, 7, 8, 9)

同样,面被列出的顺序是任意的。对于一个面的顶点如何被列出,也有一定的自由度。你可以从任意顶点开始。一旦选择了一个起始顶点,就有两种可能的顺序,对应于你可以沿着多边形的周长以两种可能的方向前进。例如,以顶点 0 开始,列表中的第一个面可以被指定为 (0,1,2,3) 或 (0,3,2,1) 中的任何一个。然而,在这种情况下,第一种可能性是正确的,原因如下。一个三维空间中的多边形可以从两个方向观察;我们可以把它想象成有着两个面,朝向相反。事实证明,通常方便的做法是认为其中一个面是多边形的 "前面",另一个是 "后面"。对于像房子这样的多面体,前面是朝向多面体外部的那一面。通常的规则是,当查看多边形的前面时,应以逆时针顺序列出多边形的顶点。当查看背面时,顶点将以顺时针顺序列出。这是OpenGL使用的默认规则。

顶点和面数据的索引面集可以表示为一对二维数组。对于房子,在Java版本中,我们可以使用

double[][] vertexList =
        {  {2,-1,2}, {2,-1,-2}, {2,1,-2}, {2,1,2}, {1.5,1.5,0},
            {-1.5,1.5,0}, {-2,-1,2}, {-2,1,2}, {-2,1,-2}, {-2,-1,-2}  };

int[][] faceList =
        {  {0,1,2,3}, {3,2,4}, {7,3,4,5}, {2,8,5,4}, {5,8,7},
            {0,3,7,6}, {0,6,9,1}, {2,1,9,8}, {6,7,8,9}  };

在大多数情况下,IFS 还会有额外的数据。例如,如果我们想要给多面体的面着色,每个面用不同的颜色,那么我们可以添加另一个数组 faceColors 来保存颜色数据。faceColors 的每个元素将是一个包含三个取值范围为 0.0 到 1.0 的双精度值的数组,给出了一个面的 RGB 颜色分量。有了这个设置,我们可以使用以下代码使用Java和JOGL来绘制多面体:

for (int i = 0; i < faceList.length; i++) {
    gl2.glColor3dv( faceColors[i], 0 );  // 设置第i个面的颜色。
    gl2.glBegin(GL2.GL_TRIANGLE_FAN);
    for (int j = 0; j < faceList[i].length; j++) {
        int vertexNum = faceList[i][j];  // 面i的顶点j的索引。
        double[] vertexCoords = vertexList[vertexNum];  // 顶点本身。
        gl2.glVertex3dv( vertexCoords, 0

----

有其他存储 IFS 数据的方法例如 C 二维数组更加麻烦我们可能会使用一维数组来存储数据在这种情况下我们将把所有的顶点坐标存储在一个单独的数组中顶点数组的长度将是顶点数的三倍顶点号为 N 的数据将从数组中的索引 3*N 开始对于面列表我们必须处理不是所有的面都有相同数量的顶点这一事实一个常见的解决方案是在每个面的数据之后在数组中添加一个 -1 C 由于无法确定数组的长度我们还需要变量来存储顶点数和面数使用这种表示房子的数据如下

```c
int vertexCount = 10;  // 顶点数。
double vertexData[] =
        {  2,-1,2, 2,-1,-2, 2,1,-2, 2,1,2, 1.5,1.5,0,
                -1.5,1.5,0, -2,-1,2, -2,1,2, -2,1,-2, -2,-1,-2  };

int faceCount = 9;  // 面数。       
int[][] faceData =
        {  0,1,2,3,-1, 3,2,4,-1, 7,3,4,5,-1, 2,8,5,4,-1, 5,8,7,-1,
            0,3,7,6,-1, 0,6,9,1,-1, 2,1,9,8,-1, 6,7,8,9,-1  };

在添加了一个 faceColors 数组来保存面的颜色数据后,我们可以使用以下 C 代码来绘制房子:

int i,j;
j = 0; // faceData 数组的索引
for (i = 0; i < faceCount; i++) {
    glColor3dv( &faceColors[ i*3 ] );  // 设置第i个面的颜色。
    glBegin(GL_TRIANGLE_FAN);
    while ( faceData[j] != -1) { // 为第i个面生成顶点。
        int vertexNum = faceData[j]; // 在 vertexData 数组中的顶点编号。
        glVertex3dv( &vertexData[ vertexNum*3 ] );
        j++;
    }
    j++;  // 将 j 递增到结束此面数据的 -1 的位置。
    glEnd();
}

请注意使用了 C 的地址操作符 &。例如,&faceColors[i3] 是 faceColors 数组中第 i3 个元素的指针。这个元素是面编号为 i 的三个颜色分量值中的第一个。这与 C 中 glColor3dv 的参数类型匹配,因为参数是指针类型。


我们可以很容易地绘制多面体的边而不是面,只需在绘制代码中使用 GL_LINE_LOOP 而不是 GL_TRIANGLE_FAN(并且可能省略颜色更改)。如果我们想要同时绘制面和边,就会遇到一个有趣的问题。这可能是一个很好的效果,但我们会在深度测试中遇到一个问题:沿着边缘的像素与面上的像素处于相同的深度。如 3.1.4小节 所讨论的,深度测试无法很好地处理这种情况。然而,OpenGL 有一个解决方案:一个称为 "多边形偏移" 的特性。这个特性可以调整多边形在裁剪坐标中的深度,以避免两个对象的深度完全相同。要应用多边形偏移,您需要通过调用以下方法来设置偏移量的数量:

glPolygonOffset(1,1);

第二个参数给出了偏移量的数量,单位由第一个参数确定。第一个参数的含义有些模糊;在所有情况下,值为 1 似乎都有效。您还需要在绘制面时启用 GL_POLYGON_OFFSET_FILL 功能。流程的大纲如下:

glPolygonOffset(1,1);
glEnable( GL_POLYGON_OFFSET_FILL );
.
.   // 绘制面。
.
glDisable( GL_POLYGON_OFFSET_FILL );
.
.   // 绘制边。
.

有一个示例程序可以绘制房子和其他一些多面体。它使用的绘制代码与我们在这里看到的非常相似,包括多边形偏移。该程序还是使用相机和轨迹球 API 的示例,这个 API3.3.5小节 中有所讨论,因此用户可以通过鼠标拖动多面体来旋转它。该程序具有菜单,允许用户打开和关闭边缘和面的渲染,以及一些其他选项。该程序的Java版本是 jogl/IFSPolyhedronViewer.java,C 版本是 glut/ifs-polyhedron-viewer.c。在 C 版本中,要访问菜单,请右键单击显示区域。多面体的数据是在 jogl/Polyhedron.javaglut/polyhedron.c 中创建的。以下是程序的实时演示版本供您尝试:

The polygons in a polygonal mesh are also referred to as "faces" (as in the faces of a polyhedron), and one of the primary means for representing a polygonal mesh is as an indexed face set, or IFS.

The data for an IFS includes a list of all the vertices that appear in the mesh, giving the coordinates of each vertex. A vertex can then be identified by an integer that specifies its index, or position, in the list. As an example, consider this "house," a polyhedron with 10 vertices and 9 faces:

123

The vertex list for this polyhedron has the form

Vertex #0.  (2, -1, 2)
Vertex #1.  (2, -1, -2)
Vertex #2.  (2, 1, -2)
Vertex #3.  (2, 1, 2)
Vertex #4.  (1.5, 1.5, 0)
Vertex #5.  (-1.5, 1.5, 0)
Vertex #6.  (-2, -1, 2)
Vertex #7.  (-2, 1, 2)
Vertex #8.  (-2, 1, -2)
Vertex #9.  (-2, -1, -2)

The order of the vertices is completely arbitrary. The purpose is simply to allow each vertex to be identified by an integer.

To describe one of the polygonal faces of a mesh, we just have to list its vertices, in order going around the polygon. For an IFS, we can specify a vertex by giving its index in the list. For example, we can say that one of the triangular faces of the pyramid is the polygon formed by vertex #3, vertex #2, and vertex #4. So, we can complete our data for the mesh by giving a list of vertex indices for each face. Here is the face data for the house. Remember that the numbers in parentheses are indices into the vertex list:

Face #0:  (0, 1, 2, 3)
Face #1:  (3, 2, 4)
Face #2:  (7, 3, 4, 5)
Face #3:  (2, 8, 5, 4)
Face #4:  (5, 8, 7)
Face #5:  (0, 3, 7, 6)
Face #6:  (0, 6, 9, 1)
Face #7:  (2, 1, 9, 8)
Face #8:  (6, 7, 8, 9)

Again, the order in which the faces are listed in arbitrary. There is also some freedom in how the vertices for a face are listed. You can start with any vertex. Once you've picked a starting vertex, there are two possible orderings, corresponding to the two possible directions in which you can go around the circumference of the polygon. For example, starting with vertex 0, the first face in the list could be specified either as (0,1,2,3) or as (0,3,2,1). However, the first possibility is the right one in this case, for the following reason. A polygon in 3D can be viewed from either side; we can think of it as having two faces, facing in opposite directions. It turns out that it is often convenient to consider one of those faces to be the "front face" of the polygon and one to be the "back face." For a polyhedron like the house, the front face is the one that faces the outside of the polyhedron. The usual rule is that the vertices of a polygon should be listed in counterclockwise order when looking at the front face of the polygon. When looking at the back face, the vertices will be listed in clockwise order. This is the default rule used by OpenGL.

123

The vertex and face data for an indexed face set can be represented as a pair of two-dimensional arrays. For the house, in a version for Java, we could use

double[][] vertexList =
        {  {2,-1,2}, {2,-1,-2}, {2,1,-2}, {2,1,2}, {1.5,1.5,0},
            {-1.5,1.5,0}, {-2,-1,2}, {-2,1,2}, {-2,1,-2}, {-2,-1,-2}  };

int[][] faceList =
        {  {0,1,2,3}, {3,2,4}, {7,3,4,5}, {2,8,5,4}, {5,8,7},
            {0,3,7,6}, {0,6,9,1}, {2,1,9,8}, {6,7,8,9}  };

In most cases, there will be additional data for the IFS. For example, if we want to color the faces of the polyhedron, with a different color for each face, then we could add another array, faceColors, to hold the color data. Each element of faceColors would be an array of three double values in the range 0.0 to 1.0, giving the RGB color components for one of the faces. With this setup, we could use the following code to draw the polyhedron, using Java and JOGL:

for (int i = 0; i < faceList.length; i++) {
    gl2.glColor3dv( faceColors[i], 0 );  // Set color for face number i.
    gl2.glBegin(GL2.GL_TRIANGLE_FAN);
    for (int j = 0; j < faceList[i].length; j++) {
        int vertexNum = faceList[i][j];  // Index for vertex j of face i.
        double[] vertexCoords = vertexList[vertexNum];  // The vertex itself.
        gl2.glVertex3dv( vertexCoords, 0 );
    }
    gl2.glEnd();
}

Note that every vertex index is used three or four times in the face data. With the IFS representation, a vertex is represented in the face list by a single integer. This representation uses less memory space than the alternative, which would be to write out the vertex in full each time it occurs in the face data. For the house example, the IFS representation uses 64 numbers to represent the vertices and faces of the polygonal mesh, as opposed to 102 numbers for the alternative representation.

Indexed face sets have another advantage. Suppose that we want to modify the shape of the polygon mesh by moving its vertices. We might do this in each frame of an animation, as a way of "morphing" the shape from one form to another. Since only the positions of the vertices are changing, and not the way that they are connected together, it will only be necessary to update the 30 numbers in the vertex list. The values in the face list will remain unchanged.


There are other ways to store the data for an IFS. In C, for example, where two-dimensional arrays are more problematic, we might use one dimensional arrays for the data. In that case, we would store all the vertex coordinates in a single array. The length of the vertex array would be three times the number of vertices, and the data for vertex number N will begin at index 3*N in the array. For the face list, we have to deal with the fact that not all faces have the same number of vertices. A common solution is to add a -1 to the array after the data for each face. In C, where it is not possible to determine the length of an array, we also need variables to store the number of vertices and the number of faces. Using this representation, the data for the house becomes:

int vertexCount = 10;  // Number of vertices.
double vertexData[] =
        {  2,-1,2, 2,-1,-2, 2,1,-2, 2,1,2, 1.5,1.5,0,
                -1.5,1.5,0, -2,-1,2, -2,1,2, -2,1,-2, -2,-1,-2  };

int faceCount = 9;  // Number of faces.       
int[][] faceData =
        {  0,1,2,3,-1, 3,2,4,-1, 7,3,4,5,-1, 2,8,5,4,-1, 5,8,7,-1,
            0,3,7,6,-1, 0,6,9,1,-1, 2,1,9,8,-1, 6,7,8,9,-1  };

After adding a faceColors array to hold color data for the faces, we can use the following C code to draw the house:

int i,j;
j = 0; // index into the faceData array
for (i = 0; i < faceCount; i++) {
    glColor3dv( &faceColors[ i*3 ] );  // Color for face number i.
    glBegin(GL_TRIANGLE_FAN);
    while ( faceData[j] != -1) { // Generate vertices for face number i.
        int vertexNum = faceData[j]; // Vertex number in vertexData array.
        glVertex3dv( &vertexData[ vertexNum*3 ] );
        j++;
    }
    j++;  // increment j past the -1 that ended the data for this face.
    glEnd();
}

Note the use of the C address operator, &. For example, &faceColors[i3] is a pointer to element number i3 in the faceColors array. That element is the first of the three color component values for face number i. This matches the parameter type for glColor3dv in C, since the parameter is a pointer type.


We could easily draw the edges of the polyhedron instead of the faces simply by using GL_LINE_LOOP instead of GL_TRIANGLE_FAN in the drawing code (and probably leaving out the color changes). An interesting issue comes up if we want to draw both the faces and the edges. This can be a nice effect, but we run into a problem with the depth test: Pixels along the edges lie at the same depth as pixels on the faces. As discussed in Subsection 3.1.4, the depth test cannot handle this situation well. However, OpenGL has a solution: a feature called "polygon offset." This feature can adjust the depth, in clip coordinates, of a polygon, in order to avoid having two objects exactly at the same depth. To apply polygon offset, you need to set the amount of offset by calling

glPolygonOffset(1,1);

The second parameter gives the amount of offset, in units determined by the first parameter. The meaning of the first parameter is somewhat obscure; a value of 1 seems to work in all cases. You also have to enable the GL_POLYGON_OFFSET_FILL feature while drawing the faces. An outline for the procedure is

glPolygonOffset(1,1);
glEnable( GL_POLYGON_OFFSET_FILL );
.
.   // Draw the faces.
.
glDisable( GL_POLYGON_OFFSET_FILL );
.
.   // Draw the edges.
.

There is a sample program that can draw the house and a number of other polyhedra. It uses drawing code very similar to what we have looked at here, including polygon offset. The program is also an example of using the camera and trackball API that was discussed in Subsection 3.3.5, so that the user can rotate a polyhedron by dragging it with the mouse. The program has menus that allow the user to turn rendering of edges and faces on and off, plus some other options. The Java version of the program is jogl/IFSPolyhedronViewer.java, and the C version is glut/ifs-polyhedron-viewer.c. To get at the menu in the C version, right-click on the display. The data for the polyhedra are created in jogl/Polyhedron.java and glut/polyhedron.c. And here is a live demo version of the program for you to try:

3.4.2 glDrawArrays和glDrawElements

glDrawArrays and glDrawElements

到目前为止,我们所见到的所有 OpenGL 命令都是原始 OpenGL 1.0 的一部分。OpenGL 1.1 添加了一些功能以提高性能。关于原始 OpenGL 的一个抱怨是绘制基元所需的函数调用太多,使用诸如 glBegin/glEndglVertex2dglColor3fv 这样的函数来绘制基元。为了解决这个问题,OpenGL 1.1 引入了函数 glDrawArrays 和 glDrawElements。这些函数在现代 OpenGL 中仍在使用,包括 WebGL。我们首先来看看 glDrawArrays。C 版本和 Java 版本的 API 之间存在一些差异。我们先考虑 C 版本,接下来会处理 Java 版本所需的更改。

使用 glDrawArrays,绘制一个基元所需的所有数据,包括顶点坐标、颜色和其他顶点属性,可以打包到数组中。一旦完成,就可以通过单个调用 glDrawArrays 来绘制基元。请记住,一个基元,比如 GL_LINE_LOOPGL_TRIANGLES,可能包含大量顶点,因此减少函数调用的数量是相当可观的。

要使用 glDrawArrays,必须将一个基元的所有顶点坐标存储在一个单一的一维数组中。您可以使用 int、float 或 double 数组,并且每个顶点可以有 2、3 或 4 个坐标。数组中的数据与您会将其作为参数传递给诸如 glVertex3f 的函数的相同数字相同。您需要调用以下方法告诉 OpenGL 在哪里找到数据:

void glVertexPointer(int size, int type, int stride, void* array)

size 参数是每个顶点的坐标数。(您必须为每个顶点提供相同数量的坐标。)类型是一个常数,告诉数组中每个数字的数据类型。可能的值有 GL_FLOAT、GL_INT 和 GL_DOUBLE。这里提供的常数必须与数组中数字的数据类型相匹配。stride 通常是 0,意味着数据值存储在数组中连续的位置;如果情况不是这样,则 stride 给出了一个顶点的数据与下一个顶点的数据之间的距离(以字节为单位)。(这样可以让您在同一个数组中存储顶点坐标以及其他数据。)最后一个参数是包含数据的数组。它被列为类型为 "void*" 的数据,这是一个指向任何类型数据的指针的 C 数据类型。(回想一下,C 中的数组变量是指针的一种,因此您可以将数组变量作为第四个参数直接传递。)例如,假设我们想在 xy 平面上绘制一个正方形。我们可以这样设置顶点数组:

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

glVertexPointer( 2, GL_FLOAT, 0, coords );

除了设置顶点坐标的位置之外,您还需要通过调用以下方法启用数组的使用:

glEnableClientState(GL_VERTEX_ARRAY);

除非启用了该状态,否则 OpenGL 将忽略顶点指针。您可以使用 glDisableClientState 来禁用顶点数组的使用。最后,为了实际绘制基元,您将调用以下函数:

void glDrawArrays( int primitiveType, int firstVertex, int vertexCount)

此函数调用对应于一次 glBegin/glEnd 的使用。primitiveType 告诉正在绘制的是哪种基元类型,例如 GL_QUADSGL_TRIANGLE_STRIP。可以使用与 glBegin 相同的十种基元类型之一。firstVertex 参数是要用于绘制基元的第一个顶点的编号。注意,位置以顶点编号表示;相应的数组索引将是顶点编号乘以每个顶点的坐标数,这是在调用 glVertexPointer 时设置的。vertexCount 参数是要使用的顶点数,就像调用 glVertex* 一样。通常,firstVertex 将为零,vertexCount 将为数组中顶点的总数。我们示例中绘制正方形的命令如下:

glDrawArrays( GL_TRIANGLE_FAN, 0, 4 );

通常,除了顶点坐标之外,每个顶点可能还有其他数据关联。例如,您可能想为每个顶点指定不同的颜色。顶点的颜色可以放入另一个数组中。您必须通过调用以下方法指定数据的位置:

void glColorPointer(int size, int type, int stride, void* array)

它的工作原理与 gVertexPointer 相同。您需要通过调用以下方法启用颜色数组:

glEnableClientState(GL_COLOR_ARRAY);

使用此设置,当您调用 glDrawArrays 时,OpenGL 将从颜色数组中同时获取每个顶点的颜色和顶点坐标。稍后,我们将遇到除坐标和颜色之外的其他类型的顶点数据,处理方式基本相同。

让我们将这些内容整合起来,以绘制标准的 OpenGL 红/绿/蓝三角形,我们在 3.1.2小节 中使用 glBegin/glEnd 绘制过。由于三角形的顶点具有不同的颜色,我们将使用一个颜色数组以及顶点数组。

float coords[6] = { -0.9,-0.9,  0.9,-0.9,  0,0.7 }; // 每个顶点两个坐标。
float colors[9] = { 1,0,0,  0,1,0,  1,0,0 };  // 每个顶点三个 RGB 值。

glVertexPointer( 2, GL_FLOAT, 0, coords );  // 设置数据类型和位置。
glColorPointer( 3, GL_FLOAT, 0, colors );

glEnableClientState( GL_VERTEX_ARRAY );  // 启用数组使用。
glEnableClientState( GL_COLOR_ARRAY );

glDrawArrays( GL_TRIANGLES, 0, 3 ); // 使用 3 个顶点,从顶点 0 开始。

实际上,并不是所有的代码都必须位于同一个位置。实际进行绘制的函数 glDrawArrays 必须位于绘制图像的显示例程中。其余的部分可以放在显示例程中,但也可以在初始化例程中完成,例如。


函数 glDrawElements 类似于 glDrawArrays,但设计用于与索引面集类似的格式的数据。使用 glDrawArraysOpenGL 按顺序从启用的数组中提取数据,先是顶点 0,然后是顶点 1,然后是顶点 2,依此类推。而使用 glDrawElements,您提供了一个顶点编号列表。OpenGL 将遍历顶点编号列表,从数组中获取指定顶点的数据。与索引面集一样,这样做的优点在于可以多次重用同一顶点。

要使用 glDrawElements 绘制一个基元,您需要一个数组来存储顶点编号。数组中的数字可以是 8、16 或 32 位整数。(它们应该是无符号整数,但常规正整数的数组也可以工作。)您还需要数组来存储顶点坐标和其他顶点数据,并且必须以与 glDrawArrays 相同的方式启用这些数组,使用诸如 glVertexArrayglEnableClientState 之类的函数。要实际绘制基元,调用以下函数:

void glDrawElements( int primitiveType, vertexCount, dataType, void *array)

这里,primitiveType 是十种基元类型之一,如 GL_LINESvertexCount 是要绘制的顶点数,dataType 指定数组中的数据类型,array 是保存顶点编号列表的数组。dataType 必须作为常量 GL_UNSIGNED_BYTEGL_UNSIGNED_SHORTGL_UNSIGNED_INT 之一给出,以分别指定 8、16 或 32 位整数。

例如,我们可以绘制一个立方体。我们可以将立方体的所有六个面作为一个 GL_QUADS 类型的基元绘制。我们需要将顶点坐标存储在一个数组中,并将面的顶点编号存储在另一个数组中。我还将使用一个颜色数组来存储顶点颜色。顶点颜色将被插值到面上的像素上,就像红/绿/蓝三角形一样。以下代码可以用来绘制立方体。再次说明,这些代码不一定要在程序的同一部分:

float vertexCoords[24] = {  // 立方体顶点的坐标。
        1,1,1,   1,1,-1,   1,-1,-1,   1,-1,1,
        -1,1,1,  -1,1,-1,  -1,-1,-1,  -1,-1,1  };

float vertexColors[24] = {  // 每个顶点的 RGB 颜色值
        1,1,1,   1,0,0,   1,1,0,   0,1,0,
        0,0,1,   1,0,1,   0,0,0,   0,1,1  };

int elementArray[24] = {  // 六个面的顶点编号。
        0,1,2,3, 0,3,7,4, 0,4,5,1,
        6,2,1,5, 6,5,4,7, 6,7,3,2  };

glVertexPointer( 3, GL_FLOAT, 0, vertexCoords );
glColorPointer( 3, GL_FLOAT, 0, vertexColors );

glEnableClientState( GL_VERTEX_ARRAY );
glEnableClientState( GL_COLOR_ARRAY );

glDrawElements( GL_QUADS, 24, GL_UNSIGNED_INT, elementArray );

请注意,第二个参数是顶点的数量,而不是四边形的数量。

示例程序 glut/cubes-with-vertex-arrays.c 使用此代码绘制了一个立方体。它使用 glDrawArrays 绘制了第二个立方体。Java 版本是 jogl/CubesWithVertexArrays.java,但您需要在理解之前阅读下一小节。还有一个 JavaScript 版本,glsim/cubes-with-vertex-arrays.html

All of the OpenGL commands that we have seen so far were part of the original OpenGL 1.0. OpenGL 1.1 added some features to increase performance. One complaint about the original OpenGL was the large number of function calls needed to draw a primitive using functions such as glVertex2d and glColor3fv with glBegin/glEnd. To address this issue, OpenGL 1.1 introduced the functions glDrawArrays and glDrawElements. These functions are still used in modern OpenGL, including WebGL. We will look at glDrawArrays first. There are some differences between the C and the Java versions of the API. We consider the C version first and will deal with the changes necessary for the Java version in the next subsection.

When using glDrawArrays, all of the data that is needed to draw a primitive, including vertex coordinates, colors, and other vertex attributes, can be packed into arrays. Once that is done, the primitive can be drawn with a single call to glDrawArrays. Recall that a primitive such as a GL_LINE_LOOP or a GL_TRIANGLES can include a large number of vertices, so that the reduction in the number of function calls can be substantial.

To use glDrawArrays, you must store all of the vertex coordinates for a primitive in a single one-dimensional array. You can use an array of int, float, or double, and you can have 2, 3, or 4 coordinates for each vertex. The data in the array are the same numbers that you would pass as parameters to a function such as glVertex3f, in the same order. You need to tell OpenGL where to find the data by calling

void glVertexPointer(int size, int type, int stride, void* array)

The size parameter is the number of coordinates per vertex. (You have to provide the same number of coordinates for each vertex.) The type is a constant that tells the data type of each of the numbers in the array. The possible values are GL_FLOAT, GL_INT, and GL_DOUBLE. The constant that you provide here must match the data type of the numbers in the array. The stride is usually 0, meaning that the data values are stored in consecutive locations in the array; if that is not the case, then stride gives the distance in bytes between the location of the data for one vertex and location for the next vertex. (This would allow you to store other data, along with the vertex coordinates, in the same array.) The final parameter is the array that contains the data. It is listed as being of type "void*", which is a C data type for a pointer that can point to any type of data. (Recall that an array variable in C is a kind of pointer, so you can just pass an array variable as the fourth parameter.) For example, suppose that we want to draw a square in the xy-plane. We can set up the vertex array with

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

glVertexPointer( 2, GL_FLOAT, 0, coords );

In addition to setting the location of the vertex coordinates, you have to enable use of the array by calling

glEnableClientState(GL_VERTEX_ARRAY);

OpenGL ignores the vertex pointer except when this state is enabled. You can use glDisableClientState to disable use of the vertex array. Finally, in order to actually draw the primitive, you would call the function

void glDrawArrays( int primitiveType, int firstVertex, int vertexCount)

This function call corresponds to one use of glBegin/glEnd. The primitiveType tells which primitive type is being drawn, such as GL_QUADS or GL_TRIANGLE_STRIP. The same ten primitive types that can be used with glBegin can be used here. The parameter firstVertex is the number of the first vertex that is to be used for drawing the primitive. Note that the position is given in terms of vertex number; the corresponding array index would be the vertex number times the number of coordinates per vertex, which was set in the call to glVertexPointer. The vertexCount parameter is the number of vertices to be used, just as if glVertex* were called vertexCount times. Often, firstVertex will be zero, and vertexCount will be the total number of vertices in the array. The command for drawing the square in our example would be

glDrawArrays( GL_TRIANGLE_FAN, 0, 4 );

Often there is other data associated with each vertex in addition to the vertex coordinates. For example, you might want to specify a different color for each vertex. The colors for the vertices can be put into another array. You have to specify the location of the data by calling

void glColorPointer(int size, int type, int stride, void* array)

which works just like gVertexPointer. And you need to enable the color array by calling

glEnableClientState(GL_COLOR_ARRAY);

With this setup, when you call glDrawArrays, OpenGL will pull a color from the color array for each vertex at the same time that it pulls the vertex coordinates from the vertex array. Later, we will encounter other kinds of vertex data besides coordinates and color that can be dealt with in much the same way.

Let's put this together to draw the standard OpenGL red/green/blue triangle, which we drew using glBegin/glEnd in Subsection 3.1.2. Since the vertices of the triangle have different colors, we will use a color array in addition to the vertex array.

float coords[6] = { -0.9,-0.9,  0.9,-0.9,  0,0.7 }; // two coords per vertex.
float colors[9] = { 1,0,0,  0,1,0,  1,0,0 };  // three RGB values per vertex.

glVertexPointer( 2, GL_FLOAT, 0, coords );  // Set data type and location.
glColorPointer( 3, GL_FLOAT, 0, colors );

glEnableClientState( GL_VERTEX_ARRAY );  // Enable use of arrays.
glEnableClientState( GL_COLOR_ARRAY );

glDrawArrays( GL_TRIANGLES, 0, 3 ); // Use 3 vertices, starting with vertex 0.

In practice, not all of this code has to be in the same place. The function that does the actual drawing, glDrawArrays, must be in the display routine that draws the image. The rest could be in the display routine, but could also be done, for example, in an initialization routine.


The function glDrawElements is similar to glDrawArrays, but it is designed for use with data in a format similar to an indexed face set. With glDrawArrays, OpenGL pulls data from the enabled arrays in order, vertex 0, then vertex 1, then vertex 2, and so on. With glDrawElements, you provide a list of vertex numbers. OpenGL will go through the list of vertex numbers, pulling data for the specified vertices from the arrays. The advantage of this comes, as with indexed face sets, from the fact that the same vertex can be reused several times.

To use glDrawElements to draw a primitive, you need an array to store the vertex numbers. The numbers in the array can be 8, 16, or 32 bit integers. (They are supposed to be unsigned integers, but arrays of regular positive integers will also work.) You also need arrays to store the vertex coordinates and other vertex data, and you must enable those arrays in the same way as for glDrawArrays, using functions such as glVertexArray and glEnableClientState. To actually draw the primitive, call the function

void glDrawElements( int primitiveType, vertexCount, dataType, void *array)

Here, primitiveType is one of the ten primitive types such as GL_LINES, vertexCount is the number of vertices to be drawn, dataType specifies the type of data in the array, and array is the array that holds the list of vertex numbers. The dataType must be given as one of the constants GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT, or GL_UNSIGNED_INT to specify 8, 16, or 32 bit integers respectively.

As an example, we can draw a cube. We can draw all six faces of the cube as one primitive of type GL_QUADS. We need the vertex coordinates in one array and the vertex numbers for the faces in another array. I will also use a color array for vertex colors. The vertex colors will be interpolated to pixels on the faces, just like the red/green/blue triangle. Here is code that could be used to draw the cube. Again, all this would not necessarily be in the same part of a program:

float vertexCoords[24] = {  // Coordinates for the vertices of a cube.
        1,1,1,   1,1,-1,   1,-1,-1,   1,-1,1,
        -1,1,1,  -1,1,-1,  -1,-1,-1,  -1,-1,1  };

float vertexColors[24] = {  // An RGB color value for each vertex
        1,1,1,   1,0,0,   1,1,0,   0,1,0,
        0,0,1,   1,0,1,   0,0,0,   0,1,1  };

int elementArray[24] = {  // Vertex numbers for the six faces.
        0,1,2,3, 0,3,7,4, 0,4,5,1,
        6,2,1,5, 6,5,4,7, 6,7,3,2  };

glVertexPointer( 3, GL_FLOAT, 0, vertexCoords );
glColorPointer( 3, GL_FLOAT, 0, vertexColors );

glEnableClientState( GL_VERTEX_ARRAY );
glEnableClientState( GL_COLOR_ARRAY );

glDrawElements( GL_QUADS, 24, GL_UNSIGNED_INT, elementArray );

Note that the second parameter is the number of vertices, not the number of quads.

The sample program glut/cubes-with-vertex-arrays.c uses this code to draw a cube. It draws a second cube using glDrawArrays. The Java version is jogl/CubesWithVertexArrays.java, but you need to read the next subsection before you can understand it. There is also a JavaScript version, glsim/cubes-with-vertex-arrays.html.

3.4.3 Java 中的数据缓冲区

Data Buffers in Java

普通的 Java 数组不适合与 glDrawElementsglDrawArrays 一起使用,部分原因是它们存储数据的格式,部分原因是在 Java 数组与图形处理单元之间传输数据的低效性。这些问题通过使用直接 NIO 缓冲区来解决。这里的术语 "nio" 指的是包 java.nio,其中包含了用于输入/输出的类。在这种情况下,"缓冲区" 是 java.nio.Buffer 类或其子类之一(如 FloatBufferIntBuffer)的对象。最后,"直接" 意味着缓冲区被优化,以便在内存和 GPU 等其他设备之间直接传输数据。与数组类似,nio 缓冲区是相同类型的元素的编号序列。例如,FloatBuffer 包含类型为 float 的值的编号序列。除了 boolean 之外,Java 的所有原始数据类型都有 Buffer 的子类。

JOGL 中,nio 缓冲区在多个使用数组的地方使用,就像在 C API 中使用数组一样。例如,JOGL 中的 GL2 类有以下 glVertexPointer 方法:

public void glVertexPointer(int size, int type, int stride, Buffer buffer)

除了最后一个参数与 C 版本不同。缓冲区可以是 FloatBufferIntBufferDoubleBuffer 类型。缓冲区的类型必须与方法中的 type 参数匹配。例如 glColorPointer 等函数工作方式相同,glDrawElements 采用以下形式

public void glDrawElements( int primitiveType, vertexCount, 
                                        dataType, Buffer buffer)

其中缓冲区可以是 IntBufferShortBufferByteBuffer 类型,以匹配 dataType UNSIGNED_INTUNSIGNED_SHORTUNSIGNED_BYTE

com.jogamp.common.nio.Buffers 包含用于处理直接 nio 缓冲区的静态实用方法。最容易使用的方法是从 Java 数组创建缓冲区。例如,方法 Buffers.newDirectFloatBuffer(array) 将一个浮点数组作为其参数,并创建与数组长度相同且包含与数组相同数据的 FloatBuffer。这些方法用于在示例程序 jogl/CubesWithVertexArrays.java 中创建缓冲区。例如,

float[] vertexCoords = {  // 立方体顶点的坐标。
            1,1,1,   1,1,-1,   1,-1,-1,   1,-1,1,
            -1,1,1,  -1,1,-1,  -1,-1,-1,  -1,-1,1  };

int[] elementArray = {  // 六个面的顶点编号。
            0,1,2,3, 0,3,7,4, 0,4,5,1,
            6,2,1,5, 6,5,4,7, 6,7,3,2  };

// 用于 glVertexPointer 和 glDrawElements 的缓冲区:            
FloatBuffer vertexCoordBuffer = Buffers.newDirectFloatBuffer(vertexCoords);
IntBuffer elementBuffer = Buffers.newDirectIntBuffer(elementArray);

然后,在绘制立方体时可以使用这些缓冲区:

gl2.glVertexPointer( 3, GL2.GL_FLOAT, 0, vertexCoordBuffer );

gl2.glDrawElements( GL2.GL_QUADS, 24, GL2.GL_UNSIGNED_INT, elementBuffer );

还有像 Buffers.newDirectFloatBuffer(n) 这样的方法,它创建长度为 n 的 FloatBuffer。请记住,nio Buffer,就像数组一样,只是给定类型的元素的线性序列。实际上,就像对数组一样,可以通过其在序列中的索引或位置引用缓冲区中的项。假设 buffer 是类型为 FloatBuffer 的变量,i 是 int,x 是 float。那么

buffer.put(i,x);

将 x 的值复制到缓冲区中编号为 i 的位置。类似地,buffer.get(i) 可以用于检索缓冲区中索引为 i 的值。这些方法使得可以以与数组类似的方式使用缓冲区。

Ordinary Java arrays are not suitable for use with glDrawElements and glDrawArrays, partly because of the format in which data is stored in them and partly because of inefficiency in transfer of data between Java arrays and the Graphics Processing Unit. These problems are solved by using direct nio buffers. The term "nio" here refers to the package java.nio, which contains classes for input/output. A "buffer" in this case is an object of the class java.nio.Buffer or one of its subclasses, such as FloatBuffer or IntBuffer. Finally, "direct" means that the buffer is optimized for direct transfer of data between memory and other devices such as the GPU. Like an array, an nio buffer is a numbered sequence of elements, all of the same type. A FloatBuffer, for example, contains a numbered sequence of values of type float. There are subclasses of Buffer for all of Java's primitive data types except boolean.

Nio buffers are used in JOGL in several places where arrays are used in the C API. For example, JOGL has the following glVertexPointer method in the GL2 class:

public void glVertexPointer(int size, int type, int stride, Buffer buffer)

Only the last parameter differs from the C version. The buffer can be of type FloatBuffer, IntBuffer, or DoubleBuffer. The type of buffer must match the type parameter in the method. Functions such as glColorPointer work the same way, and glDrawElements takes the form

public void glDrawElements( int primitiveType, vertexCount, 
                                        dataType, Buffer buffer)

where the buffer can be of type IntBuffer, ShortBuffer, or ByteBuffer to match the dataType UNSIGNED_INT, UNSIGNED_SHORT, or UNSIGNED_BYTE.

The class com.jogamp.common.nio.Buffers contains static utility methods for working with direct nio buffers. The easiest to use are methods that create a buffer from a Java array. For example, the method Buffers.newDirectFloatBuffer(array) takes a float array as its parameter and creates a FloatBuffer of the same length and containing the same data as the array. These methods are used to create the buffers in the sample program jogl/CubesWithVertexArrays.java. For example,

float[] vertexCoords = {  // Coordinates for the vertices of a cube.
            1,1,1,   1,1,-1,   1,-1,-1,   1,-1,1,
            -1,1,1,  -1,1,-1,  -1,-1,-1,  -1,-1,1  };

int[] elementArray = {  // Vertex numbers for the six faces.
            0,1,2,3, 0,3,7,4, 0,4,5,1,
            6,2,1,5, 6,5,4,7, 6,7,3,2  };

// Buffers for use with glVertexPointer and glDrawElements:            
FloatBuffer vertexCoordBuffer = Buffers.newDirectFloatBuffer(vertexCoords);
IntBuffer elementBuffer = Buffers.newDirectIntBuffer(elementArray);

The buffers can then be used when drawing the cube:

gl2.glVertexPointer( 3, GL2.GL_FLOAT, 0, vertexCoordBuffer );

gl2.glDrawElements( GL2.GL_QUADS, 24, GL2.GL_UNSIGNED_INT, elementBuffer );

There are also methods such as Buffers.newDirectFloatBuffer(n), which creates a FloatBuffer of length n. Remember that an nio Buffer, like an array, is simply a linear sequence of elements of a given type. In fact, just as for an array, it is possible to refer to items in a buffer by their index or position in that sequence. Suppose that buffer is a variable of type FloatBuffer, i is an int and x is a float. Then

buffer.put(i,x);

copies the value of x into position number i in the buffer. Similarly, buffer.get(i) can be used to retrieve the value at index i in the buffer. These methods make it possible to work with buffers in much the same way that you can work with arrays.

3.4.4 显示列表和VBO

Display Lists and VBOs

到目前为止,我们考虑的所有 OpenGL 绘图命令在同一对象被绘制多次时存在一个不幸的效率问题:每次绘制对象时,绘图命令和数据都必须传输到 GPU。应该可以将信息存储在 GPU 上,以便可以重复使用而无需重新传输。我们将讨论两种实现这一目标的技术:显示列表顶点缓冲对象VBO)。显示列表是原始的 OpenGL 1.0 的一部分,但它们不是现代 OpenGL API 的一部分。VBOOpenGL 1.5 中引入,并且在现代 OpenGL 中仍然很重要;我们在这里只会简要讨论它们,并且在介绍 WebGL 时会更详细地考虑它们。

当同一序列的 OpenGL 命令将被多次使用时,显示列表是很有用的。显示列表是一系列图形命令及其使用的数据。显示列表可以存储在 GPU 上。显示列表的内容只需要一次传输到 GPU。一旦创建了列表,就可以 "调用" 它。关键点在于,调用列表只需要一个 OpenGL 命令。虽然相同的命令列表仍然必须被执行,但只需从 CPU 传输一个命令到图形卡,然后可以利用硬件加速的全部性能以尽可能高的速度执行命令。

请注意,调用显示列表两次可能会产生两种不同的效果,因为效果可能取决于调用显示列表时的 OpenGL 状态。例如,生成球体几何体的显示列表可以在不同位置绘制球体,只要每次调用列表时都应用不同的建模变换。该列表还可以产生不同颜色的球体,只要在调用列表之间更改绘制颜色即可。

如果要使用显示列表,首先必须请求一个整数,用于将该列表标识给 GPU。这可以通过命令来完成,例如

listID = glGenLists(1);

返回值是一个 int,它将是列表的标识符。glGenLists 的参数也是一个 int,通常为 1。(实际上,您可以一次请求多个列表 ID;该参数告诉您要请求多少个。列表 ID 将是连续的整数,因此如果 listA 是 glGenLists(3) 的返回值,则三个列表的标识符将是 listA、listA + 1 和 listA + 2。)

一旦以这种方式分配了一个列表,就可以将命令存储到其中。如果 listID 是列表的 ID,您可以使用以下形式的代码执行此操作:

glNewList(listID, GL_COMPILE);
...  // 要存储在列表中的 OpenGL 命令。
glEndList();

参数 GL_COMPILE 表示您只想将命令存储到列表中,而不执行它们。如果使用替代参数 GL_COMPILE_AND_EXECUTE,那么命令将立即执行并存储在列表中以供以后重用。

创建了这样的显示列表后,可以使用以下命令调用列表:

glCallList(listID);

此命令的效果是告诉 GPU 执行它已经存储的列表。可以通过调用以下命令告诉显卡不再需要列表:

gl.glDeleteLists(listID, 1);

此方法调用中的第二个参数起到与 glGenLists 中的参数相同的作用;也就是说,它允许删除几个连续编号的列表。当您使用完列表后删除列表可以使 GPU 重新使用该列表使用的内存。


顶点缓冲对象采用了一种不同的重用信息的方法。它们只存储数据,而不是命令。VBO 类似于数组。事实上,它本质上就是一个数组,可以存储在 GPU 上以便于有效地重用。有 OpenGL 命令用于创建和删除 VBO,并将数据从 CPU 一侧的数组传输到 GPU 上的 VBO。您可以配置 glDrawArrays() 和 glDrawElements() 来从 VBO 而不是从普通数组(在 C 中)或 nio 缓冲区(在 JOGL 中)中获取数据。这意味着您可以将数据一次性发送到 GPU,然后任意次数地使用它。

我不会在这里讨论如何使用 VBO,因为它不是 OpenGL 1.1 的一部分。然而,有一个示例程序可以让您比较不同的渲染复杂图像的技术。该程序的 C 版本是 glut/color-cube-of-spheres.c,Java 版本是 jogl/ColorCubeOfSpheres.java。该程序绘制了 1331 个球,排列成一个 11x11x11 的立方体。球体的颜色不同,其中沿一个轴的颜色中有红色的数量变化,沿第二个轴的颜色中有绿色的数量变化,沿第三个轴的颜色中有蓝色的数量变化。每个球有 66 个顶点,其坐标可以使用数学函数 sin 和 cos 计算得到。该程序允许您从五种不同的渲染方法中选择,并显示使用所选方法渲染球体所需的时间。(Java 版本有一个下拉菜单用于选择方法;在 C 版本中,右键单击图像即可获得菜单。)您可以使用鼠标旋转球体的立方体,以获取更好的视图并生成更多数据以计算平均渲染时间。五种渲染技术是:

  • 直接绘制,重新计算顶点数据 —— 通过每次绘制球体时重新计算所有顶点坐标的方式,以非常愚蠢的方式绘制 1331 个球。
  • 直接绘制,预计算数据 —— 顶点坐标只计算一次并存储在一个数组中。球体使用 glBegin/glEnd 进行绘制,但在调用 glVertex* 时使用的数据是从数组中获取的,而不是每次需要时重新计算。
  • 显示列表 —— 创建包含绘制球体所需的所有命令和数据的显示列表。然后每个球体可以通过调用该显示列表一次来绘制。
  • 使用数组进行 DrawArrays —— 球体的数据存储在一个顶点数组中(或者,在 Java 中,存储在 nio 缓冲区中),并使用 glDrawArrays 调用来绘制每个球体,使用了本节早些时候讨论的技术。每次绘制球体时都必须将数据发送到 GPU 上。
  • 使用 VBO 进行 DrawArrays —— 再次使用 glDrawArrays 来绘制球体,但这次数据存储在 VBO 中而不是数组中,因此数据只需要传输到 GPU 一次。

在我的实验中,我发现,如预期的那样,显示列表和 VBO 给出了最短的渲染时间,两者之间几乎没有区别。在 C 版本的结果和 Java 版本的结果之间有一些有趣的差异,这似乎是因为 C 中的函数调用比 Java 中的方法调用更有效率。您应该在自己的计算机上尝试该程序,并比较各种渲染方法的渲染时间。

All of the OpenGL drawing commands that we have considered so far have an unfortunate inefficiency when the same object is going be drawn more than once: The commands and data for drawing that object must be transmitted to the GPU each time the object is drawn. It should be possible to store information on the GPU, so that it can be reused without retransmitting it. We will look at two techniques for doing this: display lists and vertex buffer objects (VBOs). Display lists were part of the original OpenGL 1.0, but they are not part of the modern OpenGL API. VBOs were introduced in OpenGL 1.5 and are still important in modern OpenGL; we will discuss them only briefly here and will consider them more fully when we get to WebGL.

Display lists are useful when the same sequence of OpenGL commands will be used several times. A display list is a list of graphics commands and the data used by those commands. A display list can be stored in a GPU. The contents of the display list only have to be transmitted once to the GPU. Once a list has been created, it can be "called." The key point is that calling a list requires only one OpenGL command. Although the same list of commands still has to be executed, only one command has to be transmitted from the CPU to the graphics card, and then the full power of hardware acceleration can be used to execute the commands at the highest possible speed.

Note that calling a display list twice can result in two different effects, since the effect can depend on the OpenGL state at the time the display list is called. For example, a display list that generates the geometry for a sphere can draw spheres in different locations, as long as different modeling transforms are in effect each time the list is called. The list can also produce spheres of different colors, as long as the drawing color is changed between calls to the list.

If you want to use a display list, you first have to ask for an integer that will identify that list to the GPU. This is done with a command such as

listID = glGenLists(1);

The return value is an int which will be the identifier for the list. The parameter to glGenLists is also an int, which is usually 1. (You can actually ask for several list IDs at once; the parameter tells how many you want. The list IDs will be consecutive integers, so that if listA is the return value from glGenLists(3), then the identifiers for the three lists will be listA, listA + 1, and listA + 2.)

Once you've allocated a list in this way, you can store commands into it. If listID is the ID for the list, you would do this with code of the form:

glNewList(listID, GL_COMPILE);
...  // OpenGL commands to be stored in the list.
glEndList();

The parameter GL_COMPILE means that you only want to store commands into the list, not execute them. If you use the alternative parameter GL_COMPILE_AND_EXECUTE, then the commands will be executed immediately as well as stored in the list for later reuse.

Once you have created a display list in this way, you can call the list with the command

glCallList(listID);

The effect of this command is to tell the GPU to execute a list that it has already stored. You can tell the graphics card that a list is no longer needed by calling

gl.glDeleteLists(listID, 1);

The second parameter in this method call plays the same role as the parameter in glGenLists; that is, it allows you delete several sequentially numbered lists. Deleting a list when you are through with it allows the GPU to reuse the memory that was used by that list.


Vertex buffer objects take a different approach to reusing information. They only store data, not commands. A VBO is similar to an array. In fact, it is essentially an array that can be stored on the GPU for efficiency of reuse. There are OpenGL commands to create and delete VBOs and to transfer data from an array on the CPU side into a VBO on the GPU. You can configure glDrawArrays() and glDrawElements() to take the data from a VBO instead of from an ordinary array (in C) or from an nio Buffer (in JOGL). This means that you can send the data once to the GPU and use it any number of times.

I will not discuss how to use VBOs here, since it was not a part of OpenGL 1.1. However, there is a sample program that lets you compare different techniques for rendering a complex image. The C version of the program is glut/color-cube-of-spheres.c, and the Java version is jogl/ColorCubeOfSpheres.java. The program draws 1331 spheres, arranged in an 11-by-11-by-11 cube. The spheres are different colors, with the amount of red in the color varying along one axis, the amount of green along a second axis, and the amount of blue along the third. Each sphere has 66 vertices, whose coordinates can be computed using the math functions sin and cos. The program allows you to select from five different rendering methods, and it shows the time that it takes to render the spheres using the selected method. (The Java version has a drop-down menu for selecting the method; in the C version, right-click the image to get the menu.) You can use your mouse to rotate the cube of spheres, both to get a better view and to generate more data for computing the average render time. The five rendering techniques are:

  • Direct Draw, Recomputing Vertex Data — A remarkably foolish way to draw 1331 spheres, by recomputing all of the vertex coordinates every time a sphere is drawn.
  • Direct Draw, Precomputed Data — The vertex coordinates are computed once and stored in an array. The spheres are drawn using glBegin/glEnd, but the data used in the calls to glVertex* are taken from the array rather than recomputed each time they are needed.
  • Display List — A display list is created containing all of the commands and data needed to draw a sphere. Each sphere can then be drawn by a single call of that display list.
  • DrawArrays with Arrays — The data for the sphere is stored in a vertex array (or, for Java, in an nio buffer), and each sphere is drawn using a call to glDrawArrays, using the techniques discussed earlier in this section. The data has to be sent to the GPU every time a sphere is drawn.
  • DrawArrays with VBOs — Again, glDrawArrays is used to draw the spheres, but this time the data is stored in a VBO instead of in an array, so the data only has to be transmitted to the GPU once.

In my own experiments, I found, as expected, that display lists and VBOs gave the shortest rendering times, with little difference between the two. There were some interesting differences between the results for the C version and the results for the Java version, which seem to be due to the fact that function calls in C are more efficient than method calls in Java. You should try the program on your own computer, and compare the rendering times for the various rendering methods.