在Qt中使用OpenGL(八)

  • Post author:
  • Post category:其他




前言


在Qt中使用OpenGL(一)



在Qt中使用OpenGL(二)



在Qt中使用OpenGL(三)



在Qt中使用OpenGL(四)



在Qt中使用OpenGL(五)



在Qt中使用OpenGL(六)



在Qt中使用OpenGL(七)


在之前的文章中,我们从最简单的代码开始,逐步拥有了能够构建一个简单的3D世界的能力。

本篇文章,我们主要是为了之前的代码进行一些……怎么说呢?优化?

然后我们尝试画出一个“球体”出来, 模拟出来一个之前照亮了“宇宙中的三个色子”的光源。



开启多重采样

在这里插入图片描述

相信不用我说,大家都能看出来我们之前绘制的图像有多么的粗糙了吧。

特别是模型边缘的各种锯齿。

虽然我们无法达到最高端游戏中的那种细腻的效果,但是我们也应该至少要尝试着补救一下,方法就是开启多重采样。

在Qt中,多重采样是和窗口紧密联系的,反而和OpenGL没有了多少关系。所以为了开启多重采样,我们需要设置窗口的“格式”。简单来说,就是我们要在3D窗口初始化的时候,通过格式设置多重采样:

auto newFormat = this->format();
newFormat.setSamples(16);
this->setFormat(newFormat);

在3D窗口的构造函数中添加这三句话。我们就可以开启多重采样了:

在这里插入图片描述

这就是开启了16倍多重采样的结果,是不是看起来没那么的锯齿化了?



背面裁剪

OpenGL在绘制了之后,你是可以同时看到面的正面和背面的。

什么叫做正面?默认的,你看到一个面(三角形)的顶点是逆时针排列的那一面就是正面,顺时针排列的那一面就是背面。

但你有没有想过,就像我们画出来了一个色子,它的里面实际上我们是看不到的,也就是说它的背面对于在外面看是毫无意义的。但是OpenGL却会忠实的进行绘制。

为了节省资源,我们可以开启背面裁剪,方法也很简单,就是使用

glEnable

函数,传入适当的参数即可:

glEnable(GL_CULL_FACE);

这样子,我们就可以开启背面裁剪,减少不必要的绘制了。



在Qt的窗口中绘制UI

相信大家玩游戏的时候一定注意到了,很多游戏的UI其实并不是3D的。

那么,我们能不能在绘制3D图像的同时,使用Qt提供的函数,绘制2D的UI呢?

当然可以!只要在paintGL函数中直接使用QPainter函数即可。

不过我们在绘制2D的UI之前得先做一些事情:关闭深度测试以及关闭背面裁剪。

void OpenGLWidget::paintGL()
{
	glEnable(GL_DEPTH_TEST);
	glEnable(GL_CULL_FACE);
	for (auto dice : m_models)
	{
		dice->paint();
	}
	QPainter _painter(this);
	auto _rect = this->rect();
	_painter.setPen(Qt::green);
	_painter.drawLine(_rect.center() + QPoint{ 0, 5 }, _rect.center() + QPoint{ 0, 15 });
	_painter.drawLine(_rect.center() + QPoint{ 0, -5 }, _rect.center() + QPoint{ 0, -15 });
	_painter.drawLine(_rect.center() + QPoint{ 5, 0 }, _rect.center() + QPoint{ 15, 0 });
	_painter.drawLine(_rect.center() + QPoint{ -5, 0 }, _rect.center() + QPoint{ -15, 0 });

	_painter.drawText(QPoint{ 5, 15 }, QString(u8"摄像机位置: (%1, %2, %3)")
		.arg(m_camera.pos().x(), 0, 'f', 3).arg(m_camera.pos().y(), 0, 'f', 3).arg(m_camera.pos().z(), 0, 'f', 3));
	_painter.drawText(QPoint{ 5, 30 }, QString(u8"摄像机角度: (%1, %2, %3)")
		.arg(m_camera.yaw(), 0, 'f', 3).arg(m_camera.pitch(), 0, 'f', 3).arg(m_camera.roll(), 0, 'f', 3));
}

当然,我们可以用上面的代码来先看看如果不这么做会发生什么:

在这里插入图片描述

注意,这里的文字显示还是正常的,可是我们花了大功夫试图画出来的十字架准星呢?

