周一到周五,每天一篇,北京时间早上7点准时更新~

What You’ll Learn in This Chapter(你将会在本章学到啥)

How to create buffers and textures that you can use to store data that your program can access
How to get OpenGL to supply the values of your vertex attributes automatically
How to access textures and buffers from your shaders
如何创建缓冲区和纹理贴图
如何让OpenGL自动设置你的顶点的数据
如何从shader中访问缓冲区和纹理
In the examples you’ve seen so far, either we have used hard-coded data directly in our shaders, or we have passed values to shaders one at a time. While sufficient to demonstrate the configuration of the OpenGL pipeline, this is hardly representative of modern graphics programming. Recent graphics processors are designed as streaming processors that consume and produce huge amounts of data. Passing a few values to OpenGL at a time is extremely inefficient. To allow data to be stored and accessed by OpenGL, we include two main forms of data storage—buffers and textures. In this chapter, we first introduce buffers, which are linear blocks of untyped data and can be seen as generic memory allocations. Next, we introduce textures, which are normally used to store multidimensional data, such as images or other data types

到现在为止,我们展示的那些硬编码的数据也好,从C语言里向shader中传数据也好,尽管说明了OpenGL的玩法,但是它实际上仅仅是教学需要。 我们接下来要学习的才是高效的使用OpenGL的方式。在OpenGL中主要有两种形式的数据:存储一般数据用的缓冲区和纹理缓冲区。在本章节中,我们 首先来看看存储一般数据的缓冲区,它是线性的无类型的数据块。紧接着我们来介绍纹理,你可以在里面存储多维的数据比如图片或者什么的。

Buffers(缓冲区)

In OpenGL, buffers are linear allocations of memory that can be used for a number of purposes. They are represented by names, which are essentially opaque handles that OpenGL uses to identify them. Before you can start using buffers, you have to ask OpenGL to reserve some names for you and then use them to allocate memory and put data into that memory. The memory allocated for a buffer object is called its data store. The data store of the buffer is where OpenGL stores its data. You can put data into the buffer using OpenGL commands, or you can map the buffer object, which means that you can get a pointer that your application can use to write directly into (or read directly out of) the buffer

在OpenGL中,缓冲区是线性存储的内存块,它们有各自的名字,这个名字基本上就是一个OpenGL用于区分它们的标记。在你开始使用缓冲区之前, 你需要让OpenGL给你分配一些名字,这样一来你就可以为这些名字对应的缓冲区分配内存并操作它们。你可以通过OpenGL的API给这些缓冲区里塞数据, 你也可以直接获取到这些缓冲区的地址,然后往里面写数据或者从里面读数据。

Once you have the name of a buffer, you can attach it to the OpenGL context by binding it to a buffer binding point. Binding points are sometimes referred to as targets; these terms may be used interchangeably. There are a large number of buffer binding points in OpenGL and each has a different use, although the buffers you bind to them are the same. For example, you can use the contents of a buffer to automatically supply the inputs of a vertex shader, to store the values of variables that will be used by your shaders, or as a place for shaders to store the data they produce. You can even use the same buffer for multiple purposes at the same time

当你有了缓冲区对象了之后,你就可以将它绑定到当前的OpenGL上下文。这些绑定节点有时候我们也叫它目标。OpenGL里面有很多绑定节点,尽管你绑定上去的缓冲区都一样,但每种节点有不同的用处。 比如说,你可以用缓冲区去提供shader的顶点数据,或者是用缓冲区去存储shader产生的输出数据。你甚至可以在同一时间将同一个缓冲区用于多个目的。

Creating Buffers and Allocating Memory(创建缓冲区并分配内存)

Before you can ask OpenGL to allocate memory, you need to create a buffer object to represent that allocation. Like most objects in OpenGL, buffer objects are represented by a GLuint variable, which is generally called its name. One or more buffer objects can be created using the glCreateBuffers() function, whose prototype is

在你为缓冲区对象分配内存前,你需要先创建这样一个缓冲区。跟大部分的OpenGL里的对象一样,缓冲区物体的标记是一个GLuint类型的,这就是它的名字。 你可以使用glCreateBuffers一次性去创建1个或者多个缓冲区对象

