在OpenGL中渲染数据:顶点和计算着色器

7
我想能够将一堆顶点输入到我的图形程序中,然后我想在它们上面执行以下操作:
  • 在OpenGL的图形部分中使用它们,特别是在顶点着色器中。
  • 在计算着色器中对它们进行物理计算。
根据这些要求,我认为我需要一些结构来存储我的顶点并正确地访问它们,我想到了以下内容:
  • ArrayBuffers
  • 纹理(存储信息,而不是用于纹理本身)
但是,我已经考虑到了两种方案的缺点:
ArrayBuffers:
  • 我不确定我的计算着色器如何读取,更不用说修改顶点了。但我知道如何绘制它们。
纹理:
  • 我知道如何在计算着色器中修改它们,但是我不确定如何从纹理中进行绘制。更具体地说,所需元素的数量取决于纹理中已写入(非零数据)的元素数量。
我可能忽略了一些重要的其他功能,以满足我的需求,因此我的真正问题是:
如何创建位于GPU上的顶点,并且我既可以在顶点着色器中访问它们,也可以在计算着色器中访问它们?
1个回答

17
希望这可以澄清一些误解,让你更好地理解通用着色器存储器的设置。需要理解的是缓冲对象在GL中的实际工作原理。你经常会听到人们区分诸如“顶点缓冲对象”和“统一缓冲对象”之类的东西。实际上,没有根本性的区别——无论存储什么内容,缓冲对象都被同样对待。它只是一个通用数据存储区,只有在绑定到特定点(例如GL_ARRAY_BUFFERGL_UNIFORM_BUFFER)时才具有特殊意义。

不要认为特殊用途的顶点缓冲区位于GPU上,要更一般地考虑——它实际上是未格式化的内存,如果您知道其结构,则可以读取/写入。像glVertexAttribPointer(...)之类的调用足以描述缓冲对象的数据结构,使得glDrawArrays(...)可以从缓冲对象的内存中有意义地提取每个顶点着色器调用的顶点属性。

对于计算着色器,你需要自己做同样的事情,如下所示。你需要熟悉7.6.2.2 - 标准统一块布局中讨论的规则,才能完全理解以下数据结构。

使用着色器存储块描述顶点数据结构的方式如下:
// Compute Shader SSB Data Structure and Buffer Definition

struct VtxData {
   vec4  vtx_pos;       // 4N [GOOD] -- Largest base alignment
   vec3  vtx_normal;    // 3N [BAD]
   float vtx_padding7;  //  N (such that vtx_st begins on a 2N boundary)
   vec2  vtx_st;        // 2N [BAD]
   vec2  vtx_padding10; // 2N (in order to align the entire thing to 4N)
};                      // ^^ 12 * sizeof (GLfloat) per-vtx

// std140 is pretty important here, it is the only way to guarantee the data
//   structure is aligned as described above and that the stride between
//     elements in verts[] is 0.
layout (std140, binding = 1) buffer VertexBuffer {
   VtxData verts [];
};

使用上面定义的数据结构,在计算着色器中可以使用交错顶点缓冲区。但是在这样做时需要注意数据对齐,通常情况下你可以随意使用任何对齐方式或跨度,但是这里你需要遵守std140布局规则。这意味着使用3分量向量并不总是明智的内存使用方式;你需要将其对齐到N(float)、2N(vec2)或4N(vec3/vec4)边界,这通常需要插入填充和/或巧妙地打包数据。在上面的示例中,你可以在所有由于对齐填充而浪费的空间中放下整个3分量向量的数据。

伪代码展示了如何创建和绑定双重使用的缓冲区:

struct Vertex {
   GLfloat pos       [4];
   GLfloat normal    [3];
   GLfloat padding7;
   GLfloat st        [2];
   GLfloat padding10 [2];
} *verts;

[... code to allocate and fill verts ...]

GLuint vbo;
glGenBuffers (1, &vbo);

glBindBuffer (GL_ARRAY_BUFFER, vbo);
glBufferData (GL_ARRAY_BUFFER, sizeof (Vertex) * num_verts, verts, GL_STATIC_DRAW);

glVertexAttribPointer (0, 4, GL_FLOAT, GL_FALSE, 48,  0); // Vertex Attrib. 0
glVertexAttribPointer (1, 3, GL_FLOAT, GL_FALSE, 48, 16); // Vertex Attrib. 1
glVertexAttribPointer (2, 2, GL_FLOAT, GL_FALSE, 48, 32); // Vertex Attrib. 2

glBindBufferBase      (GL_SHADER_STORAGE_BUFFER, 1, vbo); // Buffer Binding 1

@skiwi:为了简单起见,我在讨论中省略了内存屏障的内容,如果需要的话,我可以解释一下。如果您在计算着色器中更新缓冲区的内容,而该缓冲区正在作为另一个着色器阶段(例如顶点着色器)的输入使用,则内存屏障非常重要。 - Andon M. Coleman
谢谢您的解释。我已经了解了您所解释的大部分内容,因为我几乎已经完成了《OpenGL超级宝典:第6版》。但是,您成功地澄清了如何在图形部分和计算着色器部分之间“移动”(实际上仍然保留在GPU的同一缓冲区中)数据的问题。 - skiwi
然而,在详细的地形上,我可以看到这会导致大量数据存储在缓冲区中并需要渲染。难道实例化不会更轻便吗?如果您能建议如何将其与物理计算相互连接,那就太好了。 - skiwi
我们可以在这里使用std430吗? - Krupip

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接