没错,看不到了。

现在,我们在QPainter构建之前加上两句话禁用深度测试和背面裁剪:

glDisable(GL_DEPTH_TEST);
glDisable(GL_CULL_FACE);

在这里插入图片描述

完美!



线框模式

什么叫做线框模式?

就是你画出来的所有模型,都只剩下线框。

在这里插入图片描述

就是这个效果,连你UI上的文字都会变成莫名其妙的样子。

使用它其实主要是为了让你可以清楚的看到你的模型面的组成方法,这在之后我们试图绘制一个球体的时候有一些帮助。

那么,这个模式怎么使用呢?

确切的说,如果你只是用Qt中的OpenGL,可能无法达到这个效果。

因为Qt中的OpenGL,是OpenGLES,它没有实现这个功能。

不信吗?

把开启线框模式的代码加到你的代码里试试看:

	glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

编译一下:

在这里插入图片描述

看到了吗?根本找不到这个函数的实现!

不过不用怕,Qt里面没实现,我们可以用你的操作系统中实现的。

所以,我们只需要添加windows中实现OpenGL的库OpenGL32.lib就行了。

没错,哪怕你是64位程序,也可以用这个lib。

在你的工程中,在连接器的参数中加入这个额外的lib,编译就通过了。

当然,为了在线框模式下我们可以正常显示UI,还需要再绘制UI之前禁用这个模式:

glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);

也就是开启

填充

模式。

在这里插入图片描述

现在就一切正常了。



画一个球

在一个三维世界,一个球应该是最简单的“复杂几何体”了,很显然,想要画出一个求,就必须使用很多个面。

绘制球的方法也不是只有一种,但是在这里我只说一种,我将它命名为地球仪绘制法。(实际上有更加准确的叫法,但是我懒得查)

先看一下成品:

在这里插入图片描述

首先,我们忽略掉倾斜的线,只看水平和垂直的线,有没有什么发现?

没错,和你常见到的地球仪是一样的,通过经纬线可以将地球分割成一个一个的“矩形”。

当然,这么做会在南北极失效:

在这里插入图片描述

毕竟它是个球,所以在南北极的时候,就不再是一个矩形,而是一个圆形的

盖子

了。

那么,现在我们就可以思考一下,要怎么画出这个球了。

还记得我将它命名成什么吗?

地球仪绘制法!

地球仪!

地图!

没错,世界地图!

我们生活在一个球形的行星上,可是地图却是矩形的!(当然有不是矩形的)

而且你可以在世界地图上清晰地看到经纬线!

看到了吗?这就是我们绘制球体的方案。

我们只要按照世界地图的经纬线绘制矩形就行了! 只要把经纬线交点对应到三维空间中的点即可。

那么,一个球体上,经纬线交叉点的坐标怎么计算呢?

不用过多的思考,我们依旧使用3D数学的逻辑来吧。

我们只需要使用一个向量,指向北极,也就是(0,1,0),然后,首先让它沿着x轴旋转,再沿着y轴旋转,x轴旋转10°是什么意思呢?默认我们的向量指向北极,转10°就意味着靠近赤道了10°,显然,此时,我们的向量指向的地方就是就是北纬80°,东经0°!

明白现在该干什么了吗?

没错,开启两个循环,让这个向量在地球上遍历一遍所有的经纬度,它就自然而然的和世界地图上的经纬度交点对上了,也就知道每个交点的三维坐标了!

QVector3D _top{ 0,1,0 };
float _step = 10;
QVector<QVector<QVector3D>> _vertexMatrix;

for (float _yaw = 0; _yaw <= 180; _yaw += _step)
{
	_vertexMatrix << QVector<QVector3D>();
	m_col = 0;
	for (float _pitch = 0; _pitch < 360; _pitch += _step)
	{
		QMatrix4x4 _mat;
		_mat.setToIdentity();
		_mat.rotate(_yaw, 1, 0, 0);
		_mat.rotate(-_pitch, 0, 1, 0);
		auto _p = _top * _mat;
		_vertexMatrix[m_row] << _p;
		++m_col;
	}
	++m_row;
}

差不多就是这个意思。你可以把step设置的小一点,可能最终结果会稍微的精细一写。但是目前我们以10°来遍历整个地球的经纬线交点。