void glCreateBuffers(GLsizei n, GLuint* buffers);
The first parameter to glCreateBuffers(), n, is the number of buffer objects to create. The second parameter, buffers, is the address of the variable or variables that will be used to store the names of the buffer objects. If you need to create only one buffer object, set n to 1 and set buffers to the address of a single GLuint variable. If you need to create more than one buffer at a time, simply set n to that number and point buffers to the beginning of an array of at least n GLuint variables. OpenGL will just trust that the array is big enough and will write that many buffer names to the pointer that you specify

第一个参数是你需要创建多少个缓冲区对象,第二个参数是用于存储缓冲区对象的地址。需要注意的是,第二个参数需要有足够多的空间去存储对象的名字,比如你如果要10个缓冲区对象, 那么第二个参数指向的地址必须要至少可以放10个GLuint。

Each of the names you get back from glCreateBuffers() represents a single buffer object. You can bind the buffer objects to the current OpenGL context by calling glBindBuffer(), the prototype of which is

创建好了缓冲区对象之后,你就可以使用glBindBuffer去将某个缓冲区对象绑定到当前的OpenGL的上下文了

void glBindBuffer(GLenum target, GLuint buffer);
Before you can actually use the buffer objects, you need to allocate their data stores, which is another term for the memory represented by the buffer object. The functions that are used to allocate memory using a buffer object are glBufferStorage() and glNamedBufferStorage(). Their prototypes are

完成了上面的操作后,你就可以为你的缓冲区对象分配内存了,使用glBufferStoerage和glNamedBufferStorage来分配

void glBufferStorage(GLenum target,
GLsizeiptr size,
const void data,
GLbitfield flags);
void glNamedBufferStorage(GLuint buffer,
GLsizeiptr size,
const void
data,
GLbtifield flags);
The first function affects the buffer object bound to the binding point specified by target; the second function directly affects the buffer specified by buffer. The remainder of the parameters serve the same purpose in both functions. The size parameter specifies how big the storage region is to be, in bytes. The data parameteris used to pass a pointer to any data that you want to initialize the buffer with. If this is NULL, then the storage associated with the buffer object will at first be uninitialized. The final parameter, flags, is used to tell OpenGL how you’re planning to use the buffer object.

第一个函数会影响绑定在target节点上的缓冲区对象。第二个函数直接影响buffer指定的那个缓冲区对象。俩函数剩余的参数的含义是一样的。 size表示的是缓冲区对象的内存有多少字节,data表示的数据,如果data是空,则缓冲区仅分配内存,不会写入数据,如果data有数据,则会把data上的数据拷贝到缓冲区对象里去。 最后的一个参数是一个标志位,用来告诉OpenGL,我们的程序后面会如何去使用这个缓冲区对象。

Once storage has been allocated for a buffer object using either glBufferStorage() or glNamedBufferStorage(), it cannot be reallocated or respecified, but is considered immutable. To be clear, the contents of the buffer object’s data store can be changed, but its size or usage flags may not. If you need to resize a buffer, you need to delete it, create a new one, and set up new storage for that

当你为缓冲区对象分配好了内存后,你可以改变缓冲区对象里的内容,但是你不可以改变它的大小,如果你非得这么做,你只能先删除当前的缓冲区对象,然后重新创建一个新的。

The most interesting parameter to these two functions is the flags parameter. This should give OpenGL enough information to allocate memory suitable for your intended purpose and allow it to make an informed decision about the storage requirements of the buffer. flags is a GLbitfield type, which means that it’s a combination of one or more bits. The flags that you can set are shown in Table 5.1

最搞笑的参数是最后的那个参数,那个参数会告诉OpenGL你将如何使用这些缓冲区对象,这样OpenGL才能将这些缓冲区分配到合适的内存上,最后的参数可以是表5.1中的多个标志位采用位运算来进行组合

Chapter 5. Data(第五章 数据)
The flags listed in Table 5.1 may seem a little terse and probably deserve more explanation. In particular, the absence of certain flags can mean something to OpenGL, some flags may be used only in combination with others, and the specification of these flags can have an effect on what you’re allowed to do with the buffer later. We’ll provide a brief explanation of each of these flags here and then dive deeper into some of their meanings as we cover further functionality

表5.1里的这些标志位可能需要更多的说明才行,特别是,某些标志对OpenGL有着特殊的意义,有的标志只能与特定的标志进行结合使用。我们先来进行一些简短的说明, 后面我们用到那些功能的时候再进行深入讲解

