什么是顶点数组对象?

我今天刚刚开始学习 OpenGL,从这个教程: http://openglbook.com/the-book/
到了第二章,我画了一个三角形,除了 VAOs (这个缩写可以吗?) ,我什么都懂了.本教程有以下代码:

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

虽然我知道代码是必要的,但我不知道它是做什么的。虽然我从来没有使用 VaoId 超过这一点(除了破坏它) ,代码不会在没有它的情况下运行。我假设这是因为它必须被绑定,但我不知道为什么。这些代码是否需要成为每个 OpenGL 程序的一部分?本教程将 VAO 解释为:

顶点数组对象(或 VAO)是描述顶点属性如何存储在顶点缓冲区对象(或 VBO)中的对象。这意味着 VAO 不是存储顶点数据的实际对象,而是顶点数据的描述符。顶点属性可以由 glVertexAttribPointer 函数及其两个姐妹函数 glVertexAttribIPointer 和 glVertexAttribLPointer 来描述,我们将在下面讨论其中的第一个函数。

我不明白 VAO 如何描述顶点属性。我没有以任何方式描述他们。它是否从 glVertexAttribPointer 获取信息?我想就是这里了。VAO 仅仅是 glVertexAttribPointer 信息的目的地吗?

另外,我所遵循的教程是否可以接受?有没有什么我需要注意的或者更好的教程可以遵循的?

78081 次浏览

“顶点数组对象”是由 OpenGL ARB 愚蠢名称小组委员会提供给您的。

把它想象成一个几何对象。(作为一个过去的 SGI Perform 程序员,我称它们为 Geoset。)对象的实例变量/成员是您的顶点指针、法线指针、颜色指针、属性 N 指针、 ..。

当首次绑定 VAO 时,可以通过调用

glEnableClientState(GL_VERTEX_ARRAY); glVertexPointer...;
glEnableClientState(GL_NORMAL_ARRAY); glNormalPointer...;

启用哪些属性以及您提供的指针存储在 VAO 中。

在此之后,当您再次绑定 VAO 时,所有这些属性和指针也都变成最新的。因此,一个 glBindVertexArray调用等效于以前设置所有属性所需的所有代码。它可以方便地在函数或方法之间传递几何图形,而不必创建自己的结构或对象。

(一次性设置,多次使用是使用 VAO 最简单的方法,但是您也可以通过绑定属性并执行更多的启用/指针调用来更改属性。VAO 不是常量。)

回答帕特里克问题的更多信息:

新创建的 VAO 的默认值是为空(AFAIK)。没有几何图形,甚至没有顶点,所以如果你试图画它,你会得到一个 OpenGL 错误。这是合理的,比如“将所有内容初始化为 False/NULL/zero”。

您只需要在设置时使用 glEnableClientState。 VAO 会记住每个指针的启用/禁用状态。

是的,VAO 将存储 glEnableVertexAttribArrayglVertexAttrib。旧的顶点、法线、颜色、 ... 数组与属性数组、顶点 = = # 0等相同。

顶点数组对象类似于字处理程序中的

宏只是 记住你所做的操作,例如激活这个属性,绑定那个缓冲区,等等。当您调用 glBindVertexArray( yourVAOId )时,它只是 回放那些属性指针绑定和缓冲区绑定。

所以您的下一个绘制调用使用了任何被 VAO 绑定的内容。

VAO 不储存 顶点数据。没有。顶点数据存储在顶点 缓冲器或客户端内存数组中。

VAO 是一个对象,表示 OpenGL 管道的顶点提取阶段,用于向顶点着色器提供输入。

您可以像这样创建顶点数组对象

GLuint vao;
glCreateVertexArrays(1, &vao);
glBindVertexArray(vao);

首先让我们做一个简单的例子。考虑这样一个输入参数在着色器代码

layout (location = 0) in vec4 offset; // input vertex attribute

为了填充这个属性,我们可以使用

glVertexAttrib4fv(0, attrib); // updates the value of input attribute 0

虽然顶点数组对象存储这些静态属性值 你,它可以做得更多。

在创建顶点数组对象之后,我们可以开始填充它的状态。我们将要求 OpenGL 使用存储在我们提供的缓冲区对象中的数据自动填充它。每个顶点属性从绑定到多个顶点缓冲区绑定之一的缓冲区中获取数据。为此,我们使用 glVertexArrayAttribBinding(GLuint vao, GLuint attribindex, GLuint bindingindex)。此外,我们使用 glVertexArrayVertexBuffer()函数将一个缓冲区绑定到一个顶点缓冲区绑定。我们使用 glVertexArrayAttribFormat()函数来描述数据的布局和格式,最后通过调用 glEnableVertexAttribArray()来实现属性的自动填充。

启用顶点属性后,OpenGL 将根据您提供的格式和位置信息向顶点着色器提供数据 glVertexArrayVertexBuffer()glVertexArrayAttribFormat()什么时候 属性被禁用时,顶点着色器将提供与调用 glVertexAttrib*()一起提供的静态信息。