注意:我们在北极和南极的时候也遍历了,虽然最终所有所有的结果都是(0,1,0)和(0,-1,0)但是从和地图对应的角度上来说,这才是正确的对应关系。



注意:pitch旋转的时候使用了负角度,这是因为我们希望遍历结果可以和世界地图的坐标一样的从左到右,从上到下,使用-角度就可以实现这个要求。

此时你应该就意识到了,我们完成了这个步骤之后,也同时天然的就拿到了这个球体和世界地图的对应关系。就好像纹理映射关系一样。

还不赶快取下载一个世界地图过来?

我们绘制了这个球体之后就可以直接加上纹理画出一个地球了啊!

在这里插入图片描述

这是我从

中国的标准地图

服务网中下载然后裁剪的矩形投影的世界地图。

我不知道这里有没有版权问题……姑且算是没有吧……

那么,有了这个基本的映射结果,我们就可以根据这个结果创建球体的顶点了:

for (int y = 0; y < m_row-1; ++y)
{
	for (int x = 0; x < m_col; ++x)
	{
		auto _p0 = _vertexMatrix[y][x];
		auto _p1 = _vertexMatrix[y + 1][x];
		int _nextX = x + 1;
		if (_nextX == m_col)
		{
			_nextX = 0;
		}
		auto _p2 = _vertexMatrix[y + 1][_nextX];
		auto _p3 = _vertexMatrix[y][_nextX];
		m_vertices	<< Vertex{ { _p0.x(), _p0.y(), _p0.z() }, {(float)x / m_col,		(float)y / m_row}		}
					<< Vertex{ { _p1.x(), _p1.y(), _p1.z() }, {(float)x	/ m_col,		(float)(y + 1) / m_row} }
					<< Vertex{ { _p2.x(), _p2.y(), _p2.z() }, {(float)(x + 1) / m_col,	(float)(y + 1) / m_row} }
					<< Vertex{ { _p3.x(), _p3.y(), _p3.z() }, {(float)(x + 1) / m_col,	(float)y / m_row}		};
	}
}

同样的,我们按照矩形来绘制(虽然南北极的时候矩形被压缩成了三角形,贴图会不大正确,但是暂时就先这样吧),所以我们需要按照左上角,左下角,右下角,右上角这个顺序,从对应关系中提取出4个顶点,以及它们的纹理映射。

唯一需要注意的是,由于地球是一个球体,我们水平遍历的时候实际上少遍历了一个点,所以,我们需要额外的计算一下:水平方向上的最后一个点就是第一个点。(当然,你可以在之前的映射时多映射一个点,那么这里就可以不用增加额外的逻辑了)

既然纹理直接可以和地图对应上,那么我们就可以加载纹理了:

auto _texture = new QOpenGLTexture(QImage(":/world-map.jpg"));
_texture->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
_texture->setMagnificationFilter(QOpenGLTexture::Linear);
setTexture(_texture);

对于shader,基本上就是最原始的,我们刚刚可以绘制纹理时的那个shader:

#version 330 core
in vec3 vPos;
in vec2 vTexture;
out vec2 TexCoords;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;

void main()
{
	TexCoords = vTexture;

    gl_Position = projection * view * model * vec4(vPos, 1.0);
}
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;

uniform vec3 lightColor;
uniform sampler2D Texture;

void main()
{
    FragColor = vec4(texture(Texture, TexCoords).rgb * lightColor, 1.0);
} 

唯一的变化,可能就是我们增加了一个灯光颜色。

毕竟实际上刚开始我想画这个球的时候,是为了表示这是一个光源。

光源有光的颜色很合理对吧。

接下来, 初始化缓存什么的没有任何变化,按照5个float一个顶点的格式来初始化与绑定就好了

这里唯一需要注意的就是绘制的时候,需要计算出来我们一共要绘制多少个矩形:

for (int i = 0; i < m_col * (m_row - 1); ++i)
{
	glDrawArrays(GL_TRIANGLE_FAN, _index, 4);
	_index += 4;
}

OK,让我们把这个绘制地球的模型加入到我们的场景中看看效果吧:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

至今为止的代码可以从以下仓库中查看:


https://gitee.com/ninsunclosear/opengltest



版权声明:本文为github_18974657原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。