¶OpenGL(二) 绘制一个三角形
注:本文仅供自己以及OPENGL初学者共同学习进步,并无实际教学意义,不过会对自己学习中遇到的关键点加上个人解释。应该会对初学者有所帮助,如有错误请指正!
来到初学者最容易弃坑的地方了,因为这个地方实在是很多东西难以理解,不过,我们必须尽可能理解多一点,就算是算不上正确的观念,我们也只能强吃下去,等学到后面自己纠正自己,因为网上实在是找不到什么详细的教程来解释我们这些初学者的问题,也许这些在别人眼里都不算问题,只能自己一步一步慢慢摸索咯。
不过这篇文章,我会试着解释很多我之前学习这个地方的时候诸多的疑惑,我相信有很多初学者会跟我抱有相同的问题,所以我会以初学者的角度,去解释所有的东西,并且在基础上加上官方解释,以显得我们不是在学一加一等于二。(还是建议先去看完learnOpenGL中文里面的你好三角形章节,带上自己的理解,再回来看这篇文章)
来梳理一下我们这篇文章要了解的东西:
- 顶点数组对象(Vertex Array Object) VAO]=是什么,有什么作用?
- 顶点缓冲对象(Vertex Buffer Object) VBO 是什么,有什么作用,跟VAO的关系?
- 索引缓冲对象(Element Buffer Object) EBO 是什么,跟VBO有什么区别?
- 编程时的规则是怎样,跟OpenGL特性的关联。
- 重点:使用上面东西时经历的过程。
就从简单的屏幕绘制说起吧,Opengl是一个3D的工作空间,不过我们要绘制的是2D事物,在高中大家都有了解到三维坐标吧(x,y,z),在图形学里,简单来说,x是左右,y是上下,z是前后,其实还有个w,用来区分向量和点。
那么我们要绘制一个三角形,要做的工作是什么呢?
我们可能一开始都会这么想,画个三角形不就是给定三个坐标,然后让这三个坐标连起来不就行了吗。确实,对于“人”来说,就是这么简单,可惜我们要告诉一个只认得1和0的莫得感情的机器这些东西,直接对话吗?不太现实,当然,我们也不用扯的太底层,我只是说我们要完成一个什么样的过程才能让电脑明白我们的代码。
首先是给定坐标,这是必须的,那么坐标用什么表示呢,数组,都能想到吧。
1 | float vertices[] = { |
好的,坐标给出来了,不过这是我们眼中的坐标,因为我们知道,但OpenGL呢,你给它这9个浮点数,它可不能自己识别这9个数字然后自己乖乖画个三角形出来。所以我们要利用OpenGL里面的“工具”。
OK,第一项内容就是数据,看到了我们刚刚定义的三个坐标的数组,这就是我们要交给OpenGL的数据,那么OpenGL可不能直接认下你这个数组,所以要用一个东西,VBO登场了,是OpenGL里面专门存储各种数据的对象,我们要用到它,把这个数组交给VBO是我们的第一步。
那么我们开始创建一个VBO:
1 | unsigned int VBO;//变量名什么的无所谓,你叫 ABC 都行 |
先解释下glGenBuffers(),我相信有不少人仍然对这个函数里面的两个参数抱有疑惑,如果你只看了learnOpenGL的话,可能并没有实际理解这两个参数的意义。
1 | void glGenBuffers(GLsizei n,GLuint * buffers); |
目前不要理解太深了,不要被官方文档给吓到了,你只需知道第一个参数是数量,第二个参数是变量名就行。
好的,说完glGenBuffers(),最让初学者疑惑的东西来了。glBindBuffer()-绑定是什么东西,还有下面那个函数glBufferData()看上去跟我们创建的VBO没半点关系,为什么说是将我们刚刚建立的数组vertices丢进了这个VBO里。请看下面这幅图:
glBindBuffer(GL_ARRAY_BUFFER, VBO);
告诉Opengl,VBO是GL_ARRAY_BUFFER类型的缓冲对象并且将VBO绑定到GL_ARRAY_BUFFER缓冲区.
glBufferData(GL_ARRAY_BUFFER,…,…,…)目标缓冲类型GL_ARRAY_BUFFER,意思就是把数据发送到GL_ARRAY_BUFFER缓冲区上,而刚刚我们已经将VBO与GL_ARRAY_BUFFER缓冲绑定在一起了,所以任何传送给GL_ARRAY_BUFFER缓冲的数据都会传送到我们刚刚绑定的VBO身上,只要我们不用glBindBuffer()去绑定另外的VBO1,VBO2…Balaaa…,就会一直作用到VBO身上。
目前为止,数据传递就已经解决了吧,那么接下来该干什么呢,当然是给OpenGL翻译这堆数据啊,虽然我们已经有了存好数据的VBO,但是你不告诉OpenGL怎么去读取VBO的内容,还是不行的,因为它虽然能拿到这些数据,不过它不知道你是第一列是XYZ,还是第一行是XYZ,所以你得设置一个读取规则,并且让OpenGl知道。
1 | glVertexAttribPointer(0, 3, GL_FLOAT,GL_FALSE, 3 * sizeof(float), (void*)0); //告诉OPENGL如何处理顶点数据 |
先别管第二行,我们直接观察第一个函数glVertexAttribPointer(),先看下面一幅图:
和我们刚刚建立的数组:
1 | float vertices[] = { |
现在来分析glVertexAttribPointer()的参数
- 第一个参数,0,意思就是我们从第零个位置开始读取,因为我们数组中第0个就已经是我们要取的坐标之一了,也就是x1
- 第二个参数,3,也就是告诉我们,每3个数,算作一个顶点,每三个数分别代表xyz
- 第三个参数,GL_FLOAT,也就是说我们数组中的每个数据是浮点型
- 第四个参数,GL_FALSE,定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。(这个可能你得懂一点图形学才知道标准化有什么意思,不过目前就先别深究了,以后会懂的)
- 第五个参数,3 * sizeof(float),我们已经知道了每3个float数据为一个顶点,所以3个数据的长度为 3乘以一个float数据的字节大小。这是告诉opengl一个顶点的数据长度。
- 第六个参数,它表示位置数据在缓冲中起始位置的偏移量(Offset),而且由于它的类型是void,所以要强转一下。因为你数组中的第一个顶点的信息是从0开始获取的,所以这里设为0,假如你数组中第一行是其他信息,从第二行开始才是你的顶点信息,那就设为3,从第4个(0,1,2,3)数据开始读取数据。
下面解释VAO,我相信大部分初学者肯定难以理解VAO的作用在哪,毕竟VAO这名字取得我们根本想不到它的作用是什么,我尝试用最简单的方法去理解它。
就拿我们刚刚建立的VBO缓冲来当例子。
1 | unsigned int VBO;//变量名什么的无所谓,你叫 ABC 都行 |
如果不用VAO,我们用VBO就能画出一个三角形来了(只不过现在版本强制我们需要使用VAO,不然不准我们绘制,所以本菜鸟也不能去实践测试),说到底要绘制三角形,只要数据到位了,数据处理规则到位了,确切的传给显卡了,那么就能绘制出来。
这只是一个三角形,但假如我们要绘制非常非常多的图形,并且有非常多的VBO,各个VBO都有相应的读取规则呢。我们这样想,假设我们有VBO1,VBO2。
每次绘制图像前,我们都要像上面那样输入很多行,我刚把VBO1跟GL_ARRAY_BUFFER绑定,设置了读取规则,然后要我绘制VBO2中的数据,我又让VBO2跟GL_ARRAY_BUFFER绑定设置读取规则,结果现在又要我绘制VBO1,我又要重复输入上面那几行代码。就是这样一个过程:
1 | unsigned int VBO1,VBO2; |
这才两个VBO,我们就要这样不断的设置,是不是特别麻烦。
如果有VAO:
1 | unsigned int VAO1,VAO2,VBO1,VBO2; |
完成上面的操作后:
1 | //此时要使用VBO1 |
是不是感觉瞬间方便很多了。可以吧VAO理解为记忆了一个VBO的操作,并且想用只要绑定相应的VAO就能用了,不用再重新进行一遍VBO的操作。
好的,现在我们利用VAO和VBO画一个三角形~那是不可能的,因为还有个最最重要的东西,着色器,也是GPU的核心,不过我们只需要完成两个最基础的着色器,就能绘制我们想要的东西,分别是顶点着色器,还有片段着色器,因为这两个是必须品咯,其他着色器都可以不要,但这两个是绘制图形的基础,顶点着色器接收我们输入的顶点数据,也就是之前用VBO往GL_ARRAY_BUFFER里面输的东西,片段着色器就是用来给三角形上色的。因为C++又不能直接识别着色器语言,我们只能用opengl编译着色器语言,然后使用它。
首先是设置两个着色器,用字符串来写:
1 | //顶点着色器 |
还记得上面的glEnableVertexAttribArray(0);吗,再看看着色器源码里面的layout (location = 0),这两个0有什么关系呢,先来看一幅图:
VAO里面可不止能存一个VBO中的一种信息,VBO里面除了顶点数据,可能还包括颜色和纹理数据等等,我们刚刚是将顶点数据存在了类似图中的VAO1的attribute pointer 0里面,所以0是我们所需要的信息的位置,glEnableVertexAttribArray(0);是将VBO这系列操作放在0,而layout (location = 0)是告诉顶点着色器传进来的顶点信息是在VAO的0的位置。片段着色器就不多说了,相信learnOpenGL里面已经讲到我们需要理解的地步了,接下来使用着色器:
1 | unsigned int vertexShader; //输出一个图像到屏幕,至少需要顶点着色器与片段着色器 |
之后在渲染循环里使用glUseProgram(shaderProgram);就能使用我们创建的着色器程序绘制图像了。
附上源码:
1 |
|
至此,恭喜你,第一个三角形就完成了。
到了最后,我们了说一下EBO,其实实质上,EBO和VBO是一个东西,不过从功能上还是将两者区分了出来,只不过是换了个名字,VBO存的是数据,EBO存的也是数据,不过是调用数据的索引数据,比如说我们要画一个4边型,但我们绘制的是GL_TRIANGLES,所以我们用两个三角形组成一个四边形,也就是这样:
但是如果我们用VAO直接绘制,其实是有两个点重复绘制了的,也就是左上角和右下角,这两点是重复绘制了的,我们不希望这种重复绘制的事情发生,这时我们就可以用到索引缓冲对象EBO,其实看我不用EBO和用EBO要建立的两个数组,一下就能明白EBO是用来干嘛的了:1 | //不用EBO,VBO数组必须这样设 |
也就是用索引的方式去调用每个每个顶点来绘制。
直接上源码吧:
1 |
|
只是多了点EBO的部分,绘制函数改了一个罢了。
到这里,我相信你已经对VAO,VBO,EBO有初步的了解了,文章非常长,看到这里也非常不容易哈。