// First, bind a vertex buffer to the VAO
glVertexArrayVertexBuffer(vao, 0, buffer, 0, sizeof(vmath::vec4));


// Now, describe the data to OpenGL, tell it where it is, and turn on automatic
// vertex fetching for the specified attribute
glVertexArrayAttribFormat(vao, 0, 4, GL_FLOAT, GL_FALSE, 0);


glEnableVertexArrayAttrib(vao, 0);

在着色器中编码

layout (location = 0) in vec4 position;

毕竟你需要打电话到 glDeleteVertexArrays(1, &vao)


你可以通过阅读 OpenGL 超级圣经来更好地理解它。

我一直认为 VAO 是 OpenGL 使用的数据缓冲区数组。使用现代的 OpenGL,您将创建一个 VAO 和顶点缓冲区对象。

enter image description here

//vaoB is a buffer
glGenVertexArrays(1, vaoB); //creates one VAO
glBindVertexArray(vao.get(0));
glGenBuffers(vbo.length, vbo, 0); //vbo is a buffer
glBindVertexArray(vao.get(1));
glGenBuffers(vbo1.length, vbo1, 0); //vbo1 is a buffer
glBindVertexArray(vao.get(2));
glGenBuffers(vbo2.length, vbo2, 0); //vbo2 is a buffer

下一步是将数据绑定到缓冲区:

glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glBufferData(GL_ARRAY_BUFFER,vertBuf.limit()*4, vertBuf, GL_STATIC_DRAW); //vertf buf is a floatbuffer of vertices

此时 OpenGL 看到:

enter image description here

现在我们可以使用 glVertexAttribPointer 告诉 OpenGL 缓冲区中的数据代表什么:

glBindBuffer(GL_ARRAY_BUFFER, 0); //bind VBO at 0
glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0); //each vertex has 3 components of size GL_FLOAT with 0 stride (space) between them and the first component starts at 0 (start of data)

enter image description here

OpenGL 现在在缓冲区中拥有数据,并且知道数据是如何组织成顶点的。同样的过程可以应用到纹理坐标等,但纹理坐标将有两个值。

glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glBufferData(GL_ARRAY_BUFFER,coordBuf.limit()*4, coordBuf, GL_STATIC_DRAW);
glVertexAttribPointer(0, 2, GL_FLOAT, false, 0, 0);

接下来,您可以绑定纹理和绘制数组,您将想要创建一个 Vert 和 Frag 着色器,编译和附加到一个程序(不包括在这里)。

glActiveTexture(textureID); //bind our texture
glBindTexture(GL_TEXTURE_2D, textureID);
glDrawArrays(GL_TRIANGLES,0,6); //in this case 6 indices are used for two triangles forming a square

我试图理解这一点,现在我认为我做到了,这将是审慎的,发布一个代码示例的目的是 不太熟悉 OpenGL 架构的人,因为我发现前面的例子不是很有启发性,而且大多数教程 只是告诉你复制粘贴代码而不解释它。

(这是用 C + + 编写的,但是代码可以很容易地翻译成 C 语言)

在本例中,我们将呈现一个有4个顶点的矩形。每个顶点都有一个位置(vec3,xyz)、纹理坐标(vec2,uv)和颜色属性(vec4,rgba)。

我认为将每个属性分离到它们自己的数组中是最干净的:

float positions[] = {
+0.5, +0.5, 0,
+0.5, -0.5, 0,
-0.5, -0.5, 0,
-0.5, +0.5, 0
};


float colors[] = {
1, 1, 1, 1,
1, 1, 1, 1,
1, 1, 1, 1,
1, 1, 1, 1
};


float tex_coords[] = {
0, 0,
0, 1,
1, 1,
1, 0
};

我们的顶点数组对象将用这些属性描述四个顶点。

首先,我们需要创建顶点数组:

GLuint vertex_array;
glGenVertexArrays(1, &vertex_array);

每个顶点数组都有许多缓冲区,这些缓冲区可以看作是数组的属性。每个顶点数组都有一个 任意数量的“槽”为缓冲区。与缓冲区在哪个槽中一起,它将 CPU 端指针保存到 缓冲区的数据,以及 CPU 端的数据格式。我们需要让 OpenGL 知道要使用哪个槽,其中 数据是什么,以及它是如何格式化的。

缓冲区槽是索引的,所以第一个缓冲区是索引0,第二个是索引1,等等。 这些位置对应于顶点着色器中定义的 layout:

// vertex shader
std::string _noop_vertex_shader_source = R"(
#version 420


layout (location = 0) in vec3 _position_3d; // slot 0: xyz
layout (location = 1) in vec4 _color_rgba;  // slot 1: rgba
layout (location = 2) in vec2 _tex_coord;   // slot 2: uv


