前言

​避免 16 个常见的 OpenGL 陷阱原文​​​ Avoiding 16 Common OpenGL Pitfalls
Copyright 1998, 1999 by Mark J. Kilgard. Last Updated July 10, 2000 Commercial publication in written, electronic, or other forms without expressed written permission is prohibited. Electronic redistribution for educational or private use is permitted.

Every software engineer who has programmed long enough has a war story about some insidious bug that induced head scratching, late night debugging, and probably even schedule delays. More often than we programmers care to admit, the bug turns out to be self-inflicted. The difference between an experienced programmer and a novice is knowing the good practices to use and the bad practices to avoid so those self-inflicted bugs are kept to a minimum.

A programming interface pitfall is a self-inflicted bug that is the result of a misunderstanding about how a particular programming interface behaves. The pitfall may be the fault of the programming interface itself or its documentation, but it is often simply a failure on the programmer’s part to fully appreciate the interface’s specified behavior. Often the same set of basic pitfalls plagues novice programmers because they simply have not yet learned the intricacies of a new programming interface.

You can learn about the programming interface pitfalls in two ways: The hard way and the easy way. The hard way is to experience them one by one, late at night, and with a deadline hanging over your head. As a wise main once explained, “Experience is a good teacher, but her fees are very high.” The easy way is to benefit from the experience of others.

This is your opportunity to learn how to avoid 19 software pitfalls common to beginning and intermediate OpenGL programmers. This is your chance to spend a bit of time reading now to avoid much grief and frustration down the line. I will be honest; many of these pitfalls I learned the hard way instead of the easy way. If you program OpenGL seriously, I am confident that the advice below will make you a better OpenGL programmer.

If you are a beginning OpenGL programmer, some of the discussion below might be about topics that you have not yet encountered. This is not the place for a complete introduction to some of the more complex OpenGL topics covered such as mipmapped texture mapping or OpenGL’s pixel transfer modes. Feel free to simply skim over sections that may be too advanced. As you develop as an OpenGL programmer, the advice will become more worthwhile.

  1. Improperly Scaling Normals for Lighting

Enabling lighting in OpenGL is a way to make your surfaces appear more realistic. Proper use of OpenGL’s lighting model provides subtle clues to the viewer about the curvature and orientation of surfaces in your scene.

When you render geometry with lighting enabled, you supply normal vectors that indicate the orientation of the surface at each vertex. Surface normals are used when calculating diffuse and specular lighting effects. For example, here is a single rectangular patch that includes surface normals:

glBegin(GL_QUADS);
glNormal3f(0.181636,-0.25,0.951057);
glVertex3f(0.549,-0.756,0.261);
glNormal3f(0.095492,-0.29389,0.95106);
glVertex3f(0.288,-0.889,0.261);
glNormal3f(0.18164,-0.55902,0.80902);
glVertex3f(0.312,-0.962,0.222);
glNormal3f(0.34549,-0.47553,0.80902);
glVertex3f(0.594,-0.818,0.222);
glEnd();

The x, y, and z parameters for each glNormal3f call specify a direction vector. If you do the math, you will find that the length of each normal vector above is essentially 1.0. Using the first glNormal3f call as an example, observe that:

sqrt(0.1816362 + -0.252 + 0.9510572) » 1.0

For OpenGL’s lighting equations to operate properly, the assumption OpenGL makes by default is that the normals passed to it are vectors of length 1.0.

However, consider what happens if before executing the above OpenGL primitive, glScalef is used to shrink or enlarge subsequent OpenGL geometric primitives. For example:

glMatrixMode(GL_MODELVIEW);
glScalef(3.0, 3.0, 3.0);

The above call causes subsequent vertices to be enlarged by a factor of three in each of the x, y, and z directions by scaling OpenGL’s modelview matrix. glScalef can be useful for enlarging or shrinking geometric objects, but you must be careful because OpenGL transforms normals using a version of the modelview matrix called the inverse transpose modelview matrix. Any enlarging or shrinking of vertices during the modelview transformation also changes the length of normals.

Here is the pitfall: Any model view scaling that occurs is likely to mess up OpenGL’s lighting equations. Remember, the lighting equations assume that normals have a length of 1.0. The symptom of incorrectly scaled normals is that the lit surfaces appear too dim or too bright depending on whether the normals enlarged or shrunk.

The simplest way to avoid this pitfall is by calling:

glEnable(GL_NORMALIZE);

This mode is not enabled by default because it involves several additional calculations. Enabling the mode forces OpenGL to normalize transformed normals to be of unit length before using the normals in OpenGL’s lighting equations. While this corrects potential lighting problems introduced by scaling, it also slows OpenGL’s vertex processing speed since normalization requires extra operations, including several multiplies and an expensive reciprocal square root operation. While you may argue whether this mode should be enabled by default or not, OpenGL’s designers thought it better to make the default case be the fast one. Once you are aware of the need for this mode, it is easy to enable when you know you need it.

There are two other ways to avoid problems from scaled normals that may let you avoid the performance penalty of enabling GL_NORMALIZE. One is simply to not use glScalef to scale vertices. If you need to scale vertices, try scaling the vertices before sending them to OpenGL. Referring to the above example, if the application simply multiplied each glVertex3f by 3, you could eliminate the need for the above glScalef without having the enable the GL_NORMALIZE mode.

Note that while glScalef is problematic, you can safely use glTranslatef and glRotatef because these routines change the modelview matrix transformation without introducing any scaling effects. Also, be aware that glMatrixMultf can also be a source of normal scaling problems if the matrix you multiply by introduces scaling effects.

The other option is to adjust the normal vectors passed to OpenGL so that after the inverse transpose modelview transformation, the resulting normal will become a unit vector. For example, if the earlier glScalef call tripled the vertex coordinates, we could correct for this corresponding thirding effect on the transformed normals by pre-multiplying each normal component by 3.

OpenGL 1.2 adds a new glEnable mode called GL_RESCALE_NORMAL that is potentially more efficient than the GL_NORMALIZE mode. Instead of performing a true normalization of the transformed normal vector, the transformed normal vector is scaled based on a scale factor computed from the inverse modelview matrix’s diagonal terms. GL_RESCALE_NORMAL can be used when the modelview matrix has a uniform scaling factor.

  1. Poor Tessellation Hurts Lighting

OpenGL’s lighting calculations are done per-vertex. This means that the shading calculations due to light sources interacting with the surface material of a 3D object are only calculated at the object’s vertices. Typically, OpenGL just interpolates or smooth shades between vertex colors. OpenGL’s per-vertex lighting works pretty well except when a lighting effect such as a specular highlight or a spotlight is lost or blurred because the effect is not sufficiently sampled by an object’s vertices. Such under-sampling of lighting effects occurs when objects are coarsely modeled to use a minimal number of vertices.

Figure 1 shows an example of this problem. The top left and top right cubes each have an identically configured OpenGL spotlight light source shining directly on each cube. The left cube has a nicely defined spotlight pattern; the right cube lacks any clearly defined spotlight pattern. The key difference between the two models is the number of vertices used to model each cube. The left cube models each surface with over 120 distinct vertices; the right cube has only 4 vertices.

Figure 1: Two cubes rendered with identical OpenGL spotlight enabled.

(The lines should all be connected but are not due to resampling in the image above.)
At the extreme, if you tessellate the cube to the point that each polygon making up the cube is no larger than a pixel, the lighting effect will essentially become per-pixel. The problem is that the rendering will probably no longer be interactive. One good thing about per-vertex lighting is that you decide how to trade off rendering speed for lighting fidelity.

Smooth shading between lit vertices helps when the color changes are gradual and fairly linear. The problem is that effects such as spotlights, specular highlights, and non-linear light source attenuation are often not gradual. OpenGL’s lighting model only does a good job capturing these effects if the objects involved are reasonably tessellated.

Novice OpenGL programmers are often tempted to enable OpenGL’s spotlight functionality and shine a spotlight on a wall modeled as a single huge polygon. Unfortunately, no sharp spotlight pattern will appear as the novice intended; you probably will not see any spotlight affect at all. The problem is that the spotlight’s cutoff means that the extreme corners of the wall where the vertices are specified get no contribution from the spotlight and since those are the only vertices the wall has, there will be no spotlight pattern on the wall.

If you use spotlights, make sure that you have sufficiently tessellated the lit objects in your scene with enough vertices to capture the spotlight effect. There is a speed/quality tradeoff here: More vertices mean better lighting effects, but also increases the amount of vertex transformation required to render the scene.

Specular highlights (such as the bright spot you often see on a pool ball) also require sufficiently tessellated objects to capture the specular highlight well.

Keep in mind that if you use more linear lighting effects such as ambient and diffuse lighting effects where there are typically not sharp lighting changes, you can get good lighting effects with even fairly coarse tessellation.

If you do want both high quality and high-speed lighting effects, one option is to try using multi-pass texturing techniques to texture specular highlights and spotlight patterns onto objects in your scene. Texturing is a per-fragment operation so you can correctly capture per-fragment lighting effects. This can be involved, but such techniques can deliver fast, high-quality lighting effects when used effectively.

  1. Remember Your Matrix Mode

OpenGL has a number of 4 by 4 matrices that control the transformation of vertices, normals, and texture coordinates. The core OpenGL standard specifies the modelview matrix, the projection matrix, and the texture matrix.

Most OpenGL programmers quickly become familiar with the modelview and projection matrices. The modelview matrix controls the viewing and modeling transformations for your scene. The projection matrix defines the view frustum and controls the how the 3D scene is projected into a 2D image. The texture matrix may be unfamiliar to some; it allows you to transform texture coordinates to accomplish effects such as projected textures or sliding a texture image across a geometric surface.

A single set of matrix manipulation commands controls all types of OpenGL matrices: glScalef, glTranslatef, glRotatef, glLoadIdentity, glMultMatrixf, and several other commands. For efficient saving and restoring of matrix state, OpenGL provides the glPushMatrix and glPopMatrix commands; each matrix type has its own a stack of matrices.

None of the matrix manipulation commands have an explicit parameter to control which matrix they affect. Instead, OpenGL maintains a current matrix mode that determines which matrix type the previously mentioned matrix manipulation commands actually affects. To change the matrix mode, use the glMatrixMode command. For example:

glMatrixMode(GL_PROJECTION);
/* Now update the projection matrix. /
glLoadIdentity();
glFrustum(-1, 1, -1, 1, 0.0, 40.0);
glMatrixMode(GL_MODELVIEW);
/
Now update the modelview matrix. */
glPushMatrix();
glRotatef(45.0, 1.0, 1.0, 1.0);
render();
glPopMatrix();
A common pitfall is forgetting the current setting of the matrix mode and performing operations on the wrong matrix stack. If later pre assumes the matrix mode is set to a particular state, you both fail to update the matrix you intended and screw up whatever the actual current matrix is.

If this can trip up the unwary programmer, why would OpenGL have a matrix mode? Would it not make sense for each matrix manipulation routine to also pass in the matrix that it should manipulate? The answer is simple: lower overhead. OpenGL’s design optimizes for the common case. In real programs, matrix manipulations occur more often than matrix mode changes. The common case is a sequence of matrix operations all updating the same matrix type. Therefore, typical OpenGL usage is optimized by controlling which matrix is manipulated based on the current matrix mode. When you call glMatrixMode, OpenGL configures the matrix manipulation commands to efficiently update the current matrix type. This saves time compared to deciding which matrix to update every time a matrix manipulation is performed.

In practice, because a given matrix type does tend to be updated repeatedly before switching to a different matrix, the lower overhead for matrix manipulation more than makes up for the programmer’s burden of ensuring the matrix mode is properly set before matrix manipulation.

A simple program-wide policy for OpenGL matrix manipulation helps avoid pitfalls when manipulating matrices. Such a policy would require any premanipulating a matrix to first call glMatrixMode to always update the intended matrix. However in most programs, the modelview matrix is manipulated quite frequently during rendering and the other matrices change considerably less frequently overall. If this is the case, a better policy is that routines can assume the matrix mode is set to update the modelview matrix. Routines that need to update a different matrix are responsible to switch back to the modelview matrix after manipulating one of the other matrices.

Here is an example of how OpenGL’s matrix mode can get you into trouble. Consider a program written to keep a constant aspect ratio for an OpenGL-rendered scene in a window. Maintaining the aspect ratio requires updating the projection matrix whenever the window is resized. OpenGL programs typically also adjust the OpenGL viewport in response to a window resize so the code to handle a window resize notification might look like this:

void
doResize(int newWidth, int newHeight)
{
GLfloat aspectRatio = (GLfloat)newWidth / (GLfloat)newHeight;

glViewport(0, 0, newWidth, newHeight);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(60.0, aspectRatio, 0.1, 40.0);
/* WARNING: matrix mode left as projection! */
}

If this code fragment is from a typical OpenGL program, doResize is one of the few times or even only time the projection matrix gets changed after initialization. This means that it makes sense to add to a final glMatrixMode(GL_MODELVIEW)call to doResize to switch back to the modelview matrix. This allows the window’s redraw code safely assume the current matrix mode is set to update the modelview matrix and eliminate a call to glMatrixMode. Since window redraws often repeatedly update the modelview matrix, and redraws occur considerably more frequently than window resizes, this is generally a good approach.

