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

Subsections
Java's Data Buffers
Drawing With Vertex Arrays
Vertex Buffer Objects
Drawing with Array Indices

Section 3.4

Drawing Primitives


We have been exclusively using glBegin/glEnd for drawing primitives, but that is just one of the approaches to drawing that OpenGL makes available. In fact, the glBegin/glEnd paradigm is considered to be a little old-fashioned and it has even been deprecated in the latest versions of OpenGL. This section will discuss alternative approaches.

The alternative approaches are more efficient, because they make many fewer calls to OpenGL commands and because they offer the possibility of storing data in the graphics card's memory instead of retransmitting the data every time it is used. Unfortunately, the alternative drawing methods are more complicated than glBegin/glEnd. This is especially true in Java (as opposed to C), because the implementation of arrays in Java makes Java arrays unsuitable for use in certain OpenGL methods that have array parameters in C. Jogl's solution to the array problem is to use "nio buffers" instead of arrays in those cases were Java arrays are not suitable. The first subsection, below, discusses nio buffers.

To give us something to draw in this section, we'll look at ways to produce the image in the following applet. It shows a dark red cylinder inside a kind of cloud of points. The points are randomly selected points on a sphere. The sphere points are lit, with normal vectors and texture coordinates appropriate for the unit sphere. The texture that is used is a topological map of the Earth's surface. You can animate the image in the applet, and you can rotate it with the mouse.

The source code for the program can be found in VertexArrayDemo.java.


3.4.1  Java's Data Buffers

The term "buffer" was already overused in OpenGL. Jogl's use of nio buffers makes it even harder to know exactly what type of buffer is being discussed in any particular case. The term buffer ends up meaning, basically, no more than a place where data can be stored -- usually a place where data can be stored by one entity and retrieved by another entity.

A Java nio buffer is an object belonging to the class java.nio.Buffer or one of its subclasses. The package java.nio defines input/output facilities that supplement those in the older java.io package. Buffers in this package are used for efficient transfer of data between a Java program and an I/O device such as a hard drive. When used in Jogl, this type of buffer offers the possibility of efficient data transfer between a Java program and a graphics card.

Subclasses of Buffer such as FloatBuffer and IntBuffer represent buffers capable of holding data items of a given primitive type. There is one such subclass for each primitive type except boolean.

To make working with nio buffers a little easier, Jogl defines a set of utility methods for creating buffers. They can be found as static methods in class BufferUtil, from the package com.sun.opengl.util. We will be using BufferUtil.newFloatBuffer(n), which creates a buffer that can hold n floating point values, and BufferUtil.newIntBuffer(n), which creates a buffer that can hold n integers. (The buffers created by these methods are so-called "direct" buffers, which are required for the Jogl methods that we will be using. It is also possible to create a Buffer object that stores its data in a standard Java array, but buffers of that type have the same problems for OpenGL as plain Java arrays. They could be used with some Jogl methods, but not others. We will avoid them.)

A 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.

A buffer differs from an array in that a buffer has an internal position pointer that indicates a "current position" within the buffer. A buffer has relative put and get methods that work on the item at the current position, and then advance the position pointer to the next position. That is,

buffer.put(x);

stores the value of x at the current position, and advances the position pointer to the next element in the buffer. Similarly, buffer.get() returns the item at the buffer's current position and advances the pointer.

There are also methods for transferring an entire array of values to or from a buffer. These methods can be much more efficient than transferring values one at a time. For example,

buffer.put(data, start, count);

copies count values from an array, data, starting from position start in the array. The data is stored in the buffer, starting at the buffer's current position. The buffer's position pointer is moved to the first index following the block of transferred data.

The method buffer.position(i) can be used to set the buffer's current position pointer to i. To return the position pointer to the start of the buffer, you can use buffer.position(0) or buffer.rewind().

As an example, suppose that we want to store the vertices of a triangle in a FloatBuffer, buf. Say the vertices are (1.5,0,0), (0,1.5,0), and (0.5,0.5,−1). Here are three ways to do it:

