Qt中使用OpenGL(二)

前言

本篇从最简单的实现来把在 Qt 中使用 OpenGL 的流程走一遍

简单流程

新建类

首先创建一个自己的窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <QOpenGLWidget>
#include <QOpenGLFunctions_4_5_Core>

class OpenGLWidget : public QOpenGLWidget, QOpenGLFunctions_4_5_Core
{
Q_OBJECT
public:
OpenGLWidget(QWidget *parent);
~OpenGLWidget();
protected:
virtual void initializeGL();
virtual void resizeGL(int w, int h);
virtual void paintGL();
}

protected 中的三个函数继承自 QOpenGLWidget,必须要进行重载。

在 initializeGL() 中做一些基本的初始化

首先要做的就是初始化 OpenGL 函数,OpenGL 本身的 API 只提供了函数定义,所有的实现实际上是操作系统或者其他库的工作,初始化 OpenGL 函数的目的,就是加载这些 OpenGL 的实现。这个操作可以通过 initializeOpenGLFunctions() 这个函数实现。

其次要做的就是设置一些 OpenGL 的特性,比如深度测试。深度测试是指 “近处的物体会遮挡远处的物体” 这种在现实中最为基础的法则。可以通过 glEnable(GL_DEPTH_TEST) 这个函数实现。

在最后,还可以设置一下刷新时的背景颜色,可以通过 glClearColor(0.2f, 0.3f, 0.3f, 1.0f) 来实现,四个参数分别是 R,G,B,A 的值,取值范围 [0, 1]

1
2
3
4
5
6
void OpenGLWidget::initializeGL()
{
initializeOpenGLFunctions();
glEnable(GL_DEPTH_TEST);
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
}

在 OpenGL 中,颜色值的范围,RGBA 每个通道都是从 0 开始,1 结束,如果希望按照 [0, 255] 的范围来设置,可以用 (X / 255.0)

创建缓存

在绘制三维图像时,需要的输入是顶点,即用(x, y, z)表示的三维的点,三个点就能确定一个三角形。在 OpenGL 中,如果想传入顶点,就需要将顶点中的 x, y, z 每个值,一个一个的放到缓存中。

OpenGL 中存在两个概念:VAO 和 VBO

VAO 是 Vertex Array Object,顶点数组对象,VBO 是 Vertex Buffer Object,顶点缓存对象。

很显然,我们需要把顶点放到缓存中,也就是 VBO 中。 VAO 可以帮助我们在绘制多个三维对象时,将各自的绘制状态进行隔离,即每个绘制对象都可以有自己的顶点缓存,shader,以及它的各种状态,VAO 可以把这些状态保存起来,下一次执行的时候,就不需要重复设置了,也就是:一次设置,到处使用。

VAO 对应的 Qt 中的 QOpenGLVertexArrayObject 类, VBO 对应 Qt 中的 QOpenGLBuffer 类。

操作方法:创建两个对象,调用各自的创建函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <QOpenGLWidget>
#include <QOpenGLFunctions_4_5_Core>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLBuffer>

class OpenGLWidget : public QOpenGLWidget, QOpenGLFunctions_4_5_Core
{
Q_OBJECT
public:
OpenGLWidget(QWidget *parent);
~OpenGLWidget();
protected:
virtual void initializeGL();
virtual void resizeGL(int w, int h);
virtual void paintGL();
private:
QOPenGLVertexArrayObject m_vao;
QOpenGLBuffer m_vbo;
}
1
2
3
4
5
6
7
8
9
void OpenGLWidget::initializeGL()
{
initializeOpenGLFunctions();
glEnable(GL_DEPTH_TEST);
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);

m_vao.create();
m_vbo.create();
}

VAO 和 VBO 是相互对应的,如果希望创建第二个 VBO 用于绘制第二个物体,那也要再创建一个 VAO。

shader

如果把顶点数据通过缓存给了 OpenGL,OpenGL 需要通过 shader 来使用这些顶点数据,包括对坐标进行缩放、平移、旋转,给顶点染色等等。

在 Qt 中提供了 QOpenGLShaderProgram 类帮助用户使用 shader。

shader 目前有两个类型,一个类型是 vertex,用于处理顶点; 一个类型是 fragment,用于处理颜色。

1
2
3
m_program = new QOpenGLShaderProgram();
m_program->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/shape.vert");
m_program->addSHaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/shape.frag");

处理顶点的时候需要定义输入的顶点,处理颜色的时候可以什么都不用定义

1
2
3
4
5
6
#version 450 core
in vec3 vPos;
void main()
{
gl_Position = vec4(vPos, 1.0);
}

这是vertex shader,其中 in 表示 vPos 这个变量是输入的变量, 类型为 vec3 即三维向量。 gl_Position 表示最终输出的位置,是内置的,不需要我们定义,它是一个四维向量,即 X,Y,Z,W。

这个 shader 的含义就是将输入的三维向量最后加一个 1.0 的值变成四维向量,然后作为最终的输出位置。

1
2
3
4
5
# version 450 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

1
m_program->link();

使用缓存与 shader

准备好了缓存与 shader,下一步就是使用它们了。

OpenGL 想要画出一个三维对象,就必须要有顶点的输入。OpenGL 通过顶点缓存这个概念让用户可以输入顶点信息。所谓的顶点缓存,简单来说就是一个一维数组。

1
2
3
4
5
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 这个顺序排列。使用 VBO 告诉 OpenGL 如何使用这 9 个值,3 个点。

首先使用 VAO 和 VBO 的 bind()函数告诉 OpenGL 我们要使用缓存了。

1
2
m_vao.bind();
m_vbo.bind();

然后将自己定义的 9 个值,3 个点绑定到 VBO 上,通过 allocate() 函数

1
2
m_vbo.allocate(_vertex, 9 * sizeof(float));
// m_vbo.allocate(_vertex, sizeof(_vertex));

这里和我们需要内存保存数据一样, VBO 也会创建一块和顶点信息相同大小的内存,顶点信息是 9 个 float,这里也就创建 9 个 float 大小的区域。为了方便也可以使用注释掉的方法。

为了让 OpenGL 能够知道缓存中的数据是怎么排列的,我们需要为 shader 绑定一些信息。此时使用 vPos 这个输入变量的名字来告诉 OpenGL,在顶点缓存中,是按照 vPos 这个变量的类型,即 vec3 的标准来保存顶点信息的。

1
2
3
m_program->bind();
m_program->setAttributeBuffer("vPos", GL_FLOAT, 0, 3, 0);
m_program->enableAttributeArray("vPos");

一般的,当顶点缓存只有顶点信息的时候,setAttributeBuffer() 这个函数我们只需要关心前四个参数即可。即 shader 中的输入参数名称,顶点缓存中的数据类型, 顶点缓存中开始的位置,顶点缓存中一个点需要用几个数据表示,第五个参数一般取 0 。

至此,所有的准备工作都完成了。

然后做做一个清理工作,释放 VAO 和 shader。

1
2
m_program->release();
m_vao->release();

开始绘制吧

绘制部分非常简单,几行代码就可以。

1
2
3
4
5
6
7
8
void OpenGLWidget::paintGL()
{
m_vao.bind();
m_program->bind();
glDrawArrays(GL_TRIANGLES, 0, 3);
m_program->release();
m_vao.release();
}

代码分别是启用 VAO,启用 shader,用三个点话一个三角形,释放 shader,释放 VAO。