First, the GL_DYNAMIC_STORAGE_BIT flag is used to tell OpenGL that you mean to update the contents of the buffer directly—perhaps once for every time that you use thedata. If this flag is not set, OpenGL will assume that you’re not likely to need to change the contents of the buffer and might put the data somewhere that is less accessible. If you don’t set this bit, you won’t be able to use commands like glBufferSubData() to update the buffer content, although you will be able to write into it directly from the GPU using other OpenGL commands

首先是GL_DYNAMIC_STORAGE_BIT,这个标记告诉OpenGL,你需要频繁的去更新缓冲区对象,如果这个标记没有被设置的话,OpenGL为你的缓冲区对象分配的内存可以在遥远的山区, 你访问这样的缓冲区可能会造成内分泌失调等,甚至大小便失禁。如果你不设置这个东西,你可能无法使用glBufferSubData去更新缓冲区的数据,尽管你可以通过其他的OpenGL指令直接从GPU里往里面写数据

The mapping flags GL_MAP_READ_BIT, GL_MAP_WRITE_BIT, GL_MAP_PERSISTENT_BIT, and GL_MAP_COHERENT_BIT tell OpenGL if and how you’re planning to map the buffer’s data store. Mapping is the process of getting a pointer that you can use from your application that represents the underlying data store of the buffer. For example, you may map the buffer for read or write access only if you specify the GL_MAP_READ_BIT or GL_MAP_WRITE_BIT flags, respectively. Of course, you can specify both if you wish to map the buffer for both reading and writing. If you specify GL_MAP_PERSISTENT_BIT, then this flag tells OpenGL that you wish to map the buffer and then leave it mapped while you call other drawing comands. If you don’t set this bit, then OpenGL requires that you don’t have the buffers mapped while you’re using it from drawing commands. Supporting peristent maps might come at the expense of some performance, so it’s best not to set this bit unless you really need to. The final bit, GL_MAP_COHERENT_BIT, goes further and tells OpenGL that you want to be able to share data quite tightly with the GPU. If you don’t set this bit, you need to tell OpenGL when you’ve written data into the buffer, even if you don’t unmap it.

GL_MAP_READ_BIT、GL_MAP_WRITE_BIT、GL_MAP_PERSISTENT_BIT和GL_MAP_COHERENT_BIT告诉OpenGL你将如何操作缓冲区的数据。 比如只有你设置了GL_MAP_READ_BIT或者GL_MAP_WRITE_BIT的时候,你才可以通过mapping的方式去读或者写数据。你可以对缓冲区对象同时使用读和写两种标志。 GL_MAP_PERSISTENT_BIT标志告诉OpenGL,你在调用绘制指令的时候,不会取消mapping状态,如果你没有使用这个标志位,那么你在绘图的时候,必须要取消缓冲区对象的mapping状态。 GL_MAP_PERSISTENT_BIT这个标志位实际上会带来很大的系统负担, 所以最好还是不要用的比较好哦,除非你真的需要。GL_MAP_COHERENT_BIT这个标志告诉OpenGL你希望能够 与GPU共享紧凑的数据,如果你不去设置这个标志位,即使你不去unmap,你也需要告诉OpenGL,你啥时候把数据写入缓冲区了。

// The type used for names in OpenGL is GLuint
GLuint buffer;
// Create a buffer
glCreateBuffers(1, &buffer);
// Specify the data store parameters for the buffer
glNamedBufferStorage(
buffer, // Name of the buffer
1024 * 1024, // 1 MiB of space
NULL, // No initial data
GL_MAP_WRITE_BIT); // Allow map for writing
// Now bind it to the context using the GL_ARRAY_BUFFER binding point
glBindBuffer(GL_ARRAY_BUFFER, buffer);
Listing 5.1: Creating and initializing a buffer

清单5.1:创建和初始化一个缓冲区对象

After the code in Listing 5.1 has executed, buffer contains the name of a buffer object that has been initialized to represent one megabyte of storage for whatever data we choose. Using the GL_ARRAY_BUFFER target to refer to the buffer object suggests to OpenGL that we’re planning to use this buffer to store vertex data, but we’ll still be able to take that buffer and bind it to some other target later. There are a handful of ways to get data into the buffer object. You may have noticed the NULL pointer that we pass as the third argument to glNamedBufferStorage() in Listing 5.1. Had we instead supplied a pointer to some data, that data would have been used to initialize the buffer object. Using this pointer, however, allows us to set only the initial data to be stored in the buffer.

