在Qt中使用OpenGL(四)

前言

在Qt中使用OpenGL(一)
在Qt中使用OpenGL(二)
在Qt中使用OpenGL(三)
在之前的文章中,我们通过一个最简单的例子完成了在Qt中使用OpenGL绘图的全过程,然后又使用了纹理系统成功的显示了一张图片。接下来,我们就可以尝试着用OpenGL做一些它真正应该做的事情,例如绘制一个简单3D物体,然后让这个3D物体旋转起来,就像这样:
在这里插入图片描述我们使用上一个文章中的代码作为基础代码,但是,接下来我们要做的并不是写代码,而是进行一些思考,以及学习一些额外的知识。

我们要怎么画一个色子?

我们都知道,一个色子,也就是正六面体,是由六个正方形组成的。
上一篇文章中我们既然可以画出来一个带纹理的图片。那么,很显然,想要画出来一个色子,我们就需要画6张带纹理的图片,让它们组成一个正六面体。
我们知道4个(顶点坐标+纹理坐标)可以画一个矩形,那么一个正六面体,也就是6个矩形,就需要24个(顶点坐标+纹理坐标)了。
相信大家都知道正六面体是可以展开的吧,下面就是一个色子展开后的样子:
在这里插入图片描述我们就可以用这样图当作纹理。
注意,这张图是400x400分辨率的,色子的每个面是一个100x100的图片,为什么这样设计?下面有400x100的空间可是被浪费掉了。
很简单,只要我们心算一下就知道了:简化坐标。
要是我们使用400x300的大小,那么纵向的坐标就会出现0.333333333这种除不尽的情况,我们就不是非常方便来写坐标了。当然,较真的来说我们只需要写1/3.f和2/3.f就可以了,不过在这里,我们还是用这种400x400正方形的纹理吧。
注意,本次,我们不使用左下角为(0,0)右上角为(1,1)的纹理坐标系统,而是使用和图片相同的左上角为(0,0),右下角为(1,1)的纹理坐标系统,因为本次我们需要人工来处理这些坐标映射,使用和图片相同的坐标系统比较节省脑子。
然后,再让我们思考一下一个色子,它的8个顶点的坐标是什么,然后怎么组合出6个正方形出来。并且正方形顶点需要逆时针排列,然后再给每个顶点一个纹理坐标。
当然,这里就不说思考过程了,只说结果(1在前面,6在后面,2在右侧,5在左侧,3在上面,4在下面):

// 顶点缓存中前三个是顶点坐标, 后两个是纹理坐标, 一个顶点由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,	// 右上
};

绘制六个矩形

之前的绘制逻辑中,我们只需要绘制一个矩形就行了,可这一次,我们需要绘制6个。
有什么区别吗?
有的。
还记得之前说了吗?OpenGL并不关系我们绘制的两个三角形能否组成一个矩形。
所以,OpenGL并不是帮我们绘制矩形,而是帮我们绘制两个三角形
所以,我们必须按照每次两个三角形的方法来绘制6个矩形,也就是循环六次,每次绘制4个点。

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

完成了吗?
让我们修改一下纹理的路径:

m_texture = new QOpenGLTexture(QImage("D:/dice.png"));

注意,因为纹理坐标修改了,所以我们这次加载图像的时候不需要镜像了。
这就是目前我们可以看到的样子:
在这里插入图片描述
没错,如果你只是做到这里,那么你只能看到6个点。
就和你之前加载一张纹理当作图片没什么区别,根本看不出来什么3D。我们辛苦计算的6个矩形只能看到一个,不仅和上一篇文章中一样变形了(并不是一个矩形,而是随着窗口的缩放而变形),并且根本看不出来这是个色子。
那么,我们缺少了什么呢?

3D变换

当我们使用OpenGL的时候,我们需要时刻记住,我们是在一个3D空间中,而我们的显示器只能显示一张图片。
那么,如何通过一张图片,可以让我们意识到我们是在看一个3D空间呢?
那就需要依靠3D空间的一些基础逻辑了。
例如近大远小,例如近处物体会遮挡远处物体等。
那么,要如何做到呢?
简单来说,就是我们可以通过一系列的数学计算,将三维空间的一个物体,映射到一个二维平面,然后当我们观察这个二维平面时,可以有一种我们是在三维空间的某一个位置观察它的错觉
更加贴合现实的理解就是,我们用照相机给它照了一张相片,然后让你看到了这样相片。
这就是为什么我们在3D中会有摄像机这个概念,因为我们显示3D空间的方法,就好比用摄像机拍摄一般。
我最早学习OpenGL的时候,这个空间投影的概念就是被抽象为了摄像机,还有相关的API接口可以使用。可最新版本的OpenGL中,这些概念都被剔除了,我们需要自己,通过数学的方法,来将这个概念给模拟出来。也就是对顶点进行一系列的坐标变换,让它表现出我们在用一个摄像机拍摄的感觉出来。
没错,聪明的你肯定想到了,我们又要使用Shader了。
这一次,我们需要使用Shader中的另一个数据格式了,矩阵。
如果你无法理解也没关系,因为我们要做的事情,非常的简单,定义3个矩阵,然后让顶点依次乘以这些矩阵就行了。
简单来说就是要这样:

#version 330 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;
}

我们定义了3个uniform的变量,projection,view与model,它们都是mat4类型的。
它们分别叫做投影矩阵(projection),视图矩阵(view)和模型矩阵(model)。
这些矩阵有什么用呢?
简单来讲,投影矩阵负责让你看到的画面,符合近大远小的透视规律,并且保证无论你的窗口的宽高比是多少,你看到的画面都不会再变形了。
视图矩阵负责模拟一个摄像机的镜头,让你可以在3D空间中的某个位置去观察另一个位置。
模型矩阵负责让你绘制模型的时候,可以顺便对它进行缩放,旋转,平移的操作。
矩阵乘法是从右到左的,所以我们的顶点会首先进行缩放,旋转,平移,然后会被摄像机拍摄,最终会进行近大远小的透视变换。
在Qt中,这些矩阵都可以通过QMatrix4x4类进行表示,于是我们可以定义三个成员变量:

QMatrix4x4 m_projection;
QMatrix4x4 m_view;
QMatrix4x4 m_model;

投影矩阵

还记得投影矩阵可以干什么吗?
它负责让你看到的画面,符合近大远小的透视规律,并且保证无论你的窗口的宽高比是多少,你看到的画面都不会再变形。
可这在数学上要怎么实现呢?
不用怕,Qt已经提供了相应的函数,你只需要使用即可:

void OpenGLWidget::resizeGL(int w, int h)
{
	m_projection.setToIdentity();
	m_projection.perspective(60, (float)w / h, 0.001, 1000);
}

还记得我们一直没有用上的resizeGL函数嘛?这次我们终于可以使用了。
因为我们需要保证无论你的窗口的宽高比是多少,你看到的画面都不会再变形,所以我们需要具体的知道,你的窗口的大小到底是多少。
setToIndentity函数保证了你接下来的所有矩阵运算都会在一个单位矩阵上进行。
perspective函数就可以构建出一个基于你的窗口大小的近大远小的投影矩阵了。它的第一个参数表示了你要用什么样的视角来进行投影。你可以理解为你看到的画面的视野有多大。
人眼在头部不动的情况下,视野大概为60°。所以我们使用60这个值来模拟人眼看到的透视结果。
注意:这里的角度是指纵向视野的角度,横向视野并不进行限制,你的窗口越宽,看到的内容就会越多,但是靠近边缘的部分,变形也会越严重。
它的第二个参数就是你的窗口的横纵比了,记得转化为float再运算,第三和第四个参数表示距离你的摄像机多近个多远范围内的物体要被显示。超出这个范围的物体将无法被显示。

视图矩阵

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

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

第一个参数表示你的眼睛在哪里,第二个参数表示你的眼睛看向了哪里,第三个参数表示你的头顶朝向是哪里,毕竟你是可以歪着脑袋观察东西的,甚至可以倒立着看东西的,所以需要有一个向量来说明你在观察物体的时候,你的观察角度是什么。
注意,前两个向量是具体的坐标,第三个向量表示的是方向。
于是,我们就要求摄像机在(3,3,3)这个位置观察(0,0,0)这个位置,因为我们的色子的位置就在(0,0,0)并且我们色子的棱长为2,(3,3,3)这个位置应该可以很好的观察到色子。

模型矩阵

模型矩阵用于让你绘制的模型可以缩放,旋转和平移,那么,我们就可以实现让模型旋转的功能了,于是我们可以这么用:

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

rotate函数使得矩阵最终将表示绕某个轴进行一定角度的旋转。注意,这里的角度是具体的角度,如果我们每次都首先调用setToIdentity清零,则旋转的角度是不会累加的。
那么,我们希望这个模型可以一直不停的转下去,于是我们可以设置一个定时器,然后以60帧/秒的频率修改m_angel的值,每帧+1,到了360度就归0。这样,每帧的旋转角度都会递增,于是我们就可以看到一个旋转的模型了。
别忘了我们的类是一个Qt类,自带定时器的。所以我们就可以在构造函数中开启一个定时器,让它的超时时间符合60帧:

OpenGLWidget::OpenGLWidget(QWidget *parent)
	: QOpenGLWidget(parent)
{
	startTimer(1000 / 60);
}

然后我们就可以在定时器事件中来修改旋转角度和矩阵了:

void OpenGLWidget::timerEvent(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就行了)。
方法很简单,调用Shader的对应函数即可:

// 绑定变换矩阵
m_program->setUniformValue("projection", m_projection);
m_program->setUniformValue("view", m_view);
m_program->setUniformValue("model", m_model);

一定要在Shader启用后再调用。
于是,最终的paintGL函数如下:
在这里插入图片描述
至此,一切工作都完成了,我们可以运行一下程序,看看最终结果了。

下一篇:在Qt中使用OpenGL(五)

  • 16
    点赞
  • 59
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值