NOTE

简书传送门

第五章-数据

本章我们会学到什么

  • 如何创建缓冲和纹理,用它们来存储数据,以及程式如何访问数据。
  • 如何使得OpenGL自动为我们的顶点属性提供数据。
  • 如何从着色器中访问纹理和缓冲。

至今为止的示例中,我们要么在着色器直接使用硬编码的数据,要么将值一个一个地传入到着色器中。但要充分地演示OpenGL管线的构造,这很难代表现代图形编程。现代的图形处理器设计为流式处理器,可以吞吐大量的数据。一次给OpenGL传递很少的值是炒鸡没有效率的。要使得数据被OpenGL存储并访问,我们有两种主要的数据存储形式–缓冲和纹理。本章我们先介绍缓冲,它是无类型的线性数据块,可以被看成通用的内存配额。然后我们介绍纹理,它一般用来存储多维度数据,比如图像或者其他数据类型。

缓冲

在OpenGL中,缓冲是线性内存配额,可被用于多种用途。它们通过名字(names)来表示,名字就是OpenGL用来识别它们的句柄。在我们使用缓冲之前,先得要OpenGL为我们保留一些名字,然后用它们来分配内存并把数据放进去。为一个缓冲对象分别的内存被称为数据仓储(data store)。缓冲的数据仓储是OpenGL存放缓冲数据的地方。我们可以使用OpenGL命令来将数据放入缓冲,或者我们可以映射(map)缓冲对象得到一个指针,然后我们的应用可以使用这个指针直接读写缓冲。

一旦我们得到一个缓冲的名字,我们可以将它绑定到一个缓冲绑定点(buffer binding point)从而将它附加到OpenGL上下文。绑定点有时称为目标(targets),这些术语可以互换使用(从严格的技术角度来讲,目标 targets和绑定点 binding point是有区别的,一个目标可以有多个绑定点,不过,大多数情况下还是很容易理解真正的含义的)。在OpenGL中有很多的缓冲绑定点,并且每个都有不同的用处,尽管它们绑定的缓冲对象可能是同一个。比如:我们可以用缓冲的内容为顶点着色器自动提供输入;存储着色器会用到的变量的值;或者作为着色器存储生成数据的地方。我们甚至可以同时将一个缓冲用于多种用途。

创建缓冲并分配内存

在我们让OpenGL分配内存之前,我们需要先创建一个缓冲对象来表示这个配额。就像OpenGL中大多数对象一样,缓冲对象用GLuint变量来表示,这个变量也称为它的名字(names)。使用glCreateBuffers()函数可以创建一个或多个缓冲对象,它的原型为:

void glCreateBuffers(GLsizei n, GLuint* buffers);

glCreateBuffers()的第一个参数n,是要创建的缓冲对象的数目。第二个参数buffers,是用来存储缓冲对象名字的变量的地址。如果我们只需要创建一个缓冲对象,将n设置为1,buffers设置为单个GLuint变量的地址即可。如果我们需要一次创建多个缓冲,将n设置为指定的数目,buffers指向包含至少n个GLuint变量的数组地址即可。OpenGL会假定这个数组足够大,它会向指定的地址写入n个缓冲的名字。

glCreateBuffers()获取到的每个名字都代表一个缓冲对象。我们可以调用glBindBuffer()将缓冲对象绑定到当前OpenGL上下文,glBindBuffer()的原型为:

void glBindBuffer(GLenum target, GLuint buffer);

在我们真正使用缓冲对象之前,我们需要分配它们的数据仓储(data stores),数据仓储是缓冲对象所使用内存的另一个术语。用来给一个缓冲对象分配内存的函数为glBufferStorage()glNamedBufferStorage()。它们的原型为:

void glBufferStorage(GLenum target,
                     GLsizeiptr size,
                     const void* data,
                     GLbitfield flags);
void glNamedBufferStorage(GLuint buffer,
                          GLsieiptr size,
                          const void* data,
                          GLbitfield flags);

第一个函数作用于绑定到target上绑定点的缓冲对象,第二个函数直接作用于buffer指定的缓冲。其余的参数在两个函数中都是一样的。size参数指定存储区域有多个字节大小。data参数是一个指向任何数据的指针,用来初始化缓冲。如果dataNULL,那缓冲对象关联的存储在一开始不会被初始化。最后的参数flags,用来指示OpenGL我们计划如何使用这个缓冲对象。

一旦我们使用glBufferStorage()或者glNamedBufferStorage()分配了缓冲对象的存储,存储就不能再重新分配或者重新指定,它可被当成是不可改变的。再清晰一点说,缓冲对象的数据仓储内容是可被改变的,但它的大小或者用途标志是不可更改的。如果我们要改变一个缓冲的大小,我们得删除它,创建一个新的,然后为这个新的缓冲设置新的存储。

这两个函数最有趣的参数是flags。这个参数可以让OpenGL为我们开辟合适的内存提供足够的参考信息,并使得OpenGL为缓冲的存储需求做出明智的抉择。flags是一个GLbitfield类型,这意味着它可以一个或多个位的组合。可以设置的标志值如表5.1。