out vec2 _vertex_tex_coord;
out vec4 _vertex_color_rgba;


void main()
{
gl_Position = vec4(_position_3d.xy, 1, 1);  // forward position to fragment shader
_vertex_color_rgba = _color_rgba;   // forward color to fragment shader
_vertex_tex_coord = _tex_coord;     // forward tex coord to fragment shader
}
)";

我们可以看到,position 属性位于位置0,color 属性位于1,tex 坐标位于2。我们把这些存起来 澄清一下:

// property locations from our shader
const auto vertex_pos_location = 0;
const auto vertex_color_location = 1;
const auto vertex_tex_coord_location = 2;

我们现在需要告诉 OpenGL 关于上面概述的每个缓冲区的信息:

// bind the array, this makes OpenGL aware that we are modifying it with future calls
glBindVertexArray(vertex_array);


// create the position buffer
glGenBuffers(1, &position_buffer);


// bind the buffer so opengl knows we are currently operating on it
glBindBuffer(GL_ARRAY_BUFFER, position_buffer);


// tell opengl where the data pointer is
glBufferData(GL_ARRAY_BUFFER, sizeof(positions), positions, GL_STATIC_DRAW);


// tell opengl how the data is formatted
glVertexAttribPointer(vertex_pos_location, 3, GL_FLOAT, GL_FALSE, 0, (void*) 0);


// tell opengl that this slot should be used
glEnableVertexAttribArray(vertex_pos_location);

这里,我们生成一个缓冲区来保存位置数据 正确的位置,3个元素(因为位置是 xyz 坐标) ,没有偏移或跨步。 因为我们的所有属性都有一个单独的数组,所以两个属性都可以保留为0。

与位置类似,我们为 color 和 tex coord 属性生成并填充缓冲区:

// color
glGenBuffers(1, &color_buffer); // generate
glBindBuffer(GL_ARRAY_BUFFER, color_buffer); // bind
glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW); // set pointer
glVertexAttribPointer(vertex_color_location, 4, GL_FLOAT, GL_FALSE, 0, (void*) 0); // set data format
glEnableVertexAttribArray(vertex_color_location); // enable slot


// tex coords
glGenBuffers(1, &tex_coord_buffer);
glBindBuffer(GL_ARRAY_BUFFER, tex_coord_buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(tex_coords), tex_coords, GL_STATIC_DRAW);
glVertexAttribPointer(vertex_tex_coord_location, 2, GL_FLOAT, GL_FALSE, 0, (void*) 0);
glEnableVertexAttribArray(vertex_tex_coord_location);

我们为颜色选择了4个元素,因为它们是 RGBA 格式的,为了显而易见的原因,为 tex 坐标选择了2个元素。

我们最后需要渲染一个顶点数组是一个 元素缓冲区元素缓冲区。这些可以被看作是 这些索引定义了顶点将以何种顺序呈现 矩形作为三角扇中的两个三角形,因此我们选择以下元素缓冲区:

// vertex order
static uint32_t indices[] = {
0, 1, 2, 1, 2, 3
};


glGenBuffers(1, &element_buffer); // generate
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, element_buffer); // bind
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW) // set pointer

我们不需要启用元素缓冲区槽,它是独立于顶点数组。我们不必在这里指定元素缓冲区的格式,这将在呈现步骤的 glDrawElements中完成。

为什么要这样?所有这些函数都告诉 OpenGL 在哪里查找顶点的数据。指定指向 正确的缓冲区数据及其布局,如果我们现在在呈现步骤中绑定顶点数组:

glUseProgram(shader.get_program_id()); // shader program with our vertex shader


glBindVertexArray(vertex_array);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, element_buffer);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

其中6是元素缓冲区中的元素数。

这就是正确更新顶点着色器中的 in值所需的全部内容。OpenGL 将把数据从 我们的 CPU 端 positionscolorstex_coords分别进入正确的顶点着色器位置0,1和2。 我们不需要绑定其他任何东西,顶点数组记住我们给它的内容,并为我们完成它,这就是为什么它很方便,应该是现代 OpenGL 的首选。


总之:

每个顶点数组有 n 个用于任意属性的缓冲区和1个元素缓冲区。对于每个属性/缓冲区,我们需要

A)产生它(glGenBuffers)
B)装订(glBindBuffer(GL_ARRAY_BUFFER)
C)告诉 OpenGL 数据在 RAM (glBufferData)中的位置
D)告诉 OpenGL 数据是如何格式化的(glVertexAttribPointer)
E)告诉 OpenGL 使用该插槽(glEnableVertexAttribArray) < br >

对于元素缓冲区,我们只需要生成它,将它绑定到 GL_ELEMENT_ARRAY_BUFFER,然后告诉 opengl 数据在哪里。

希望这能帮助我们了解一些事情。我几乎可以肯定这篇文章中会有一些事实性的错误 我对 OpenGL 也基本上是新手,但这是我概念化它以使代码工作的方式。