1
0
mirror of https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git synced 2025-08-22 20:25:28 +08:00
Files
LearnOpenGL-CN/docs/04 Advanced OpenGL/07 Advanced Data.md
2025-01-22 15:34:32 +08:00

7.6 KiB
Raw Blame History

高级数据

原文 | Advanced Data ---|--- 作者 | JoeyDeVries 翻译 | Krasjet 校对 | 暂未校对

我们在OpenGL中大量使用缓冲来储存数据已经有很长时间了。这一节中我们将讨论一些其他的操作缓冲的方法。

OpenGL中的缓冲只是一个管理特定内存块的对象没有其它更多的功能了。在我们将它绑定到一个缓冲目标(Buffer Target)时,我们才赋予了其意义。当我们绑定一个缓冲到GL_ARRAY_BUFFER时,它就是一个顶点数组缓冲,但我们也可以很容易地将其绑定到GL_ELEMENT_ARRAY_BUFFER。OpenGL内部会为每个目标储存一个缓冲并且会根据目标的不同以不同的方式处理缓冲。

到目前为止我们一直是调用glBufferData函数来填充缓冲对象所管理的内存这个函数会分配一块GPU内存并将数据添加到这块内存中。如果我们将它的data参数设置为NULL,那么这个函数将只会分配内存,但不进行填充。这在我们需要预留(Reserve)特定大小的内存,之后回到这个缓冲一点一点填充的时候会很有用。

除了使用一次函数调用填充整个缓冲之外我们也可以使用glBufferSubData填充缓冲的特定区域。这个函数需要一个缓冲目标、一个偏移量、数据的大小和数据本身作为它的参数。这个函数不同的地方在于我们可以提供一个偏移量指定从何处开始填充这个缓冲。这能够让我们插入或者更新缓冲内存的某一部分。要注意的是缓冲需要有足够的已分配内存所以对一个缓冲调用glBufferSubData之前必须要先调用glBufferData。

glBufferSubData(GL_ARRAY_BUFFER, 24, sizeof(data), &data); // 范围: [24, 24 + sizeof(data)]

将数据导入缓冲的另外一种方法是请求缓冲内存的指针直接将数据复制到缓冲当中。通过调用glMapBuffer函数OpenGL会返回当前绑定缓冲的内存指针供我们操作

float data[] = {
  0.5f, 1.0f, -0.35f
  ...
};
glBindBuffer(GL_ARRAY_BUFFER, buffer);
// 获取指针
void *ptr = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
// 复制数据到内存
memcpy(ptr, data, sizeof(data));
// 记得告诉OpenGL我们不再需要这个指针了
glUnmapBuffer(GL_ARRAY_BUFFER);

当我们使用glUnmapBuffer函数告诉OpenGL我们已经完成指针操作之后OpenGL就会知道你已经完成了。在解除映射(Unmapping)之后指针将会不再可用并且如果OpenGL能够成功将您的数据映射到缓冲中这个函数将会返回GL_TRUE

如果要直接映射数据到缓冲而不事先将其存储到临时内存中glMapBuffer这个函数会很有用。比如说你可以从文件中读取数据并直接将它们复制到缓冲内存中。

分批顶点属性

通过使用glVertexAttribPointer我们能够指定顶点数组缓冲内容的属性布局。在顶点数组缓冲中我们对属性进行了交错(Interleave)处理,也就是说,我们将每一个顶点的位置、法线和/或纹理坐标紧密放置在一起。既然我们现在已经对缓冲有了更多的了解,我们可以采取另一种方式。

我们可以做的是,将每一种属性类型的向量数据打包(Batch)为一个大的区块而不是对它们进行交错储存。与交错布局123123123123不同我们将采用分批(Batched)的方式111122223333。

当从文件中加载顶点数据的时候,你通常获取到的是一个位置数组、一个法线数组和/或一个纹理坐标数组。我们需要花点力气才能将这些数组转化为一个大的交错数据数组。使用分批的方式会是更简单的解决方案我们可以很容易使用glBufferSubData函数实现

float positions[] = { ... };
float normals[] = { ... };
float tex[] = { ... };
// 填充缓冲
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(positions), &positions);
glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions), sizeof(normals), &normals);
glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions) + sizeof(normals), sizeof(tex), &tex);

这样子我们就能直接将属性数组作为一个整体传递给缓冲而不需要事先处理它们了。我们仍可以将它们合并为一个大的数组再使用glBufferData来填充缓冲但对于这种工作使用glBufferSubData会更合适一点。

我们还需要更新顶点属性指针来反映这些改变:

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), 0);  
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)(sizeof(positions)));  
glVertexAttribPointer(
  2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)(sizeof(positions) + sizeof(normals)));

注意stride参数等于顶点属性的大小因为下一个顶点属性向量能在3个或2个分量之后找到。

这给了我们设置顶点属性的另一种方法。使用哪种方法都是可行的,它只是设置顶点属性的一种更整洁的方式。但是,推荐使用交错方法,因为这样一来,每个顶点着色器运行时所需要的顶点属性在内存中会更加紧密对齐。

复制缓冲

当你的缓冲已经填充好数据之后你可能会想与其它的缓冲共享其中的数据或者想要将缓冲的内容复制到另一个缓冲当中。glCopyBufferSubData能够让我们相对容易地从一个缓冲中复制数据到另一个缓冲中。这个函数的原型如下

void glCopyBufferSubData(GLenum readtarget, GLenum writetarget, GLintptr readoffset,
                         GLintptr writeoffset, GLsizeiptr size);

readtargetwritetarget参数需要填入复制源和复制目标的缓冲目标。比如说,我们可以将VERTEX_ARRAY_BUFFER缓冲复制到VERTEX_ELEMENT_ARRAY_BUFFER缓冲,分别将这些缓冲目标设置为读和写的目标。当前绑定到这些缓冲目标的缓冲将会被影响到。

但如果我们想读写数据的两个不同缓冲都为顶点数组缓冲该怎么办呢我们不能同时将两个缓冲绑定到同一个缓冲目标上。正是出于这个原因OpenGL提供给我们另外两个缓冲目标叫做GL_COPY_READ_BUFFERGL_COPY_WRITE_BUFFER。我们接下来就可以将需要的缓冲绑定到这两个缓冲目标上,并将这两个目标作为readtargetwritetarget参数。

接下来glCopyBufferSubData会从readtarget中读取size大小的数据,并将其写入writetarget缓冲的writeoffset偏移量处。下面这个例子展示了如何复制两个顶点数组缓冲:

glBindBuffer(GL_COPY_READ_BUFFER, vbo1);
glBindBuffer(GL_COPY_WRITE_BUFFER, vbo2);
glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, sizeof(vertexData));

我们也可以只将writetarget缓冲绑定为新的缓冲目标类型之一:

float vertexData[] = { ... };
glBindBuffer(GL_ARRAY_BUFFER, vbo1);
glBindBuffer(GL_COPY_WRITE_BUFFER, vbo2);
glCopyBufferSubData(GL_ARRAY_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, sizeof(vertexData));

有了这些关于如何操作缓冲的额外知识我们已经能够以更有意思的方式使用它们了。当你越深入OpenGL时这些新的缓冲方法将会变得更加有用。在[下一节](08 Advanced GLSL.md)中在我们讨论Uniform缓冲对象(Uniform Buffer Object)时我们将会充分利用glBufferSubData。