A tempting approach might be to call glGetIntegerv to retrieve the current matrix mode state and then only change the matrix mode when it was not what you need it to be. After performing its matrix manipulations, you could even restore the original matrix mode state.

This is however almost certainly a bad approach. OpenGL is designed for fast rendering and setting state; retrieving OpenGL state is often considerably slower than simply setting the state the way you require. As a rule, glGetIntegerv and related state retrieval routines should only be used for debugging or retrieving OpenGL implementation limits. They should never be used in performance critical code. On faster OpenGL implementations where much of OpenGL’s state is maintained within the graphics hardware, the relative cost of state retrieval commands is considerably higher than in largely software-based OpenGL implementations. This is because state retrieval calls must stall the graphics hardware to return the requested state. When users run OpenGL programs on high-performance expensive graphics hardware and do not see the performance gains they expect, in many cases the reason is invocations of state retrieval commands that end up stalling the hardware to retrieve OpenGL state.

In cases where you do need to make sure that you restore the previous matrix mode after changing it, try using glPushAttrib with the GL_TRANSFORM_BIT bit set and then use glPopAttrib to restore the matrix mode as needed. Pushing and popping attributes on the attribute stack can be more efficient than reading back the state and later restoring it. This is because manipulating the attribute stack can completely avoid stalling the hardware if the attribute stack exists within the hardware. Still the attribute stack is not particularly efficient since all the OpenGL transform state (including clipping planes and the normalize flag) must also be pushed and popped.

The advice in this section is focused on the matrix mode state, but pitfalls that relate to state changing and restoring are common in OpenGL. OpenGL’s explicit state model is extremely well suited to the stateful nature of graphics hardware, but can be an unwelcome burden for programmers not used to managing graphics state. With a little experience though, managing OpenGL state becomes second nature and helps ensure good hardware utilization.

The chief advantage of OpenGL’s stateful approach is that well-written OpenGL rendering code can minimize state changes so that OpenGL can maximize rendering performance. A graphics- interface that tries to hide the inherently stateful nature of well-designed graphics hardware ends up either forcing redundant state changes or adds extra overhead by trying to eliminate such redundant state changes. Both approaches give up performance for convenience. A smarter approach is relying on the application or a high-level graphics library to manage graphics state. Such a high-level approach is typically more efficient in its utilization of fast graphics hardware when compared to attempts to manage graphics state in a low-level library without high-level knowledge of how the operations are being used.

If you want more convenient state management, consider using a high-level graphics library such as Open Inventor or IRIS Performer that provide both a convenient programming model and efficient high-level management of OpenGL state changes.

  1. Overflowing the Projection Matrix Stack

OpenGL’s glPushMatrix and glPopMatrix commands make it very easy to perform a set of cumulative matrix operations, do rendering, and then restore the matrix state to that before the matrix operations and rendering. This is very handy when doing hierarchical modeling during rendering operations.

For efficiency reasons and to permit the matrix stacks to exist within dedicated graphics hardware, the size of OpenGL’s various matrix stacks are limited. OpenGL mandates that all implementations must provide at least a 32-entry modelview matrix stack, a 2-entry projection matrix stack, and a 2-entry texture matrix stack. Implementations are free to provide larger stacks, and glGetIntergerv provides a means to query an implementation’s actual maximum depth.

Calling glPushMatrix when the current matrix mode stack is already at its maximum depth generates a GL_STACK_UNDERFLOW error and the responsible glPushMatrix is ignored. OpenGL applications guaranteed to run correctly on all OpenGL implementations should respect the minimum stack limits cited above (or better yet, query the implementation’s true stack limit and respect that).

This can become a pitfall when software-based OpenGL implementations implement stack depth limits that exceed the minimum limits. Because these stacks are maintained in general purpose memory and not within dedicated graphics hardware, there is no substantial expense to permitting larger or even unlimited matrix stacks as there is when the matrix stacks are implemented in dedicated hardware. If you write your OpenGL program and test it against such implementations with large or unlimited stack sizes, you may not notice that you exceeded a matrix stack limit that would exist on an OpenGL implementation that only implemented OpenGL’s mandated minimum stack limits.

The 32 required modelview stack entries will not be exceeded by most applications (it can still be done so be careful). However, programmers should be on guard not to exceed the projection and texture matrix stack limits since these stacks may have as few as 2 entries. In general, situations where you actually need a projection or texture matrix that exceed two entries are quite rare and generally avoidable.

Consider this example where an application uses two projection matrix stack entries for updating a window:

void
renderWindow(void)
{
render3Dview();
glPushMatrix();
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glLoadIdentity();
gluOrtho2D(0, 1, 0, 1);
render2Doverlay();
glPopMatrix();
glPopMatrix();
glMatrixMode(GL_MODELVIEW);
}

The window renders a 3D scene with a 3D perspective projection matrix (initialization not shown), then switches to a simple 2D orthographic projection matrix to draw a 2D overlay.

Be careful because if the render2Doverlay tries to push the projection matrix again, the projection matrix stack will overflow on some machines. While using a matrix push, cumulative matrix operations, and a matrix pop is a natural means to accomplish hierarchical modeling, the projection and texture matrices rarely require this capability. In general, changes to the projection matrix are to switch to an entirely different view (not to make a cumulative matrix change to later be undone). A simple matrix switch (reload) does not need a push and pop stack operation.

If you find yourself attempting to push the projection or texture matrices beyond two entries, consider if there is a simpler way to accomplish your manipulations that will not overflow these stacks. If not, you are introducing a latent interoperability problem when you program is run on high-performance hardware-intensive OpenGL implementations that implement limited projection and texture matrix stacks.

  1. Not Setting All Mipmap Levels

When you desire high-quality texture mapping, you will typically specify a mipmapped texture filter. Mipmapping lets you specify multiple levels of detail for a texture image. Each level of detail is half the size of the previous level of detail in each dimension. So if your initial texture image is an image of size 32x32, the lower levels of detail will be of size 16x16, 8x8, 4x4, 2x2, and 1x1. Typically, you use the gluBuild2DMipmaps routine to automatically construct the lower levels of details from you original image. This routine re-samples the original image at each level of detail so that the image is available at each of the various smaller sizes.

Mipmap texture filtering means that instead of applying texels from a single high-resolution texture image, OpenGL automatically selects from the best pre-filtered level of detail. Mipmapping avoids distracting visual artifacts that occur when a distant textured object under-samples its associated texture image. With a mipmapped minimization filter enabled, instead of under-sampling a single high resolution texture image, OpenGL will automatically select the most appropriate levels of detail.

One pitfall to be aware of is that if you do not specify every necessary level of detail, OpenGL will silently act as if texturing is not enabled. The OpenGL specification is very clear about this: “If texturing is enabled (and TEXTURE_MIN_FILTER is one that requires a mipmap) at the time a primitive is rasterized and if the set of arrays 0 through n is incomplete, based on the dimensions of array 0, then it is as if texture mapping were disabled.”

The pitfall typically catches you when you switch from using a non-mipmapped texture filter (like GL_LINEAR) to a mipmapped filter, but you forget to build complete mipmap levels. For example, say you enabled non-mipmapped texture mapping like this:

glEnable(GL_TEXTURE_2D);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 3, width, height, GL_RGB, GL_UNSIGNED_BYTE, imageData);

At this point, you could render non-mipmapped textured primitives. Where you could get tripped up is if you naively simply enabled a mipmapped minification filter. For example:

glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

The problem is that you have changed the minification filter, but not supplied a complete set of mipmap levels. Not only do you not get the filtering mode you requested, but also subsequent rendering happens as if texture mapping were not even enabled.

The simple way to avoid this pitfall is to use gluBuild2DMipmaps (or gluBuild1DMipmaps for 1D texture mapping) whenever you are planning to use a mipmapped minification filter. So this works:

glEnable(GL_TEXTURE_2D);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
gluBuild2DMipmaps(GL_TEXTURE_2D, depth, width, height, GL_RGB, GL_UNSIGNED_BYTE, imageData);

The above code uses a mipmap filter and uses gluBuild2DMipmaps to make sure all the levels are populated correctly. Subsequent rendering is not just textured, but properly uses mipmapped filtering.

Also, understand that OpenGL considers the mipmap levels incomplete not simply because you have not specified all the mipmap levels, but also if the various mipmap levels are inconsistent. This means that you must consistently specify border pixels and each successive level must be half the size of the previous level in each dimension.

  1. Reading Back Luminance Pixels

You can use OpenGL’s glReadPixels command to read back rectangular regions of a window into your program’s memory space. While reading back a color buffer as RGB or RGBA values is straightforward, OpenGL also lets you read back luminance values, but it can a bit tricky to get what you probably expect. Retrieving luminance values is useful if you want to generate a grayscale image.

When you read back luminance values, the conversion to luminance is done as a simple addition of the distinct red, green, and blue components with result clamped between 0.0 and 1.0. There is a subtle catch to this. Say the pixel you are reading back is 0.5 red, 0.5 green, and 0.5 blue. You would expect the result to then be a medium gray value. However, just adding these components would give 1.5 that would be clamped to 1.0. Instead of being a luminance value of 0.5, as you would expect, you get pure white.

A naive reading of luminance values results in a substantially brighter image than you would expect with a high likelihood of many pixels being saturated white.

The right solution would be to scale each red, green, and blue component appropriately. Fortunately, OpenGL’s pixel transfer operations allow you to accomplish this with a great deal of flexibility. OpenGL lets you scale and bias each component separately when you send pixel data through OpenGL.

For example, if you wanted each color component to be evenly averaged during pixel read back, you would change OpenGL’s default pixel transfer state like this:

glPixelTransferf(GL_RED_SCALE,0.3333);
glPixelTransferf(GL_GREEN_SCALE,0.3334);
glPixelTransferf(GL_BLUE_SCALE,0.3333);

With OpenGL’s state set this way, glReadPixels will have cut each color component by a third before adding the components during luminance conversion. In the previous example of reading back a pixel composed of 0.5 red, 0.5 green, and 0.5 blue, the resulting luminance value is 0.5.

However, as you may be aware, your eye does not equally perceive the contribution of the red, green, and blue color components. A standard linear weighting for combining red, green, and blue into luminance was defined by the National Television Standard Committee (NTSC) when the US color television format was standardized. These weightings are based on the human eye’s sensitivity to different wavelengths of visible light and are based on extensive research. To set up OpenGL to convert RGB to luminance according to the NTSC standard, you would change OpenGL’s default pixel transfer state like this:

glPixelTransferf(GL_RED_SCALE, 0.299);
glPixelTransferf(GL_GREEN_SCALE, 0.587);
glPixelTransferf(GL_BLUE_SCALE, 0.114);

If you are reading back a luminance version of an RGB image that is intended for human viewing, you probably will want to use the NTSC scale factors.

Something to appreciate in all this is how OpenGL itself does not mandate a particular scale factor or bias for combining color components into a luminance value; instead, OpenGL’s flexible pixel path capabilities give the application control. For example, you could easily read back a luminance image where you had suppressed any contribution from the green color component if that was valuable to you by setting the green pixel transfer scale to be 0.0 and re-weighting red and blue appropriately.

You could also use the biasing capability of OpenGL’s pixel transfer path to enhance the contribution of red in your image by adding a bias like this:

glPixelTransferf(GL_RED_BIAS, 0.1);

That will add 0.1 to each red component as it is read back.Please note that the default scale factor is 1.0 and the default bias is 0.0. Also be aware that these same modes are not simply used for the luminance read back case, but all pixel or texture copying, reading, or writing. If you program changes the scales and biases for reading luminance values, it will probably want to restore the default pixel transfer modes when downloading textures.

  1. Watch Your Pixel Store Alignment

OpenGL’s pixel store state controls how a pixel rectangle or texture is read from or written to your application’s memory. Consider what happens when you call glDrawPixels. You pass a pointer to the pixel rectangle to OpenGL. But how exactly do pixels in your application’s linear address space get turned into an image?

The answer sounds like it should be straightforward. Since glDrawPixels takes a width and height in pixels and a format (that implies some number of bytes per pixel), you could just assume the pixels were all packed in a tight array based on the parameters passed to glDrawPixels. Each row of pixels would immediately follow the previous row.

In practice though, applications often need to extract a sub-rectangle of pixels from a larger packed pixel rectangle. Or for performance reasons, each row of pixels is setup to begin on some regular byte alignment. Or the pixel data was read from a file generated on a machine with a different byte order (Intel and DEC processors are little-endian; Sun, SGI, and Motorola processors are big-endian).

Figure 2: Relationship of the image layout pixel store modes.

So OpenGL’s pixel store state determines how bytes in your application’s address space get unpacked from or packed to OpenGL images. Figure 2 shows how the pixel state determines the image layout. In addition to the image layout, other pixel store state determines the byte order and bit ordering for pixel data.

One likely source of surprise for OpenGL programmers is the default state of the GL_PACK_ALIGNMENT and GL_UNPACK_ALIGNMENT values. Instead of being 1, meaning that pixels are packed into rows with no extra bytes between rows, the actual default for these modes is 4.

Say that your application needs to read back an 11 by 8 pixel area of the screen as RGB pixels (3 bytes per pixel, one byte per color component). The following glReadPixels call would read the pixels:

glReadPixels(x, y, 11, 8, GL_RGB, GL_UNSIGNED_BYTE, pixels);

