本文章首发于我的个人博客,希望大家多多支持!
Hi! This is Showhoop Studio!
如果要从代码层面去理解渲染管线的工作,学习使用OpenGL编程可以说是一个不错的选择。这里我将记录下自己的一下学习笔记,以便日后复习和引用。对于刚刚开始学习或者准备入门学习OpenGL的人,推荐去看LearnOpenGL,除了理论知识之外,这个教程会同时手把手教你搭建自己的OpenGL程序!
VAO,Vertex Array Object:顶点数组对象,
VBO,Vertex Buffer Object:顶点缓冲对象,
EBO(或IBO),Element Buffer Object(或Index Buffer Object):索引缓冲对象。
我们先来介绍顶点缓冲对象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是由一系列 顶点属性指针(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
即浮点值。float
组成,下一个位置数据的开始必定在三个float
之后,所以我们指定步长为3 * sizeof(float)
,这样我们就将每个顶点的位置数据分隔开了。void*
,所以我们需要进行奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量。由于数据在数组的开头,所以这里是0(如果你没有理解也没关系,之后会具体介绍这一个参数)。使用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之间的关系:
glVertexAttribPointer
就是设置一个指针。现在,我们来吧重点放到第二个物体的配置上,因为它有两种顶点属性,一种是顶点位置,另一种是纹理坐标。还记得我们之前所讲的glVertexAttribPointer
中的最后一个参数吗?我们将通过这里来进行具体的介绍。我们在配置第二个物体的顶点属性时,调用了两次函数,显然每一次对应一种属性。第一次调用是在配置位置属性:
0
,这是由顶点着色器中的定义layout (location = 0) in vec3 position
决定的,3
,因为顶点位置属性由3个浮点值组成,5 * sizeof(float)
,也就是对应一个顶点的所有的属性的大小,位置属性占3个浮点值,纹理坐标占2个浮点值。(void*)0
,因为位置属性是从一个顶点属性的起始开始算起的(也是就是0),再结合第二个参数,属性指针就知道获取位置属性要从一个顶点的所有属性内,从0开始读取3个浮点值。对于第二次调用,我们的目的是获取纹理坐标:
1
,这是由顶点着色器中的定义layout (location = 1) in vec2 texcoord
决定的(当然你也可以指定其他数字,只要不重复即可),2
,因为纹理坐标属性由2个浮点值组成,(void*)3 * sizeof(float)
,因为纹理坐标属性紧密排列在位置属性之后,位置属性从0
开始占用了3 * sizeof(float)
的大小,那么纹理坐标属性必然是从3 * sizeof(float)
开始读取2个浮点值,一共是5个浮点值,也就是一个顶点所有属性的大小。现在,你应该对最后一个参数有了更加深刻的理解。
要解释索引缓冲对象,我们先来假设一个情景:我们要绘制两个三角形来组成一个矩形(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
欢迎在评论区留下自己的问题,我会尽快给出回复。你也可以指出文章的纰漏,或是给出你对文章的其他看法。你也可以在文章页面右侧的在线聊天室与我取得联系。如果你喜欢这篇文章,可以点击文末或者页面左侧的分享按钮分享给其他人,非常感谢!