(1) Absolute put, starting at position 0:

      buf.put(0,1.5f);  buf.put(1,0);     buf.put(2,0);   // first vertex
      buf.put(3,0);     buf.put(4,1.5f);  buf.put(5,0);   // second vertex
      buf.put(6,0.5f);  buf.put(7,0.5f);  buf.put(8,-1);  // third vertex

(2) Relative put; starts at current position; no need to specify indices:

      buf.put(1.5f);  buf.put(0);     buf.put(0);   // first vertex
      buf.put(0);     buf.put(1.5f);  buf.put(0);   // second vertex
      buf.put(0.5f);  buf.put(0.5f);  buf.put(-1);  // third vertex
      
(3) Bulk put, copying the data from an array:

      float[] vert = {  1.5,0,0,  0,1.5,0,  0.5,0.5,-1  };
      buf.put(vert,0,9);

You should note that the absolute put methods used in case (1) do not move the buffer's internal pointer. The relative puts in cases (2) and (3) advance the pointer nine positions in each case -- and when you use them, you might need to reset the buffer's position pointer later.


3.4.2  Drawing With Vertex Arrays

As an alternative to using glBegin/glEnd, you can place your data into nio buffers and let OpenGL read the data from the buffers as it renders the primitives. (In C or C++, you would use arrays rather than buffers.) We start with a version of this technique that has been available since OpenGL 1.1, and so can be used on all current systems. The technique is referred to as using vertex arrays, although nio buffers, not arrays, are used in Jogl, and you can store other data such as normal vectors in the buffers, not just vertices.

To use vertex arrays, you must store vertices in a buffer, and you have to tell OpenGL that you are using that buffer with the following method from the class GL:

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

The size here is the number of coordinates per vertex, which can be 2, 3, or 4. (You have to provide the same number of coordinates for each vertex.) The stride is usually 0, meaning that the data values are stored in consecutive locations in the buffer; if that is not the case, then stride gives the distance in bytes between the location of one value and the location of the next value. The type is a constant that tells the data type of each of the numbers in the buffer. The possible values are GL.GL_FLOAT, GL.GL_INT, and GL.GL_DOUBLE. The constant that you provide here must match the type of the buffer. For example, if you want to use float values for the vertex coordinates, specify GL.GL_FLOAT as the type and use a buffer of type FloatBuffer. The vertex data is assumed to start at the buffer's current position. Usually, that will be at the beginning of the buffer. (In particular, note that you might need to set the buffer's position pointer to the appropriate value by calling buffer.position() before calling gl.glVertexPointer(); this will certainly be true if you use relative put commands to put data into the buffer.)

In addition to calling glVertexPointer, you must enable the use of the buffer by calling

gl.glEnableClientState(GL.GL_VERTEX_ARRAY);

Use gl.glDisableClientState(GL.GL_VERTEX_ARRAY) to disable the use of the array. OpenGL ignores the vertex pointer except when this state is enabled.

Finally, in order to actually use the vertex data from the buffer, use the following method from class GL:

void glDrawArrays(int mode, int first, int count);

This method call corresponds to one use of glBegin/glEnd. The mode tells which primitive type is being drawn, such as GL.GL_POLYGON or GL.GL_TRIANGLE_STRIP. The same ten primitive types that can be used with glBegin can be used here. first is the index in the buffer of the first vertex that is to used for drawing the primitive. Note that the position is given in terms of vertex number, not number of floating point values. The count is the number of vertices to be used, just as if glVertex were called count times.

Let's see how this could be used to draw the rectangle in the xy-plane with corners at (−1,−1) and (1,1). We need a variable of type FloatBuffer, which would probably be an instance variable:

private FloatBuffer rectVertices;

The buffer itself has to be allocated and filled with data, perhaps in the init() method:

rectVertices = BufferUtil.newFloatBuffer(8);
float[] data = {  -1,-1,  1,-1,  1,1,  -1,1  };
rectVertices.put(data,0,8);
rectVertices.rewind();

