Qt中使用OpenGL(四)

如何画一个骰子

整理顶点

一个骰子是有六个正方形组成。

7f21f9473db04cdea103984f41fd0fe7

根据骰子纹理资源,不再使用(0,0)为左下角和(1,1)为右上角,而是使用和图片相同的左上角为(0,0),右下角为(1,1)。(因为需要人工处理这些坐标映射,所以使用和图片相同的坐标系统比较节省脑子)

骰子每个面的安排如下:1在前面,6在后面,2在右侧,5在左侧,3在上面,4在下面

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
// 顶点缓存中前三个是 顶点坐标, 后两个是 纹理坐标, 一个顶点由 5 个 float 组成
float _vertex[] = {
// 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, // 右上
};

绘制六个矩形

每次按照两个三角形的方法来绘制六个矩形,循环六次,每次 4 个点。

1
2
3
4
5
// 绘制
for(int i=0; i<6; ++i)
{
glDrawArrays(GL_TRIANGLE_FAN, i*4, 4);
}

加载纹理

1
m_texture = new QOpenGLTexture(QImage("path/to/image.png"));

因为在设计纹理坐标时,和图片坐标保持了一致,所以此时不用镜像。

三维变换

因为模型是三维的,而显示器只能显示一张图片,所以可以通过一系列的数学计算,将三维空间的一个物体,映射到一个二维平面,当我们观察这个二维平面时,可以有一种我们是在三维空间的某一个位置观察它的错觉。

实际在用的时候,只需要定义三个矩阵,然后让顶点依此乘以矩阵就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
#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;
}

这里定义了三个 uniform 变量,分别叫做 投影矩阵,视图矩阵和模型矩阵。

  • 投影矩阵负责让你看到的画面符合近大远小的透视规律,并且保证无论窗口的宽高比是多少,看到的画面都不会变形
  • 视图矩阵负责模拟一个摄像机的镜头,让你可以在三维空间的某一个位置去观察另一个位置
  • 模型矩阵负责让你绘制模型的时候,可以对它进行缩放、平移、旋转的操作

在 Qt 中,这些矩阵可以通过 QMatrix4x4 类进行表示

1
2
3
QMatrix4x4 m_projection;
QMatrix4x4 m_view;
QMatrix4x4 m_model;

投影矩阵

1
2
3
4
5
void OpenGLWdiget::resizeGL(int w, int h)
{
m_projection.setToIdentity();
m_projection.perspective(60, (float) w / h, 0.001, 1000);
}

setToIdentity() 函数保证接下来的所有矩阵运算都会在一个单位矩阵上进行。

perspective() 函数就可以构建出一个基于窗口大小的近大远小的投影矩阵了:

  • 第一个参数表示用什么样的视角来进行投影。可以理解为看到的画面的视野有多大。人眼在头部不动的情况下,视野大概是 60 度。(这里的角度是指纵向视野的角度,不对横向视野进行限制,窗口越宽,看到的内容就会越多,但是靠近边缘的部分变形也会越严重)
  • 第二个参数是窗口的横纵比。
  • 第三、四个参数是离摄像机多近到多远范围内的物体要被显示。超出这个范围顶点物体将无法显示。

视图矩阵

视图矩阵用于模拟摄像机,所以可以用 QMatrix4x4 类的 lookAt 函数来设置视角。

1
2
m_view.setToIdentity();
m_view.lookAt(QVector3D(3,3,3), QVector3D(0,0,0). QVector3D(0,1,0));

lookAt 函数:

  • 第一个参数表示你的眼睛在哪里
  • 第二个参数表示你的眼睛看向了哪里
  • 第三个参数表示你的头顶朝向是哪里

模型矩阵

模型矩阵用于让绘制的模型可以缩放、旋转和平移。

1
2
m_model.setToIdentity();
model.rotate(m_angle, 0, 1, 0);

rotate 函数表示绕某个轴进行一定角度的旋转,这里的角度是指具体的角度,如果每次都首先调用 setToIdentity 清零,则旋转的角度不会累加。

如果希望模型可以一直转下去,可以设置一个定时器,以 60 帧/秒的频率修改 m_angle 的值,每帧 +1,到了 360 度就归零。

Qt 中自带定时器,可以在构造函数中开启一个定时器

1
2
3
4
OpenGLWidget::OpenGLWidget(QWidget *parent) : QOpenGLWidget(parent)
{
startTimer(1000 / 60);
}

然后在定时器事件中修改旋转角度和矩阵

1
2
3
4
5
6
7
8
9
10
void OpenGLWdiget::tinerEvent(QTimerEvent * event)
{
m_angle+=1;
if(m_angle >= 360){
m_angle = 0;
}
m_model.setToIdentity();
m_model.rotate(m_angle, 0, 1, 0);
repaint();
}

最后调用 repaint() 函数告诉 OpenGL 重新绘制

应用矩阵

设置了这些矩阵后,就可以告诉 OpenGL 这些矩阵的存在了。调用 shader 的对应函数就可以

1
2
3
4
// 绑定变换矩阵
m_shaderProgram->setUniformValue("projection", m_projection);
m_shaderProgram->setUniformValue("view", m_view);
m_shaderProgram->setUniformValue("model", m_model);

最终 paintGL 函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void OpenGLWidget::paintGL()
{
m_texture->bind();
m_vao.bind();
m_shaderProgram->bind();
// 绑定变换矩阵
m_shaderProgram->setUniformValue("projection", m_projection);
m_shaderProgram->setUniformValue("view", m_view);
m_shaderProgram->setUniformValue("model", m_model);
// 绘制
for(int i=0; i<6; ++i)
{
glDrawArrays(GL_TRIANGLE_FAN, i*4, 4);
}
m_shaderProgram->release();
m_vao.release();
m_texture->release();
}

展示效果如下

image-20220420135029537