How large should the pixels array need to be to store the image? Assume that the GL_UNPACK_ALIGNMENT state is still 4 (the initial value). Naively, your application might call:

pixels = (GLubyte*) malloc(3 * 11 * 8); /* Wrong! */

Unfortunately, the above code is wrong since it does not account for OpenGL’s default 4-byte row alignment. Each row of pixels will be 33 bytes wide, but then each row is padded to be 4 byte aligned. The effective row width in bytes is then 36. The above malloc call will not allocate enough space; the result is that glReadPixels will write several pixels beyond the allocated range and corrupt memory.

With a 4 byte row alignment, the actual space required is not simply BytesPerPixel ´ Width ´ Height, but instead ((BytesPerPixel ´ Width + 3) >> 2) << 2) ´ Height. Despite the fact that OpenGL’s initial pack and unpack alignment state is 4, most programs should not use a 4 byte row alignment and instead request that OpenGL tightly pack and unpack pixel rows. To avoid the complications of excess bytes at the end of pixel rows for alignment, change OpenGL’s row alignment state to be “tight” like this:

glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glPixelStorei(GL_PACK_ALIGNMENT, 1);

Be extra cautious when your program is written assuming a 1 byte row alignment because bugs caused by OpenGL’s initial 4 byte row alignment can easily go unnoticed. For example, if such a program is tested only with images and textures of width divisible by 4, no memory corruption problem is noticed since the test images and textures result in a tight row packing. And because lots of textures and images, by luck or design, have a width divisible by 4, such a bug can easily slip by your testing. However, the memory corruption bug is bound to surface as soon as a customer tries to load a 37 pixel width image.

Unless you really want a row alignment of 4, be sure you change this state when using pixel rectangles, 2D and 1D textures, bitmaps, and stipple patterns. And remember that there is a distinct pack and unpack row alignment.

  1. Know Your Pixel Store State

Keep in mind that your pixel store state gets used for textures, pixel rectangles, stipple patterns, and bitmaps. Depending on what sort of 2D image data you are passing to (or reading back from) OpenGL, you may need to load the pixel store unpack (or pack) state.

Not properly configuring the pixel store state (as described in the previous section) is one common pitfall. Yet another pitfall is changing the pixel store modes to those needed by a particular OpenGL commands and later issuing some other OpenGL commands requiring the original pixel store mode settings. To be on the safe side, it is usually a good idea to save and restore the previous pixel store modes when you need to change them.

Here is an example of such a save and restore. The following code saves the pixel store unpack modes:

GLint swapbytes, lsbfirst, rowlength, skiprows, skippixels, alignment;

/* Save current pixel store state. */
glGetIntegerv(GL_UNPACK_SWAP_BYTES, &swapbytes);
glGetIntegerv(GL_UNPACK_LSB_FIRST, &lsbfirst);
glGetIntegerv(GL_UNPACK_ROW_LENGTH, &rowlength);
glGetIntegerv(GL_UNPACK_SKIP_ROWS, &skiprows);
glGetIntegerv(GL_UNPACK_SKIP_PIXELS, &skippixels);
glGetIntegerv(GL_UNPACK_ALIGNMENT, &alignment);

/* Set desired pixel store state. */
glPixelStorei(GL_UNPACK_SWAP_BYTES, GL_FALSE);
glPixelStorei(GL_UNPACK_LSB_FIRST, GL_FALSE);
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

Then, this code restores the pixel store unpack modes:

/* Restore current pixel store state. */
glPixelStorei(GL_UNPACK_SWAP_BYTES, swapbytes);
glPixelStorei(GL_UNPACK_LSB_FIRST, lsbfirst);
glPixelStorei(GL_UNPACK_ROW_LENGTH, rowlength);
glPixelStorei(GL_UNPACK_SKIP_ROWS, skiprows);
glPixelStorei(GL_UNPACK_SKIP_PIXELS, skippixels);
glPixelStorei(GL_UNPACK_ALIGNMENT, alignment);

Similar code could be written to save and restore OpenGL’s pixel store pack modes (change UNPACK to PACK in the code above).

With OpenGL 1.1, the coding effort to save and restore these modes is simpler. To save, the pixel store state, you can call:

glPushClientAttrib(GL_CLIENT_PIXEL_STORE_BIT);

Then, this code restores the pixel store unpack modes:

glPopClientAttrib(GL_CLIENT_PIXEL_STORE_BIT);

The above routines (introduced in OpenGL 1.1) save and restore the pixel store state by pushing and popping the state using a stack maintained within the OpenGL library.

Observant readers may wonder why glPushClientAttrib is used instead of the shorter glPushAttrib routine. The answer involves the difference between OpenGL client-side and server-side state. It is worth clearly understanding the practical considerations that surround the distinction between OpenGL’s server-side and client-side state.

There is not actually the option to use glPushAttrib to push the pixel store state because glPushAttrib and glPopAttrib only affects the server-state attribute stack and the pixel pack and unpack pixel store state is client-side OpenGL state.

Think of your OpenGL application as a client of the OpenGL rendering service provided by the host computer’s OpenGL implementation.

The pixel store modes are client-side state. However, most of OpenGL’s state is server-side. The term server-side state refers to the fact that the state actually resides within the OpenGL implementation itself, possibly within the graphics hardware itself. Server-side OpenGL state is concerned with how OpenGL commands are rendered, but client-side OpenGL state is concerned with how image or vertex data is extracted from the application address space.

Server-side OpenGL state is often expensive to retrieve because the state may reside only within the graphics hardware. To return such hardware-resident state (for example with glGetIntegerv) requires all preceding graphics commands to be issued before the state is retrievable. While OpenGL makes it possible to read back nearly all OpenGL server-side state, well-written programs should always avoid reading back OpenGL server-side state in performance sensitive situations.

Client-side state however is not state that will ever reside only within the rendering hardware. This means that using glGetIntegerv to read back pixel store state is relatively inexpensive because the state is client-side. This is why the above code that explicitly reads back each pixel store unpack mode can be recommended. Similar OpenGL code that tried to save and restore server-side state could severely undermine OpenGL rendering performance.

Consider that whether it is better to use glGetIntegerv and glPixelStorei to explicitly save and restore the modes or whether you use OpenGL 1.1’s glPushClientAttrib and glPopClientAttrib will depend on your situation. When pushing and popping the client attribute stack, you do have to be careful not to overflow the stack. An advantage to pushing and popping the client attribute state is that both the pixel store and vertex array client-side state can be pushed or popped with a single call. Still, you may find that only the pack or only the unpack modes need to be saved and restored and sometimes only one or two of the modes. If that is the case, an explicit save and restore may be faster.

  1. Careful Updating that Raster Position

OpenGL’s raster position determines where pixel rectangles and bitmaps will be rasterized. The glRasterPos2f family of commands specifies the coordinates for the raster position. The raster position gets transformed just as if it was a vertex. This symmetry makes it easy to position images or text within a scene along side 3D geometry. Just like a vertex, the raster position is logically an (x,y,z,w) coordinate. It also means that when the raster position is specified, OpenGL’s modelview and projection matrix transformations, lighting, clipping, and even texture coordinate generation are all performed on the raster position vertex in exactly the same manner as a vertex coordinate passed to OpenGL via glVertex3f.

While this is all very symmetric, it rarely if ever makes sense to light or generate a texture coordinate for the raster position. It can even be quite confusing when you attempt to render a bitmap based on the current color and find out that because lighting is enabled, the bitmap color gets determined by lighting calculations. Similarly, if you draw a pixel rectangle with texture mapping enabled, your pixel rectangle may end up being modulated with the single texel determined by the current raster texture coordinate.

Still another symmetric, but generally unexpected result of OpenGL’s identical treatment of vertices and the raster position is that, just like a vertex, the raster position can be clipped. This means if you specify a raster position outside (even slightly outside) the view frustum, the raster position is clipped and marked “invalid”. When the raster position is invalid, OpenGL simply discards the pixel data specified by the glBitmap, glDrawPixels, and glCopyPixls commands.

Figure 3: The enclosing box represents the view frustum and viewport. Each line of text is preceded by a dot indicating where the raster position is set before rendering the line of text. The dotted underlining shows the pixels that will actually be rasterized from each line of text. Notice that none of the pixels in the lowest line of text are rendered because the line’s raster position is invalid.

Consider how this can surprise you. Say you wanted to draw a string of text with each character rendered with glBitmap. Figure 3 shows a few situations. The point to notice is that the text renders as expected in the first two cases, but in the last case, the raster position’s placement is outside the view frustum so no pixels from the last text string are drawn.

It would appear that there is no way to begin rendering of a string of text outside the bounds of the viewport and view frustum and render at least the ending portion of the string. There is a way to accomplish what you want; it is just not very obvious. The glBitmap command both draws a bitmap and then offsets the raster position in relative window coordinates. You can render the final line of text if you first position the raster position within the view frustum (so that the raster position is set valid), and then you offset the raster position by calling glBitmap with relative raster position offsets. In this case, be sure to specify a zero-width and zero-height bitmap so no pixels are actually rendered.

Here is an example of this:

glRasterPos2i(0, 0);
glBitmap(0, 0, 0, 0, xoffset, yoffset, NULL);
drawString(“Will not be clipped.”);

This code fragment assumes that the glRasterPos2i call will validate the raster position at the origin. The code to setup the projection and modelview matrix to do that is not show (setting both matrices to the identity matrix would be sufficient).

Figure 4: Various raster position scenarios. A, raster position is within the view frustum and the image is totally with the viewport. B, raster position is within the view frustum but the image is only partially within the viewport; still fragments are generated outside the viewport. C, raster position is invalid (due to being placed outside the view frustum); no pixels are rasterized. D, like case B except glPixelZoom(1,-1) has inverted the Y pixel rasterization direction so the image renders top to bottom.

  1. The Viewport Does Not Clip or Scissor

It is a very common misconception that pixels cannot be rendered outside the OpenGL viewport. The viewport is often mistaken for a type of scissor. In fact, the viewport simply defines a transformation from normalized device coordinates (that is, post-projection matrix coordinates with the perspective divide applied) to window coordinates. The OpenGL specification makes no mention of clipping or culling when describing the operation of OpenGL’s viewport.

Part of the confusion comes from the fact that, most of the time, the viewport is set to be the window’s rectangular extent and pixels are clipped to the window’s rectangular extent. But do not confuse window ownership clipping with anything the viewport is doing because the viewport does not clip pixels.

Another reason that it seems like primitives are clipped by the viewport is that vertices are indeed clipped against the view frustum. OpenGL’s view frustum clipping does guarantee that no vertex (whether belonging to a geometric primitive or the raster position) can fall outside the viewport.

So if vertices cannot fall outside the view frustum and hence cannot be outside the viewport, how do pixels get rendered outside the viewport? Might it be an idle statement to say that the viewport does not act as a scissor if indeed you cannot generate pixels outside the viewport? Well, you can generate fragments that fall outside the viewport rectangle so it is not an idle statement.

The last section has already hinted at one way. While the raster position vertex must be specified to be within the view frustum to validate the raster position, once valid, the raster position (the state of which is maintained in window coordinates) can be moved outside the viewport with the glBitmap call’s raster position offset capability. But you do not even have to move the raster position outside the viewport to update pixels outside of the viewport rectangle. You can just render a large enough bitmap or image so that the pixel rectangle exceeds the extent of the viewport rectangle. Figure 4 demonstrates image rendering outside the viewport.

The other case where fragments can be generated outside the viewport is when rasterizing wide lines and points or smooth points, lines, and polygons. While the actual vertices for wide and smooth primitives will be clipped to fall within the viewport during transformation, at rasterization time, the widened rasterization footprint of wide or smooth primitives may end up generating fragments outside the boundaries of the viewport rectangle.

Indeed, this can turn into a programming pitfall. Say your application renders a set of wide points that slowly wander around on the screen. Your program configures OpenGL like this:

glViewport(0, 0, windowWidth, windowHeight);
glLineWidth(8.0);

What happens when a point slowly slides off the edge of the window? If the viewport matches the window’s extents as indicated by the glViewport call above, you will notice that a point will disappear suddenly at the moment its center is outside the window extent. If you expected the wide point to gradually slide of the screen, that is not what happens!

Keep in mind that the extra pixels around a wide or antialiased point are generated at rasterization time, but if the point’s vertex (at its center) is culled during vertex transformation time due to view frustum clipping, the widened rasterization never happens. You can fix the problem by widening the viewport to reflect the fact that a point’s edge can be up to four pixels (half of 8.0) from the point’s center and still generate fragments within the window’s extent. Change the glViewport call to:

glViewport(-4, -4, windowWidth+4, windowHeight+4);

With this new viewport, wide points can still be rasterized even if the hang off the window edge. Note that this will also slightly narrow your rectangular region of view, so if you want the identical view as before, you need to compensate by also expanding the view frustum specified by the projection matrix.

Note that if you really do require a rectangular 2D scissor in your application, OpenGL does provide a true window space scissor. See glEnable(GL_SCISSOR_TEST)andglScissor.

  1. Setting the Raster Color

Before you specify a vertex, you first specify the normal, texture coordinate, material, and color and then only when glVertex3f (or its ilk) is called will a vertex actually be generated based on the current per-vertex state. Calling glColor3f just sets the current color state. glColor3f does not actually create a vertex or any perform any rendering. The glVertex3f call is what binds up all the current per-vertex state and issues a complete vertex for transformation.