清单5.1的代码执行完毕后,你就拿到了一个大小为1MB的缓冲区对象了。GL_ARRAY_BUFFER告诉OpenGL,我们将使用这个缓冲区对象存储顶点数据,但是我们依然可以在后面 把它用作其他用途。我们有很多种方法给缓冲区对象里面塞数据,注意到清单5.1的第三个参数,我们给它传了个NULL,表示我们只申请内存,而不传数据。如果这里第三个参数是有数据的,那么这些 数据将会被传递到缓冲区对象的内存上去

Another way get data into a buffer is to give the buffer to OpenGL and tell it to copy data there. This allows you to dynamically update the content of a buffer after it has already been initialized. To do this, we call either glBufferSubData() or glNamedBufferSubData(), passing the size of the data we want to put into the buffer, the offset in the buffer where we want it to go, and a pointer to the data in memory that should be put into the buffer. glBufferSubData() and glNamedBufferSubData() are declared as follows:

另一个写数据的方法就是在创建缓冲区之后,使用glBufferSubData或者是glNamedBufferSubData函数,这些API让你可以动态的去更新缓冲区对象的内容。offset是写入数据位置的偏移, size是数据的大小,data是我们要写入的数据

void glBufferSubData(GLenum target,
GLintptr offset,
GLsizeiptr size,
const GLvoid data);
void glNamedBufferSubData(GLuint buffer,
GLintptr offset,
GLsizeiptr size,
const void
data);
To update a buffer object using glBufferSubData(), you must have told OpenGL that you want to put data into it that way. To do this, include GL_DYNAMIC_STORAGE_BIT in the flags parameter to glBufferStorage() or glNamedBufferStorage(). Like glBufferStorage() and glNamedBufferStorage(), glBufferSubData() affects the buffer bound to the binding point specified by target, and glNamedBufferSubData() affects the buffer object specified by buffer. Listing 5.2 shows how we can put the data originally used in Listing 3.1 into a buffer object, which is the first step in automatically feeding a vertex shader with data.

为了使用glBufferSubData更新缓冲区对象的内容,你需要在创建它的时候使用GL_DYNAMIC_STORAGE_BIT。 glBufferSubData会影响到绑定节点的缓冲区对象,glNamedBufferSubData会影响由buffer指定的缓冲区对象的数据内容。清单5.2展示了我们如何把清单3.1中的数据用今天学的方式传给缓冲区对象

// This is the data that we will place into the buffer object
static const float data[] =
{
0.25, -0.25, 0.5, 1.0,
-0.25, -0.25, 0.5, 1.0,
0.25, 0.25, 0.5, 1.0
};// Put the data into the buffer at offset zero
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(data), data);
Listing 5.2: Updating the content of a buffer with glBufferSubData()

清单5.2,使用glBufferSubData更新缓冲区对象的内容

Another method for getting data into a buffer object is to ask OpenGL for a pointer to the memory that the buffer object represents and then copy the data there yourself. This is known as mapping the buffer. Listing 5.3 shows how to do this using the glMapNamedBuffer() function

另一个往缓冲区对象里写数据的方法是使用mapping方法,如下所示

// This is the data that we will place into the buffer object
static const float data[] =
{
0.25, -0.25, 0.5, 1.0,
-0.25, -0.25, 0.5, 1.0,
0.25, 0.25, 0.5, 1.0
};
// Get a pointer to the buffer's data store
void * ptr = glMapNamedBuffer(buffer, GL_WRITE_ONLY);
// Copy our data into it...
memcpy(ptr, data, sizeof(data));
// Tell OpenGL that we're done with the pointer
glUnmapNamedBuffer(GL_ARRAY_BUFFER);
Listing 5.3: Mapping a buffer’s data store with glMapNamedBuffer()

As with many other buffer functions in OpenGL, there are two versions—one that affects the buffer bound to one of the targets of the current context, and one that operates directly on a buffer whose name you specify. Their prototypes are

跟其他众多的缓冲区操作函数一样,mapping方法有两个版本,如下所示

void glMapBuffer(GLenum target,
GLenum usage);
void
glMapNamedBuffer(GLuint buffer,
GLenum usage);
To unmap the buffer, we call either glUnmapBuffer() or glUnmapNamedBuffer(), as shown in Listing 5.3. Their prototypes are

unmapping操作使用对应的glUnmapBuffer或者glUnmapNamedBuffer

