OpenGL是一个跨平台的、用来渲染3D图形的标准API,Qt对OpenGL提供了强大的支持。Qt Widgets模块中的QOpenGLWidget类提供了一个可以渲染OpenGL图形的部件,通过该部件可以轻松地将OpenGL图形整合到Qt应用程序中。
使用OpenGL绘制图像介绍
QOpenGLWidget类是一个用来渲染OpenGL图形的部件,它提供了在Qt应用程序中显示OpenGL图形的功能。这个类使用起来很简单,只需要继承该类,然后像使用其他QWidget部件一样来使用它即可。QOpenGLWidget提供了3个方便的虚函数,可以在子类中重新实现它们来执行典型的OpenGL任务:
>initializeGL():设置OpenGL资源和状态、该函数只在第一次调用resizeGL()或paintGL()前被调用一次;
>resizeGL():设置OpenGL的视口、投影等。每次部件改变大小时都会调用该函数;
>painter():渲染OpenGL场景。每当部件需要更新时都会调用该函数。
从OpenGL 2.0开始引入着色器的概念,除了固定功能的管道以外,增加了一种可编程着色管道,可以通过着色器控制顶点和片段的处理。从OpenGL3.1开始,固定功能的管线被废弃并删除了,于是必须使用着色器来完成工作。着色器是使用OpenGL着色语言(OpenGL Shading Language,GLSL)编写的一个小型函数。绘图时需要至少指定两个着色器:顶点着色器和片段着色器。Qt中QOpenGLShader类用来创建和编译着色器,支持使用OpenGL着色语言GLSL和OpenGL/ES着色语言GLSL/ES编写的着色器。QOpenGLShaderProgram类用来创建并设置着色器程序,可以链接多个着色器,并在OpenGL当前环境中绑定着色器程序。QOpenGLFunction类提供了对OpenGL ES 2.0 API的访问接口,QOpenGLExtraFunctions类提供了对OpenGL ES 3.0和3.1API的访问接口。QAbstractOpenGLFunctions是一个类族的基类,类族中的类涉及了所有的OpenGL版本,并为相应版本OpenGL的所有函数提供了访问接口。
绘制点的简单例子如下:
myopenglwiegt.h
//myopenglwiegt.h
#ifndef MYOPENGLWIEGT_H
#define MYOPENGLWIEGT_H
#include<QOpenGLWidget>
#include<QOpenGLFunctions>
class QOpenGLShaderProgram;
//多继承,自定义的MyopenGLWiegt类同时继承自QOpenGLWidget和QOpenGLFunctions
//这样就可以在类中直接使用QOpenGLFunctions中的OpenGL函数,而不需要创建QOpenGLFuctions对象
//这里声明了一个QOpenGLShaderProgram对象指针,作为着色器程序
class MyopenGLWiegt:public QOpenGLWidget,protected QOpenGLFunctions
{
Q_OBJECT
public:
MyopenGLWiegt(QWidget *parent = 0);
protected:
void initializeGL();
void paintGL();
void resizeGL(int width,int height);
private:
QOpenGLShaderProgram *program;
};
#endif // MYOPENGLWIEGT_H
myopenglwiegt.cpp
#include "myopenglwiegt.h"
#include<QOpenGLShaderProgram>
MyopenGLWiegt::MyopenGLWiegt(QWidget *parent):QOpenGLWidget (parent)
{
}
/*
* 这里首先调用QOpenGLFunctions::initializeOpenGLFunctions()对OpenGL函数进行初始化,这样QOpenGLFunctions中的函数只能在当前环境中使用
* 然后进行了着色器的i相关设置,使用QOpenGLShader创建了一个顶点着色器和一个片段着色器,并使用compileSourceCode()函数为着色器设置了源码并进行了编译
* 下面创建了着色器程序QOpenGLShaderProgram对象,使用addShapder()将前面已经编译好的着色器添加进来,然后调用link()函数将所有加入到程序中的着色器链接到一起
* 最后调用bind()函数将该着色器程序绑定到当前OpenGLhuanjingzhong1
* (为了使程序尽量简单,这里直接在程序中编写了着色器源码,对于较复杂的着色器源码,一般是写在文件中的,可以使用compileSourceFile()进行加载编译。
* 这个程序只是绘制一个白色的点,所以只需要指定一个顶点vec4和渲染颜色vec4,这里的vec4类型是GLSL的4位浮点数向量)
*/
void MyopenGLWiegt::initializeGL()
{
//为当前环境初始化OpenGL函数
initializeOpenGLFunctions();
//创建顶点着色器
QOpenGLShader *vshader = new QOpenGLShader(QOpenGLShader::Vertex,this);
const char *vsrc =
"void main(){ \n"
"gl_Position = vec4(0.0,0.0,1.0,1.0);\n"
"| \n";
vshader->compileSourceCode(vsrc);
//创建片段着色器
QOpenGLShader *fshader = new QOpenGLShader(QOpenGLShader::Fragment,this);
const char *fsrc =
"void main(){ \n"
" gl_FragColor = vec4(1.0,1.0,1.0,1.0);\n"
"} \n";
fshader->compileSourceCode(fsrc);
//创建着色器程序
program = new QOpenGLShaderProgram;
program->addShader(vshader);
program->addShader(fshader);
program->link();
program->bind();
}
/*
* 作为简单示例,这里直接调用glDrawArrays()函数来进行OpenGL图形绘制
* void glDrawArrays(GLenum mode,GLint first,GLsizei count)该函数使用当前绑定的顶点数组来建立几何图形
* 第一个参数mode设置了构建图形的类型,GL_POINTS(点)、GL_LINES(线)、GL_LINE_STRIP(条带线)、GL_LINE_LOOP(循环线)、GL_TRIANGLES(独立三角形)、GL_TRIANGLE_STRIP(三角形条带)、GL_TRIANGLE_FAN(三角形扇面)
* 第2个参数first指定元素起始位置,第3个参数count为元素位置
* 就是用顶点数组中索引为first-first+count-1的元素为顶点来绘制mode指定的图形
*/
void MyopenGLWiegt::paintGL()
{
glDrawArrays(GL_POINTS,0,1);
}
void MyopenGLWiegt::resizeGL(int width, int height)
{
}
main.cpp
#include<QApplication>
#include"myopenglwiegt.h"
int main(int argc,char* argv[])
{
QApplication app(argc,argv);
MyopenGLWiegt w;
// w.resize(400,300);
w.show();
return app.exec();
}
绘制多边形
想要绘制复杂的图形,就需要设置更多的顶点,设置顶点一般使用数组来实现,然后将数组中的顶点数据输入人到顶点着色器中。为了获取更好的性能,一般还会使用缓冲。
使用顶点数组
继续在前面的程序中进行更改,首先将顶点着色器源码更改如下:
const char *vsrc =
"in vec4 vPosition; \n"
"void main(){ \n"
" gl_Position = vPosition; \n"
"} \n";
然后更改paintGL()函数:
/*
* 这里定义了一个顶点数组vertices,一共4行,每行定义一个顶点位置。在前面的例子中已经看到,顶点位置是vec4类型的,应该有4个值,但是这里每行只有两个值,其实vec4的默认值为(0,0,0,1)
* 当仅制定了了X和Y坐标时,其他两个坐标将被自动指定为0和1。这里以原点为中心设置了一个正方形的4个顶点,首先是左上角的顶点,然后沿逆时针方向设置了其他3个顶点,定点顺序可以是顺时针也可以是逆时针
* 逆时针绘制出来的是正面,而顺时针绘制出来的是反面。
* attributeLocation()可以返回变量在着色器程序参数列表中的位置,这里获取了vPosition的位置
* 然后使用glVertexAttribPointer()将vPosition与顶点数组vertices进行关联
* 最后需要使用GlEnableVertexAttribArray()来启动顶点数组,这样就完成了所有设置
* [void glVertexAttribPointer(GLuint index,GLint size,GLenum type,GLboolean normalized,GLsizei stride,const void *pointer)
* 该函数设置着色器中变量索引为index的变量对应的数据值。其中,index参数就是要输入如变量的位置索引;
* size表示每个顶点需要更新的分量数目,例如,这里vertices每行只有2个值,所以size为2;
* type指定了数组中元素的类型,例如,这里vertices是GLfloat类型的,所以这里type为GLfloat
* normalized设置顶点数据在存储前是否需要进行归一化
* strider是数组中每两个元素之间的大小偏移值,一般设置为0即可
* pointer设置顶点数组指针或者缓存内的偏移量,这里使用了顶点数组,所以直接设置为vertices即可]
*
*
*/
void MyopenGLWiegt::paintGL()
{
int w = width();
int h = height();
int side = qMin(w,h);
glViewport((w - side)/2,(h - side)/2,side,side);
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
//顶点位置
GLfloat vertices[]={
-0.8f,0.8f,
-0.8f,-0.8f,
0.8f,-0.8f,
0.8f,0.8f
};
GLuint vPosition = program->attributeLocation("vPosition");
glVertexAttribPointer(vPosition,2,GL_FLOAT,GL_FALSE,0,vertices);
glEnableVertexAttribArray(vPosition);
glDrawArrays(GL_TRIANGLE_FAN,0,4);
}
使用缓冲
前面程序使用的是顶点数组中指定的数据会保存在客户端内存中,在进行glDrawArrays()等绘图调用时,这些数据必须从客户内存复制到图形内存。为了避免每次绘图时都复制这些数据,可以将其缓存到图形内存中。缓存对象在OpenGL服务器中创建,这样当需要顶点、索引、纹理图像等数据时,客户端程序就不需要每次都进行上传。Qt中的QOpenGLBuffer类用来创建并管理OpenGL缓存对象。
继续在前面程序中进行更改,先在myopenglwidget.h文件中添加头文件包含:#include<QOpenGLBuffer>
,然后添加private变量:QOpenGLBuffer vbo;
然后到myopenglwidget.cpp中,在paintGL()函数创建vertices数组后面添加如下代码:
vbo.create();
vbo.bind();
vbo.allocate(vertices,8*sizeof(GLfloat));
首先调用create()函数在OpenGL服务器中创建缓存对象,然后使用bind()函数将与该对象相关联的缓存绑定到当前OpenGL环境,allocate()函数在缓存中为数组分配空间并将缓存初始化为数组的内容。当创建好换,就可以通过为顶点着色器输入数据了。下面将paintGL()函数中调用glVertexAttribPointer()函数替换为:program->setAttributeBuffer(vPosition,GL_FLOAT,0,2,0);
该函数与glVertexAttribPointer()函数类似,其函数原型为void QOpenGLShaderProgram::setAttributeBuffer(int location, GLenum type, int offset, int tupleSize, int stride = 0)
该函数用来为着色器中location位置的变量设置顶点缓存,offset制定了缓存中要使用数据的偏移量。通过调用该函数就可以将vPosition变量与缓存中的顶点数据进行关联。
绘制彩色3D图形
主要是为图形的每个顶点进行着色,然后添加其他的面来形成明显的3D效果。
为图形设置顶点颜色
继续在前面的程序中进行更改,首先更改顶点着色器源码如下:
//这里声明了输入变量vColor和输出变量color,并将vColor获取的颜色数据传递给color
const char *vsrc =
"in vec4 vPosition; \n"
"in vec4 vColor; \n"
"out vec4 color; \n"
"void main(){ \n"
" color = vColor; \n"
" gl_Position = vPosition; \n"
"} \n";
下面改变片段着色器如下:
//这里声明了一个输入变量color,用来和顶点着色器的输出变量color对应,
//而输出变量fColor可以将color输入的颜色数据输出到着色管线中用来为图形着色。
const char *fsrc =
"in vec4 color; \n"
"out vec4 fColor; \n"
"void main(){ \n"
" fColor = color; \n"
"} \n";
下面改变片段着色器如下:下面到paintGL()函数中国,在glDrawArrays()函数调用之前添加如下代码:
//这里创建了一个颜色数组,共4行,分别为4个顶点进行着色。
//为了简便,这里直接在前面创建的缓冲中写入了颜色数组数据,并为vColor变量指定了缓存
GLfloat colors[]={
1.0f,0.0f,0.0f,
0.0f,1.0f,0.0f,
0.0f,0.0f,1.0f,
1.0f,1.0f,1.0f
};
vbo.write(8*sizeof(GLfloat),colors,12*sizeof(GLfloat));
GLuint vColor = program->attributeLocation("vColor");
program->setAttributeBuffer(vColor,GL_FLOAT,8*sizeof(GLfloat),3,0);
glEnableVertexAttribArray(vColor);
write()函数的原型为:void QOpenGLBuffer::write(int offset, const void *data, int count)
该函数会替换掉缓存中已有的内容,参数offset是要替换数据开始位置的偏移值,因为前面已经添加的顶点数组大小为8*sizeof(GLfloat),所以这里需要将这个值作为偏移值。为了不覆盖已有的数据,这里需要对缓存进行扩容,将前面程序中allocate()函数调用更改如下:
//因为顶点数据有8个元素,颜色数组有12个元素,所以这里的大小设置为20*sizeof(GLfloat)
vbo.allocate(vertices,20*sizeof(GLfloat));
实现3D效果
继续在前面程序中进行更改,首先更改顶点数组为:
//该数组每行指定了4个顶点,每个顶点由3个元素组成,因为要设置3D效果,所以每个顶点都指定了Z坐标
GLfloat vertices[2][4][3]=
{
{{-0.8f,0.8f,0.8f},{-0.8f,-0.8f,0.8f},{0.8f,-0.8f,0.8f},{0.8f,0.8f,0.8f}},
{{0.8f,0.8f,0.8f},{0.8f,-0.8f,0.8f},{0.8f,-0.8f,-0.8f},{0.8f,0.8f,-0.8f}}
};
然后更改allocate()调用如下:
//这里顶点数组有24个元素,后面颜色数组对应的也有24个元素,所以缓存大小为48*sizeof(GLfloat)
vbo.allocate(vertices,48*sizeof(GLfloat));
下面更改设置vPosition的setAttributeBuffer()函数:
//因为现在数组中每个顶点由3个元素指定,所以这里第4个参数设置为3
program->setAttributeBuffer(vPosition,GL_FLOAT,0,3,0);
下面更改颜色数组如下:
GLfloat colors[2][4][3]={
{{1.0f,0.0f,0.0f},{0.0f,1.0f,0.0f},{0.0f,0.0f,1.0f},{1.0f,1.0f,1.0f}},
{{1.0f,0.0f,0.0f},{0.0f,1.0f,0.0f},{0.0f,0.0f,1.0f},{1.0f,1.0f,1.0f}}
};
然后更改write()函数调用:
vbo.write(24*sizeof(GLfloat),colors,24*sizeof(GLfloat));
下面更改vColor的setAttributeBuffer()函数:
program->setAttributeBuffer(vColor,GL_FLOAT,24*sizeof(GLfloat),3,0);
最后将绘制函数更改如下:
//这里要绘制两个面,所以用for()函数调用了2次glDrawArrays()函数进行绘制,
//第一次绘制用去了4个顶点,所以第2次调用时设置了起始位置为4
for(int i=0;i<2;i++)
{
glDrawArrays(GL_TRIANGLE_FAN,i*4,4);
}
因为角度问题只能看到前面的面。下面通过使用透视投影矩阵对顶点进行变换来改变显示图形的角度,在调用绘制函数的这两行代码之前添加如下代码:
//QMatrix4x4类可以表示一个3D空间中的4X4变换矩阵,
//perspective()函数用来设置透视投影矩阵,这里设置了视角为45°,纵横比为窗口的纵横比,最近的位置为0.1,最远的位置为100.
//然后使用translate()函数平移X、Y和Z轴,这里将Z轴平移-3,即向屏幕里移动。
//rotate()可以设置旋转角度,4个参数分别用来设置角度和X、Y、Z轴,
//最后使用setUniformValue()函数将矩阵关联到顶点着色器的matrix变量。
QMatrix4x4 matrix;
matrix.perspective(45.0f,(GLfloat)w/(GLfloat)h,0.1f,100.0f);
matrix.translate(0,0,-3);
matrix.rotate(-60,0.1,0); //绕t轴逆时针旋转
program->setUniformValue("matrix",matrix);
最后将顶点着色器源码更改如下:
const char *vsrc =
"in vec4 vPosition; \n"
"in vec4 vColor; \n"
"out vec4 color; \n"
"uniform mat4 matrix; \n"
"void main(){ \n"
" color = vColor; \n"
" gl_Position = matrix * vPosition; \n"
"} \n";
使用纹理贴图
前面的程序生成了正方体的两个面,为了实现更加真实的3D效果,还可以使用图片作为2个面的纹理贴图。Qt的QOpenGLTexture类封装了一个QpenGL纹理对象,可以使用该类来设置纹理。