前言
在Qt中使用OpenGL(一)
在上一篇文章中,我们结合了一个实际的例子了解了在Qt中使用OpenGL的全部过程。但是肯定对于初次接触的人来说哪怕知道了整个过程,依旧是两眼一抹黑的搞不懂到底要怎么做。
本文章,就从最简的实现来把在Qt中使用OpenGL的全部过程给走一遍。
新建一个窗口类,用于显示
很显然,既然是Qt程序,那么我们首先要做的就是创建一个QWidget。
对于OpenGL,我们就要使用QOpenGLWidget这个类。
同时,对于OpenGL,我们还需要使用它提供的函数,但是OpenGL的版本比较多,每个版本提供的函数也不大一样,Qt帮助我们提供了很多类,方便我们使用。
怎么使用呢?那就是:只要即继承了这个类,那么在这个类的内部,你就可以使用OpenGL的函数。
看看Qt提供的这些类吧。密密麻麻不是吗?可是我们不需要关心用哪个。
为什么?
因为你现在是一个新手,不需要了解那么多的东西。你只需要知道,我们用一个类就行了:
于是,结合之前的我们要创建一个QOpenGLWidget的需求,我们就可以创建出来一个我们自己的窗口了:
protected:中的三个函数继承自QOpenGLWidget,无论如何你都会用到这三个函数,所以直接重载就行了。
这是一个最简单的窗口,它本身是没有任何功能的。
如果你尝试显示这个Widget,你会看到如下画面:
接下来让我们做一些初始化。
在initializeGL()函数中做一些基本的初始化
首先要做的,是初始化OpenGL函数。
注意,OpenGL本身的API只提供了“函数定义”,所以所有的实现实际上是操作系统或者其它库的工作。
初始化OpenGL函数的目的,就是加载这些OpenGL的实现。
这个操作可以通过以下函数来实现:
initializeOpenGLFunctions();
其次要做的,就是设置一些OpenGL的特性,例如深度测试。
深度测试是指,“近处的物体会遮挡远处的物体”这种在现实中最为基础的法则。
这个操作可以通过以下函数来实现:
glEnable(GL_DEPTH_TEST);
在最后,我们还可以设置一下刷新时的背景颜色是什么。
还记得上一节的那个黑黑的窗口吗?那个颜色就是我们要设置的。
这个操作可以通过以下函数来实现:
glClearColor(0, 0.5, 0.7, 1);
四个参数分别为R,G,B,A,的值,取值范围[0, 1]。
于是,当前的最简单的初始化就是这样:
右侧蓝色的窗口就是我们设置了背景颜色后的结果。
注意,在OpenGL中,颜色值的范围,RGBA每个通道都是从0开始,1结束。如果希望按照[0, 255]的范围来设置,可以用(X / 255.0)进行
创建缓存
众所周知,计算机中进行的一切计算,都需要有输入。
那么,我们绘制3D图像,需要的输入是什么呢?
答案就是顶点。即:用(x,y,z)三个坐标表示的三维的点。
简单来讲,3个点,就可以确认一个三角形。OpenGL的世界中,想要绘制3D图像,你需要的,就是传进去一些顶点。
那么,怎么传入顶点呢?
答案就是将顶点中的x,y,z每个值,一个一个的放到缓存中。
OpenGL中存在两个概念:
一个叫做VAO,一个叫做VBO。
VAO指的是顶点列表对象,VBO指的是顶点缓存对象。
很显然,我们需要将顶点放到缓存,也就是VBO中,但VAO是什么呢?
简单来说,VAO可以帮助我们在绘制多个3D物品时,将各自物品的绘制状态给隔离。即:每个物品都可以有自己的顶点缓存,shader,以及其它的各种各样的状态。VAO会帮你把这些状态保存下来,下一次执行的时候,你就不需要重复的设置这些状态了。简单来说就是:一次设置,到处使用。
VAO对应的Qt中的概念是QOpenGLVertexArrayObject
类,VBO对应的Qt中的概念是QOpenGLBuffer
。
你一定注意到了,VBO并不是什么VertexBufferObject,而就是简简单单的OpenGLBuffer,因为对于OpenGL,顶点缓存和其它的缓存没有什么区别……它都是缓存。
那么,知道了这些之后我们要怎么操作呢?很简单,创建两个对象,然后调用它们的创建函数即可。
当然,不要忘记引用对应的头文件:
我们将两个对象作为类的成员变量创建了出来,然后在初始化函数中调用了它们的create函数,这样它们就创建好了。
VAO和VBO是互相对应的,你要是希望创建第二个VBO用于绘制第二物体,那也要再创建一个VAO才行。
Shader
最新的OpenGL与之前OpenGL的最大区别,应该就是新版本的OpenGL默认就是动态渲染管线了,于是为了可以控制渲染流程,我们就需要Shader。
对于简单理解就是:
我们如果把顶点数据通过缓存给了OpenGL,那么OpenGL要如何使用这些顶点数据呢?
老版本的OpenGL的使用方法是固定的,我们也无法改变。
但是新版本的OpenGL的使用方法不是固定,并且必须由我们自己来定义。
什么意思?
你给我了三个顶点,那么我要怎么用?
是直接把三个顶点画成一个三角形?还是先来个坐标变换进行缩放,平移,旋转,然后再给每个顶点染个色,最后在画个三角形?这都是用户自己决定的。
在Qt中,提供了QOpenGLShaderProgram
类帮助用户来使用shader。
这里,我们先不要关心shader中的各种语法,而是使用最基础的逻辑:什么都不改变。
即:如果给了我三个顶点,那么就直接用这三个顶点画一个白颜色的三角形。
那么,我们的shader就应该这么写:
如果要解释一下就是:
shader目前有两个类型,一个类型是vertex,一个类型是fragment
一个用于处理顶点,一个用于处理颜色。
处理顶点的时候需要定义输入的顶点。
处理颜色的时候可以什么都不用定义。
shader的语言与C语言大差不差,很容易理解。
#version 330 core
in vec3 vPos;
void main()
{
gl_Position = vec4(vPos, 1.0);
}
其中in表示vPos这个变量是输入的变量,类型为vec3即三维向量。
gl_Position表示最终输出的位置,内置的,不需要我们定义,它是一个四维向量,即X,Y,Z,W。
于是很明显,这个shader的含义就是将输入的三维向量最后加载一个1.0的值变成四维向量,然后作为最终的输出位置,即什么都不变。
颜色那边也同样好理解:
#version 330 core
void main()
{
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
gl_FragColor 是表示最终输出的颜色,内置的,不需要我们定义,它是一个四维向量,即R,G,B,A。
于是很明显,这个shader的含义就是最终输出的颜色为(1.0,1.0,1.0,1.0),即白色。
对于shader目前需要了解的就这点了。
最终,我们需要连接一下这个shader,即:
m_program->link();
至此,我们的最简单的shader就准备好了。
使用缓存与Shader
到目前为止,我们的所有操作都是准备。那么,当我们准备好了缓存和shader,下一步就是使用它们了。
我们都知道,OpenGL想要画出一个3D物体,就必须有顶点的输入。OpenGL通过顶点缓存这个概念让用户可以输入顶点信息。
而所谓的顶点缓存,简单点来说,就是一个很简单的,一维数组。例如:
float _vertex[] = {
0.0, 0.5, 0.0,
-0.5, -0.5, 0.0,
0.5, -0.5, 0.0,
};
这个数组储存了9个float值,我们可以将其看作三个点,按照x,y,z,x,y,z,x,y,z这个顺序排列。
那么,我们怎么将这9个值,3个点告诉OpenGL呢?即怎么使用VBO呢?
VBO和VAO是匹配出现的,在我们定义好这个三个点后,我们就可以使用VAO和VBO的bind()
函数告诉OpenGL我们要用缓存了,即:
m_vao.bind();
m_vbo.bind();
然后,我们将我们自己定义的9个值,3个点绑定到vbo上,即通过VBO的allocate()
函数:
m_vbo.allocate(_vertex, 9 * sizeof(float));
// m_vbo.allocate(_vertex, sizeof(_vertex));
这里可以简单的理解一下,就好比我们需要内存保存数据一般,VBO也会创建一块和我们顶点信息相同大小的内存,既然我们的顶点信息是9个float,那这里也就需要使用9个float的大小了。当然,为了方便我们还可以尝试用注释掉的第二种方法。
至此,缓存中已经包含了我们的提供的顶点信息了。
可这就结束了吗?OpenGL就会自动的处理这些顶点信息了吗?
不,OpenGL不会。因为它完全不知道要拿这个缓存中的数据怎么办。
它甚至不知道这里面到底有几个顶点。
不要怀疑,OpenGL真的不知道,万一,你这9个数据,前6个数据代表两个点,后三个数据是垃圾数据没有任何含义怎么办?
是的,这个缓存中所谓的9个数据3个点,是我们自己在大脑中定义的,对于OpenGL来说,它从来没有制定过这么一个标准,也没人告诉它有这么一个标准。
于是,为了让OpenGL能够知道缓存中的数据到底是怎么排列的,我们就需要通过为shader绑定一些信息来告诉OpenGL。
还记得之前我们定义的Shader中,顶点类型的Shader中有一个三维向量的输入吗?它的名字vPos是我们自己定义的,所以我们此时就要使用vPos这个输入变量的名字来告诉OpenGL,我们的顶点缓存中,是按照vPos这个变量的类型,即vec3的标准来保存顶点信息的。
于是我们就可以这么写:
m_program->bind();
m_program->setAttributeBuffer("vPos", GL_FLOAT, 0, 3, 0);
m_program->enableAttributeArray("vPos");
一般的,当顶点缓存只有顶点信息的时候,setAttributeBuffer()
这个函数我们只需要关心前4个参数即可。即shader中的输入参数的名字,顶点缓存中的数据类型,顶点缓存中从第几个点开始才是有用的数据(绝大部分第一个点就是,于是写0即可),顶点缓存中一个点需要用几个数据表示(既要和顶点缓存中的结构匹配,也要和shader中的输入变量的类型匹配,在本例子中为3)。第五个参数一般情况下取0即可。
至此,我们的所有准备工作都完成了。
然后做一个最后的清理工作,释放VAO和Shader:
m_program->release();
m_vao.release();
至此,所有的初始化工作完成。
我们的初始化代码如下图所示:
开始画三角形吧!
既然我们的准备工作完成了,那么接下来就是绘制三角形了吧。
那么,让我们开始吧,直接上最终的代码,因为本例子中,绘图部分非常的简单:
对,你没看错,代码只有5行,也非常好理解。
分别是启用VAO,启用Shader,用三个点画一个三角形,释放Shader,释放VAO。
至此,一个最简单流程就走完了。
你会发现,我们只使用了void initializeGL()
初始化,void paintGL()
绘图,但是我们并没有使用void resizeGL(int w, int h)
。难道它没用吗?
目前来说的确是没用的……因为我们用了最简单的流程啊……
下一篇:在Qt中使用OpenGL(三)