The last line, which moves the buffer's position pointer back to zero, is essential, since positions of data in the buffer are specified relative to that pointer. With this setup, we can draw the rectangle in the display() method like this:

gl.glVertexPointer(2, GL.GL_FLOAT, 0, rectVertices);
gl.glEnableClientState(GL.GL_VERTEX_ARRAY);
gl.glNormal3f(0,0,1);
gl.glDrawArrays(GL.GL_POLYGON, 0, 4);  // Generate the polygon using 4 vertices.
gl.glDisableClientState(GL.GL_VERTEX_ARRAY);

The "2" that is used as the first parameter to glVertexPointer says that each vertex consists of two floating point values, giving the coordinates of a vertex that lies in the xy-plane, just as for glVertex2f.

In this example, one normal vector will work for all the vertices, and there are no texture coordinates. In general, we will need a different normal vector and possibly texture coordinates for each vertex. We just have to store these data in their own arrays, in much the same way that we did the vertex data.

The following methods are used in the same way as glVertexPointer, to specify the buffers that hold normal vector and texture coordinate data:

public void glNormalPointer(int type, int stride, Buffer buffer)
public void glTexCoordPointer(int size, int type, int stride, Buffer buffer)

Note that glNormalPointer does not have a size parameter. For normal vectors, you must always give three numbers for each vector, so the size would always be 3 in any case.

To tell OpenGL to use the data from the buffers, you have to enable the appropriate client state, using the method calls

gl.glEnableClientState(GL.GL_NORMAL_ARRAY);
gl.glEnableClientState(GL.GL_TEXTURE_COORD_ARRAY);

When you call glDrawArrays, if GL_NORMAL_ARRAY is enabled, then normal vectors will be retrieved from the buffer specified by glNormalPointer. One normal vector will be retrieved for each vertex, and the normals in the normal buffer must correspond one-to-one with the vertices in the vertex buffer. The texture coordinate buffer works in the same way.


For a more extended example, we will look at how to draw the cylinder and sphere in the applet at the beginning of this section.

The sphere consists of thousands of randomly generated points on the unit sphere (that is, the sphere of radius 1 centered at the origin), with appropriate texture coordinates and normal vectors. The entire set of points can be drawn with one call to glDrawArrays, using GL_POINTS as the primitive type. (This example emphasizes the fact that points are essentially free-floating vertices that can have their own normals and texture coordinates.) We need buffers to hold vertices, normals, and texture coordinates. However, in this particular case, the normal to the unit sphere at a given point has the same coordinates as the point itself, so in fact I use the same set of data for the normals as for the vertices, stored in the same buffer. Here is the method that is used to create the data, where sphereVertices and sphereTexCoords are FloatBuffers and spherePointCount is the number of points to be generated:

private void createPointSphere() {
    spherePointCloud = BufferUtil.newFloatBuffer(spherePointCount*3);
    sphereTexCoords = BufferUtil.newFloatBuffer(spherePointCount*2);
    for (int i = 0; i < spherePointCount; i++) {
       double s = Math.random();
       double t = Math.random();
       sphereTexCoords.put(2*i,(float)s);
       sphereTexCoords.put(2*i+1,(float)t);
       double u = s * Math.PI * 2;
       double z = t * 2 - 1;
       double r = Math.sqrt(1-z*z);
       double x = r * Math.cos(u);
       double y = r * Math.sin(u);
       spherePointCloud.put(3*i,(float)x);
       spherePointCloud.put(3*i+1,(float)y);
       spherePointCloud.put(3*i+2,(float)z);
    }
}

You don't need to understand the math, but note that each vertex requires three floats while each set of texture coordinates requires two. Here, I've used indexed put commands to store the data into the buffers. In the method for creating the data for the cylinder, I used relative put, which has the advantage that I don't need to computer the correct index for each number that I put in the buffer. I don't present that method here, but you can find it in the source code.

Once the data has been stored in the buffers, it's easy to draw the sphere. We have to set up pointers to the data, enable the appropriate client states, and call glDrawArrays to generate the points from the data. In this case, the normals and the vertices are identical, and the data for them are taken from the same buffer.

