本文的主要内容是在现代OpenGL+Qt学习笔记之二程序框架的基础上,在OpenGL部件中绘制一个彩色三角形。涉及内容有顶点着色器、片元着色器、着色程序和OpenGL缓存对象等
在程序中,着色器(shader)都表示一小段GLSL程序,它可以说是现代OpenGL区别于传统OpenGL的最主要特性。在现代OpenGL中,通过着色器程序可以实现和代替固定管线中个的大部分函数的功能。现代OpenGL渲染管线中有4大着色器,分别是顶点着色器、几何着色器、细分着色器和片元着色器。其中,顶点着色器和片元着色器是所有现代OpenGL程序必不可少的两个着色器。就连绘制一个简单的点也需要用到。本文绘制的彩色三角形当然更不例外。
顶点着色器(Vertex Shader)的作用是对顶点数据进行处理,实现的功能主要是计算顶点的颜色和位置,并将数据输出到下一个着色器程序(在这里是片元着色器)中。
本文用到的顶点着色器内容如下。
#version 440
in vec3 VertexPosition;
in vec3 VertexColor;
out vec3 Color;
void main()
{
Color = VertexColor;
gl_Position = vec4( VertexPosition, 1.0 );
}
在程序的根目录下新建一个shader文件夹,用来存放所有的着色程序,将这段代码复制到一个文本文件中,并将文件名改为basic.vert,文件放到刚新建的shader文件夹中备用。
限定符in和out指定后面的变量是输出变量还是输入变量。上面的着色器有两个输入变量,分别是vec3类型的顶点位置(VertexPosition)和颜色(VertexColor),还有一个vec3类型的输出变量用来输出顶点的颜色(Color),如上所述,输出变量会被传递到下一个着色器中,作为输入变量,现在可以查看下面的片元着色器的代码,其输入的确是有一个vec3类型的变量的。
main()函数时着色器的入口程序,且没有返回值,这是GLSL的语法!在main()函数中,主要做了两件事,一个是用输入变量初始化输出变量的值:
Color = VertexColor;
然后是设定该顶点的位置,途径是通过初始化一个预定义的vec4类型的变量gl_Position:
gl_Position = vec4( VertexPosition, 1.0 );
这里为什么要将顶点的位置设置为4维呢?以后再说!反正主要是为了方便做变换什么的,数学真神奇!
片元着色器(Fragment Shader)具体干嘛的我还真不太清楚,主要是决定最终的每个要绘制的像素的颜色用的。片元着色器会在决定每一个片元(像素)的颜色时执行一次,一般会是并行的,其方式是通过顶点着色器给的数据,对当前要处理的片元的颜色进行插值。插值的部分暂时不需要我们去实现,这里我们直接接收由顶点着色器输出的变量数据,并将其输出即可。
本文用到的片元着色器内容如下。
#version 440
in vec3 Color;
out vec4 FragColor;
void main()
{
FragColor = vec4( Color, 1.0);
}
可以看到有一个输入变量和一个输出变量FragColor,main()函数中做的唯一一件事情就是讲vec3的Color转化为vec4的FragColor,OpenGL中用到的颜色时RGBA的,其中第4个分量是指定这个颜色的透明度的,这里设置为1.0,就是不透明。我们这里不会开启Blend模式,设置为其它值也没啥用。
将上面的内容也放在一个文本文件中,并将文件命名为basic.frag,将文件放到shader文件夹中。
上面的代码都是在文本文件中写的着色器程序,要使用这些程序,需要将它们加载到主程序中,并对其进行编译和链接。接下来就是在主程序中的操作了。Qt封装的着色器类QOpenGLShader和着色程序类QOpenGLShaderProgram,可是实现在主程序中对着色器进行编译和链接。
为了使用两个着色器,我们先将basic.vert和basic.frag作为资源文件,添加到Qt的项目中,怎么添加项目资源文件,我就不多介绍了。最后添加的两个资源的路径分别是:/shader/basic.vert和:/shader/basic.frag,这两个路径会在接下来用到。
要使用一个着色器,首先需要对着色器进行编译,Qt中封装的编译着色器的类使QOpenGLShader。在openglwidget.cpp中添加头文件:
#include
再在initializeGL()函数的
initializeOpenGLFunctions();
语句之后添加下面的代码。
// vertex shader
QOpenGLShader *vshader = new QOpenGLShader(QOpenGLShader::Vertex, this);
vshader->compileSourceFile("://shader/basic.vert");
// fragment shader
QOpenGLShader *fshader = new QOpenGLShader(QOpenGLShader::Fragment, this);
fshader->compileSourceFile("://shader/basic.frag");
上面的代码分别实现了两个着色器的新建和编译,注意新建着色器时的参数分别是QOpenGLShader::Vertex和QOpenGLShader::Fragment,分别表示新建的着色器时顶点着色器和片元着色器。
链接着色器的目的是使得各着色器的输出变量和输出变量对应。Qt提供了QOpenGLShaderProgram类,用来链接着色器到同一个着色程序中。首先在在openglwidget.h中添加QOpenGLShaderProgram类的前置声明:
class QOpenGLShaderProgram;
并添加私有成员变量
QOpenGLShaderProgram *program;
再在openglwidget.cpp中添加头文件:
#include
再在编译着色器的代码之后添加下列代码:
// shader program
program = new QOpenGLShaderProgram;
program->addShader(vshader);
program->addShader(fshader);
program->link();
program->bind();
以上代码的作用,首先是对着色程序对象program进行实例化,然后通过addShader()函数将两个着色器添加到着色程序对象program中,再调用link()函数对着色程序进行链接。bind()的作用是将该程序绑定到当前的OpenGL上下文中,意思是接下来渲染要使用的着色程序就是program了。一个OpenGL程序可以有多个着色程序,在必要时可以进行切换。
先把要做的事情做完!在openglwidget.h文件中包含下列头文件:
#include
添加一个该类型的私有成员:
QOpenGLBuffer vbo;
最后在paintGL()函数的最后,添加如下代码:
// vertices array
GLfloat positionData[] = {
-0.8f, -0.8f, 0.0f,
0.8f, -0.8f, 0.0f,
0.0f, 0.8f, 0.0f };
GLfloat colorData[] = {
1.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 1.0f };
// vertex buffer object
vbo.create();
vbo.bind();
// vertex positions
vbo.allocate(positionData, 18*sizeof(GLfloat));
GLuint vPosition = program->attributeLocation("VertexPosition");
program->setAttributeBuffer(vPosition, GL_FLOAT, 0, 3, 0);
glEnableVertexAttribArray(vPosition);
// vertex colors
vbo.write(9*sizeof(GLfloat), colorData, 9*sizeof(GLfloat));
GLuint vColor = program->attributeLocation("VertexColor");
program->setAttributeBuffer(vColor, GL_FLOAT, 9*sizeof(GLfloat), 3, 0);
glEnableVertexAttribArray(vColor);
这里首先新建了一个QOpenGLBuffer类型的顶点缓冲区对象vbo。QOpenGLBuffer是Qt提供的一个用来表示缓冲区对象的类,常用的缓冲区对象有顶点缓冲区对象VertexBuffer、索引缓冲区对象IndexBuffer等。默认的就是顶点缓冲区对象QOpenGLBuffer::VertexBuffer,所以后面就不需要做什么更改了,直接让它进行默认的初始化即可。
接下来介绍paintGL()函数中添加的大段代码的作用。先是两个数组变量positionData和colorData,都是有9个float元素的数组。其中第一个数组存储的依次是我们要绘制的三角形的三个顶点的三维坐标;第二个数组存储的依次是要绘制的三角形的3个顶点的RGB颜色。
接下来两行代码,分别是创建顶点缓冲区对象(create())和绑定顶点缓冲区对象到当前的OpenGL上下文中(bind())。创建是在服务器(显卡)上创建的,而绑定和前面的着色程序绑定意思差不多,也就是告诉OpenGL上下文,我现在要使用这个顶点缓冲区对象了,后面进行的任何操作也是在这个缓冲区对象上进行的。
接下来的4行代码。第1行是在服务器上为缓冲区分配空间、并数组positionData中的数据对服务器上的数据进行初始化。注意到,顶点的位置只有9个GLfloat的大小,为什么申请的空间是18个GLfloat大小呢,原因是这里还提前为颜色数据分配了刚好9个GLfloat大小的空间。接下来的2行代码的作用是找到着色程序中的输入变量VertexPosition对应的位置,再将其和缓冲区中的数据进行绑定(刚刚初始化的缓冲区),这里使用的setAttributeBuffer()函数的原型是:
void QOpenGLShaderProgram::setAttributeBuffer(int location, GLenum type, int offset, int tupleSize, int stride = 0)
location指定着色程序中变量的地址索引;type指定缓冲区中变量的类型;offset指定要读取的数据到缓冲区开始位置的偏移值,这里是0,因为顶点的位置是从缓冲区开始进行存储的;tupleSize指定变量的维数;stride指定每个变量(这里是3个数值)到下一个变量之间的跨度值,这里因为是连续存储的,所以跨度是0.这样就能实现缓冲区中的位置数据和着色程序中位置变量的一一对应了。
最后一行调用glEnableVertexAttribArray()是开启顶点属性,这里提供的索引是位置索引,所以开启的顶点属性是位置属性,这样数据才会真的从缓冲区向着色程序进行传递(这里如果不传递会有bug)。
再接下来的4行代码是对颜色数据进行同样过程的操作。不同的是不需要再用allocate()分配新的空间了,而是直接调用write()函数将数据写到缓冲区中,wirte()函数的第一个参数是偏移值,因为前9个GLfloat大小的空间写入了位置信息,所以就要偏移9个GLfloat大小的空间。此外setAttributeBuffer()函数中的偏移值也同样需要设置成9*sizeof(GLfloat)。
至此,数据就全部复制到了缓冲区中,并与着色程序中的输入变量进行了关联。接下来,如何绘制缓冲区中的数据呢?
在paintGL()函数的最后,再添加下列代码:
// draw
glDrawArrays(GL_TRIANGLES, 0, 3 );
这就是缓冲区中的彩色三角形的绘制了。glDrawArrays()是OpenGL API,用来绘制缓冲区中的数据,其第1个参数指定需要绘制的图元的类型;第2个参数制定了图元的第一个点在缓冲区中的索引;第3个参数是要绘制的顶点的数据,要绘制三角形,其数目必然是3的倍数,否则我也不知道会发生什么。。关于绘制,就简单介绍这么多,其实日后绘制更加复杂的物体,主要用到的绘制函数是glDrawElements(),等到那个时候再好好介绍这两个函数吧。
如果过程没有问题,那么运行程序应该会出现下面的运行结果,显示一个彩色三角形。
本笔记涉及的内容已经比较多了,很多东西也是按照我的理解写的,我自己不了解的要么不多写,要么就说不了解,反正暂时不影响这些基础的学习。在日后的学习过程中再慢慢加深理解就行了。如果有更好的理解,或者本文中有明显的错误,欢迎批评指正。
源码地址:http://download.csdn.net/download/chaojiwudixiaofeixia/9972990