The raster position is updated similarly. Only when glRasterPos3f (or its ilk) is called does all the current per-vertex state get transformed and assigned to the raster position.

A common pitfall is attempting to draw a string of text with a series of glBitmap calls where different characters in the string are different colors. For example:

      glColor3f(1.0, 0.0, 0.0); /* RED */
glRasterPos2i(20, 15);
glBitmap(w, h, 0, 0, xmove, ymove, red_bitmap);

glColor3f(0.0, 1.0, 0.0); /* GREEN /
glBitmap(w, h, 0, 0, xmove, ymove, green_bitmap);
/
WARNING: Both bitmaps render red. */

Unfortunately, glBitmap’s relative offset of the raster position just updates the raster position location. The raster color (and the other remaining raster state values) remain unchanged.

The designers of OpenGL intentionally specified that glBitmap should not latch into place the current per-vertex state when the raster position is repositioned by glBitmap. Repeated glBitmap calls are designed for efficient text rendering with mono-chromatic text being the most common case. Extra processing to update per-vertex state would slow down the intended most common usage for glBitmap.

If you do want to switch the color of bitmaps rendered with glBitmap, you will need to explicitly call glRasterPos3f (or its ilk) to lock in a changed current color.

  1. OpenGL’s Lower Left Origin

Given a sheet of paper, people write from the top of the page to the bottom. The origin for writing text is at the upper left-hand margin of the page (at least in European languages). However, if you were to ask any decent math student to plot a few points on an X-Y graph, the origin would certainly be at the lower left-hand corner of the graph. Most 2D rendering APIs mimic writers and use a 2D coordinate system where the origin is in the upper left-hand corner of the screen or window (at least by default). On the other hand, 3D rendering APIs adopt the mathematically minded convention and assume a lower left-hand origin for their 3D coordinate systems.

If you are used to 2D graphics APIs, this difference of origin location can trip you up. When you specify 2D coordinates in OpenGL, they are generally based on a lower left-hand coordinate system. Keep this in mind when using glViewport, glScissor, glRasterPos2i, glBitmap, glTexCoord2f, glReadPixels, glCopyPixels, glCopyTexImage2D, glCopyTexSubImage2D, gluOrtho2D, and related routines.

Another common pitfall related to 2D rendering APIs having an upper left-hand coordinate system is that 2D image file formats start the image at the top scan line, not the bottom scan line. OpenGL assumes images start at the bottom scan line by default. If you do need to flip an image when rendering, you can use glPixelZoom(1,-1) to flip the image in the Y direction. Note that you can also flip the image in the X direction. Figure 4 demonstrates using glPixelZoom to flip an image.

Note that glPixelZoom only works when rasterizing image rectangles with glDrawPixels or glCopyPixels. It does not work with glBitmap or glReadPixels. Unfortunately, OpenGL does not provide an efficient way to read an image from the frame buffer into memory starting with the top scan line.

  1. Setting Your Raster Position to a Pixel Location

A common task in OpenGL programming is to render in window coordinates. This is often needed when overlaying text or blitting images onto codecise screen locations. Often having a 2D window coordinate system with an upper left-hand origin matching the window system’s default 2D coordinate system is useful.

Here is code to configure OpenGL for a 2D window coordinate system with an upper left-hand origin where w and h are the window’s width and height in pixels:

glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(0, w, h, 0, -1, 1);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();

Note that the bottom and top parameters (the 3rd and 4th parameters) to glOrtho specify the window height as the top and zero as the bottom. This flips the origin to put the origin at the window’s upper left-hand corner.

Now, you can safely set the raster position at a pixel position in window coordinates like this

glVertex2i(x, y);
glRasterPos2i(x, y);

One pitfall associated with setting up window coordinates is that switching to window coordinates involves loading both the modelview and projection matrices. If you need to “get back” to what was there before, use glPushMatrix and glPopMatrix (but remember the pitfall about assuming the projection matrix stack has more than two entries).

All this matrix manipulation can be a lot of work just to do something like place the raster position at some window coordinate. Brian Paul has implemented a freeware version of the OpenGL API called Mesa. Mesa implements an OpenGL extension called MESA_window_pos that permits direct efficient setting of the raster position without disturbing any other OpenGL state. The calls are:

glWindowPos4fMESA(x,y,z,w);
glWindowPos2fMESA(x,y)

Here is the equivalent implementation of these routines in unextended OpenGL:

void glWindowPos4fMESAemulate(GLfloat x,GLfloat y,GLfloat z,GLfloat w) { GLfloat fx, fy;

/* Push current matrix mode and viewport attributes. */
glPushAttrib(GL_TRANSFORM_BIT | GL_VIEWPORT_BIT);

/* Setup projection parameters. */
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glLoadIdentity();
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glLoadIdentity();
glDepthRange(z, z);
glViewport((int) x - 1, (int) y - 1, 2, 2);
/* Set the raster (window) position. */
fx = x - (int) x;
fy = y - (int) y;
glRasterPos4f(fx, fy, 0.0, w);
/* Restore matrices, viewport and matrix mode. */
glPopMatrix();
glMatrixMode(GL_PROJECTION);
glPopMatrix();

glPopAttrib();
}

void glWindowPos2fMESAemulate(GLfloat x, GLfloat y)
{
glWindowPos4fMESAemulate(x, y, 0, 1);
}

Note all the extra work the emulation routines go through to ensure that no OpenGL state is disturbed in the process of setting the raster position. Perhaps commercial OpenGL vendors will consider implementing this extension.

  1. Careful Enabling Color Material

OpenGL’s color material feature provides a less expensive way to change material parameters. With color material enabled, material colors track the current color. This means that instead of using the relatively expensive glMaterialfv routine, you can use the glColor3f routine.

Here is an example using the color material feature to change the diffuse color for each vertex of a triangle:

glColorMaterial(GL_FRONT, GL_DIFFUSE);
glEnable(GL_COLOR_MATERIAL);
glBegin(GL_TRIANGLES);
glColor3f(0.2, 0.5, 0.8);
glVertex3f(1.0, 0.0, 0.0);
glColor3f(0.3, 0.5, 0.6);
glVertex3f(0.0, 0.0, 0.0);
glColor3f(0.4, 0.2, 0.2);
glVertex3f(1.0, 1.0, 0.0);
glEnd();

Consider the more expensive code sequence needed if glMaterialfv is used explicitly:

GLfloat d1 = { 0.2, 0.5, 0.8, 1.0 };
GLfloat d2 = { 0.3, 0.5, 0.6, 1.0 };
GLfloat d3 = { 0.4, 0.2, 0.2, 1.0 };

glBegin(GL_TRIANGLES);
glMaterialfv(GL_FRONT,GL_DIFFUSE,d1);
glVertex3f(1.0, 0.0, 0.0);
glMaterialfv(GL_FRONT,GL_DIFFUSE,d2);
glVertex3f(0.0, 0.0, 0.0);
glMaterialfv(GL_FRONT,GL_DIFFUSE,d3);
glVertex3f(1.0, 1.0, 0.0);
glEnd();

If you are rendering objects that require frequent simple material changes, try to use the color material mode. However, there is a common pitfall encountered when enabling the color material mode. When color material is enabled, OpenGL immediately changes the material colors controlled by the color material state. Consider the following piece of code to initialize a newly create OpenGL rendering context:

GLfloat a[] = { 0.1, 0.1, 0.1, 1.0 };
glColor4f(1.0, 1.0, 1.0, 1.0);

glMaterialfv(GL_FRONT, GL_AMBIENT, a);
glEnable(GL_COLOR_MATERIAL); /* WARNING: Ambient and diffuse material latch immediately to the current color. */ glColorMaterial(GL_FRONT, GL_DIFFUSE);
glColor3f(0.3, 0.5, 0.6);

What state will the front ambient and diffuse material colors be after executing the above code fragment? While the programmer may have intended the ambient material state to be (0.1, 0.1, 0.1, 1.0) and the diffuse material state to be (0.3, 0.5, 0.6, 1.0), that is not quite what happens.

The resulting diffuse material state is what the programmer intended, but the resulting ambient material state is rather unexpectedly (1.0, 1.0, 1.0, 1.0). How did that happen? Well, remember that the color material mode immediately begins tracking the current color when enabled. The initial value for the color material settings is GL_FRONT_AND_BACK and GL_AMBIENT_AND_DIFFUSE (probably not what you expected!).

Since enabling the color material mode immediately begins tracking the current color, both the ambient and diffuse material states are updated to be (1.0, 1.0, 1.0, 1.0). Note that the effect of the initial glMaterialfv is lost. Next, the color material state is updated to just change the front diffuse material. Lastly, the glColor3f invocation changes the diffuse material to (0.3, 0.5, 0.6, 1.0). The ambient material state ends up being (1.0, 1.0, 1.0, 1.0).

The problem in the code fragment above is that the color material mode is enabled before calling glColorMaterial. The color material mode is very effective for efficient simple material changes, but to avoid the above pitfall, always be careful to set glColorMaterialbefore you enable GL_COLOR_MATERIAL.

  1. Much OpenGL State Affects All Primitives

A fragment is OpenGL’s term for the bundle of state used to update a given pixel on the screen. When a primitive such as a polygon or image rectangle is rasterized, the result is a set of fragments that are used to update the pixels that the primitive covers. Keep in mind that all OpenGL rendering operations share the same set of per-fragment operations. The same applies to OpenGL’s fog and texturing rasterization state.

For example, if you enabled depth testing and blending when you render polygons in your application, keep in mind that when you overlay some 2D text indicating the application’s status that you probably want to disable depth testing and blending. It is easy to forget that this state also affects images drawn and copied with glDrawPixels and glCopyPixels.

You will quickly notice when this shared state screws up your rendering, but also be aware that sometimes you can leave a mode enabled such as blending without noticing the extra expense involved. If you draw primitives with a constant alpha of 1.0, you may not notice that the blending is occurring and simply slowing you down.

This issue is not unique to the per-fragment and rasterization state. The pixel path state is shared by the draw pixels (glDrawPixels), read pixels (glReadPixels), copy pixels (glCopyPixels), and texture download (glTexImage2D) paths. If you are not careful, it is easy to get into situations where a texture download is screwed up because the pixel path was left configured for a pixel read back.

  1. Be Sure to Allocate Ancillary Buffers that You Use

If you intend to use an ancillary buffer such as a depth, stencil, or accumulation buffer, be sure that you application actually requests all the ancillary buffers that you intend to use. A common interoperability issue is developing an OpenGL application on a system with only a few frame buffer configurations that provide all the ancillary buffers that you use. For example, your system has no frame buffer configuration that advertises a depth buffer without a stencil buffer. So on your development system, you “get away with” not explicitly requesting a stencil buffer.

The problem comes when you take your supposedly debugged application and run it on a new fancy hardware accelerated OpenGL system only to find out that the application fails miserably when attempting to use the stencil buffer. Consider that the fancy hardware may support extra color resolution if you do not request a stencil buffer. If you application does not explicitly request the stencil buffer that it uses, the fancy hardware accelerated OpenGL implementation determines that the frame buffer configuration with no stencil but extra color resolution is the better choice for your application. If your application would have correctly requested a stencil buffer things would be fine. Make sure that you allocate what you use.

Conclusion

I hope that this review of various OpenGL pitfalls saves you much time and debugging grief. I wish that I could have simply read about these pitfalls instead of learning most of them the hard way.

Visualization has always been the key to enlightenment. If computer graphics changes the world for the better, the fundamental reason why is that computer graphics makes visualization easier.

翻译

避免 16 个常见的 OpenGL 陷阱
版权所有 1998, 1999 由 Mark J. Kilgard。最后更新日期 2000 年 7 月 10 日 未经明确书面许可,禁止以书面、电子或其他形式进行商业出版。允许用于教育或私人用途的电子再分发。

每个编程时间足够长的软件工程师都有一个关于一些潜在错误的战争故事,这些错误导致头疼、深夜调试,甚至可能导致计划延迟。比我们程序员愿意承认的更多,错误结果是自己造成的。有经验的程序员和新手之间的区别在于知道要使用的好做法和要避免的坏做法,因此将那些自己造成的错误保持在最低限度。

编程接口陷阱是一种自我造成的错误,它是由于对特定编程接口的行为方式的误解造成的。陷阱可能是编程接口本身或其文档的错误,但通常只是程序员未能完全理解接口的指定行为。通常,同样的基本陷阱困扰着新手程序员,因为他们只是还没有了解新编程接口的复杂性。

您可以通过两种方式了解编程接口的陷阱:困难的方式和简单的方式。困难的方法是在深夜和最后期限悬而未决的情况下逐一体验它们。正如一位明智的校长曾经解释过的那样,“经验是个好老师,但她的学费很高。” 简单的方法是从他人的经验中受益。

这是您学习如何避免初学者和中级 OpenGL 程序员常见的 19 个软件陷阱的机会。这是您现在花一些时间阅读的机会,以避免在此过程中产生太多的悲伤和沮丧。我会诚实;许多这些陷阱是我通过艰难的方式而不是简单的方式学到的。如果你认真地编写 OpenGL,我相信下面的建议会让你成为一个更好的 OpenGL 程序员。

