Qt中使用OpenGL(五)

在构建三维世界时,我们不可能为所有我们想要画的东西都编写顶点坐标和纹理坐标,所以就引入了模型的概念

模型的基类

一般情况下,构建一个三维世界我们会使用模型这个概念。一个模型可以理解为一个物体,比如一个茶壶、一个人、一棵树,他们都可以是一个模型,我们只需要指定这些模型的位置和旋转角度,乃至大小,就可以构建出简单的三维世界。

在OpenGL 中,想要画出一个东西,至少需要两个东西:顶点和纹理。我们需要在绘制的时候激活不同的纹理,然后绘制模型。

所以一个模型需要以下内容:

  • 顶点信息(位置坐标和纹理坐标)
  • 纹理(可能有多个纹理)
  • 在三维世界中的变换(即模型矩阵,对模型进行缩放、旋转、平移)
  • VAO, VBO, shader

基本模型类架构如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// model.h
#ifndef MODEL_H
#define MODEL_H

#include <QOpenGLWidget>
#include <QOpenGLFunctions_4_5_Core>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLBuffer>
#include <QOpenGLShaderProgram>
#include <QOpenGLTexture>

struct Vertex {
QVector3D pos;
QVector2D texture;
};
const static float VertexFloatCount = 5;

class Model : public QOpenGLFunctions_4_5_Core
{
public:
Model();
~Model();

void setScale(float val)
{
m_scale = val;
}
void setRotate(const QVector3D &rotate)
{
m_rotate = rotate;
}
void setPos(const QVector3D &pos)
{
m_pos = pos;
}

QVector3D getRotate()

{
return m_rotate;
}
void setVertices(const QVector<Vertex> &vertices)
{
m_vertices = vertices;
}
void setTexture(QOpenGLTexture *texture, int index = -1);
void setShaderProgram(QOpenGLShaderProgram *program)
{
m_program = program;
}

QMatrix4x4 model();

virtual void init();
virtual void update();
virtual void paint(const QMatrix4x4 &projection, const QMatrix4x4 &view);
protected:
QVector3D m_pos{ 0, 0, 0 };
QVector3D m_rotate{ 0, 0, 0 };
float m_scale = 1;
QVector<Vertex> m_vertices;
QMap<int, QOpenGLTexture *> m_textures;
QOpenGLVertexArrayObject m_vao;
QOpenGLBuffer m_vbo;
QOpenGLShaderProgram *m_program = nullptr;

};

#endif // MODEL_H

模型类继承 QOpenGLFunctions_4_5_Core,是为了方便使用 OpenGL 的函数。成员变量都为 protected,因为这是作为模型基类,具体实现细节要交给继承的类来做。

可以做的只有如何保存纹理以及生成模型矩阵

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// model.cpp
#include "model.h"

// 必须得写,哪怕是空方法,否则编译不通过
Model::Model()
{
}

// 必须得写,哪怕是空方法,否则编译不通过
Model::~Model()
{
}

void Model::setTexture(QOpenGLTexture *texture, int index)
{
if(index == -1) {
if(m_textures.isEmpty()) {
index = 0;
} else {
index = m_textures.keys().last() + 1;
}
}
m_textures.insert(index, texture);
}

QMatrix4x4 Model::model()
{
QMatrix4x4 _mat;
_mat.setToIdentity();
_mat.translate(m_pos);
_mat.rotate(m_rotate.x(), 1, 0, 0);
_mat.rotate(m_rotate.y(), 0, 1, 0);
_mat.rotate(m_rotate.z(), 0, 0, 1);
_mat.scale(m_scale);
return _mat;
}

// 必须得写,哪怕是空方法,否则编译不通过
void Model::init()
{
}

// 必须得写,哪怕是空方法,否则编译不通过
void Model::update()
{
}

// 必须得写,哪怕是空方法,否则编译不通过
void Model::paint(const QMatrix4x4 &projection, const QMatrix4x4 &view)
{
}

