本文主要解决一个问题:
如何渲染一个三角形?
本章中,会有大量的新名词和解释,大量的函数出现,建议找个安静的地方慢慢啃这块骨头。
首先,先从直觉上来想想要渲染一个三角形我们需要做些什么?大概需要这三个步骤:
- 1、定义三个顶点。
- 2、将三个顶点的边两两相连。
- 3、将内部的区域涂成一种或几种颜色。
我们就从这几个方面来画出我们的三角形。
一、顶点
在OpenGL中,所有的顶点都是三维空间内的顶点,不过这不是问题,我们可以把深度定义为0来保证其在一个平面上。于是,我们定义三个点:(-0.5, 0.5, 0.0),(0.5, -0.5, 0.0),(0.0, 0.5, 0.0),由于在OpenGL中所有的顶点都会被规范化(将顶点的位置变换到-1到1之间),我们就直接定义规范化后的点,方便处理。我们的代码就像这个样子(编程是一种手写编码的过程,不是Ctrl+C、Ctrl+V的过程,手写一遍代码还是非常必要的):
我们有了顶点数组,如何让OpenGL知道呢?这里有个新东西,叫顶点缓存对象(Vertex Buffer Object,简称VBO),表示存储在GPU显存中的大量顶点数据。我们可以通过这个对象,一次性向GPU发送大量的数据,而不是一次次地从CPU中发送数据到GPU,这是个很慢的过程。
1、使用VBO
唯一ID:OpenGL中有太多的东西,我们需要一个唯一的ID,就像身份证一样来标识出哪个是哪个。这个ID不能我们自己来定,只能告诉OpenGL说,我需要一个唯一ID,你给我一个吧。然后,OpenGL就会给你一个没用过的唯一ID,这个过程是由glGenBuffers来实现的。
指明缓存对象类型:OpenGL中有很多缓存对象,虽然它给了我们一个ID,但不知道这个ID是用来表示什么缓存对象的。我们要明确告诉它,这是一个顶点缓存对象。绑定的作用,是将原来有的东西替换成我们新的东西,这样,接下来对GL_ARRAY_BUFFER的操作都是对我们新东西的操作了。
将顶点数据传到VBO:
- 参数1:我们的顶点数据需要拷贝到的地方。(之前我们绑定的VBO)
- 参数2:数组的大小
- 参数3:数组的地址
- 参数4:指定显卡要采用什么方式来管理我们的数据,GL_STATIC_DRAW表示这些数据不会经常改变。
2、顶点着色器
从OpenGL 3.3开始,OpenGL的渲染方式就从固定功能管线转向了可编程功能管线。而可编程的意思,就是可以通过许许多多的着色器来实现多种多样的效果,这些效果要比固定的效果好的多。顶点着色器就是这些许许多多的着色器中的一种。
先放出一段代码(GLSL语言编写,语法与C极为类似):
- 第一行:指明了使用OpenGL的版本以及运行模式(版本号3.3,核心模式)
- 第二行:指明了需要从上一个步骤中获取一个vec3类型的位置数据,数据位置在输入数据的0偏移位置(类似于输入了一块数据,我们要的数据在头部)
- 第七行(main函数内部):将顶点的位置直接赋值成输入的位置,gl_Position是一个内置的变量,用来表示顶点位置的。
2.1编译着色器
代码自然是要编译链接之后才能执行的,这里我们先说编译,链接的部分等讲完片元着色器再一起说,先不用纠结。
首先,创建一个着色器对象,返回值是这个对象的唯一ID
然后,我们将源码附加到着色器对象上并且编译这个着色器对象
3、片元着色器
同样,先放出代码
类似的,我们输出一个颜色供片元使用,这个颜色是橙色。一个片元,包含了OpenGL用来渲染一个像素的所有信息。
3.1编译着色器
我们同样需要创建一个着色器对象,然后将源码附加到着色器对象上,然后编译。代码如下:
4、着色器程序对象
着色器程序对象是最终将所有着色器连接起来的对象。把所有的着色器对象和这个着色器程序对象连接起来之后,我们就能使用这个着色器程序对象了。
将着色器都连接到程序对象上时,OpenGL会将上一个阶段的输出连接到下一个阶段的输入上。
我们需要三步来使用着色器程序对象,现在已经很熟悉了。创建、附加、链接。
5、使用着色器程序对象
代码很简单,只有一行
6、清理着色器
将已经附加过的着色器都清除掉,我们现在不需要它们了(过河拆桥???)。
7、指明顶点属性
顶点着色器给了我们极大的便利性,我们可以输入想要输入的任何属性格式。因此,我们也就要告诉OpenGL我们顶点的格式是什么样子的。我们定义的顶点格式如下:
可以看到一些重要信息,我们的起始顶点的偏移为0,每个位置分量占用4字节的空间,一个顶点占用12字节空间。
于是,我们调用glVertexAttribPointer函数来指定顶点格式。
- 参数1:指明我们想要配置的顶点属性。类似编号的东西,之前我们设置了location = 0,就是我们在这里用到的0.
- 参数2:顶点属性的大小。我们用到的顶点是一个vec3的结果,所以大小为3.
- 参数3:数据的类型。我们使用的是float类型
- 参数4:指明数据是否要被规范化。这里我们设置成FALSE,不用规范化,因为我们已经规范化好了。
- 参数5:表示属性的跨度。正如之前我们分析的,我们的跨度是12,就是3倍的float类型。
- 参数6:指明了数据的起始偏移量。这里转成了一个void*指针类型比较奇怪,我们以后再聊。
我们获取的顶点属性是由VBO决定的,而glVertexAttribPointer操作的是当前绑定到GL_ARRAY_BUFFER上的VBO,所以,我们当前操作的就是我们之前生成并绑定的那个VBO。
glEnableVertexAttribArray(0)是用来让顶点属性生效的,参数0就是我们之前配置的那个顶点属性的位置。
二、绘制三角形
OpenGL中并没有将我们上面设想的两步单独弄出来,只需要指明要画的是三角形,而且是实体三角形,这两步就能自动完成了。
所以,当我们做完上面那么多步骤之后,我们只需要调用一行代码就可以了。
到这里,似乎我们要做的事情都已经做完,只要编译运行代码就能看到我们想要的三角形了。然而,事实并不像我们想象的那样。运行的代码还是一片青灰色,根本看不到三角形的影子。加了一个VAO进去之后,就能显示出三角形来了。看看教程,原来这个VAO已经是OpenGL渲染管线中不可缺少的一部分了。所以,我们要郑重其事地来了解一下VAO。
三、VAO
全称是顶点数组对象(Vertex Array Object),作用是来保存对顶点属性的调用。这样,当我们需要这些顶点属性的时候,只需要简单地绑定VAO,不需要再设置一遍顶点属性就可以进行绘制了。(是不是觉得没有必要调用,我不想保存,只想显示。所以笔者才尝试不用VAO看看能不能显示出三角形,结果是,没戏,只能乖乖的用它。)
VAO会保存两种东西:
- 其一、对glEnableVertexAttribArray或者是DisableVertexAttribArray的调用。
- 其二、使用glVertexAttribPointer设置的顶点属性以及与顶点属性相关连的VBO。
生成VAO并绑定的代码如下:
unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
将这段代码添加到生成VBO的代码之后就可以编译运行了。
嗯,不出所料,我们的三角形显示出来了。
最后,源码可以参考这里,强烈建议自己手输一遍代码,不自己写一遍还算是程序员么?
总结
这是一张顶点处理的流程图,我们所做的工作就是处理其中的一些阶段。今天我们处理的是顶点着色和片元着色,之后在学光照和纹理的时候我们依旧是处理这些着色,可见这两个阶段是多么重要。
参考资料:
www.learningopengl.com(推荐学习)