OpenGL学习 (一)
太长时间没有接触过OpenGL了!由于完成SSAO要阅读比较多的OpenGL代码,在此对前期OpenGL的内容稍作回顾!
本节涉及知识:VAO,VBO,EBO等
VAO,VBO,EBO
VAO,VBO
1 | unsigned int VAO; |
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBufferData
是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。它的第一个参数是目标缓冲的类型:顶点缓冲对象(Vertex Buffer Objects, VBO)当前绑定到GL_ARRAY_BUFFER目标上。第二个参数指定传输数据的大小(以字节为单位);用一个简单的sizeof计算出顶点数据大小就行。第三个参数是我们希望发送的实际数据。
第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式
- GL_STATIC_DRAW :数据不会或几乎不会改变。
- GL_DYNAMIC_DRAW:数据会被改变很多。
- GL_STREAM_DRAW :数据每次绘制时都会改变。
如果缓冲中的数据每次渲染调用时都保持原样,那么它使用类型最好是GL_STATIC_DRAW
。如果一个缓冲中的数据将频繁被改变,那么使用的类型就是GL_DYNAMIC_DRAW
或GL_STREAM_DRAW
,这样就能确保显卡把数据放在能够高速写入的内存部分。
EBO
假设我们要绘制一个矩形。我们可以绘制两个三角形来组成一个矩形(OpenGL主要处理三角形)。这会生成下面的顶点的集合:
1 | float vertices[] = { |
可以看到,有几个顶点叠加了。我们指定了右下角和左上角两次!一个矩形只有4个而不是6个顶点,这样就产生50%的额外开销。当我们有包括上千个三角形的模型之后这个问题会更糟糕,这会产生一大堆浪费。更好的解决方案是只储存不同的顶点,并设定绘制这些顶点的顺序。这样子我们只要储存4个顶点就能绘制矩形了,之后只要指定绘制的顺序就行了。如果OpenGL提供这个功能就好了,对吧?
很幸运,索引缓冲对象(Element Buffer Object,EBO,也叫Index Buffer Object,IBO)的工作方式正是这样的。和顶点缓冲对象一样,EBO也是一个缓冲,它专门储存索引,OpenGL调用这些顶点的索引来决定该绘制哪个顶点。所谓的索引绘制(Indexed Drawing)正是我们问题的解决方案。首先,我们先要定义(不重复的)顶点,和绘制出矩形所需的索引:
1 | float vertices[] = { |
数据组织好之后,来使用它们吧!
1 | //创建EBO |
对于VAO,VBO,EBO的理解
OpenGL是一个状态机,OpenGL在任何时间都只会有一个状态是处于运行中的,我们把这个运行中的状态称为context,如上图,虚线代表着这个上下文。context只会认得当下正在操作的VAO,同时context也只能操作一个VBO。因此要对VAO和VBO进行操作要分别bind它们,而对于其他的VAO和VBO当前context是无法操作的。
bind VAO是为了选择一个VAO到context中来(显存中可能存在多个VAO),然后bind VBO选择一个VBO为当前的VAO服务(通过一个“界面”Array-Buffer)。VBO中存储着从CPU传递过来的数据,VAO帮助我们理清楚VBO中这些数据的含义,VAO中有着大概0~15的栏位,每个栏位可以代表着相关意义比如位置,法线,纹理贴图等等。另外假如要操作别的VBO那么就直接把别的VBO bind过来即可,未来VAO需要哪些数据的时候就去哪个VBO找。
如果要使用索引功能,这个功能是走的另外一个“界面”Element-Buffer。传进来的索引数据不会占有之前的0~15的属性栏位,它们会在一个固定的地方。
glDrawElements函数从当前绑定到GL_ELEMENT_ARRAY_BUFFER目标的EBO中获取索引。这意味着我们必须在每次要用索引渲染一个物体时绑定相应的EBO,这还是有点麻烦。
当然context除了可以使用这些之外,还可以设置一些功能,如glEnabel打开的剔除功能等等。
1 | //背面剔除 |
数据的准备与传送
vertex Shader之前准备数据的过程是: 读入.obj文件,用读取文件的相应方法把所有的数据读入到一个数组里面,此后经历CPU和GPU之前狭长的传送带到达GPU,把之前的数据存放在VBO中,但是VBO中可能存储了各种各样的数据,如何去理解这些数据代表什么意思呢?此时就需要用到VAO,VAO上有很多的栏位,VAO从VBO中挖出各种各样的数据到各个栏位中。(比如第0个栏位存放顶点数据,第1个存放法线数据。。。)
shader创建及编译
1 | /*compile shader*/ |
glShaderSource
函数把要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量,上面代码中只有一个。第三个参数是顶点着色器真正的源码,第四个参数我们先设置为NULL。
shader链接
1 | /*link shader*/ |
着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。
经过shader的编写,编译,链接,debug之后,我们可以调用glUseProgram函数,用刚创建的程序对象作为它的参数,以激活这个程序对象:
1 | glUseProgram(ID); |
在glUseProgram函数调用之后,每个着色器调用和渲染调用都会使用这个程序对象(也就是之前写的着色器)了。
链接顶点属性(Linking Vertex Attributes)
仅仅经过以上的这些设置还不够,OpenGL还不知道它该如何解释内存中的顶点数据,以及它该如何将顶点数据链接到顶点着色器的属性上。我们需要告诉OpenGL怎么做。
顶点着色器允许我们指定任何以顶点属性(vertex attributes)为形式的输入。这使其具有很强的灵活性的同时,它还意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。所以,我们必须在渲染前指定OpenGL该如何解释顶点数据—使用glVertexAttribPointer
函数告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)
1 | glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); |
以上面代码块作为列子对glVertexAttribPointer
函数参数稍作讲解。
- a)第一个参数指定我们要配置的顶点属性。即我们通过指定顶点属性位置值(0~15号),来指定我们要把数据解析到哪一个顶点属性中。b)该项和vertex shader的关系通过一个例子来说明:在vertex shader中我们可能见过这样的一句话
layout(location = 0) in vec3 aPos;
,其含义就是aPos
将从位置0把数据取出来然后使用。- 第二个参数指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。
- 第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)。
- 下个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为
GL_TRUE
,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。GL_FALSE
则表示你不需要标准化。- 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个float之后,我们把步长设置为
3 * sizeof(float)
。- 最后一个参数的类型是
void*
,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。void*
是一个什么样的类型呢?void*
能包容地接受各种类型的指针。也就是说,如果你期望接口能够接受任何类型的参数,你可以使用void*
类型。
上述代码块描述的是这样一份顶点缓冲数据:
- 位置数据被储存为32位(4字节)浮点值。
- 每个位置包含3个这样的值。
- 在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列(Tightly Packed)。
- 数据中第一个值在缓冲开始的位置。
当然这只是一种比较简单的情况,以后可能会把uv坐标,法线坐标,位置坐标等等统统放在一个顶点缓冲中,那个时候解析起来可能就比较复杂了。
使用VAO绘制物体的流程
1 | // ..:: 初始化代码 :: .. |
就这么多了!前面做的一切都是等待这一刻,一个储存了我们顶点属性配置和应使用的VBO的顶点数组对象。一般当你打算绘制多个物体时,你首先要生成/配置所有的VAO(和必须的VBO及属性指针),然后储存它们供后面使用。当我们打算绘制物体的时候就拿出相应的VAO,绑定它,绘制完物体后,再解绑VAO。
1 | glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);//这句话可以不写 |
使用glDrawElements时,我们会使用当前绑定的索引缓冲对象中的索引进行绘制。第一个参数指定了我们绘制的模式,这个和glDrawArrays的一样。第二个参数是我们打算绘制顶点的个数,这里填6,也就是说我们一共需要绘制6个顶点。第三个参数是索引的类型,这里是GL_UNSIGNED_INT。最后一个参数里我们可以指定EBO中的偏移量(或者传递一个索引数组,但是这是当你不在使用索引缓冲对象的时候),但是我们会在这里填写0。
glDrawElements函数从当前绑定到GL_ELEMENT_ARRAY_BUFFER目标的EBO中获取索引。这意味着我们必须在每次要用索引渲染一个物体时绑定相应的EBO,这还是有点麻烦。不过顶点数组对象同样可以保存索引缓冲对象的绑定状态。VAO绑定时正在绑定的索引缓冲对象会被保存为VAO的元素缓冲对象。绑定VAO的同时也会自动绑定EBO,因此glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO)
可以不写。
当目标是 GL_ELEMENT_ARRAY_BUFFER 的时候,VAO会储存glBindBuffer的
函数调用。这也意味着它也会储存解绑调用,所以确保你没有在解绑VAO之前解绑索引数组缓冲,否则它就没有这个EBO配置了。准确来说不要在VAO解绑之前解绑了EBO,否则绘制会出错。
参考资料
LearnOpenGL CN