opengl superbible 5 edition chapter 12——Advanced Geometry Management

  • Post author:
  • Post category:其他


In this chapter, we go over some of the more advanced features of OpenGL related to geometry management. This includes figuring out what got rendered and getting information back from OpenGL about the amount of geometry it processed. A way of storing the intermediate results of rendering for later is covered, and we talk about how to synchronize

two OpenGL contexts so that one context can consume data produced by the other. We see how to manage our own geometry data in the graphics card’s memory and how to control the way that OpenGL processes batch primitives like triangle fans and line strips. We also see how to make an OpenGL application that offloads rendering large amounts of

geometry to the graphics card.

many of these techniques are designed to improve performance and maximize the amount of work that gets done by your GPU. some, however, enable u to ues the gpu for new and interesting techniques that otherwise would not possible.

Gathering Information about the OpenGL Pipeline—Queries

u would like to ask opengl if it drew anything as a result of the functions u called. this seems like a strange question. u just called a long sequence of opengl functions; u sent a lot of geometry to the opengl pipeline, so surely sth. was drawn. well remember, even geometry that would like within the bounds of the screen may not actually change any pixels. there are a number of reasons for this, including triangles being discarded due to back-face culling or fragments failing the depth test or being discarded by the fragment shader. it can be useful to know if any pixels made it to the screen or even to know exactly how many made it. as an example, consider a game where there are many characters or objects on the screen. your game engine may need to know if your player can see some other object, such as an enemy, bonus item, or another player. it is certainly possible to construct a complex line of sight test based on the games geometry assets. but, it is much simpler just to ask the GPU if it actually drew any part of the object in question.

The way to ask the GPU this question is through the occlusion query. The name is somewhat misleading as it’s really more of a visibility query. The answer is zero, or false, if there are no pixels drawn and nonzero, or true, if there are some pixels drawn. So the question is really “is this visible?” rather than “is this occluded?” Perhaps it should have been called

a visibility query. In any case, a query is an OpenGL object representing a question. There are several types of query objects representing all kinds of different questions, and an occlusion query represents the question, “Did you draw anything?”

preparing query?

Remember way back to your early days in school. The teacher wanted you to raise your hand before asking a question. This was almost like reserving your place in line for asking the question—the teacher didn’t know yet what your question was going to be, but she knew that you had something to ask. OpenGL is similar. Before we can ask a question, we

have to reserve a spot so that OpenGL knows that the question is coming. Questions in OpenGL are represented by query objects, and much like any other object in OpenGL, query objects must be reserved, or generated. To do this, call glGenQueries, passing it the number of queries you want to reserve and the address of a variable (or array) where you

would like the names of the query objects to be placed:

void glGenQueries(GLsizei n, GLuint *ids);

The function reserves some query objects for you and gives you their names so that you can refer to them later. You can generate as many query objects you need in one go:

GLuint one_query;
GLuint ten_queries[10];
glGenQueries(1, &one_query);
glGenQueries(10, ten_queries);

In this example, the first call to glGenQueries generates a single query object and returns its name in the variable one_query. The second call to glGenQueries generates ten query objects and returns ten names in the array ten_queries. In total, 11 query objects have been created, and OpenGL has reserved 11 unique names to represent them. It is very unlikely, but still possible that OpenGL will not be able to create a query for you, and in this case it returns zero as the name of the query. A well-written application always checks that glGenQueries returns a nonzero value for the name of each requested query object. If there is a failure, OpenGL keeps track of the reason, and you can find that out by calling glGetError. Each query object reserves a small but measurable amount of resources from OpenGL. These resources must be returned to OpenGL because if they are not, OpenGL may run out of space for queries and fail to generate more for the application later. To return the resources to OpenGL, call glDeleteQueries:

void glDeleteQueries(GLsizei n, const GLuint *ids);

This works similarly to glGenQueries—it takes the number of query objects to delete and the address of a variable or array holding their names:

glDeleteQueries(10, ten_queries);
glDeleteQueries(1, &one_query);

After the queries are deleted, they are essentially gone for good. The names of the queries can’t be used again unless they are given back to you by another call to glGenQueries.

Issuing a Query

Once you’ve reserved your spot using glGenQueries, you can ask a question. OpenGL doesn’t automatically keep track of the number of pixels it has drawn. It has to count, and it must be told when to start counting. To do this, use glBeginQuery. The glBeginQuery function takes two parameters: The first is the question you’d like to ask, and the second is the name of the query object that you reserved earlier:

glBeginQuery(GL_SAMPLES_PASSED, one_query);

GL_SAMPLES_PASSED represents the question u are asking. “how many samples passed the depth test?” here, opengl counts samples because u might be rendering to a multisampled display format, and in that case, there could be more than one sample per pixel. in the case of a normal, single-sampled format, there is one sample per pixel and therefore a one-to-one mapping of samples to pixels. every time a sample makes it past the depth test 这里的it指代啥 (meaning that had not previously been discarded by the fragment shader), opengl counts one. it adds up all the samples from all the rendering it is doing and stores the answer in part of the space reserved for the query object.

now opengl is counting samples (or pixels); u can render as normal, and opengl keeps track of the pixels generated as a result. anything that u render is counted toward the total. when u want opengl to add up everything rendered since u told it to start counting, u tell it to stop by calling glEndQuery:

glEndQuery(GL_SAMPLES_PASSED);

this tells opengl to stop counting samples that have passed the depth test and made it through the fragment shader without being discarded. all the pixels generated by all the drawing commands between the call to glBeginQuery and glEndQuery are added up.

retrieving query results

now that the pixels produced by your drawing commands have been counted, u need to retrieve them from opengl. This is accomplished by calling

glGetQueryObjectuiv(the_query, GL_QUERY_RESULT, &result);

this instructs opengl to place the count associated with the query object into your variable. if no pixels were produced as a result of the drawing commands between the last call to glBeginQuery and glEndQuery for the query object, result will be zero. if anything actually made it to the screen, result will contain the number of pixels written. by rendering an object between a call t oglBeginQuery and glEndQuery and then checking if result is zero or not, u can determine whether the object is visible.

because opengl operates as a pipeline, it may have many drawing commands queued up back-to-back waiting to be processed. it could be the case that not all of the drawing commands issued before the last call to glEndQuery have finished producing pixels. in fact, some may not have even started to be executed. in that case, glGetQueryObjectuiv causes opengl to wait until everything between glBeginQuery and glEndQuery has been rendered, and it is ready to return an accurate count. if u are planning to use a query object as a performance optimization, this is certainly not what u want. all these short delays could add up and eventualy slow down your application! the good news is that it is possible to ask opengl if it is finished rendering anything that might affect the result of the query and therefore has a result available for u. to do this, call

glGetQueryObjectuiv(the_query, GL_QUERY_RESULT_AVAILABLE, &result);

if the result of the query object is not immediately available and trying to retrieve it would cause your application to have to wait for opengl to finish what it is workng on, result becomes GL_FALSE. if opengl is ready and has your answer, result becomes GL_TRUE. this means that retrieving the result from opengl will not cause any delays. Now you can do useful work while you wait for OpenGL to be ready to give you your pixel count, or you can make decisions based on whether the result is available to you. For example, if you would have skipped rendering something had result been zero, you could choose to just go ahead and render it anyway rather than waiting for the result of the query.

storing data in gpu memory

