关于VAO,VBO和EBO的理解-OpenGL学习笔记

本文章首发于我的个人博客,希望大家多多支持!


Hi! This is Showhoop Studio!

如果要从代码层面去理解渲染管线的工作,学习使用OpenGL编程可以说是一个不错的选择。这里我将记录下自己的一下学习笔记,以便日后复习和引用。对于刚刚开始学习或者准备入门学习OpenGL的人,推荐去看LearnOpenGL,除了理论知识之外,这个教程会同时手把手教你搭建自己的OpenGL程序!


什么是VAO,VBO和EBO

VAO,Vertex Array Object:顶点数组对象,
VBO,Vertex Buffer Object:顶点缓冲对象,
EBO(或IBO),Element Buffer Object(或Index Buffer Object):索引缓冲对象。

顶点缓冲对象VBO

我们先来介绍顶点缓冲对象VBO。简单来说,VBO是位于GPU内存(即显存)中的一块内存区域,存储了大量顶点数据。使用VBO的好处就是可以让我们一次性发送大量数据到GPU上,毕竟从CPU到GPU的数据传输在计算机层面来看是一个相对较慢的过程。

这个缓冲对象有一个独一无二的ID,所以我们可以使用glGenBuffers函数和一个缓冲ID来生成一个VBO对象:

unsigned int VBO;
glGenBuffers(1, &VBO);

在OpenGL中有很多种缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。我们可以使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER上:

glBindBuffer(GL_ARRAY_BUFFER, VBO);

在这之后,我们使用任何在GL_ARRAY_BUFFER目标上的缓冲调用都会用来配置当前绑定的缓冲(VBO)。然后我们调用glBufferData函数可以把定义好的顶点数据复制到缓冲的内存中:

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

因为我们的重点是理解这些对象,而不是如何在OpenGL中使用,所以这里不具体介绍这个函数每个参数的意义,如果你想要了解可以参考这里。

那么,VBO到底是什么呢?我们可以在之前的简单说明下,具体的解释一番。之前说到,VBO中存储了大量顶点数据,而这些顶点数据是紧密排列的,即使是不同的数据(如法线和颜色)也可能彼此相连。我们假设目前只定义了顶点的位置数据(OpenGL中的顶点数据均为浮点值),那么它们在VBO中排列就会像下图所展示出来的那样:

我们知道,一个浮点数为4个字节,顶点位置一共由三个浮点数值来表示,分别是x、y和Z,因为这是我们定义的。然而在VBO中,并不会像图片上那样有明显的分割线,所以GPU不知道该如何处理这些数据。

顶点数组对象VAO

有了顶点数组对象VAO的帮助,上面的问题就可以得到解决了。VAO是由一系列 顶点属性指针(Vertex Attribute Pointer) 组成的,每一个指针都会指向VBO中的一块区域。那么,VAO是如何实现这一点的呢?

首先,和VBO对象一样,我们也要创建一个新的VAO对象并绑定:

unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);

在把顶点数组中的数据复制到缓冲中之后,开始配置顶点属性指针,告诉OpenGL如何解析VBO中的数据:

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
// 顶点属性默认是禁用的,启用顶点属性
glEnableVertexAttribArray(0);

通过上述的glVertexAttribPointer函数,我们便完成了顶点属性指针的配置:

  • 第一个参数指定我们要配置的顶点属性,这一点要结合顶点着色器来看。在顶点着色器中,其顶点数据的输入需要location来指定,下面就是一个例子。在这个示例中,我们给顶点着色器定义了一个输入,也就是顶点的位置,它被指定在输入布局的0的位置,所以第一个参数我们指定为0,告诉OpenGL,这个指针所指向的VBO中的数据要传入着色器中顶点的位置数据里。