具体如何初始化、如何绘制、如何 update ,目前无法知道,但是:

  1. init 函数要在三维窗口的 initializeGL() 函数中使用,用于创建和填充缓存
  2. paint 函数要在三维窗口的 paintGL() 函数中使用,用于具体绘制
  3. 投影矩阵和视图矩阵要通过外部传入参数
  4. update 函数与模型自身逻辑相关,比如旋转等

创建色子模型

色子模型的类声明较为简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// dice.h
#ifndef DICE_H
#define DICE_H

#include "model.h"

class Dice : public Model
{
public:
Dice();
~Dice();

virtual void init() override;
virtual void update() override;
virtual void paint(const QMatrix4x4 &projection, const QMatrix4x4 &view) override;
private:
// 临时缓存,用于将定点信息转化为顶点缓存中需要的格式
float *m_vertexBuffer = nullptr;
// 记录了临时缓存中保存的定点数量,如果缓存中顶点的数量比较少,就需要重新的初始化缓存,
// 如果缓存中的定点数量比较多,就不需要重新初始化缓存
int m_vertexCount = 0;
};

#endif // DICE_H

m_vertexBuffer 是一个临时缓存,用于将顶点转化为顶点缓存需要的格式

m_vertexCount 记录了临时缓存中保存的顶点数量,如果缓存中的顶点数量比较少,就需要重新初始化缓存,如果缓存中顶点数量较多,那么久不需要重新初始化缓存。

构造函数

在色子的构造函数中,初始化色子的顶点信息,加载纹理和 shader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// Dice()
Dice::Dice()
: Model()
{
setVertices({
// 1
{{-1, 1, 1,}, {0.50, 0.25}}, // 左上
{{-1, -1, 1,}, {0.50, 0.50}}, // 左下
{{ 1, -1, 1,}, {0.75, 0.50}}, // 右下
{{ 1, 1, 1,}, {0.75, 0.25}}, // 右上
// 6
{{ 1, 1, -1,}, {0.00, 0.25}}, // 左上
{{ 1, -1, -1,}, {0.00, 0.50}}, // 左下
{{-1, -1, -1,}, {0.25, 0.50}}, // 右下
{{-1, 1, -1,}, {0.25, 0.25}}, // 右上
// 2
{{ 1, 1, 1,}, {0.75, 0.25}}, // 左上
{{ 1, -1, 1,}, {0.75, 0.50}}, // 左下
{{ 1, -1, -1,}, {1.00, 0.50}}, // 右下
{{ 1, 1, -1,}, {1.00, 0.25}}, // 右上
// 5
{{-1, 1, -1,}, {0.25, 0.25}}, // 左上
{{-1, -1, -1,}, {0.25, 0.50}}, // 左下
{{-1, -1, 1,}, {0.50, 0.50}}, // 右下
{{-1, 1, 1,}, {0.50, 0.25}}, // 右上
// 3
{{-1, 1, -1,}, {0.00, 0.00}}, // 左上
{{-1, 1, 1,}, {0.00, 0.25}}, // 左下
{{ 1, 1, 1,}, {0.25, 0.25}}, // 右下
{{ 1, 1, -1,}, {0.25, 0.00}}, // 右上
// 4
{{ 1, -1, 1,}, {0.00, 0.50}}, // 左上
{{ 1, -1, -1,}, {0.00, 0.75}}, // 左下
{{-1, -1, -1,}, {0.25, 0.75}}, // 右下
{{-1, -1, 1,}, {0.25, 0.50}}, // 右上
});
setTexture(new QOpenGLTexture(QImage("C:/Users/Administrator.DESKTOP-8AHQPST/Pictures/texture/shaizi.png")));
auto _program = new QOpenGLShaderProgram();
_program->addShaderFromSourceCode(QOpenGLShader::Vertex, u8R"(
#version 450 core
in vec3 vPos;
in vec2 vTexture;
out vec2 oTexture;
uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
void main()
{
gl_Position = projection * view * model * vec4(vPos, 1.0);
oTexture = vTexture;
}
)");
_program->addCacheableShaderFromSourceCode(QOpenGLShader::Fragment, u8R"(
#version 450 core
in vec2 oTexture;
uniform sampler2D uTexture;
void main()
{
gl_FragColor = texture(uTexture, oTexture);
}
)");
setShaderProgram(_program);
}