so far, all geometry u have been using (vertices, colors, normals, and other vertex attribute data) has been managed by the GLTools library. when u have called functions like GLBatch::CopyVertexData or GLBatch::CopyNormalData, the pointer u have specified is a real pointer to an area of memory containing vertex coordinates, colors, normals, and other data u would like to render. if each time u called glDrawArrays, glDrawElements, or some other opengl function that required vertex data, the information was taken from the application’s memory, on a high performance system with a local GPU, this would mean that the data would be transferred from the application’s memory (attached to the CPU) across the bus connecting the CPU to the GPU (usually PCI-Express) to the GPU’s local memory so that it can work it. this would take so much time that it would slow down the application significantly. on a remote system, the data might be transferred across a network connection to the server for rendering. this can be devastating for performance.

When the GPU accesses memory that is local to it (physically attached to the video card, for example), it is often several times, perhaps even orders of magnitude faster than accessing the same data stored in system memory. In the case of a remote rendering system, accessing local GPU memory can literally be tens of thousands of times faster than sending the data across a network connection. If the data to be rendered is more or less the same every frame, or if many copies of the same data will be rendered in a single frame, it is advantageous to copy the data to the GPU’s local memory once and then reuse that copy over and over again.

To allow this to happen, the various classes in GLTools manage buffers in the GPU’s local memory and hide the complexities of this from you. In fact, though, it’s not particularly difficult to manage these buffers yourself. You will need to do this eventually as you start to write more complex applications that require data other than simple position, color,

and normal vectors.

In this section, you learn how to ensure that vertex data and other information required by the GPU is available and is stored in its memory. To do this, you use buffer objects containing the data that is supplied by your application. You learn how to manage these objects, how to tell OpenGL what you intend to use them for, and how to best keep your data in GPU memory.

Using Buffers to Store Vertex Data

In OpenGL, it is possible to store vertex attribute data such as positions, colors, or anything else needed by the vertex shader in a buffer object. Buffer objects are OpenGL objects that represent storage for data and have already been introduced earlier in this book. Here, we use an OpenGL buffer as a vertex buffer object (VBO). A VBO is a buffer object that represents storage for vertex data. Data can be placed in these buffers with hints that tell OpenGL how you plan to use it, and OpenGL can then use those hints to decide what it will do with that data. If the data is to be used more than once or twice, OpenGL will more than likely copy it into the fast memory attached to the graphics card.

Because a nontrivial application may require several VBOs and many vertex attributes, a special container object called a vertex array object (VAO) is available to OpenGL to manage all of this state. VAOs are discussed in more detail in the next section. However, because there is no default VAO, you need to create and bind one before you can use any of the code in this section. Some code like the following should be sufficient:

glGenVertexArrays(1, &vao);
glBindVertexArray(vao);

This creates and binds a single VAO. This can stay bound for the duration of your application, and you’ll be in a position to use and manipulate vertex buffers. To create one or more buffer objects, call

glGenBuffers(1, &one_buffer);

or

glGenBuffers(10, ten_buffers);

To store vertex data into or retrieve vertex data from a buffer, it must be bound to the GL_ARRAY_BUFFER binding. To do this, call

glBindBuffer(GL_ARRAY_BUFFER, one_buffer);

Once bound, you can use many of the functions requiring a buffer binding as a parameter to manipulate the buffer object. Examples of these functions are glBufferData, glBufferSubData, glMapBuffer, and glCopyBuffer.

when glVertexAttribPointer is called, the value of the attribute pointer is not interpreted as a real, physical pointer to data in memory. the pointer is actually interpreted as an offset into the buffer object that is bound to the GL_ARRAY_BUFFER binding at the time of the call.

also, a record of the currently bound buffer is made in the current VAO and used for that attribute. that is, not only does glVertexAttribPointer tell opengl the offset into the buffer that a vertex attribute’s data can be found, but it also tells opengl which buffer contains the data.