表5.1 缓冲存储标志:

Flags                               Description
GL_DYNAMIC_STORAGE_BIT              缓冲的内容可以直接更新
GL_MAP_READ_BIT                     缓冲的数据仓储可被映射进行读取
GL_MAP_WRITE_BIT                    缓冲的数据仓储可被映射进行写入
GL_MAP_PERSISTENT_BIT               缓冲的数据仓储可被持久映射
GL_MAP_COHERENT_BIT                 缓冲的映射是无缝的
GL_CLIENT_STORAGE_BIT               如果其他所有的条件都能满足,就将存储放在本地客户端(CPU),否则放在服务端(GPU)

表5.1列举的标志看起来有一点过于简洁,需要一些更多的解释。特别是有一些重要的标志的缺失会影响到OpenGL,有一些标志只能和其他的组合使用,这些标志的指定会影响到我们之后能对缓冲做些什么。我们在此会对这些标志做一个简短的解释,在之后涉及到深层次的功能时会深入了解其中的一些含义。

首先GL_DYNAMIC_STORAGE_BIT标志用以指示OpenGL我们会直接更新缓冲的内容–可能每次我们使用这些数据时。如果没有设置这个标志,OpenGL会假设我们不会改变缓冲的内容,并将数据放到不易访问的地方。如果没有设置这个标志,我们无法使用glBufferSubData()之类的命令来更新缓冲的内容,尽管我们可以在GPU中使用其他OpenGL命令直接写入。

映射标志GL_MAP_READ_BITGL_MAP_WRITE_BITGL_MAP_PERSISTENT_BITGL_MAP_COHERENT_BIT指示OpenGL我们是否以及如何计划映射缓冲的数据仓储。映射就是获取一个指针,这个指针表示缓冲的底层数据仓储,我们可以在应用中使用它。比如我们可以指定GL_MAP_READ_BIT或者GL_MAP_WRITE_BIT来映射缓冲分别只进行读或者写访问。当然如果我们想映射缓冲用以读以及写,可以将这两个标志都指定。如果我们指定GL_MAP_PERSISTENT_BIT,这个标志指示OpenGL我们要映射这个缓冲,并在我们调用其他绘制命令时将缓冲仍置于已映射状态。如果我们不设置这个标志,那我们在绘制命令中使用缓冲时OpenGL会将其置于未映射状态。支持持久映射(persistent map)会对性能产生一些花销,所以除非我们真的需要,不然最好不要设置这个标志。最后GL_MAP_COHERENT_BIT标志会指示OpenGL我们想要和GPU共享十分紧密的数据。如果我们未设置这个标志位,当我们写入数据到缓冲后需要告诉OpenGL,就算我们并没有映射这个缓冲。

清单 5.1 创建并初始化一个缓冲:

// The type used for names in OpenGL is GLuint
GLuint buffer;

// Create buffer
glCreateBuffer(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);

清单5.1的代码执行后,buffer包含一个缓冲对象的名字,缓冲对象已经被初始化了,用以表示我们选定数据的1兆字节存储。使用GL_ARRAY_BUFFER目标引用缓冲对象提示OpenGL我们计划使用这个缓冲存储顶点数据,不过之后我们仍可以将这个缓冲绑定到其他的目标上。有好几种方法将数据放入缓冲对象。你可能已注意到在清单5.1中我们将NULL作为第三个参数传递给glNamedBufferStorage()。若我们代之以一个指向一些数据的指针,这些数据会用来初始化这个缓冲对象。然而使用这个指针我们只能让初始数据存入缓冲中。

将数据放入缓冲的另一种方法是把缓冲给OpenGL并指示它将数据拷贝到那。这使得我们可以在缓冲初始化之后动态地更新它的内容。我们可以调用glBufferSubData()或者glNamedBufferSubData()来做这件事,传递我们要放入到缓冲中的数据的大小,从哪开始的偏移,以及要放入缓冲的数据的内存指针。glBufferSubData()glNamedBufferSubData()声明如下:

void glBufferSubData(GLenum target,
                     GLintptr offset,
                     GLsizeiptr size,
                     const GLvoid* data);
void glNamedBufferSubData(GLuint buffer,
                          GLintptr offset,
                          GLsizeiptr size,
                          const void* data);

要使用glBufferSubData()来更新一个缓冲对象,我们必须告诉OpenGL我们想以这种方式来放入数据。在传递给glBufferStorage()或者glNamedBufferStorage()flags参数中包含GL_DYNAMIC_STORAGE_BIT以期达成此目的。一如glBufferStorage()glNamedBufferStorage()glBufferSubData()作用于target目标的绑定点绑定的缓冲,glNamedBufferStorage()作用于buffer指定的缓冲对象。清单5.2展示了我们如果将数据(原先是清单3.1中使用过的)放入到缓冲对象中,这是为顶点着色器自动供应数据的第一步。

清单5.2 用glBufferSubData()更新缓冲的内容:

// 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);

