1.7. Drawing Geometry
As you can see from Figure 1.1, data for drawing geometry (points, lines, and polygons) starts off in application-controlled memory (1). This memory may be on the host CPU, or, with the help of some recent additions to OpenGL or under-the-covers data caching by the OpenGL implementation, it may actually reside in video memory on the graphics accelerator. Either way, the fact is that it is memory that contains geometry data that the application can cause to be drawn.
1.7.1. Geometry Specification
The geometric primitives supported in OpenGL are points, lines, line strips, line loops, polygons, triangles, triangle strips, triangle fans, quadrilaterals, and quadrilateral strips. There are three main ways to send geometry data to OpenGL. The first is the vertex-at-a-time method, which calls glBegin to start a primitive and calls glEnd to end it. In between are commands that set specific VERTEX ATTRIBUTES such as vertex position, color, normal, texture coordinates, secondary color, edge flags, and fog coordinates, using calls such as glVertex, glColor, glNormal, and glTexCoord. (A number of variants of these function calls allow the application to pass these values with various data types as well as to pass them by value or by reference.) Up through version 1.5 of OpenGL, there was no way to send arbitrary (user-defined) pervertex data. The only per-vertex attributes allowed were those specifically defined in the OpenGL specification. OpenGL 2.0 added a method for sending arbitrary per-vertex data; that method is described in Section 7.7 "Specifying Vertex Attributes."
When the vertex-at-a-time method is used, the call to glVertex signals the end of the data definition for a single vertex, and it may also define the completion of a primitive. After glBegin is called and a primitive type is specified, a graphics primitive is completed whenever glVertex is called enough times to completely specify a primitive of the indicated type. For independent triangles, a triangle is completed every third time glVertex is called. For triangle strips, a triangle is completed when glVertex is called for the third time, and an additional connecting triangle is completed for each subsequent call to glVertex.
The second method of drawing primitives is to use vertex arrays. With this method, applications store vertex attributes in user-defined arrays, set up pointers to the arrays, and use glDrawArrays, glMultiDrawArrays, glDrawElements, glMultiDrawElements, glDrawRangeElements, or glInterleavedArrays to draw a large number of primitives at once. Because these entry points can efficiently pass large amounts of geometry data to OpenGL, application developers are encouraged to use them for portions of code that are extremely performance critical. Using glBegin and glEnd requires a function call to specify each attribute of each vertex, so the function call overhead can become substantial when objects with thousands of vertices are drawn. In contrast, vertex arrays can be used to draw a large number of primitives with a single function call after the vertex data is organized into arrays. Processing the array data in this fashion can be faster because it is often more efficient for the OpenGL implementation to deal with data organized in this way. The current array of color values is specified with glColorPointer, the current array of vertex positions is specified with glVertexPointer, the current array of normal vectors is specified with glNormalPointer, and so on. The function glInterleavedArrays can specify and enable several interleaved arrays simultaneously (e.g., each vertex might be defined with three floating-point values representing a normal followed by three floating-point values representing a vertex position.)
The preceding two methods are referred to as drawing in IMMEDIATE MODE because primitives are rendered as soon as they have been specified. The third method involves storing either the vertex-at-a-time function calls or the vertex array calls in a DISPLAY LIST, an OpenGL-managed data structure that stores commands for later execution. Display lists can include commands to set state as well as commands to draw geometry. Display lists are stored on the server side and can be processed later with glCallList or glCallLists. This is not illustrated in Figure 1.1, but it is another way that data can be provided to the OpenGL processing pipeline. The definition of a display list is initiated with glNewList, and display list definition is completed with glEndList. All the commands issued between those two calls become part of the display list, although certain OpenGL commands are not allowed within display lists. Depending on the implementation, DISPLAY LIST MODE can provide a performance advantage over immediate mode. Storing commands in a display list gives the OpenGL implementation an opportunity to optimize the commands in the display list for the underlying hardware. It also gives the implementation the chance to store the commands in a location that enables better drawing performance, perhaps even in memory on the graphics accelerator. Of course, some extra computation or data movement is usually required to implement these optimizations, so applications will typically see a performance benefit only if the display list is executed more than once.
New API calls in version 1.5 of OpenGL permitted vertex array data to be stored in server-side memory. This mechanism typically provides the highest performance rendering because the data can be stored in memory on the graphics accelerator and need not be transferred over the I/O bus each time it is rendered. The API also supports the concept of efficiently streaming data from client to server. The glBindBuffer command creates a buffer object, and glBufferData and glBufferSubData specify the data values in such a buffer. glMapBuffer can map a buffer object into the client's address space and obtain a pointer to this memory so that data values can be specified directly. The command glUnmapBuffer must be called before the values in the buffer are accessed by subsequent GL rendering commands. glBindBuffer can also make a particular buffer object part of current state. If buffer object 0 is bound when calls are made to vertex array pointer commands such as glColorPointer, glNormalPointer, glVertexPointer, and so on, the pointer parameter to these calls is understood to be a pointer to client-side memory. When a buffer object other than 0 is bound, the pointer parameter is understood to be an offset into the currently bound buffer object. Subsequent calls to one of the vertex array drawing commands (e.g., glMultiDrawArrays) can thus obtain their vertex data from either client- or server-side memory or a combination thereof.
OpenGL supports the rendering of curves and surfaces with evaluators. Evaluators use a polynomial mapping to produce vertex attributes such as color, normal, and position that are sent to the vertex processing stage just as if they had been provided by the client. See the OpenGL specification for a complete description of this functionality.
1.7.2. Per-Vertex Operations
No matter which of these methods is used, the net result is that geometry data is transferred into the first stage of processing in OpenGL, VERTEX PROCESSING (2). At this point, vertex positions are transformed by the modelview and projection matrices, normals are transformed by the inverse transpose of the upper leftmost 3 x 3 matrix taken from the modelview matrix, texture coordinates are transformed by the texture matrices, lighting calculations are applied to modify the base color, texture coordinates may be automatically generated, color material state is applied, and point sizes are computed. All of these things are rigidly defined by the OpenGL specification. They are performed in a specific order, according to specific formulas, with specific items of OpenGL state controlling the process.
Because the most important things that occur in this stage are transformation and lighting, the vertex processing stage is sometimes called TRANSFORMATION AND LIGHTING, or, more familiarly, T&L. There is no application control to this process other than modifying OpenGL state values: turning lighting on or off with glEnable/glDisable; changing lighting attributes with glLight and glLightModel; changing material properties with glMaterial; or modifying the modelview matrix by calling matrix manipulation functions such as glMatrixMode, glLoadMatrix, glMultMatrix, glRotate, glScale, glTranslate. At this stage of processing, each vertex is treated independently. The vertex position computed by the transformation stage is used in subsequent clipping operations. The transformation process is discussed in detail in Section 1.9.
Lighting effects in OpenGL are controlled by manipulation of the attributes of one or more of the simulated light sources defined in OpenGL. The number of light sources supported by an OpenGL implementation is specifically limited to GL_MAX_LIGHTS. This value can be queried with glGet and must be at least 8. Each simulated light source in OpenGL has attributes that cause it to behave as a directional light source, a point light source, or a spotlight. Light attributes that can be adjusted by an application include the color of the emitted light, defined as ambient, diffuse, and specular RGBA intensity values; the light source position; attenuation factors that define how rapidly the intensity drops off as a function of distance; and direction, exponent, and cutoff factors for spotlights. These attributes can be modified for any light with glLight. Individual lights can be turned on or off by a call to glEnable/glDisable with a symbolic constant that specifies the affected light source.
Lighting produces a primary and secondary color for each vertex. The entire process of lighting can be turned on or off by a call to glEnable/glDisable with the symbolic constant GL_LIGHTING. If lighting is disabled, the values of the primary and secondary color are taken from the last color value set with the glColor command and the last secondary color set with the glSecondaryColor command.
The effects from enabled light sources are used in conjunction with surface material properties to determine the lit color at a particular vertex. Materials are characterized by the color of light they emit; the color of ambient, diffuse, and specular light they reflect; and their shininess. Material properties can be defined separately for front-facing surfaces and for back-facing surfaces and are specified with glMaterial.
1.7.3. Primitive Assembly
After vertex processing, all the attributes associated with each vertex are completely determined. The vertex data is then sent on to a stage called PRIMITIVE ASSEMBLY (3). At this point the vertex data is collected into complete primitives. Points require a single vertex, lines require two, triangles require three, quadrilaterals require four, and general polygons can have an arbitrary number of vertices. For the vertex-at-a-time API, an argument to glBegin specifies the primitive type; for vertex arrays, the primitive type is passed as an argument to the function that draws the vertex array. The primitive assembly stage effectively collects enough vertices to construct a single primitive, and then this primitive is passed on to the next stage of processing. The reason this stage is needed is that at the very next stage, operations are performed on a set of vertices, and the operations depend on the type of primitive. In particular, clipping is done differently, depending on whether the primitive is a point, line, or polygon.
1.7.4. Primitive Processing
The next stage of processing (4), actually consists of several distinct steps that have been combined into a single box in Figure 1.1 just to simplify the diagram. The first step that occurs is clipping. This operation compares each primitive to any user-defined clipping planes set by calling glClipPlane as well as to the VIEW VOLUME established by the MODELVIEW-PROJECT MATRIX, which is the concatenation of the modelview and projection matrices. If the primitive is completely within the view volume and the user-defined clipping planes, it is passed on for subsequent processing. If it is completely outside the view volume or the user-defined clipping planes, the primitive is rejected, and no further processing is required. If the primitive is partially in and partially out, it is divided (CLIPPED) in such a way that only the portion within the clip volume and the user-defined clipping planes is passed on for further processing.
Another operation that occurs at this stage is perspective projection. If the current view is a perspective view, each vertex has its x, y, and z components divided by its homogeneous coordinate w. Following this, each vertex is transformed by the current viewport transformation (set with glDepthRange and glViewport) to generate window coordinates. Certain OpenGL states can be set to cause an operation called CULLING to be performed on polygon primitives at this stage. With the computed window coordinates, each polygon primitive is tested to see whether it is facing away from the current viewing position. The culling state can be enabled with glEnable, and glCullFace can be called to specify that back-facing polygons will be discarded (culled), front-facing polygons will be discarded, or both will be discarded.
Geometric primitives that are passed through the OpenGL pipeline contain a set of data at each of the vertices of the primitive. At the next stage (5), primitives (points, lines, or polygons) are decomposed into smaller units corresponding to pixels in the destination frame buffer. This process is called RASTERIZATION. Each of these smaller units generated by rasterization is referred to as a FRAGMENT. For instance, a line might cover five pixels on the screen, and the process of rasterization converts the line (defined by two vertices) into five fragments. A fragment comprises a window coordinate and depth and other associated attributes such as color, texture coordinates, and so on. The values for each of these attributes are determined by interpolation between the values specified (or computed) at the vertices of the primitive. At the time they are rasterized, vertices have a primary color and a secondary color. The glShadeModel function specifies whether these color values are interpolated between the vertices (SMOOTH SHADING) or whether the color values for the last vertex of the primitive are used for the entire primitive (FLAT SHADING).
Each type of primitive has different rasterization rules and different OpenGL state. Points have a width controlled by glPointSize and other rendering attributes that are defined by glPointParameter. OpenGL 2.0 added the ability to draw an arbitrary shape at each point position by means of a texture called a POINT SPRITE. Lines have a width that is controlled with glLineWidth and a stipple pattern that is set with glLineStipple. Polygons have a stipple pattern that is set with glPolygonStipple. Polygons can be drawn as filled, outline, or vertex points depending only on the value set with glPolygonMode. The depth values for each fragment in a polygon can be modified by a value that is computed with the state set with glPolygonOffset. The orientation of polygons that are to be considered front facing can be set with glFrontFace. The process of smoothing the jagged appearance of a primitive is called ANTIALIASING. Primitive antialiasing can be enabled with glEnable and the appropriate symbolic constant: GL_POINT_SMOOTH, GL_LINE_SMOOTH, or GL_POLYGON_SMOOTH.
1.7.6. Fragment Processing
After fragments have been generated by rasterization, a number of operations occur on fragments. Collectively, these operations are called FRAGMENT PROCESSING (6). Perhaps the most important operation that occurs at this point is called TEXTURE MAPPING. In this operation, the texture coordinates associated with the fragment are used to access a region of graphics memory called TEXTURE MEMORY (7). OpenGL defines a lot of state values that affect how textures are accessed as well as how the retrieved values are applied to the current fragment. Many extensions have been defined to this area that is rather complex to begin with. We spend some time talking about texturing operations in Section 1.10.
Other operations that occur at this point are FOG (modifying the color of the fragment depending on its distance from the view point) and COLOR SUM (combining the values of the fragment's primary color and secondary color). Fog parameters are set with glFog, and secondary colors are vertex attributes that can be passed in with the vertex attribute command glSecondaryColor or that can be computed by the lighting stage.
1.7.7. Per-Fragment Operations
After fragment processing, fragments are submitted to a set of fairly simple operations called PER-FRAGMENT OPERATIONS (8). These include tests like the following:
Blending, dithering, and logical operations are also considered per-fragment operations. The blending operation calculates the color to be written into the frame buffer using a blend of the fragment's color, the color stored in the frame buffer, and the blending state as established by glBlendFunc, glBlendColor, and glBlendEquation. Dithering is a method of trading spatial resolution for color resolution, but today's graphics accelerators contain enough frame buffer memory to make this trade-off unnecessary. The final fragment value is written into the frame buffer with the logical operation set by glLogicOp.
Each of the per-fragment operations is conceptually simple and nowadays can be implemented efficiently and inexpensively in hardware. Some of these operations also involve reading values from the frame buffer (i.e., color, depth, or stencil). With today's hardware, all these back-end rendering operations can be performed at millions of pixels per second, even those that require reading from the frame buffer.
1.7.8. Frame Buffer Operations
Things that control or affect the whole frame buffer are called FRAME BUFFER OPERATIONS (9). Certain OpenGL state controls the region of the frame buffer into which primitives are drawn. OpenGL supports display of stereo images as well as double buffering, so a number of choices are available for the rendering destination. Regions of the frame buffer are called BUFFERS and are referred to as the front, back, left, right, front left, front right, back left, back right, front and back, and aux0, aux1, and so on up to the number of auxiliary buffers supported minus 1. Any of these buffers can be set as the destination for subsequent rendering operations with glDrawBuffer. Multiple buffers can be established as the destination for rendering with glDrawBuffers. Regions within the draw buffer(s) can be write protected. The glColorMask function determines whether writing is allowed to the red, green, blue, or alpha components of the destination buffer. The glDepthMask function determines whether the depth components of the destination buffer can be modified. The glStencilMask function controls the writing of particular bits in the stencil components of the destination buffer. Values in the frame buffer can be initialized with glClear. Values that will be used to initialize the color components, depth components, stencil components, and accumulation buffer components are set with glClearColor, glClearDepth, glClearStencil, and glClearAccum, respectively. The accumulation buffer operation can be specified with glAccum.
For performance, OpenGL implementations often employ a variety of buffering schemes in order to send larger batches of graphics primitives to the 3D graphics hardware. To make sure that all graphics primitives for a specific rendering context are progressing toward completion, an application should call glFlush. To make sure that all graphics primitives for a particular rendering context have finished rendering, an application should call glFinish. This command blocks until the effects of all previous commands have been completed. Blocking can be costly in terms of performance, so glFinish should be used sparingly.
The overall effect of these stages is that graphics primitives defined by the application are converted into pixels in the frame buffer for subsequent display. But so far, we have discussed only geometric primitives such as points, lines, and polygons. OpenGL also renders bitmap and image data.