void glUnmapBuffer(GLenum target);
void glUnmapNamedBuffer(GLuint buffer);
Mapping a buffer is useful if you don’t have all the data handy when you call the function. For example, you might be about to generate the data, or to read it from a file. If you wanted to use glBufferSubData() (or the initial pointer passed to glBufferData()), you’d have to generate or read the data into temporary memory and then get OpenGL to make another copy of the data into the buffer object. If you map a buffer, you can simply read the contents of the file directly into the mapped buffer. When you unmap it, if OpenGL can avoid making a copy of the data, it will. Regardless of whether we used glBufferSubData() or glMapBuffer() and an explicit copy to get data into our buffer object, it now contains a copy of data[] and we can use it as a source of data to feed our vertex shader

mapping的好处在于,你不必要在C语言中保存一份数据的拷贝,你可以直接去操作GPU上的数据。比如,如果你从文件中读入数据,去更新缓冲区对象的内容, 如果使用glBufferSubData的方式,你必须先从文件里把内容读到内存,然后再使用glBufferSubData更新数据,如果你使用mapping的方式,那么 你可以直接从文件中读入数据到缓冲区对象中。

The glMapBuffer() and glMapNamedBuffer() functions can sometimes be a little heavy handed. They map the entire buffer, and do not provide any information about the type of mapping operation to be performed besides the usage parameter. Even that serves only as a hint. A more surgical approach can be taken by calling either glMapBufferRange() or glMapNamedBufferRange(), whose prototypes are

使用glMapBuffer和glMapNamedBuffer有时候会太暴力了,因为它mapping了整个缓冲区,另一个更推荐的方式是glMapBufferRange和glMapNamedBufferRange

void glMapBufferRange(GLenum target,
GLintptr offset,
GLsizeiptr length,
GLbitfield access);
void
glMapNamedBufferRange(GLuint buffer,
GLintptr offset,
GLsizeiptr length,
GLbitfield access);
As with the glMapBuffer() and glMapNamedBuffer() functions, there are two versions of these functions—one that affects a currently bound buffer and one that affects a directly specified buffer object. These functions, rather than mapping the entire buffer object, map only a specific range of the buffer object. This range is given using the offset and length parameters. The parameter contains flags that tell OpenGL how the mapping should be performed. These flags can be a combination of any of the bits listed in Table 5.2.

跟glMapBuffer和glMapNamedBuffer一样,有两个版本-一个影响当前绑定节点的缓冲区对象,一个影响由第一个参数指定的缓冲区对象。 比起mapping整个缓冲区,这俩函数只mapping了一部分。offset指出从哪个偏移地址开始mapping,length表示要mapping多大的数据块。 最后的标志位如表5.2所示。

Chapter 5. Data(第五章 数据)
As with the bits that you can pass to glBufferStorage(), these bits can control some advanced functionality of OpenGL and, in some cases, their correct usage depends on other OpenGL functionality. However, these bits are not hints and OpenGL will enforce their correct usage. You should set GL_MAP_READ_BIT if you plan to read from the buffer and GL_MAP_WRITE_BIT if you plan to write to it. Reading or writing into the mapped range without setting the appropriate bits is an error. The GL_MAP_PERSISTENT_BIT and GL_MAP_COHERENT_BIT flags have similar meanings to their identically named counterparts in glBufferStorage(). All four of these bits are required to match between when you specify storage and when you request a mapping. That is, if you want to map a buffer for reading using the GL_MAP_READ_BIT flag, then you must also specify the GL_MAP_READ_BIT flag when you call glBufferStorage()

与创建缓冲区时那些标志不同的是,这里的标志位设置不是提示,而是强制设置,也就是说,如果你mapping的时候设置了读的标志位,那么后面却往里面写数据,就会出错。 还有一点需要注意的是,为了这里能够实现读写操作,你需要在创建该缓冲区对象的时候,就设置相应的标志位

We’ll dig deeper into the remaining flags when we cover synchronization primitives a little later in the book. However, because of the additional control and stronger contract provided by glMapBufferRange() and glMapNamedBufferRange(), it isgenerally preferred to call these functions rather than glMapNamedBuffer() (or glMapBuffer()). You should get into the habbit of using these functions even if you’re not using any of their more advanced features

我们将在后面的内容中更加深入的聊其他的那些标志位。你应该更多的使用glMapBufferRange的API而不是glMapBuffer,因为这样你能获得更多的性能方面的提升。

本日的翻译就到这里,明天见,拜拜~~

第一时间获取最新桥段,请关注东汉书院以及图形之心公众号

东汉书院,等你来玩哦