init 函数

在 init 函数中,创建各种缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// init()
void Dice::init()
{
initializeOpenGLFunctions();
if(!m_vao.isCreated())
{
m_vao.create();
}
if(!m_vbo.isCreated())
{
m_vbo.create();
}
if(!m_program->isLinked())
{
m_program->link();
}

if(m_vertexCount < m_vertices.count())
{
if(m_vertexBuffer)
{
delete[] m_vertexBuffer;
}
m_vertexBuffer = new float[m_vertices.count() * VertexFloatCount];
m_vertexCount = m_vertices.count();
int _offset = 0;
for(auto &vertex: m_vertices)
{
m_vertexBuffer[_offset] = vertex.pos.x(); _offset++;
m_vertexBuffer[_offset] = vertex.pos.y(); _offset++;
m_vertexBuffer[_offset] = vertex.pos.z(); _offset++;
m_vertexBuffer[_offset] = vertex.texture.x(); _offset++;
m_vertexBuffer[_offset] = vertex.texture.y(); _offset++;
}
}

m_vao.bind();
m_vbo.bind();
m_vbo.allocate(m_vertexBuffer, sizeof (float)*m_vertices.count() * VertexFloatCount);
m_program->bind();
// 绑定顶点坐标信息 从 0*sizeof(flaot)开始读取 3 个 float, 一个顶点有 VertexFLoatCount 个 float 数据,所以下一个数据要偏移 VertexFloatCount * sizeof(float) 个字节
m_program->setAttributeBuffer("vPos", GL_FLOAT, 0*sizeof (float), 3, VertexFloatCount * sizeof (float));
m_program->enableAttributeArray("vPos");
// 绑定纹理坐标信息 从 3*sizeof(float)开始读取 2 个 float,一个顶点有 VertexFLoatCount 个 float 数据,所以下一个数据要偏移 VertexFloatCount * sizeof(float) 个字节
m_program->setAttributeBuffer("vTexture", GL_FLOAT, 3*sizeof (float),2,VertexFloatCount * sizeof (float));
m_program->enableAttributeArray("vTexture");
m_program->release();

m_vbo.release();
m_vao.release();
}

paint 函数

在 paint 函数中,对色子进行绘制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// paint()
void Dice::paint(const QMatrix4x4 &projection, const QMatrix4x4 &view)
{
for(auto index : m_textures.keys())
{
m_textures[index]->bind(index);
}
m_vao.bind();
m_program->bind();
// 绑定变换矩阵
m_program->setUniformValue("projection", projection);
m_program->setUniformValue("view", view);
m_program->setUniformValue("model", model());
// 绘制
for(int i=0; i<6; i++)
{
glDrawArrays(GL_TRIANGLE_FAN, i*4,4);
}
m_program->release();
m_vao.release();
for(auto texture: m_textures)
{
texture->release();
}
}

使用色子模型