// Tell OpenGL where to find the data:

gl.glVertexPointer(3, GL.GL_FLOAT, 0, sphereVertices);
gl.glNormalPointer(GL.GL_FLOAT, 0, sphereVertices);
gl.glTexCoordPointer(2, GL.GL_FLOAT, 0, sphereTexCoords);

// Tell OpenGL which arrays are being used:

gl.glEnableClientState(GL.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL.GL_NORMAL_ARRAY);
gl.glEnableClientState(GL.GL_TEXTURE_COORD_ARRAY);

// Turn on texturing, and draw the sphere:

sphereTexture.enable();
gl.glDrawArrays(GL.GL_POINTS, 0, spherePointCount); // Generate the points!

// At this point, texturing and client states could be disabled.

Things are just a little more complicated for the cylinder, since it requires three calls to glDrawArrays, one for the side of the cylinder, one for the top, and one for the bottom. We can, however, store the vertices for all three parts in the same buffer, as long as we remember the starting point for each part. The data is stored in a FloatBuffer named cylinderPoints. The vertices for the side of the cylinder start at position 0, for the top at position cylinderTopStart, and for the bottom at cylinderBottomStart. (That is, the side used cylinderTopStart vertices, and the first vertex for the top is at index cylinderTopStart in the list of vertices.) The normals for the side of the cylinder are stored in a FloatBuffer named cylinderSideNormals. For the top and bottom, the normal vector can be set by a single call to glNormal3f, since the same normal is used for each vertex. This means that GL_NORMAL_ARRAY has to be enabled while drawing the side but not while drawing the top and bottom of the cylinder. Putting all this together, the cylinder can be drawn as follows:

gl.glVertexPointer(3,GL.GL_FLOAT,0,cylinderPoints);
gl.glNormalPointer(GL.GL_FLOAT,0,cylinderSideNormals);

// Draw the side, using data from the vertex buffer and from the normal buffer.
  
gl.glEnableClientState(GL.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL.GL_NORMAL_ARRAY);
gl.glDrawArrays(GL.GL_QUAD_STRIP, 0, (cylinderVertexCount+1)*2);
gl.glDisableClientState(GL.GL_NORMAL_ARRAY);  // Turn off normal array.
                                              // Leave vertex array enabled.

// Draw the top and bottom, using data from vertex buffer only.
  
gl.glNormal3f(0,0,1); // Normal for all vertices for the top.
gl.glDrawArrays(GL.GL_TRIANGLE_FAN, cylinderTopStart, cylinderVertexCount+2);
gl.glNormal3f(0,0,-1); // Normal for all vertices for the bottom.
gl.glDrawArrays(GL.GL_TRIANGLE_FAN, cylinderBottomStart, cylinderVertexCount+2);
    
gl.glDisableClientState(GL.GL_VERTEX_ARRAY);  // Turn off vertex array.

In addition to vertex, normal, and texture coordinate arrays, glDrawArrays can use several other arrays, including a color array that holds color values and generic vertex attribute arrays that hold data for use in GLSL programs.


3.4.3  Vertex Buffer Objects

Vertex arrays speed up drawing by greatly reducing the number of calls to OpenGL routines. However, the data for the routines still has to be transferred to the graphics card each time it is used, and this can still be a bottleneck on performance. OpenGL Version 1.5 introduced another technique that offers the possibility of reducing and in some cases eliminating this bottleneck. The technique is referred to as vertex buffer objects or VBOs.

A vertex buffer object is a region of memory managed by OpenGL that can store the data from vertex arrays. Note that these buffers are not the same sort of thing as Java nio buffers. When VBOs are used, the data from the Java nio buffers specified by methods such as glVertexPointer and glNormalPointer is (at least potentially) copied into a VBO which can reside in memory inside the graphics card or in system memory that is more easily accessible to the graphics card.