layout (location = 0) in vec3 position;
  • 第二个参数指定顶点属性的大小。顶点的位置属性是一个vec3,由3个值组成,所以大小是3。
  • 第三个参数指定数据类型,GL_FLOAT即浮点值。
  • 第四个参数定义我们是否希望数据被标准化,一般置为GL_FALSE。
  • 第五个参数叫做步长(Stride),这个非常重要,它告诉我们连续的顶点数据之间的间隔。一个位置数据由三个float组成,下一个位置数据的开始必定在三个float之后,所以我们指定步长为3 * sizeof(float),这样我们就将每个顶点的位置数据分隔开了。
  • 最后一个参数是void*,所以我们需要进行奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量。由于数据在数组的开头,所以这里是0(如果你没有理解也没关系,之后会具体介绍这一个参数)。

使用VAO的好处显而易见:

  1. 首先,它告诉了OpenGL如何解析VBO中的数据,
  2. 其次,在有很多种不同物体存在的情况下,我们可以将它们的属性链接到不同的VAO上,在绘制的时候,绑定不同的VAO就可以了。

想要把不同物体的顶点数据链接到不同的VAO上,需要给这些物体各指定一个不同的VBO。而顶点属性具体从哪个VBO中获取数据则是通过在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的。所以我们在用顶点属性获取不同VBO的数据之前要更换VAO和VBO的绑定。不妨看一段示例代码来帮助理解。需要注意的一点是,我们在配置顶点属性指针之前要先绑定VAO,在启用之后会自动解绑,也就是说,你在绘制之前要再一次绑定VAO。在你已经配置了多个VAO的情况下,你想绘制哪个图形,就绑定哪个VAO。

// 绑定第一个物体的VAO
glBindVertexArray(VAO1);
// 绑定第一个物体的VBO
glBindBuffer(GL_ARRAY_BUFFER, VBO1);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices1), vertices1, GL_STATIC_DRAW);
// 设置顶点指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

// 绑定第二个物体的VAO
glBindVertexArray(VAO2);
// 绑定第二个物体的VBO
glBindBuffer(GL_ARRAY_BUFFER, VBO2);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices2), vertices2, GL_STATIC_DRAW);
// 设置顶点指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttributePointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);

// 在绘制之前,如果你想绘制第一个图形就绑定VAO1
glBindVertexArray(VAO1);

对应上述代码的图片描述如下:

通过图片,我们可以更清楚的看到VAO和VBO之间的关系:

  1. 每一种相同的物体对应一个VAO和一个VBO,
  2. VAO中的一个指针对应了VBO中的一种顶点属性,有几种顶点属性,就需要几个指针。换句话说,调用一次glVertexAttribPointer就是设置一个指针。

现在,我们来吧重点放到第二个物体的配置上,因为它有两种顶点属性,一种是顶点位置,另一种是纹理坐标。还记得我们之前所讲的glVertexAttribPointer中的最后一个参数吗?我们将通过这里来进行具体的介绍。我们在配置第二个物体的顶点属性时,调用了两次函数,显然每一次对应一种属性。第一次调用是在配置位置属性:

  1. 第一个参数我们指定为0,这是由顶点着色器中的定义layout (location = 0) in vec3 position决定的,
  2. 第二个参数指定为3,因为顶点位置属性由3个浮点值组成,
  3. 第五个参数需要注意,与之前不同的是,这里我们需要指定步长为5 * sizeof(float),也就是对应一个顶点的所有的属性的大小,位置属性占3个浮点值,纹理坐标占2个浮点值。
  4. 最后的参数指定为(void*)0,因为位置属性是从一个顶点属性的起始开始算起的(也是就是0),再结合第二个参数,属性指针就知道获取位置属性要从一个顶点的所有属性内,从0开始读取3个浮点值。

对于第二次调用,我们的目的是获取纹理坐标:

  1. 第一个参数我们指定为1,这是由顶点着色器中的定义layout (location = 1) in vec2 texcoord决定的(当然你也可以指定其他数字,只要不重复即可),
  2. 第二个参数我们指定为2,因为纹理坐标属性由2个浮点值组成,
  3. 第五个参数与之前无异,
  4. 最后一个参数指定为(void*)3 * sizeof(float),因为纹理坐标属性紧密排列在位置属性之后,位置属性从0开始占用了3 * sizeof(float)的大小,那么纹理坐标属性必然是从3 * sizeof(float)开始读取2个浮点值,一共是5个浮点值,也就是一个顶点所有属性的大小。