在三维窗口的声明中,我们使用 QVector<Model *> m_models 来保存色子模型(为了通用性,所以使用父类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// OpenGLWidget.h
#ifndef TREEOPENGLWIDGET_H
#define TREEOPENGLWIDGET_H

#include <QOpenGLWidget>
#include <QOpenGLFunctions_4_5_Core>
#include <QTimer>
#include <QTime>
#include <QWheelEvent>
#include "Camera.h"
#include "dice.h"

class TreeOpenGLWidget : public QOpenGLWidget, QOpenGLFunctions_4_5_Core
{
Q_OBJECT
public:
explicit TreeOpenGLWidget(QWidget *parent = nullptr);
~TreeOpenGLWidget();
protected:
virtual void initializeGL();
virtual void resizeGL(int w, int h);
virtual void paintGL();
void wheelEvent(QWheelEvent *event);
void keyPressEvent(QKeyEvent *event);
void mouseMoveEvent(QMouseEvent *event);
void mousePressEvent(QMouseEvent *event);
signals:
public slots:
void on_timeout();
private:
QTimer m_timer;
QTime m_time;
Camera m_camera;
QMatrix4x4 m_projection;

QMatrix4x4 m_view;
QVector<Model *> m_models;
};

#endif // TREEOPENGLWIDGET_H

在 OpenGLWidget 的构造函数中对 camera 及 timer 进行初始化与连接

1
2
3
4
5
6
7
8
TreeOpenGLWidget::TreeOpenGLWidget(QWidget *parent) : QOpenGLWidget(parent)
{
connect(&m_timer, SIGNAL(timeout()), this, SLOT(on_timeout()));
m_timer.start(timeOutmSec);
m_time.start();
m_camera.Position = viewInitPos;
setFocusPolicy(Qt::StrongFocus);
}

在 initializeGL 函数中,我们只需要创建对象,并调用他们的初始化函数,然后推入 m_models 即可

1
2
3
4
5
6
7
8
9
10
11
12
// initailizeGL()
void TreeOpenGLWidget::initializeGL()
{
initializeOpenGLFunctions();
glEnable(GL_DEPTH_TEST);
for (int i = 0; i < 3 ; i++ ) {
auto _dice = new Dice();
_dice->init();
_dice->setPos({0, i * 3.0f, 0});
m_models << _dice;
}
}

在绘制的时候,遍历 m_models,然后调用 paint 函数

1
2
3
4
5
6
7
8
9
10
11
12
// paintGL()
void TreeOpenGLWidget::paintGL()
{
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
m_projection.setToIdentity();
m_view.setToIdentity();
m_projection.perspective(m_camera.Zoom, (float)width() / height(), 0.1, 100);
m_view = m_camera.GetViewMatrix();
for(auto dice : m_models) {
dice->paint(m_projection, m_view);
}
}

为了控制色子旋转,在计时器结束时,修改每个色子模型的 rotate 矩阵

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// on_timeout()
void TreeOpenGLWidget::on_timeout()
{
float _speed = 1;
for(auto dice : m_models) {
float _y = dice->getRotate().y() + _speed;
if(_y >= 360) {
_y -= 360;
}
dice->setRotate({0, _y, 0});
++_speed;
}
update();
}

三维窗口的控制函数按照 Camera类中的即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
void TreeOpenGLWidget::wheelEvent(QWheelEvent *event)
{
m_camera.ProcessMouseScroll(event->angleDelta().y() / 120);
}

void TreeOpenGLWidget::keyPressEvent(QKeyEvent *event)
{
float deltaTime = timeOutmSec / 50.0f;
switch (event->key()) {
case Qt::Key_W:
m_camera.ProcessKeyboard(FORWARD, deltaTime);
break;
case Qt::Key_S:
m_camera.ProcessKeyboard(BACKWARD, deltaTime);
break;
case Qt::Key_A:
m_camera.ProcessKeyboard(LEFT, deltaTime);
break;
case Qt::Key_D:
m_camera.ProcessKeyboard(RIGHT, deltaTime);
break;
case Qt::Key_Q:
m_camera.ProcessKeyboard(UP, deltaTime);
break;
case Qt::Key_E:
m_camera.ProcessKeyboard(DOWN, deltaTime);
break;
case Qt::Key_Space:
m_camera.Position = viewInitPos;
break;
default:
break;
}
}

void TreeOpenGLWidget::mouseMoveEvent(QMouseEvent *event)
{
makeCurrent();
if(event->buttons() & Qt::RightButton) {
auto currentPos = event->pos();
QPoint deltaPos = currentPos - lastPos;
lastPos = currentPos;
m_camera.ProcessMouseMovement(deltaPos.x(), -deltaPos.y());
}
doneCurrent();
}

void TreeOpenGLWidget::mousePressEvent(QMouseEvent *event)
{
makeCurrent();
lastPos = event->pos();
doneCurrent();
}