Before using VBOs, you should be sure that the version of OpenGL is 1.5 or higher. It is a good idea to test this and store the answer in a boolean variable. In Jogl, you can do this with:

version_1_5 = gl.isExtensionAvailable("GL_VERSION_1_5");

VBOs are allocated by the glGenBuffers method, which creates one or more VBOs and returns an integer ID number for each buffer created. The ID numbers are stored in an array. For example, four VBOs can be created like this:

int[] bufferID = new int[4];    // Get 4 buffer IDs.
gl.glGenBuffers(4,bufferID,0);

The third parameter is an offset that tells the starting index in the array where the IDs should be stored; it is usually 0 (and is absent in the C API).

Now, most of the OpenGL routines that work with VBOs do not mention a VBO index. Instead, they work with the "current VBO." For use with vertex arrays, the current VBO is set by calling

gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vboID);

where vboID is the ID number of the VBO that is being made current. Later, we'll see another possible value for the first parameter of this method. It's important to understand that this method does nothing except to say, "OK, from now on anything I do with VBOs that is relevant to vertex arrays should be applied to VBO number vboID." It is a switch that directs future commands to a particular VBO. You can also call gl.glBindBuffer(GL.GL_ARRAY_BUFFER,0) to direct commands away from VBOs altogether; this can be important because the meaning of some commands changes when they are used with VBOs.

To specify the data that is to be stored in a VBO, use the glBufferData method from the GL class. Note that this method supplies data for the VBO whose ID has been selected using glBindBuffer:

public void glBufferData(int target, int size, Buffer data, int usage)

The target, for now, should be GL.GL_ARRAY_BUFFER (and in general should match the first parameter in glBindBuffer). size is the size of the data in bytes, and the data is stored in data, which is a Java nio Buffer. (Note that the data should already be there; even though the data will be used by future commands, this method specifies the data itself, not just the location of the data.) The usage parameter is particularly interesting. It is a "hint" that tells OpenGL how the data will be used. OpenGL will try to store the data in the optimal location for its intended use. For our purposes in this section, the possible values are

The VertexArrayDemo application actually uses VBOs when the OpenGL version is 1.5 or higher. VBOs are used to store the vertex, normal, and texture coordinate data for the sphere and cylinder. Four VBOs are used. Here is the code that creates the VBO's and provides them with data:

if (version_1_5) {
    int[] bufferID = new int[4];    // Get 4 buffer IDs for the data.
    gl.glGenBuffers(4,bufferID,0);
 
    spherePointBufferID = bufferID[0];    // VBO for sphere vertices/normals.
    sphereTexCoordID = bufferID[1];       // VBO for sphere texture coords.
    cylinderPointBufferID = bufferID[2];  // VBO for cylinder vertices.
    cylinderNormalBufferID = bufferID[3]; // VBO for cylinder normals.
    
    gl.glBindBuffer(GL.GL_ARRAY_BUFFER, spherePointBufferID);
    gl.glBufferData(GL.GL_ARRAY_BUFFER, spherePointCount*3*4, 
                                          sphereVertices, GL.GL_STATIC_DRAW);
    
    gl.glBindBuffer(GL.GL_ARRAY_BUFFER, sphereTexCoordID);
    gl.glBufferData(GL.GL_ARRAY_BUFFER, spherePointCount*2*4, 
                                        sphereTexCoords, GL.GL_STATIC_DRAW);
    
    gl.glBindBuffer(GL.GL_ARRAY_BUFFER, cylinderPointBufferID);
    gl.glBufferData(GL.GL_ARRAY_BUFFER, ((cylinderVertexCount+1)*4+2)*3*4, 
                                         cylinderPoints, GL.GL_STATIC_DRAW);
    
    gl.glBindBuffer(GL.GL_ARRAY_BUFFER, cylinderNormalBufferID);
    gl.glBufferData(GL.GL_ARRAY_BUFFER, (cylinderVertexCount+1)*2*3*4, 
                                   cylinderSideNormals, GL.GL_STATIC_DRAW);
   
    gl.glBindBuffer(GL.GL_ARRAY_BUFFER, 0);  // Leave no buffer ID bound.
}