it is therefore possible to use multiple buffers——one for each attribute——simultaneously by calling glBindBuffer followed by glVertexAttribPointer for each attribute. it is also possible to store several different attributes in a single buffer by interleaving them. to do this cal glVertexAttribPointer with the stride parameter set to the distance (in bytes) between attributes of the same type. finally, because each vertex attribute has its own set of parameters, including offset, stride, and buffer binding, it is possible to use a combination of interleaved and separate buffers. for example, a single model could have positions and normals interleaved 交叉 in one buffer and texture coordinates in a second separate bufffer. this would allow different textures to be used with different texture coodinates on the same model by changing only the buffer binding for the texture coodinate vertex attribute.

the following example, shown in listing 12.9 creates a single buffer, binds it to the GL_ARRAY_BUFFER binding, places some data in it, and then sets a vertex attribute pointer to refer to that buffer. there is one large chunk of data placed into the buffer (the data array), and it occupies the whole buffer.

LISTING 12.9 Allocating and Initializing a Single VBO

// This variable will hold the name of our buffer.
GLuint my_buffer;
// This array contains the data that we’ll initialize the buffer with.
// Often, the data is actually stored in a file rather than a raw C array.
static const GLfloat data[] = { 1.0f, 2.0f, 3.0f, 4.0f, ... };
// Create a buffer.
glGenBuffers(1, &my_buffer);
// A well behaved application would check that buffer creation
// succeeded here. We’re just going to bind it and hope for the best.
glBindBuffer(GL_ARRAY_BUFFER, my_buffer);
// There is no storage space allocated for the buffer until we put
// some data into it. This copies the contents of the ‘data’ array
// into the buffer.
glBufferData(GL_ARRAY_BUFFER, sizeof(data), data, GL_STATIC_DRAW);
// Now, we set the vertex attribute pointer. The location is zero (we
// somehow know this), the size is 4 (the attribute in the vertex
// shader is declared as vec4), we have floating point data that is
// not normalized. Stride is zero because the data in this case is
// tightly packed. Finally, notice that we’re passing zero as the
// pointer to the data. This is legal because it will be interpreted as
// an offset into ‘my_buffer’,
// and the data really does start at offset zero.
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, (const GLvoid *)0);

instanced rendering

there will probably be times when u want to draw the same obejct many times. imagine a fleet 舰队 of starships, or a field of grass. there could be thouands of copies of what are essentially identical sets of geometry, modified only slightly from instance to instance. a simple application might just loop over all of the individual blades of grass in a field and render them separately, calling glDrawArrays once for each blade 叶片 and perhaps updating a set of shader uniforms on each iteration. supposing each blade of grass were made up of a strip of four triangles, the code might look sth. like listing 12.13.

LISTING 12.13 Drawing the Same Geometry Many Times

glBindVertexArray(grass_vao);
for (int n = 0; n < number_of_blades_of_grass; n++) 
{
	SetupGrassBladeParameters();
	glDrawArrays(GL_TRIANGLE_STRIP, 0, 6);
}

how many blades of grass are there in a filed? what is the value of number of number_of_blades_of_grass? it could be thousands, maybe millions. each blade of grass is likely to take up a very small area on the screen, and the number of vertices representing the blade is also very small. your graphics card does not really have a lot of work to do so render a single blade of grass, and the system is likely to spend most of its time sending commands to opengl rather than actually drawing anything. opengl address this through instanced rendering, which is a way to ask it to draw many copies to the same geometry.

instanced rendering is a method provided by opengl to specify that u want to draw many copies to the same geometry with a single function call. this functionality is accessed through instanced rendering functions, such as

void glDrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei primcount);

and

void glDrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void * indices, GLsizei primcount);

These two functions behave much like glDrawArrays and glDrawElements, except that they tell OpenGL to render primcount copies of the geometry. The first parameters of each (mode, first, and count for glDrawArraysInstanced, and mode, count, type, and indices for glDrawElementsInstanced) take the same meaning as in the regular, noninstanced versions of the functions. When you call one of these functions,

OpenGL makes any preparations it needs to draw your geometry (such as copying vertex data to the graphics card’s memory, for example) only once and then renders the same vertices many times.



版权声明:本文为wodownload2原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。