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.
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:
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 parenthese 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.
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[i*3] is a pointer to element number i*3 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:
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 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. We 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, this would not necessarily be all 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.
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.
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:
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.