The if statement tests whether the version of OpenGL is high enough to support vertex buffer objects, using the variable version_1_5 which was discussed above. Note how glBindBuffer is used to select the VBO to which the following glBufferData method will apply. Also, note that the size specified in the second parameter to glBufferData is given in bytes. Since a float value takes up four bytes, the size is obtained by multiplying the number of floats by 4.


Of course, we also have to use the data from the VBOs! The VBOs are holding vertex array data. The glDrawArrays and glEnableClientState commands are used in exactly the same way whether or not we are using VBOs. However, the command for telling OpenGL where to find that data is a little different when using VBOs. When the data is stored in VBOs, alternative forms of the glVertexPointer, glNormalPointer, and glTexCoordPointer methods are used:

public void glVertexPointer(int size, int type, int stride, long vboOffset)
public void glNormalPointer(int type, int stride, long vboOffset)
public void glTexCoordPointer(int size, int type, int stride, long vboOffset)

The difference is the last parameter, which is now an integer instead of a Buffer. (In the C API, there is only one version of each command, with a pointer as the fourth parameter, but that parameter is interpreted differently depending on whether VBOs are being used or not.) The vboOffset gives the starting position of the data within the VBO. This offset is given as the number of bytes from the beginning of the VBO; the value is often zero. The VBO in question is not mentioned. As usual, it is the VBO that has been most recently specified by a call to gl.glBindBuffer(GL.GL_ARRAY_BUFFER,vboID).

We can now look at the complete sphere-drawing method from VertexArrayDemo, which uses VBOs when the OpenGL version is 1.5 or higher. Note that this only makes a difference when telling OpenGL the location of the data. Once that's done, the code for drawing the sphere is identical whether VBOs are used or not:

private void drawPointSphere(GL gl, int pointCt) {
    if (version_1_5) {
          // Use glBindBuffer to say what VBO to work on, then use
          // glVertexPointer to set the position where the data starts
          // in the buffer (in this case, at the start of the buffer).
        gl.glBindBuffer(GL.GL_ARRAY_BUFFER, spherePointBufferID);
        gl.glVertexPointer(3, GL.GL_FLOAT, 0, 0);
          // Use the same buffer for the normal vectors.
        gl.glNormalPointer(GL.GL_FLOAT, 0, 0); 
          // Now, set up the texture coordinate pointer in the same way.
        gl.glBindBuffer(GL.GL_ARRAY_BUFFER, sphereTexCoordID);
        gl.glTexCoordPointer(2, GL.GL_FLOAT, 0, 0);
        gl.glBindBuffer(GL.GL_ARRAY_BUFFER,0); // Leave no VBO bound.
    }
    else {
           // Use glVertexPointer, etc., to set the buffers from which
           // the various kinds of data will be read.
        gl.glVertexPointer(3,GL.GL_FLOAT,0,sphereVertices);
        gl.glNormalPointer(GL.GL_FLOAT,0,sphereVertices);
        gl.glTexCoordPointer(2, GL.GL_FLOAT, 0, sphereTexCoords);
    }
    gl.glEnableClientState(GL.GL_VERTEX_ARRAY);
    gl.glEnableClientState(GL.GL_NORMAL_ARRAY);
    gl.glEnableClientState(GL.GL_TEXTURE_COORD_ARRAY);
    sphereTexture.enable();
    gl.glDrawArrays(GL.GL_POINTS, 0, spherePointCount); // Generate the points!
    gl.glDisableClientState(GL.GL_VERTEX_ARRAY);
    gl.glDisableClientState(GL.GL_NORMAL_ARRAY);
    gl.glDisableClientState(GL.GL_TEXTURE_COORD_ARRAY);
    sphereTexture.disable();
 }

Obviously, vertex buffer arrays are non-trivial to use. The reward is the possibility of better performance when rendering complex scenes.


3.4.4  Drawing with Array Indices