如果您是 OpenGL 初学者,下面的一些讨论可能是关于您尚未遇到的主题。这里不是完整介绍一些更复杂的 OpenGL 主题的地方,例如 mipmapped 纹理映射或 OpenGL 的像素传输模式。随意简单地浏览可能太高级的部分。随着您作为 OpenGL 程序员的发展,这些建议将变得更有价值。

  1. 不正确地缩放照明法线

在 OpenGL 中启用照明是一种使您的表面看起来更逼真的方法。正确使用 OpenGL 的光照模型可为观看者提供有关场景中曲面曲率和方向的微妙线索。

当您在启用照明的情况下渲染几何体时,您需要提供法线向量来指示每个顶点处的表面方向。计算漫反射和镜面反射照明效果时使用表面法线。例如,这是一个包含表面法线的单个矩形补丁:

glBegin(GL_QUADS);
glNormal3f(0.181636,-0.25,0.951057);
glVertex3f(0.549,-0.756,0.261);
glNormal3f(0.095492,-0.29389,0.95106);
glVertex3f(0.288,-0.889,0.261);
glNormal3f(0.18164,-0.55902,0.80902);
glVertex3f(0.312,-0.962,0.222);
glNormal3f(0.34549,-0.47553,0.80902);
glVertex3f(0.594,-0.818,0.222);
glEnd();

每个调用的x、y和z参数glNormal3f指定一个方向向量。如果你算一下,你会发现上面每个法向量的长度本质上都是1.0。以第一个glNormal3f调用为例,观察:

sqrt(0.1816362 + -0.252 + 0.9510572) » 1.0

为了让 OpenGL 的光照方程正常运行,OpenGL 默认假设传递给它的法线是长度为 1.0 的向量。

但是,请考虑如果在执行上述 OpenGL 图元之前,glScalef用于缩小或放大后续 OpenGL 几何图元会发生什么情况。例如:

glMatrixMode(GL_MODELVIEW);
glScalef(3.0, 3.0, 3.0);

上述调用通过缩放 OpenGL 的模型视图矩阵,导致后续顶点在x、y和z方向的每个方向上被放大三倍。 glScalef可用于放大或缩小几何对象,但您必须小心,因为 OpenGL 使用称为逆转置模型视图矩阵的模型视图矩阵版本转换法线。在模型视图转换期间任何顶点的放大或缩小也会改变法线的长度。

这里有一个陷阱:发生的任何模型视图缩放都可能弄乱 OpenGL 的光照方程。请记住,光照方程假设法线的长度为 1.0。不正确缩放法线的症状是,根据法线是放大还是缩小,照亮的表面显得太暗或太亮。

避免这种陷阱的最简单方法是调用:

glEnable(GL_NORMALIZE);

默认情况下不启用此模式,因为它涉及几个额外的计算。启用该模式会强制 OpenGL 在使用 OpenGL 光照方程中的法线之前将转换后的法线归一化为单位长度。虽然这纠正了由缩放引入的潜在光照问题,但它也降低了 OpenGL 的顶点处理速度,因为归一化需要额外的操作,包括多次乘法和昂贵的平方根倒数运算。虽然您可能会争论是否应该默认启用此模式,但 OpenGL 的设计者认为最好使默认情况成为快速模式。一旦您意识到需要此模式,就很容易在您知道需要时启用它。

还有另外两种方法可以避免缩放法线的问题,这些方法可以让您避免启用GL_NORMALIZE. 一种是不用于glScalef缩放顶点。如果您需要缩放顶点,请在将它们发送到 OpenGL 之前尝试缩放顶点。参考上面的示例,如果应用程序只是将每个glVertex3f乘以 3,则无需glScalef启用该GL_NORMALIZE模式即可消除上述需要。

请注意,虽然glScalef有问题,但您可以安全地使用glTranslatef,glRotatef因为这些例程会在不引入任何缩放效果的情况下更改模型视图矩阵转换。此外,请注意,glMatrixMultf如果您乘以的矩阵引入了缩放效果,这也可能是正常缩放问题的根源。

另一种选择是调整传递给 OpenGL 的法线向量,以便在逆转模型视图变换后,生成的法线将成为单位向量。例如,如果前面的glScalef调用将顶点坐标增加了三倍,我们可以通过将每个法线分量预乘 3 来校正对转换法线的相应三分效应。

OpenGL 1.2 添加了一种新glEnable模式,称为GL_RESCALE_NORMAL可能比该GL_NORMALIZE模式更高效。不是对转换后的法线向量执行真正的归一化,而是基于从逆模型视图矩阵的对角项计算的比例因子对转换后的法线向量进行缩放。GL_RESCALE_NORMAL当模型视图矩阵具有统一的缩放因子时可以使用。

  1. 不好的镶嵌会影响照明

OpenGL 的光照计算是按顶点完成的。这意味着由于光源与 3D 对象的表面材料相互作用而产生的着色计算仅在对象的顶点处计算。通常,OpenGL 只是在顶点颜色之间插入或平滑阴影。OpenGL 的逐顶点光照效果非常好,除非在高光或聚光灯等光照效果由于对象顶点未充分采样而丢失或模糊时。当对象被粗略建模以使用最少数量的顶点时,就会发生这种光照效果的欠采样。

图 1 显示了此问题的一个示例。左上角和右上角的立方体各有一个配置相同的 OpenGL 聚光光源,直接照射在每个立方体上。左边的立方体有一个很好定义的聚光灯图案;右边的立方体没有任何明确定义的聚光灯图案。两个模型之间的主要区别在于用于为每个立方体建模的顶点数量。左边的立方体用超过 120 个不同的顶点对每个表面进行建模;正确的立方体只有 4 个顶点。

图 1:使用相同的 OpenGL 聚光灯渲染的两个立方体。

(线条应该全部连接,但不是由于上图中的重新采样。)
在极端情况下,如果您将立方体细分到组成立方体的每个多边形不大于一个像素的程度,则照明效果基本上将变为逐像​​素。问题是渲染可能不再是交互式的。每顶点光照的一个好处是您可以决定如何权衡渲染速度和光照保真度。

当颜色变化是渐进的且相当线性时,照明顶点之间的平滑阴影会有所帮助。问题是聚光灯、镜面高光和非线性光源衰减等效果通常不是渐进的。如果所涉及的对象被合理地镶嵌,OpenGL 的照明模型只能很好地捕捉这些效果。

OpenGL 新手程序员通常很想启用 OpenGL 的聚光灯功能,并将聚光灯照射到建模为单个巨大多边形的墙上。不幸的是,不会出现新手想要的清晰的聚光灯图案;您可能根本看不到任何聚光灯的影响。问题是聚光灯的截止意味着指定顶点的墙壁的极端角落不会受到聚光灯的影响,并且由于这些是墙壁仅有的顶点,因此墙壁上不会有聚光灯图案。

如果您使用聚光灯,请确保您使用足够的顶点对场景中的发光对象进行了充分细分,以捕捉聚光灯效果。这里有一个速度/质量的权衡:更多的顶点意味着更好的照明效果,但也会增加渲染场景所需的顶点变换量。

镜面高光(例如您经常在台球上看到的亮点)也需要足够细分的对象才能很好地捕捉镜面高光。

请记住,如果您使用更线性的照明效果,例如环境和漫射照明效果,其中通常没有明显的照明变化,即使是相当粗糙的细分,您也可以获得良好的照明效果。

如果你想既高品质和高速的照明效果,一种选择是使用多通道纹理技术,质感镜面高光和聚光灯图案投射到场景中的对象的尝试。贴图是针对每个片段的操作,因此您可以正确捕捉每个片段的光照效果。这可能会涉及到,但这些技术在有效使用时可以提供快速、高质量的照明效果。

  1. 记住你的矩阵模式

OpenGL 有许多 4 x 4 矩阵,用于控制顶点、法线和纹理坐标的变换。核心 OpenGL 标准规定了模型视图矩阵、投影矩阵和纹理矩阵。

大多数 OpenGL 程序员很快就会熟悉模型视图和投影矩阵。模型视图矩阵控制场景的查看和建模转换。投影矩阵定义视锥体并控制如何将 3D 场景投影到 2D 图像中。纹理矩阵可能有些人不熟悉;它允许您转换纹理坐标以实现诸如投影纹理或在几何表面上滑动纹理图像等效果。

单组矩阵操纵命令控制所有类型的OpenGL矩阵:glScalef,glTranslatef,glRotatef,glLoadIdentity,glMultMatrixf,和几个其他命令。为了有效地保存和恢复矩阵状态,OpenGL 提供了glPushMatrix 和glPopMatrix命令;每个矩阵类型都有自己的矩阵堆栈。

没有一个矩阵操作命令有一个明确的参数来控制它们影响哪个矩阵。取而代之的是,OpenGL 维护了一个当前的矩阵模式,该模式决定了前面提到的矩阵操作命令实际影响的矩阵类型。要更改矩阵模式,请使用该glMatrixMode命令。例如:

glMatrixMode(GL_PROJECTION);
/* Now update the projection matrix. /
glLoadIdentity();
glFrustum(-1, 1, -1, 1, 0.0, 40.0);
glMatrixMode(GL_MODELVIEW);
/
Now update the modelview matrix. */
glPushMatrix();
glRotatef(45.0, 1.0, 1.0, 1.0);
render();
glPopMatrix();
一个常见的陷阱是忘记矩阵模式的当前设置并在错误的矩阵堆栈上执行操作。如果稍后 pre 假设矩阵模式设置为特定状态,那么你们都无法更新您想要的矩阵并且搞砸了无论实际当前矩阵是什么。

如果这会绊倒粗心的程序员,为什么 OpenGL 会有矩阵模式?每个矩阵操作例程还传入它应该操作的矩阵是否有意义?答案很简单:降低开销。OpenGL 的设计针对常见情况进行了优化。在实际程序中,矩阵操作比矩阵模式更改更频繁。常见的情况是一系列矩阵运算都更新相同的矩阵类型。因此,典型的 OpenGL 使用是通过根据当前矩阵模式控制操作哪个矩阵来优化的。当您调用 时glMatrixMode,OpenGL 会配置矩阵操作命令以有效更新当前矩阵类型。与每次执行矩阵操作时决定更新哪个矩阵相比,这节省了时间。

在实践中,因为给定的矩阵类型在切换到不同的矩阵之前往往会重复更新,矩阵操作的较低开销弥补了程序员在矩阵操作之前确保正确设置矩阵模式的负担。

OpenGL 矩阵操作的简单程序范围策略有助于避免操作矩阵时的陷阱。这样的策略将要求任何预先操作矩阵首先调用glMatrixMode总是更新预期的矩阵。然而,在大多数程序中,模型视图矩阵在渲染过程中被非常频繁地操作,而其他矩阵总体上变化的频率要低得多。如果是这种情况,更好的策略是例程可以假设矩阵模式设置为更新模型视图矩阵。需要更新不同矩阵的例程负责在操作其他矩阵之一后切换回模型视图矩阵。

这是 OpenGL 的矩阵模式如何让您陷入困境的示例。考虑一个为在窗口中为 OpenGL 渲染场景保持恒定纵横比而编写的程序。保持纵横比需要在调整窗口大小时更新投影矩阵。OpenGL 程序通常还会根据窗口大小调整调整 OpenGL 视口,因此处理窗口大小调整通知的代码可能如下所示:

void
doResize(int newWidth, int newHeight)
{
GLfloat aspectRatio = (GLfloat)newWidth / (GLfloat)newHeight;

glViewport(0, 0, newWidth, newHeight);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(60.0, aspectRatio, 0.1, 40.0);
/* WARNING: matrix mode left as projection! */
}

如果此代码片段来自典型的 OpenGL 程序,doResize则是初始化后投影矩阵发生更改的少数甚至仅有一次。这意味着添加到最终glMatrixMode(GL_MODELVIEW)调用doResize以切换回模型视图矩阵是有意义的。这允许窗口的重绘代码安全地假设当前矩阵模式设置为更新模型视图矩阵并消除对 的调用glMatrixMode。由于窗口重绘经常重复更新模型视图矩阵,并且重绘发生的频率远高于窗口大小调整,因此这通常是一种很好的方法。

一种诱人的方法可能是调用glGetIntegerv以检索当前矩阵模式状态,然后仅在它不是您需要的矩阵模式时才更改它。执行其矩阵操作后,您甚至可以恢复原始矩阵模式状态。

然而,这几乎肯定是一种糟糕的方法。OpenGL 专为快速渲染和设置状态而设计;检索 OpenGL 状态通常比简单地按照您需要的方式设置状态要慢得多。通常,glGetIntegerv相关的状态检索例程应该仅用于调试或检索 OpenGL 实现限制。他们永远不应该用于性能关键代码。在较快的 OpenGL 实现中,大部分 OpenGL 的状态都在图形硬件中维护,状态检索命令的相对成本远高于主要基于软件的 OpenGL 实现。这是因为状态检索调用必须停止图形硬件才能返回请求的状态。当用户在高性能昂贵的图形硬件上运行 OpenGL 程序并且没有看到他们期望的性能提升时,在许多情况下,原因是调用状态检索命令最终导致硬件停止检索 OpenGL 状态。