另一种将数据放入缓冲对象的方法是让OpenGL得到一个表示缓冲对象内存的指针,然后自个把数据拷贝到目的地。这就是熟知的映射(mapping)缓冲。清单5.3展示了用glMapNamedBuffer()来达成此目的。

清单5.3 用glMapNamedBuffer()映射一个缓冲的数据仓储:

// 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 point 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(buffer);

就好像OpenGL中很多其他的函数一样,有两个版本–一个作用于当前上下文中目标绑定的缓冲,一个直接作用于用名字指定的缓冲。它们的原型如下:

void* glMapBuffer(GLenum target,
                  GLenum usage);
void* glMapNamedBuffer(GLuint buffer,
                       GLenum usage);

我们调用glUnmapBuffer()或者glUnmapNamedBuffer()来取消对缓冲的映射,就像清单5.3中所示。它们的原型为:

void glUnmapBuffer(GLenum target);
void glUnmapNamedBuffer(GLuint buffer);

当我们调用一个函数时如果我们并没有准备好所有的数据,此时映射一个缓冲就很有用处了。比如我们可能要生成数据,或者从文件中读入数据。如果我们要使用glBufferSubData()(或者传递给glBufferData()的初始指针),我们得将生成或读入的数据先放到一个临时的内存中,然后让OpenGL生成一份数据的拷贝放入到缓冲对象。如果我们映射了一个缓冲,我们可以简单地将文件的内容直接读入到映射的缓冲中。当我们取消对它的映射时,如果OpenGL可以避免生成一份数据的拷贝,那它就不会生成拷贝。不管我们是用glBufferSubData()还是glMapBuffer()加一份放入到缓冲对象的数据的显示拷贝,之后缓冲包含了data[]的一份拷贝,然后我们就可以使用缓冲做为数据源来为顶点着色器提供数据。

glMapBuffer()glMapNamedBuffer()函数有时候过于手动了。它们映射整个缓冲,并且除了usage参数外不会为要执行的映射操作类型提供任何信息。甚至于usage参数只是做为提示而已。一个更人性化的方法是用glMapBufferRange()或者glMapNamedBufferRange(),它们的原型为:

void* glMapBufferRange(GLenum target,
                       GLintptr offset,
                       GLsizeiptr length,
                       GLbitfield access);

void* glMapNamedBufferRange(GLuint buffer,
                            GLintptr offset,
                            GLsizeiptr length,
                            GLbitfield access);

一如glMapBuffer()glMapNamedBuffer()函数,这些函数有两个版本–一个作用于当前绑定的缓冲,一个作用于直接指定的缓冲对象。这两个函数并不是映射整个缓冲对象,而是映射缓冲对象指定的一个区域。这个区域使用offsetlength参数进行指定。access包含了一些标志,用以告诉OpenGL映射应该如何执行。这些标志可以是表5.2中任意标志位的组合。

表5.2 缓冲映射标志:

Flag                            Description
GL_MAP_READ_BIT                 缓冲数据仓储映射用以读入
GL_MAP_WRITE_BIT                缓冲数据仓储映射用以写出
GL_MAP_PERSISTENT_BIT           缓冲数据仓储可被持久映射
GL_MAP_COHERENT_BIT             缓冲映射是无缝的
GL_MAP_INVALIDATE_RANGE_BIT     告诉OpenGL我们不再在乎指定区域内的数据
GL_MAP_INVALIDATE_BUFFER_BIT    告诉OpenGL我们不再在乎整个缓冲的数据
GL_MAP_FLUSH_EXPLICIT_BIT       我们保证告诉OpenGL在映射区域修改的数据
GL_MAP_UNSYNCHRONIZED_BIT       告诉OpenGL我们会自己执行所有的同步

一如我们可以传递给glBufferStorage()的标志位,这些标志位可以控制一些OpenGL的高级功能,并且在某些情况下,它们得以正确使用依赖于其他OpenGL功能。然而这些标志位并不是提示,OpenGL会强制要求正确使用它们。如果我们打算从缓冲中进行读取那我们要设置GL_MAP_READ_BIT,如果我们打算写入到缓冲那我们要设置GL_MAP_WRITE_BIT。对映射区域进行读写而没有设置相应的标志为将会引发错误。GL_MAP_PERSISTENT_BITGL_MAP_COHERENT_BIT标志与glBufferStorage()中同名的标志有着相同的含义。这四个标志位在请求映射时必须与指定数据仓储的时候一致。换言之,如果我们使用GL_MAP_READ_BIT映射一个缓冲以进行读取,那我们在调用glBufferStorage()(或者glNamedBufferStorage())时必须也指定了GL_MAP_READ_BIT标志。

当我们在本书后面涉及到图元同步时我们会深入这里其他的标志。不过因为glMapBufferRange()glMapNamedBufferRange()提供的额外控制和更强的约束,我们应该倾向于使用这些函数,而不是glMapBuffer()(或者glMapNamedBuffer())。就算我们不使用它们更多的高级特性我们也应该养成使用它们的习惯。

未完待续

更多推荐

OpenGL超级宝典7th简体中文-第五章-数据