现在,你应该对最后一个参数有了更加深刻的理解。

索引缓冲对象EBO

要解释索引缓冲对象,我们先来假设一个情景:我们要绘制两个三角形来组成一个矩形(OpenGL主要处理三角形),我们要定义如下顶点集合:

float vertices[] = {
    // 第一个三角形
    0.5f, 0.5f, 0.0f,   // 右上角
    0.5f, -0.5f, 0.0f,  // 右下角
    -0.5f, 0.5f, 0.0f,  // 左上角

    // 第二个三角形
    0.5f, -0.5f, 0.0f,  // 右下角
    -0.5f, -0.5f, 0.0f, // 左下角
    -0.5f, 0.5f, 0.0f   // 左上角
}

显而易见,一个矩形只有四个顶点,而我们定义了六个顶点,因为我们指定了 右下角左上角 两次!这样就产生了额外开销,而当我们有包括上千个三角形的模型之后问题会更糟糕。更好的解决方案是,只储存不同的顶点,并设定绘制这些顶点的顺序,而EBO恰好提供了这样的功能。

我们首先要定义不重复的顶点和每个顶点的索引(顶点索引从0开始):

float vertices[] = {
    0.5f, 0.5f, 0.0f,   // 右上角
    0.5f, -0.5f, 0.0f,  // 右下角
    -0.5f, -0.5f, 0.0f, // 左下角
    -0.5f, 0.5f, 0.0f   // 左上角
}

unsigned int indices[] = {
    0, 1, 3, // 第一个三角形
    1, 2, 3  // 第二个三角形
}

与VBO类似,下一步我们要创建EBO对象并绑定:

unsigned int EBO;
glGenBuffer(1, &EBO);
// 还记得吗?我们之前提到过,OpenGL允许同时绑定多个Buffer,
// 但不能是同一类型,这里我们指定为GL_ELEMENT_ARRAY_BUFFER
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

要注意的是,我们传递了GL_ELEMENT_ARRAY_BUFFER当作缓冲目标。最后一件要做的事是用glDrawElements来替换glDrawArrays函数,来指明我们从索引缓冲渲染。使用glDrawElements时,我们会使用当前绑定的索引缓冲对象中的索引进行绘制:

// 跟VAO类似,在绘制之前需要再绑定一次,但这不是必须的(之后有解释)
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

第一个参数指定了我们绘制的模式,这个和glDrawArrays的一样。第二个参数是我们打算绘制顶点的个数,这里填6,也就是说我们一共需要绘制6个顶点。第三个参数是索引的类型,这里是GL_UNSIGNED_INT。最后一个参数里我们可以指定EBO中的偏移量(或者传递一个索引数组,但是这是当你不在使用索引缓冲对象的时候),但是我们会在这里填写0。

glDrawElements函数从当前绑定到GL_ELEMENT_ARRAY_BUFFER目标的EBO中获取索引。这意味着我们必须在每次要用索引渲染一个物体时绑定相应的EBO,这还是有点麻烦。不过顶点数组对象同样可以保存索引缓冲对象的绑定状态。VAO绑定时正在绑定的索引缓冲对象会被保存为VAO的元素缓冲对象。绑定VAO的同时也会自动绑定EBO。


引用和参考

[1] LearnOpenGL link


欢迎在评论区留下自己的问题,我会尽快给出回复。你也可以指出文章的纰漏,或是给出你对文章的其他看法。你也可以在文章页面右侧的在线聊天室与我取得联系。如果你喜欢这篇文章,可以点击文末或者页面左侧的分享按钮分享给其他人,非常感谢!

你可能感兴趣的:(Computer,Graphics)