如果您确实需要确保在更改之前恢复之前的矩阵模式,请尝试使用设置glPushAttrib的GL_TRANSFORM_BIT位,然后glPopAttrib根据需要使用恢复矩阵模式。在属性堆栈上推送和弹出属性比读回状态然后恢复它更有效。这是因为如果属性堆栈存在于硬件中,则操纵属性堆栈可以完全避免硬件停顿。属性堆栈仍然不是特别有效,因为所有 OpenGL 变换状态(包括裁剪平面和规范化标志)也必须被压入和弹出。

本节中的建议侧重于矩阵模式状态,但与状态更改和恢复相关的陷阱在 OpenGL 中很常见。OpenGL 的显式状态模型非常适合图形硬件的状态特性,但对于不习惯管理图形状态的程序员来说可能是一种不受欢迎的负担。尽管有一点经验,管理 OpenGL 状态成为第二天性,并有助于确保良好的硬件利用率。

OpenGL 有状态方法的主要优点是编写良好的 OpenGL 渲染代码可以最大限度地减少状态变化,从而使 OpenGL 可以最大限度地提高渲染性能。试图隐藏设计良好的图形硬件的固有状态特性的图形接口最终要么强制冗余状态更改,要么通过尝试消除此类冗余状态更改来增加额外开销。为了方便,这两种方法都放弃了性能。一种更智能的方法是依靠应用程序或高级图形库来管理图形状态。与在不了解操作如何使用的高级知识的情况下尝试在低级库中管理图形状态相比,这种高级方法在利用快速图形硬件方面通常更有效。

如果您想要更方便的状态管理,请考虑使用高级图形库,例如 Open Inventor 或 IRIS Performer,它们提供方便的编程模型和高效的 OpenGL 状态更改的高级管理。

  1. 溢出投影矩阵堆栈

OpenGLglPushMatrix 和glPopMatrix命令使得执行一组累积矩阵运算、进行渲染、然后将矩阵状态恢复到矩阵运算和渲染之前的状态变得非常容易。这在渲染操作期间进行分层建模时非常方便。

出于效率原因并允许矩阵堆栈存在于专用图形硬件中,OpenGL 的各种矩阵堆栈的大小是有限的。OpenGL 要求所有实现必须提供至少一个 32 条目的模型视图矩阵堆栈、一个 2 条目投影矩阵堆栈和一个 2 条目纹理矩阵堆栈。实现可以自由地提供更大的堆栈,并glGetIntergerv提供一种查询实现的实际最大深度的方法。

glPushMatrix 当当前矩阵模式堆栈已经处于其最大深度时调用会产生GL_STACK_UNDERFLOW错误并且负责者glPushMatrix 被忽略。保证在所有 OpenGL 实现上正确运行的 OpenGL 应用程序应该遵守上面引用的最小堆栈限制(或者更好的是,查询实现的真实堆栈限制并尊重它)。

当基于软件的 OpenGL 实现实现超过最小限制的堆栈深度限制时,这可能成为一个陷阱。因为这些堆栈保存在通用存储器中而不是在专用图形硬件中,所以当矩阵堆栈在专用硬件中实现时,允许更大甚至无限的矩阵堆栈不会产生大量费用。如果您编写 OpenGL 程序并针对此类具有大型或无限堆栈大小的实现进行测试,您可能不会注意到您超出了矩阵堆栈限制,而该限制存在于仅实现 OpenGL 的强制最小堆栈限制的 OpenGL 实现中。

大多数应用程序都不会超过 32 个所需的模型视图堆栈条目(仍然可以这样做,所以要小心)。但是,程序员应该注意不要超过投影和纹理矩阵堆栈限制,因为这些堆栈可能只有 2 个条目。通常,您实际上需要超过两个条目的投影或纹理矩阵的情况非常少见,并且通常是可以避免的。

考虑以下示例,其中应用程序使用两个投影矩阵堆栈条目来更新窗口:

void
renderWindow(void)
{
render3Dview();
glPushMatrix();
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glLoadIdentity();
gluOrtho2D(0, 1, 0, 1);
render2Doverlay();
glPopMatrix();
glPopMatrix();
glMatrixMode(GL_MODELVIEW);
}

该窗口使用 3D 透视投影矩阵(未显示初始化)渲染 3D 场景,然后切换到简单的 2D 正交投影矩阵以绘制 2D 叠加。

要小心,因为如果render2Doverlay再次尝试推送投影矩阵,投影矩阵堆栈将在某些机器上溢出。虽然使用矩阵推送、累积矩阵运算和矩阵弹出是完成分层建模的自然方法,但投影和纹理矩阵很少需要这种功能。一般情况下,对投影矩阵的更改是切换到完全不同的视图(而不是进行累积矩阵更改以便以后撤消)。一个简单的矩阵切换(重新加载)不需要推送和弹出堆栈操作。

如果您发现自己试图将投影或纹理矩阵推送到两个条目之外,请考虑是否有一种更简单的方法来完成您的操作,而不会溢出这些堆栈。如果不是,当您在实现有限投影和纹理矩阵堆栈的高性能硬件密集型 OpenGL 实现上运行程序时,您将引入潜在的互操作性问题。

  1. 不设置所有 Mipmap 级别

当您需要高质量的纹理映射时,您通常会指定一个 mipmapped 纹理过滤器。Mipmapping 允许您为纹理图像指定多个细节级别。每个细节级别是每个维度中前一个细节级别的一半。因此,如果您的初始纹理图像是大小为 32x32 的图像,那么较低级别的细节将是大小为 16x16、8x8、4x4、2x2 和 1x1。通常,您使用该gluBuild2DMipmaps例程从原始图像自动构建较低级别的细节。此例程在每个细节级别对原始图像重新采样,以便在各种较小尺寸中的每一个都可以使用该图像。

Mipmap 纹理过滤意味着,OpenGL 不会从单个高分辨率纹理图像中应用纹素,而是自动从最佳的预过滤细节级别中进行选择。Mipmapping 避免了当远处的纹理对象对其相关纹理图像进行欠采样时出现的分散视觉伪影。启用 mipmapped 最小化过滤器后,OpenGL 将自动选择最合适的细节级别,而不是对单个高分辨率纹理图像进行欠采样。

需要注意的一个陷阱是,如果您没有指定每一个必要的细节级别,OpenGL 将像未启用纹理一样默默地运行。OpenGL 规范对此非常清楚:“如果TEXTURE_MIN_FILTER在对基元进行光栅化时启用了纹理(并且需要 mipmap),并且如果数组 0 到n的集合不完整,则基于数组 0 的维度,那么就好像纹理映射被禁用了。”

当您从使用非 mipmapped 纹理过滤器(如GL_LINEAR)切换到使用 mipmapped 过滤器时,通常会遇到陷阱,但您忘记构建完整的 mipmap 级别。例如,假设您启用了非 mipmapped 纹理映射,如下所示:

glEnable(GL_TEXTURE_2D);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 3, width, height, GL_RGB, GL_UNSIGNED_BYTE, imageData);

此时,您可以渲染非 mipmapped 纹理图元。如果你天真地简单地启用了一个 mipmapped 缩小过滤器,你可能会被绊倒。例如:

glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

问题是您更改了缩小过滤器,但没有提供完整的 mipmap 级别集。你不仅没有得到你请求的过滤模式,而且后续的渲染也好像没有启用纹理映射一样。

避免此陷阱的简单方法是在计划使用 mipmapped 缩小过滤器时使用gluBuild2DMipmaps(或gluBuild1DMipmaps用于 1D 纹理映射)。所以这有效:

glEnable(GL_TEXTURE_2D);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
gluBuild2DMipmaps(GL_TEXTURE_2D, depth, width, height, GL_RGB, GL_UNSIGNED_BYTE, imageData);

上面的代码使用 mipmap 过滤器并用于gluBuild2DMipmaps确保正确填充所有级别。后续渲染不仅仅是纹理化,而是正确使用 mipmapped 过滤。

另外,请了解 OpenGL 认为 mipmap 级别不完整不仅仅是因为您没有指定所有 mipmap 级别,而且还因为各种 mipmap 级别不一致。这意味着您必须始终如一地指定边界像素,并且每个连续级别必须是每个维度中前一级别大小的一半。

  1. 读回亮度像素

您可以使用 OpenGL 的glReadPixels命令将窗口的矩形区域读回程序的内存空间。虽然将颜色缓冲区读回为 RGB 或 RGBA 值很简单,但 OpenGL 还允许您读回亮度值,但要获得您可能期望的值可能有点棘手。如果要生成灰度图像,检索亮度值非常有用。

当您读回亮度值时,亮度的转换是通过简单添加不同的红色、绿色和蓝色分量来完成的,结果被限制在 0.0 和 1.0 之间。这有一个微妙的问题。假设您读回的像素是 0.5 红色、0.5 绿色和 0.5 蓝色。您会期望结果是中等灰度值。但是,仅添加这些组件就会得到 1.5,而 1.5 会被限制为 1.0。正如您所期望的,亮度值不是 0.5,而是纯白色。

对亮度值的天真读取会导致图像比您预期的要亮得多,并且许多像素很可能是饱和的白色。

正确的解决方案是适当地缩放每个红色、绿色和蓝色分量。幸运的是,OpenGL 的像素传输操作允许您以极大的灵活性完成此操作。当您通过 OpenGL 发送像素数据时,OpenGL 允许您分别缩放和偏置每个组件。

例如,如果您希望在像素回读期间对每个颜色分量进行均匀平均,您可以像这样更改 OpenGL 的默认像素传输状态:

glPixelTransferf(GL_RED_SCALE,0.3333);
glPixelTransferf(GL_GREEN_SCALE,0.3334);
glPixelTransferf(GL_BLUE_SCALE,0.3333);

以这种方式设置 OpenGL 的状态,glReadPixels将在亮度转换期间添加组件之前将每个颜色组件削减三分之一。在前面读回由 0.5 个红色、0.5 个绿色和 0.5 个蓝色组成的像素的示例中,产生的亮度值为 0.5。

但是,您可能已经意识到,您的眼睛无法同等感知红色、绿色和蓝色分量的贡献。当美国彩色电视格式标准化时,国家电视标准委员会 (NTSC) 定义了用于将红色、绿色和蓝色组合成亮度的标准线性加权。这些权重基于人眼对不同波长可见光的敏感度,并基于广泛的研究。要设置 OpenGL 以根据 NTSC 标准将 RGB 转换为亮度,您需要像这样更改 OpenGL 的默认像素传输状态:

glPixelTransferf(GL_RED_SCALE, 0.299);
glPixelTransferf(GL_GREEN_SCALE, 0.587);
glPixelTransferf(GL_BLUE_SCALE, 0.114);

如果您正在回读供人类观看的 RGB 图像的亮度版本,您可能需要使用 NTSC 比例因子。

在这一切中值得欣赏的是 OpenGL 本身如何不强制要求特定的比例因子或偏差来将颜色分量组合成亮度值;相反,OpenGL 灵活的像素路径功能赋予应用程序控制权。例如,通过将绿色像素传输比例设置为 0.0 并适当地重新加权红色和蓝色,您可以轻松地回读已抑制绿色分量的任何贡献的亮度图像,如果这对您有价值的话。

您还可以使用 OpenGL 像素传输路径的偏置功能,通过添加如下偏置来增强图像中红色的贡献:

glPixelTransferf(GL_RED_BIAS, 0.1);

这将在读回时为每个红色分量增加 0.1。请注意,默认比例因子为 1.0,默认偏差为 0.0。还要注意,这些相同的模式不仅用于亮度回读情况,还用于所有像素或纹理复制、读取或写入。如果您编程更改读取亮度值的比例和偏差,则下载纹理时可能需要恢复默认像素传输模式。

7.注意你的像素商店对齐

OpenGL 的像素存储状态控制如何从应用程序的内存中读取或写入像素矩形或纹理。考虑调用 时会发生什么glDrawPixels。您将指向像素矩形的指针传递给 OpenGL。但是应用程序的线性地址空间中的像素究竟是如何变成图像的呢?

答案听起来应该很简单。由于glDrawPixels采用以像素为单位的宽度和高度以及格式(这意味着每个像素有一定数量的字节),因此您可以假设像素都基于传递给glDrawPixels. 每行像素将紧跟在前一行之后。

但在实践中,应用程序通常需要从较大的打包像素矩形中提取像素的子矩形。或者出于性能原因,每行像素都设置为以某种常规字节对齐开始。或者,像素数据是从在具有不同字节顺序的机器上生成的文件中读取的(Intel 和 DEC 处理器是小端;Sun、SGI 和摩托罗拉处理器是大端)。

图 2:图像布局像素存储模式的关系。

因此,OpenGL 的像素存储状态决定了应用程序地址空间中的字节如何从 OpenGL 图像解包或打包到 OpenGL 图像。图 2 显示了像素状态如何决定图像布局。除了图像布局之外,其他像素存储状态决定了像素数据的字节顺序和位顺序。

OpenGL 程序员可能会感到惊讶的一个原因是GL_PACK_ALIGNMENT和GL_UNPACK_ALIGNMENT值的默认状态。不是 1,这意味着像素被打包成行,行之间没有额外的字节,这些模式的实际默认值为 4。

假设您的应用程序需要将屏幕的 11 x 8 像素区域读回为 RGB 像素(每个像素 3 个字节,每个颜色分量一个字节)。以下glReadPixels调用将读取像素:

glReadPixels(x, y, 11, 8, GL_RGB, GL_UNSIGNED_BYTE, pixels);

像素阵列需要多大才能存储图像?假设GL_UNPACK_ALIGNMENT状态仍然是 4(初始值)。天真地,您的应用程序可能会调用:

pixels = (GLubyte*) malloc(3 * 11 * 8); /* Wrong! */

不幸的是,上面的代码是错误的,因为它没有考虑 OpenGL 的默认 4 字节行对齐方式。每行像素将是 33 字节宽,但随后每行都被填充为 4 字节对齐。以字节为单位的有效行宽则为 36。上述malloc调用将不会分配足够的空间;结果是glReadPixels将写入超出分配范围的多个像素并损坏内存。

对于 4 字节行对齐,所需的实际空间不仅仅是 BytesPerPixel ´ Width ´ Height,而是((BytesPerPixel ´ Width + 3) >> 2) << 2) ´ Height。尽管 OpenGL 的初始打包和解包对齐状态为 4,但大多数程序不应使用 4 字节行对齐,而是要求 OpenGL 紧密打包和解包像素行。为了避免在像素行末尾出现过多字节对齐的并发症,将 OpenGL 的行对齐状态更改为“紧密”,如下所示:

glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glPixelStorei(GL_PACK_ALIGNMENT, 1);

当你的程序假设 1 字节行对齐编写时要格外小心,因为 OpenGL 最初的 4 字节行对齐引起的错误很容易被忽视。例如,如果这样的程序仅使用宽度可被 4 整除的图像和纹理进行测试,则不会注意到内存损坏问题,因为测试图像和纹理会导致行压缩。并且由于许多纹理和图像,幸运或设计,宽度可以被 4 整除,这样的错误很容易在您的测试中溜走。但是,一旦客户尝试加载 37 像素宽度的图像,内存损坏错误就会浮出水面。

除非您真的希望行对齐为 4,否则请确保在使用像素矩形、2D 和 1D 纹理、位图和点画图案时更改此状态。请记住,有一个不同的打包和解包行对齐方式。

  1. 了解你的像素商店状态

请记住,您的像素存储状态用于纹理、像素矩形、点画图案和位图。根据您传递给(或从)OpenGL 读取的 2D 图像数据类型,您可能需要加载像素存储解包(或打包)状态。

未正确配置像素存储状态(如上一节所述)是一个常见的陷阱。另一个陷阱是将像素存储模式更改为特定 OpenGL 命令所需的模式,然后发布一些其他需要原始像素存储模式设置的 OpenGL 命令。为了安全起见,在需要更改之前保存和恢复以前的像素存储模式通常是个好主意。

以下是此类保存和恢复的示例。以下代码保存像素存储解包模式:

GLint swapbytes, lsbfirst, rowlength, skiprows, skippixels, alignment;

/* Save current pixel store state. */
glGetIntegerv(GL_UNPACK_SWAP_BYTES, &swapbytes);
glGetIntegerv(GL_UNPACK_LSB_FIRST, &lsbfirst);
glGetIntegerv(GL_UNPACK_ROW_LENGTH, &rowlength);
glGetIntegerv(GL_UNPACK_SKIP_ROWS, &skiprows);
glGetIntegerv(GL_UNPACK_SKIP_PIXELS, &skippixels);
glGetIntegerv(GL_UNPACK_ALIGNMENT, &alignment);

/* Set desired pixel store state. */
glPixelStorei(GL_UNPACK_SWAP_BYTES, GL_FALSE);
glPixelStorei(GL_UNPACK_LSB_FIRST, GL_FALSE);
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

然后,此代码恢复像素存储解包模式:

/* Restore current pixel store state. */
glPixelStorei(GL_UNPACK_SWAP_BYTES, swapbytes);
glPixelStorei(GL_UNPACK_LSB_FIRST, lsbfirst);
glPixelStorei(GL_UNPACK_ROW_LENGTH, rowlength);
glPixelStorei(GL_UNPACK_SKIP_ROWS, skiprows);
glPixelStorei(GL_UNPACK_SKIP_PIXELS, skippixels);
glPixelStorei(GL_UNPACK_ALIGNMENT, alignment);

可以编写类似的代码来保存和恢复 OpenGL 的像素存储包模式(在上面的代码中更改UNPACK为PACK)。

使用 OpenGL 1.1,保存和恢复这些模式的编码工作更加简单。要保存像素存储状态,您可以调用:

glPushClientAttrib(GL_CLIENT_PIXEL_STORE_BIT);

然后,此代码恢复像素存储解包模式:

glPopClientAttrib(GL_CLIENT_PIXEL_STORE_BIT);

上述例程(在 OpenGL 1.1 中引入)通过使用 OpenGL 库中维护的堆栈推送和弹出状态来保存和恢复像素存储状态。

细心的读者可能想知道为什么glPushClientAttrib使用它而不是较短的glPushAttrib例程。答案涉及 OpenGL 客户端和服务器端状态之间的差异。有必要清楚地了解围绕 OpenGL 的服务器端和客户端状态之间的区别的实际考虑。

实际上没有glPushAttrib用于推送像素存储状态的选项,因为glPushAttrib它glPopAttrib仅影响服务器状态属性堆栈,并且像素打包和解包像素存储状态是客户端 OpenGL 状态。

将您的 OpenGL 应用程序视为由主机的 OpenGL 实现提供的 OpenGL 渲染服务的客户端。

像素存储模式是客户端状态。但是,OpenGL 的大部分状态是服务器端的。术语服务器端状态是指状态实际上驻留在 OpenGL 实现本身中的事实,可能驻留在图形硬件本身中。服务器端 OpenGL 状态与如何呈现 OpenGL 命令有关,而客户端 OpenGL 状态与如何从应用程序地址空间中提取图像或顶点数据有关。

服务器端 OpenGL 状态的检索通常很昂贵,因为该状态可能仅存在于图形硬件中。要返回此类硬件驻留状态(例如使用glGetIntegerv),需要在可检索状态之前发出所有先前的图形命令。虽然 OpenGL 可以回读几乎所有 OpenGL 服务器端状态,但编写良好的程序应始终避免在性能敏感情况下回读 OpenGL 服务器端状态。

然而,客户端状态并不是只存在于渲染硬件中的状态。这意味着使用glGetIntegerv回读像素存储状态相对便宜,因为状态是客户端。这就是为什么可以推荐上述显式读回每个像素存储解包模式的代码的原因。试图保存和恢复服务器端状态的类似 OpenGL 代码可能会严重破坏 OpenGL 渲染性能。

考虑使用glGetIntegerv和glPixelStorei显式保存和恢复模式是否更好,或者是否使用 OpenGL 1.1glPushClientAttrib并glPopClientAttrib取决于您的情况。在推送和弹出客户端属性堆栈时,您必须小心不要使堆栈溢出。推送和弹出客户端属性状态的一个优点是像素存储和顶点数组客户端状态都可以通过单个调用推送或弹出。不过,您可能会发现只需要保存和恢复打包模式或解包模式,有时只需要保存和恢复一种或两种模式。如果是这种情况,显式保存和恢复可能会更快。

  1. 小心更新光栅位置

OpenGL 的光栅位置决定了像素矩形和位图的光栅化位置。该glRasterPos2f命令的家庭规定了光栅位置的坐标。栅格位置就像一个顶点一样被变换。这种对称性使得沿侧面 3D 几何体定位场景中的图像或文本变得容易。就像顶点一样,栅格位置在逻辑上是一个(x,y,z,w)坐标。这也意味着当指定了光栅位置时,OpenGL 的模型视图和投影矩阵变换、光照、裁剪,甚至纹理坐标生成都在光栅位置顶点上以与通过 传递给 OpenGL 的顶点坐标完全相同的方式执行glVertex3f。

虽然这一切都是非常对称的,但它很少对光栅位置进行光照或生成纹理坐标。当您尝试根据当前颜色渲染位图并发现由于启用了照明,位图颜色由照明计算确定时,甚至会感到非常困惑。类似地,如果您在启用纹理映射的情况下绘制像素矩形,则您的像素矩形最终可能会使用由当前光栅纹理坐标确定的单个纹素进行调制。

OpenGL 对顶点和光栅位置的相同处理的另一个对称但通常出乎意料的结果是,就像顶点一样,光栅位置可以被裁剪。这意味着如果您在视锥体之外(甚至稍微超出)指定一个光栅位置,则该光栅位置将被裁剪并标记为“无效”。当光栅位置是无效的,OpenGL的简单地丢弃由指定的像素数据glBitmap,glDrawPixels以及glCopyPixls命令。

图 3:封闭框代表视锥体和视口。每行文本前面都有一个点,指示在渲染文本行之前设置光栅位置的位置。虚线下划线显示将实际从每行文本光栅化的像素。请注意,由于该行的光栅位置无效,因此没有渲染最低文本行中的任何像素。

考虑一下这会给您带来怎样的惊喜。假设您想绘制一串文本,其中每个字符都使用glBitmap. 图 3 显示了几种情况。需要注意的一点是,在前两种情况下文本按预期呈现,但在最后一种情况下,光栅位置的位置在视锥体之外,因此不会绘制来自最后一个文本字符串的像素。

似乎没有办法开始渲染视口和视锥体边界之外的文本字符串并至少渲染字符串的结尾部分。有一种方法可以完成你想要的;这不是很明显。该glBitmap命令既绘制位图,然后在相对窗口坐标中偏移光栅位置。如果首先将光栅位置定位在视锥体中(以便将光栅位置设置为有效),然后通过调用glBitmap相对光栅位置偏移来偏移光栅位置,则可以渲染最后一行文本。在这种情况下,请务必指定零宽度和零高度位图,这样实际上不会渲染任何像素。

这是一个例子:

glRasterPos2i(0, 0);
glBitmap(0, 0, 0, 0, xoffset, yoffset, NULL);
drawString(“Will not be clipped.”);

此代码片段假定glRasterPos2i调用将验证原点的光栅位置。设置投影和模型视图矩阵的代码没有显示(将两个矩阵设置为单位矩阵就足够了)。

图 4:各种光栅位置方案。A、光栅位置在视锥内,图像完全在视口内。B、光栅位置在视锥内,但图像仅部分在视口内;仍然在视口外生成片段。C、光栅位置无效(由于被放置在视锥体外);没有像素被光栅化。D,与情况 B 类似,只是glPixelZoom(1,-1)反转了 Y 像素光栅化方向,因此图像从上到下呈现。

10.视口不剪裁或剪刀

像素不能在 OpenGL 视口外渲染是一个非常普遍的误解。视口经常被误认为是一种剪刀。事实上,视口只是定义了从标准化设备坐标(即应用了透视划分的后投影矩阵坐标)到窗口坐标的转换。OpenGL 规范在描述 OpenGL 视口的操作时没有提及裁剪或剔除。

部分混乱来自这样一个事实,即大多数时候,视口被设置为窗口的矩形范围,像素被裁剪到窗口的矩形范围。但不要将窗口所有权裁剪与视口正在执行的任何操作混淆,因为视口不会裁剪像素。

看起来像基元被视口裁剪的另一个原因是顶点确实被裁剪在视锥体上。OpenGL 的视锥体裁剪确实保证没有顶点(无论是属于几何图元还是栅格位置)可以落在视口之外。

因此,如果顶点不能落在视锥之外,因此不能在视口外,那么像素如何在视口外渲染?如果您确实无法在视口外生成像素,那么说视口不会充当剪刀,这可能是一种闲话吗?好吧,您可以生成位于视口矩形之外的片段,因此它不是空闲语句。

最后一节已经暗示了一种方式。虽然必须将光栅位置顶点指定为在视锥体内以验证光栅位置,但一旦有效,光栅位置(其状态保持在窗口坐标中)可以使用 glBitmap 调用的光栅位置偏移量移动到视口外能力。但是您甚至不必移动视口外的光栅位置来更新视口矩形外的像素。您可以渲染足够大的位图或图像,以便像素矩形超出视口矩形的范围。图 4 演示了视口外的图像渲染。

可以在视口外生成片段的另一种情况是光栅化宽线和点或平滑点、线和多边形时。虽然宽和平滑图元的实际顶点将在转换过程中被裁剪以落入视口内,但在光栅化时,宽或平滑图元的加宽光栅化覆盖区可能最终会在视口矩形边界之外生成片段。

事实上,这可能会变成一个编程陷阱。假设您的应用程序呈现一组在屏幕上缓慢游荡的宽点。你的程序像这样配置 OpenGL:

glViewport(0, 0, windowWidth, windowHeight);
glLineWidth(8.0);

当一个点慢慢滑出窗口边缘时会发生什么?如果视口与glViewport上面调用所指示的窗口范围相匹配,您会注意到一个点将在其中心超出窗口范围时突然消失。如果您期望宽点逐渐滑动屏幕,那不会发生!

请记住,宽点或抗锯齿点周围的额外像素是在光栅化时生成的,但如果该点的顶点(在其中心)由于视锥体裁剪而在顶点转换期间被剔除,则不会发生加宽的光栅化。您可以通过加宽视口来解决这个问题,即一个点的边缘距离该点的中心最多可达 4 个像素(8.0 的一半),并且仍然在窗口范围内生成片段。将glViewport调用更改为:

glViewport(-4, -4, windowWidth+4, windowHeight+4);

使用这个新视口,即使宽点悬停在窗口边缘,仍然可以光栅化。请注意,这也会稍微缩小您的矩形视图区域,因此如果您想要与以前相同的视图,则需要通过扩展投影矩阵指定的视锥来进行补偿。

请注意,如果您的应用程序中确实需要一个矩形 2D 剪刀,OpenGL 确实提供了一个真正的窗口空间剪刀。见glEnable(GL_SCISSOR_TEST)和glScissor。

  1. 设置光栅颜色

在指定顶点之前,首先指定法线、纹理坐标、材质和颜色,然后只有在glVertex3f调用(或其同类)时才会根据当前每个顶点的状态实际生成顶点。调用glColor3f只是设置当前的颜色状态。glColor3f实际上并不创建顶点或执行任何渲染。该glVertex3f呼叫就是结合了目前所有的每个顶点的状态和问题转化为一个完整的顶点。

光栅位置也同样更新。只有当glRasterPos3f(或其同类)被调用时,所有当前每个顶点的状态才会被转换并分配给光栅位置。

一个常见的陷阱是尝试通过一系列glBitmap调用绘制文本字符串,其中字符串中的不同字符具有不同的颜色。例如:

      glColor3f(1.0, 0.0, 0.0); /* RED */
glRasterPos2i(20, 15);
glBitmap(w, h, 0, 0, xmove, ymove, red_bitmap);

glColor3f(0.0, 1.0, 0.0); /* GREEN /
glBitmap(w, h, 0, 0, xmove, ymove, green_bitmap);
/
WARNING: Both bitmaps render red. */

不幸的是,glBitmap的光栅位置的相对偏移量只是更新了光栅位置的位置。光栅颜色(和其他剩余的光栅状态值)保持不变。

OpenGL 的设计者特意指定,glBitmap当光栅位置被重新定位时,不应锁定当前每个顶点的状态glBitmap。重复glBitmap调用旨在实现高效的文本渲染,单色文本是最常见的情况。更新每个顶点状态的额外处理会减慢glBitmap.

如果您确实想切换使用 呈现的位图的颜色glBitmap,则需要显式调用glRasterPos3f(或其同类)以锁定已更改的当前颜色。

  1. OpenGL 的左下角原点

给定一张纸,人们从页面的顶部写到底部。书写文本的起点位于页面的左上角(至少在欧洲语言中)。但是,如果您让任何体面的数学学生在 XY 图形上绘制几个点,原点肯定会在图形的左下角。大多数 2D 渲染 API 模仿作者并使用 2D 坐标系,其中原点位于屏幕或窗口的左上角(至少在默认情况下)。另一方面,3D 渲染 API 采用数学思维约定并假设其 3D 坐标系的原点位于左下角。

如果您习惯于 2D 图形 API,这种原点位置的差异可能会让您感到困惑。当您在 OpenGL 中指定 2D 坐标时,它们通常基于左下角坐标系。使用时,请记住这一点glViewport,glScissor,glRasterPos2i,glBitmap,glTexCoord2f,glReadPixels,glCopyPixels,glCopyTexImage2D,glCopyTexSubImage2D,gluOrtho2D,和相关程序。

与具有左上角坐标系的 2D 渲染 API 相关的另一个常见缺陷是 2D 图像文件格式从顶部扫描线而不是底部扫描线开始图像。默认情况下,OpenGL 假定图像从底部扫描线开始。如果在渲染时确实需要翻转图像,可以使用glPixelZoom(1,-1)沿 Y 方向翻转图像。请注意,您还可以在 X 方向翻转图像。图 4 演示了如何使用glPixelZoom翻转图像。

请注意,glPixelZoom仅在使用glDrawPixels或光栅化图像矩形时才有效glCopyPixels。它不适用于glBitmap或glReadPixels。不幸的是,OpenGL 没有提供一种有效的方法来将图像从帧缓冲区读取到内存中,从顶部扫描线开始。

  1. 将您的光栅位置设置为像素位置

OpenGL 编程中的一个常见任务是在窗口坐标中进行渲染。当将文本或位图图像叠加到编解码屏幕位置时,通常需要这样做。通常,拥有一个左上角原点与窗口系统默认的 2D 坐标系相匹配的 2D 窗口坐标系是很有用的。

以下是为具有左上角原点的 2D 窗口坐标系配置 OpenGL 的代码,其中w和h是窗口的宽度和高度(以像素为单位):

glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(0, w, h, 0, -1, 1);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();

注意底部和顶部参数(第三和第四个参数)glOrtho指定窗口高度为顶部,零为底部。这将翻转原点以将原点置于窗口的左上角。

现在,您可以像这样安全地将光栅位置设置在窗口坐标中的像素位置

glVertex2i(x, y);
glRasterPos2i(x, y);

与设置窗口坐标相关的一个陷阱是切换到窗口坐标涉及加载模型视图和投影矩阵。如果您需要“返回”到之前的内容,请使用glPushMatrixand glPopMatrix(但请记住假设投影矩阵堆栈具有两个以上条目的陷阱)。

所有这些矩阵操作都需要做很多工作,例如将光栅位置放置在某个窗口坐标处。Brian Paul 实现了一个名为 Mesa 的 OpenGL API 的免费软件版本。Mesa 实现了一个名为 OpenGL 的扩展MESA_window_pos,它允许直接有效地设置光栅位置,而不会干扰任何其他 OpenGL 状态。这些电话是:

glWindowPos4fMESA(x,y,z,w);
glWindowPos2fMESA(x,y)

这是这些例程在未扩展 OpenGL 中的等效实现:

void glWindowPos4fMESAemulate(GLfloat x,GLfloat y,GLfloat z,GLfloat w) { GLfloat fx, fy;

/* Push current matrix mode and viewport attributes. */
glPushAttrib(GL_TRANSFORM_BIT | GL_VIEWPORT_BIT);

/* Setup projection parameters. */
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glLoadIdentity();
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glLoadIdentity();
glDepthRange(z, z);
glViewport((int) x - 1, (int) y - 1, 2, 2);
/* Set the raster (window) position. */
fx = x - (int) x;
fy = y - (int) y;
glRasterPos4f(fx, fy, 0.0, w);
/* Restore matrices, viewport and matrix mode. */
glPopMatrix();
glMatrixMode(GL_PROJECTION);
glPopMatrix();

glPopAttrib();
}

void glWindowPos2fMESAemulate(GLfloat x, GLfloat y)
{
glWindowPos4fMESAemulate(x, y, 0, 1);
}

请注意仿真例程进行的所有额外工作,以确保在设置光栅位置的过程中不会干扰 OpenGL 状态。也许商业 OpenGL 供应商会考虑实现这个扩展。

  1. 小心启用颜色材料

OpenGL 的颜色材质功能提供了一种更便宜的方式来更改材质参数。启用颜色材料后,材料颜色会跟踪当前颜色。这意味着glMaterialfv您可以使用glColor3f例程而不是使用相对昂贵的例程。

下面是一个使用颜色材料功能更改三角形每个顶点的漫反射颜色的示例:

glColorMaterial(GL_FRONT, GL_DIFFUSE);
glEnable(GL_COLOR_MATERIAL);
glBegin(GL_TRIANGLES);
glColor3f(0.2, 0.5, 0.8);
glVertex3f(1.0, 0.0, 0.0);
glColor3f(0.3, 0.5, 0.6);
glVertex3f(0.0, 0.0, 0.0);
glColor3f(0.4, 0.2, 0.2);
glVertex3f(1.0, 1.0, 0.0);
glEnd();

如果glMaterialfv显式使用,请考虑所需的更昂贵的代码序列:

GLfloat d1 = { 0.2, 0.5, 0.8, 1.0 };
GLfloat d2 = { 0.3, 0.5, 0.6, 1.0 };
GLfloat d3 = { 0.4, 0.2, 0.2, 1.0 };

glBegin(GL_TRIANGLES);
glMaterialfv(GL_FRONT,GL_DIFFUSE,d1);
glVertex3f(1.0, 0.0, 0.0);
glMaterialfv(GL_FRONT,GL_DIFFUSE,d2);
glVertex3f(0.0, 0.0, 0.0);
glMaterialfv(GL_FRONT,GL_DIFFUSE,d3);
glVertex3f(1.0, 1.0, 0.0);
glEnd();

如果您正在渲染需要频繁更改简单材质的对象,请尝试使用颜色材质模式。但是,在启用颜色材料模式时会遇到一个常见的陷阱。启用颜色材质后,OpenGL 会立即更改颜色材质状态控制的材质颜色。考虑以下代码来初始化一个新创建的 OpenGL 渲染上下文:

GLfloat a[] = { 0.1, 0.1, 0.1, 1.0 };
glColor4f(1.0, 1.0, 1.0, 1.0);

glMaterialfv(GL_FRONT, GL_AMBIENT, a);
glEnable(GL_COLOR_MATERIAL); /* WARNING: Ambient and diffuse material latch immediately to the current color. */ glColorMaterial(GL_FRONT, GL_DIFFUSE);
glColor3f(0.3, 0.5, 0.6);

执行上述代码片段后,前端环境和漫反射材质颜色将处于什么状态?虽然程序员可能希望环境材质状态为 (0.1, 0.1, 0.1, 1.0),而漫反射材质状态为 (0.3, 0.5, 0.6, 1.0),但事实并非如此。

最终的漫反射材质状态是程序员想要的,但最终的环境材质状态却出乎意料地 (1.0, 1.0, 1.0, 1.0)。那是怎么发生的?好吧,请记住颜色材质模式在启用后立即开始跟踪当前颜色。颜色材料设置的初始值是GL_FRONT_AND_BACK和GL_AMBIENT_AND_DIFFUSE(可能不是您所期望的!)。

由于启用颜色材质模式会立即开始跟踪当前颜色,因此环境和漫反射材质状态都更新为 (1.0, 1.0, 1.0, 1.0)。请注意,初始效果glMaterialfv丢失。接下来,更新颜色材质状态以仅更改正面漫反射材质。最后,glColor3f调用将漫反射材质更改为 (0.3, 0.5, 0.6, 1.0)。环境材料状态最终为 (1.0, 1.0, 1.0, 1.0)。

上面代码片段的问题是在调用之前启用了颜色材质模式glColorMaterial。颜色材质模式对于高效的简单材质更改非常有效,但为了避免上述陷阱,glColorMaterial在启用之前请务必小心设置GL_COLOR_MATERIAL。

  1. 许多 OpenGL 状态影响所有基元

片段是 OpenGL 的术语,表示用于更新屏幕上给定像素的状态束。当诸如多边形或图像矩形之类的图元被光栅化时,结果是一组片段,用于更新图元覆盖的像素。请记住,所有 OpenGL 渲染操作都共享相同的每片段操作集。这同样适用于 OpenGL 的雾和纹理光栅化状态。

例如,如果您在应用程序中渲染多边形时启用了深度测试和混合,请记住,当您覆盖一些指示应用程序状态的 2D 文本时,您可能希望禁用深度测试和混合。很容易忘记,这种状态也会影响用glDrawPixels和绘制和复制的图像glCopyPixels。

您会很快注意到这种共享状态何时会破坏您的渲染,但也请注意,有时您可以启用混合等模式,而不会注意到所涉及的额外费用。如果您使用 1.0 的常量 alpha 绘制图元,您可能不会注意到正在发生混合并且只会减慢您的速度。

这个问题并不是每个片段和光栅化状态所独有的。像素路径状态由绘制像素 ( glDrawPixels)、读取像素 ( glReadPixels)、复制像素 ( glCopyPixels) 和纹理下载 ( glTexImage2D) 路径共享。如果您不小心,很容易陷入纹理下载被搞砸的情况,因为像素路径被保留配置为像素读回。

  1. 务必分配您使用的辅助缓冲区

如果您打算使用辅助缓冲区,例如深度、模板或累积缓冲区,请确保您的应用程序实际请求了您打算使用的所有辅助缓冲区。一个常见的互操作性问题是在只有少数帧缓冲区配置的系统上开发 OpenGL 应用程序,这些配置提供了您使用的所有辅助缓冲区。例如,您的系统没有帧缓冲区配置来通告没有模板缓冲区的深度缓冲区。因此,在您的开发系统上,您“侥幸逃脱”并没有明确请求模板缓冲区。

当您将所谓的调试应用程序运行在一个新的硬件加速 OpenGL 系统上时,却发现应用程序在尝试使用模板缓冲区时惨遭失败。考虑一下,如果您不请求模板缓冲区,那么精美的硬件可能会支持额外的颜色分辨率。如果您的应用程序没有明确请求它使用的模板缓冲区,那么花哨的硬件加速 OpenGL 实现确定没有模板但具有额外颜色分辨率的帧缓冲区配置是您的应用程序的更好选择。如果您的应用程序正确请求了模板缓冲区,那一切都会很好。确保你分配你使用的东西。

结论

我希望这篇对各种 OpenGL 陷阱的回顾可以为您节省大量时间和调试烦恼。我希望我可以简单地阅读这些陷阱,而不是通过艰难的方式学习其中的大部分。

形象化一直是启蒙的关键。如果计算机图形使世界变得更好,那么根本原因是计算机图形使可视化更容易。