The glDrawArrays method is great for drawing primitives given as a list of vertex coordinates. However, it can't be used for data in indexed face set (IFS) format. For that, there is the glDrawElements method. For an indexed face set, a face is specified by a sequence of integers representing indices into a list of vertices. glDrawElements can work directly with data in this format. To give us an example to work with, we consider an "icosphere," a polygonal approximation of a sphere that is obtained by subdividing the faces of an icosahedron. You get a better approximation by subdividing the faces more times. Here is an applet that shows an icosphere. You can select the number of subdivisions, and you can rotate the picture by mouse:

The source code for the program can be found in IcosphereIFS.java.

For use with glDrawElements, you can set up buffers to hold vertex coordinates, normal coordinates, and texture coordinates, exactly as you would for glDrawArrays (including the use of vertex buffer objects, if desired). Now, however, the elements in the arrays are stored in arbitrary order, not the order in which they will be used to generate primitives. The order needed for generating primitives will be specified by a separate list of indices into the arrays. Note, however, that the order of elements in the various arrays must correspond. That is, the first vector in the normal array and the first set of texture coordinates in the texture array must correspond to the first vertex in the vertex array, the second item in the normal and texture arrays must correspond to the second vertex, and so on.

For the icosphere example, the geometry consists of a large nubmer of triangles, and the whole thing can be drawn with a single use of the GL_TRIANGLES primitive. Let's assume that the vertex coordinates have been stored in a Java nio FloatBuffer named icosphereVertexBuffer. Because the points are points on the unit sphere, we can use the same set of coordinates as unit normal vectors. And let's say that the face data (the list of vertex indices for each triangle) is in an IntBuffer named icosphereIndexBuffer. We could actually draw the icosphere directly using glBegin/glEnd: Assuming indexCount is the number of integers in icosphereIndexBuffer, the following code would work:

gl.glBegin(GL.GL_TRIANGLES);
   for (int i = 0; i < indexCount; i++) {
      int vertexIndex = icosphereIndexBuffer.get(i); // Index of i-th vertex.
      float vx = icosphereVertexBuffer.get(vertexIndex); // Get vertex coords.
      float vy = icosphereVertexBuffer.get(vertexIndex);
      float vz = icosphereVertexBuffer.get(vertexIndex);
      gl.glNormal3f(vx,vy,vz);  // Use vertex coords as normal vector.
      gl.glVertex3f(vx,vy,vz);  // Generate the i-th vertex.
   }
gl.glEnd();

But glDrawElements is meant to replace precisely this sort of code. Before using it, we must use glVertexPointer and glNormalPointer to set up a vertex pointer and normal pointer, as for glDrawArrays. How we do this depends on whether we are using vertex buffer objects, but it is done exactly as above.

VBOs can also be used to store the face index data for use with glDrawElements, and how we use glDrawElements also depends on whether or not a vertex buffer object is used. Unfortunately, the use of VBOs for this purpose is not exactly parallel to their use for vertex and normal data. If we do not use a VBO for the face data, then the face data is passed directly to glDrawArrays, as follows:

gl.glDrawElements(GL.GL_TRIANGLES, indexCount, 
                                GL.GL_UNSIGNED_INT, icosphereIndexBuffer);

The first parameter is the type of primitive that is being drawn. The second is the number of vertices that will be generated. The third is the type of data in the buffer. And the fourth is the nio Buffer that holds the face data. The face data consists of one integer for each vertex, giving an index into the data that has already been set up by glVertexPointer and related methods. Note that there is no indication of the position within the buffer where the data begins. The data begins at the buffer's current internal position pointer. If necessary, you can set that pointer by calling the buffer's position method before calling glDrawElements. (This is one place where are are forced to work with the internal buffer pointer, if you want to store data for more than one call to glDrawElements in the same buffer.)

Now, let's look at what happens when a VBO is used to hold the face data. In this case, the data must be loaded into the VBO before it can be used. For this application, the first parameter to glBindBuffer and glBufferData must be GL.GL_ELEMENT_ARRAY_BUFFER:

gl.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, icopsphereIndexID);
gl.glBufferData(GL.GL_ELEMENT_ARRAY_BUFFER, indexCount*4, 
                            icosphereIndexBuffer, GL.GL_STATIC_DRAW);

At the time when glDrawElements is called, the same buffer ID must be bound, and the fourth parameter to glDrawElements is replaced by an integer giving the position of the data within the VBO, given as the number of bytes from the start of the VBO:

gl.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, icopsphereIndexID);
gl.glDrawElements(GL.GL_TRIANGLES, indexCount, GL.GL_UNSIGNED_INT, 0);

See the source code, IcosphereIFS.java, to see how all this is used in the context of a full program.


This has been an admittedly short introduction to glDrawElements, but hopefully it is similar enough to glDrawArrays that the information given here is enough to get you started using it. Let's look at another, simpler, example that shows how you might use glDrawElements to draw an IFS representing a simple polyhedron. We will use the data for the same pyramid that was used as an example in the previous section:

float[][] vertexList = {  {1,0,1}, {1,0,-1}, {-1,0,-1}, {-1,0,1}, {0,1,0}  };
int[][] faceList   = {  {4,3,0},  {4,0,1},  {4,1,2},  {4,2,3},  {0,3,2,1}  };

Since this is such a small object, we will not attempt to use vertex buffer objects. We will draw one face at a time, using a single normal vector for the face; I will specify the appropriate normal directly. Here is the code for drawing the pyramid (which would actually be scattered in several parts of a program):

// Declare variables to hold the data, probably as instance variables:

FloatBuffer vertexBuffer;
IntBuffer faceBuffer;

// Create the Java nio buffers, and fill them with data, perhaps in init():

vertexBuffer = BufferUtil.newFloatBuffer(15);
faceBuffer = BufferUtil.newIntBuffer(16);

float[] vertexList = {  1,0,1, 1,0,-1, -1,0,-1, -1,0,1, 0,1,0  };
int[] faceList   = {  4,3,0,  4,0,1,  4,1,2,  4,2,3,  0,3,2,1  };

vertexBuffer.put(vertexList, 0, 15);
vertexBuffer.rewind();

faceBuffer.put(faceList, 0, 16); // No need to rewind; position is set later.

// Set up for using a vertex array, in init() or display():

gl.glEnableClientState(GL.GL_VERTEX_ARRAY);
gl.glVertexPointer(3, GL.GL_FLOAT, 0, vertexBuffer);

// Do the actual drawing, one face at a time.  Set the position in
// the faceBuffer to indicate the start of the data in each case:

faceBuffer.position(0);
gl.glNormal3f(0, 0.707f, 0.707f);
gl.glDrawElements(GL.GL_POLYGON, 3, GL.GL_UNSIGNED_INT, faceBuffer);

faceBuffer.position(3);
gl.glNormal3f(0.707f, 0.707f, 0);
gl.glDrawElements(GL.GL_POLYGON, 3, GL.GL_UNSIGNED_INT, faceBuffer);

faceBuffer.position(6);
gl.glNormal3f(0, 0.707f, -0.707f);
gl.glDrawElements(GL.GL_POLYGON, 3, GL.GL_UNSIGNED_INT, faceBuffer);

faceBuffer.position(9);
gl.glNormal3f(-0.707f, 0.707f, 0);
gl.glDrawElements(GL.GL_POLYGON, 3, GL.GL_UNSIGNED_INT, faceBuffer);

faceBuffer.position(12);
gl.glNormal3f(0, -1, 0);
gl.glDrawElements(GL.GL_POLYGON, 4, GL.GL_UNSIGNED_INT, faceBuffer);

This has not been a complete survey of the OpenGL routines that can be used for drawing primitives. For example, there is a glDrawRangeElements routine, introduced in OpenGL version 1.2, that is similar to glDrawElements but can be more efficient. And glMultiDrawArrays, from version 1.4, can be used to do the job of multiple calls to glDrawArrays at once. Interested readers can consult an OpenGL reference.


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