在Qt中使用OpenGL(三)

前言

在Qt中使用OpenGL(一)
在Qt中使用OpenGL(二)
在之前的文章中,我们首先了解了在Qt中使用OpenGL的全流程,然后我们使用了一个最简单的例子将这个流程给过了一遍。
那么,在本篇文章中,我们将对“最简单的例子”进行一定的修改,以便达成我们接下来的目标:如果为我们绘制出来的东西,加上“纹理”

什么是纹理

既然我们要研究怎么加上纹理,那么我们就需要首先知道什么是纹理。
首先,我们上一个版本的程序,只是绘制出来了一个“白色的三角形”,事实上,在真实的应用里,这种“绘制白色三角形”的需求是不可能出现的,我们大部分时间遇到的情况应该是“添加一个模型”对吧。
那么,你有没有意识到,当你添加一头牛的模型时,它看起来像是一头牛的原因,除了它的形状是一头牛之外,是不是它身上的各个部位的颜色也像是一头牛呢?
这些颜色是怎么来的呢?难道是用shader给牛身上的每个顶点指定了颜色?就像之前我们给顶点指定了白色?
对,也不对。
是的,这些看起来正确的颜色的确是通过shader进行指定的。但是不对的地方是,它并不是通过给每个顶点设置颜色实现的,而是通过“纹理映射”实现。
简单理解,就是将“一层皮”,套到了3D世界中的牛的身上,牛身上的每个顶点都对应到了这层皮上的一个位置,这样,只要每个顶点对应的位置是正确的,那么我们看到的牛就和现实中的牛差距就不会太大了。
那么,为了可以用最简单的例子来解释纹理,我们不使用牛,而是选择最简化的场景,假设我有一张很漂亮的风景画:
请添加图片描述我想让这个风景画出现在3D世界中,作为画廊中的一幅画。怎么办呢?

绘制矩形与设定纹理映射坐标

当然,在我们想要在3D世界中增加一幅画,首先还有一个问题要解决。很显然,这张图是个矩形,而我们之前做过的唯一的事情只有绘制三角形。
那么怎么绘制一个矩形呢?
聪明的你大概也猜出来了,没错,我们吧矩形沿对角线剪开,不就是两个三角形了?所以我们只要画两个三角形不就行了?
没错,就是这个道理。我们只要画两个三角形就行了。
然后呢,OpenGL很贴心的提供了多种绘制多个三角形的方法,其中有一个方法,使得我们只要“逆时针定义4个顶点”就可以方便的绘制出可以组成矩形的两个三角形了。
怎么理解呢?
在这里插入图片描述
这是默认情况下,OpenGL的屏幕坐标。也就是说,如果我们想要绘制一个占满整个屏幕的矩形,也就是两个三角形,我们需要逆时针,定义4个顶点。
很显然,在这个屏幕坐标中,左上就是(-1,1),左下就是(-1,-1),右下就是(1,-1),右上就是(1,1),那么我们按照这个顺序就可以定义四个顶点了。(z值得话,我们只需要都使用0就行了,简单吧。)当然,你可以任意的调整顶点的顺序,但是顶点必须是逆时针排列顺序。

float _vertex[] = {
//  顶点			
	-1,  1, 0,	// 左上
	-1, -1, 0,	// 左下
	 1, -1, 0,	// 右下
	 1,  1, 0,	// 右上
};

至此,我们画两个三角形的准备工作就完成了。
但是还有一个工作没有完成,那就是纹理映射坐标。
是的,当我们需要考虑纹理的时候,从顶点的时候就需要考虑了。为什么?
还记得之前说法吗?

简单理解,就是将“一层皮”,套到了3D世界中的牛的身上,牛身上的每个顶点都对应到了这层皮上的一个位置

是的,你需要将每一个顶点,都对应到纹理上的一个位置。这个对应方法,就是使用“纹理映射坐标”。
简单来讲,就是用(x,y)这个二维坐标来表示纹理上的一个位置,其中,x与y的值域为[0,1]。
其中,(0,0)表示左下角。(1,1)点表示右上角。当然,你也可以把(0,0)当作左上角,(1,1)当作右下角,就和一般情况下我们编辑图片时那样。具体你要怎么看待这个坐标系统,实际上不重要。重要的是,一定要保证有一个统一的标准。目前,我们的统一标准是(0,0)表示左下角,(1,1)点表示右上角。
了解了这些,我们就可以给顶点附加纹理映射坐标了,方法很简答,只需要在每个顶点坐标后面,附加两个代表纹理映射坐标的float值就行了,就像这样:

	// 顶点缓存中前三个是顶点坐标, 后两个是纹理坐标, 一个顶点由5个float值组成
	float _vertex[] = {
	//  顶点			纹理
		-1,  1, 0,	0, 1, // 左上
		-1, -1, 0,	0, 0, // 左下
		 1, -1, 0,	1, 0, // 右下
		 1,  1, 0,	1, 1, // 右上
	};

注意,当你这么做了,就意味着你的一个顶点有5个float值了,而不是之前的3个。
还记得之前的文章里说过吗?OpenGL根本不知道什么9个值3个顶点的标准,现在明白为什么了嘛?因为这种标准根本就不存在啊!现在,当我们需要“纹理映射”的时候,原先9个值3个顶点就变成了20个值,4个顶点了。每个顶点需要5个值来表示!
那么,我们要如何告诉OpenGL这个新的顶点缓存标准呢?

如何使用包含纹理的顶点缓存

很显然,之前我们使用了setAttributeBuffer()这个函数告诉了OpenGL每个顶点有3个值,那么我们同样需要使用setAttributeBuffer()这个函数告诉OpenGL,现在规则改变了,每个点有5个值,其中前三个是顶点坐标,后两个是纹理坐标。
可是,我们要怎么操作呢?
首先,我们需要做的是,修改Shader。
没错,还记得我们之前的Shader里面,定义了1个vec3类型的输入嘛?
这也就意味着OpenGL只能处理一个vec3数据,也就是3个float类型。可我们一个顶点有5个float类型,怎么办?那就再加一个vec2类型的输入不就好了嘛!

#version 330 core
in vec3 vPos;
in vec2 vTexture;
out vec2 oTexture;
void main()
{
    gl_Position = vec4(vPos, 1.0);
    oTexture = vTexture;
}

于是我们的顶点Shader就变成这个样子了,也就是获取坐标的同时也要获取纹理坐标,然后将纹理坐标发送给其它的Shader。
请注意,我们除了添加了一个输入变量vTexture(in开头),我们同时还定义了一个输出变量oTexture(out开头),这是为什么呢?
因为Shader是一种链式调用的代码,顶点Shader会自动的从顶点缓存中获取数据,(也不是自动,因为我们需要告诉OpenGL如何操作),可FragmentShader并不会,所以我们需要从顶点缓存中获取数据,然后将获取到的数据传给FragmentShader。
那么,到了FragmentShader,又要做什么操作呢?

#version 330 core
in vec2 oTexture;
uniform sampler2D uTexture;
void main()
{
    gl_FragColor = texture(uTexture, oTexture);
} 

如图所示,我们只要定义一个名字和顶点Shader中定义的输出变量名字一致的输入变量,顶点Shader中的输出变量就会自动的传递到FragmentShader中的输入变量中。然后我们就可以运用OpenGL内置的函数texture来进行纹理映射了。没错,texture这个函数的意义就是将纹理指定位置映射到顶点上。
请注意,这里我们有了一个除了in和out之外的另一个指示器,uniform,简单来说它和in一样,也表示了一种输入,但是,和in不一样的是,它的来源可以是任何地方。而in呢?要么是来自其它shader的输出,要么只能来自于顶点缓存的输入。你可以在不修改顶点缓存的情况下,在任何合理的地方输入uniform的值,例如我们可以根据需要,动态的改变纹理。而sampler2D 这个类型就表示二维纹理了。
好了,至此,顶点,纹理坐标,shader我们都修改好了,让我们告诉OpenGL要如何使用顶点缓存吧。
简单来说,就是这样:

	// 绑定顶点坐标信息, 从0 * sizeof(float)字节开始读取3个float, 因为一个顶点有5个float数据, 所以下一个数据需要偏移5 * sizeof(float)个字节
	m_program->setAttributeBuffer("vPos", GL_FLOAT, 0 * sizeof(float), 3, 5 * sizeof(float));
	m_program->enableAttributeArray("vPos");
	// 绑定纹理坐标信息, 从3 * sizeof(float)字节开始读取2个float, 因为一个顶点有5个float数据, 所以下一个数据需要偏移5 * sizeof(float)个字节
	m_program->setAttributeBuffer("vTexture", GL_FLOAT, 3 * sizeof(float), 2, 5 * sizeof(float));
	m_program->enableAttributeArray("vTexture");

我们都知道setAttributeBuffer()函数的第1个参数表示shader中变量的名字,第2个参数表示数据类型,那怎么理解setAttributeBuffer()函数的第3个参数呢?
第3个参数表示在一个顶点中从哪个位置开始,是你需要的数据。
很显然,我们一个顶点由5个float值组成,前三个表示位置,后两个表示纹理坐标,于是对于位置,从第0个位置开始,也就是 0 * sizeof(float)个字节,而纹理坐标从第3个位置开始,也就是 3 * sizeof(float)个字节
setAttributeBuffer()函数的第4个参数是指有几个数据,坐标有3个数据,纹理坐标有2个数据
setAttributeBuffer()函数的第5个参数表示你的顶点缓存,多少数据表示一个顶点。我们的顶点缓存,5个float值表示一个顶点,于是就是5 * sizeof(float))个字节。
至此,顶点与纹理坐标都处理好了,接下来就是绘制了。

绘制带纹理的矩形

聪明的你一定发现了,到现在为止,除了纹理坐标,我们实际上根本没有处理任何纹理上的东西。
比如,我们根本就没有从文件中加载一张图片当作纹理啊!
没错,那么,我们就再修改一下初始化,加载一张图片当作纹理吧。
首先,在Qt中,OpenGL的纹理使用QOpenGLTexture类表示的,于是我们加载一张图片当作纹理就可以这么写:

m_texture = new QOpenGLTexture(QImage("D:/20210928.jpg").mirrored());

只要我们在类的头文件中定义了QOpenGLTexture *m_texture = nullptr;,那么初始化的时候,我们就可以创建纹理了。
有没有发现什么奇怪的事情?没错,我们加载图片的时候,使用mirrored()这个函数,为什么?查一下这个函数就会发现,它默认会将图片沿y轴进行镜像翻转。
还记得介绍纹理的坐标系统的说的话吗?

其中,(0,0)表示左下角。(1,1)点表示右上角。当然,你也可以把(0,0)当作左上角,(1,1)当作右下角,就和一般情况下我们编辑图片时那样。

因为我们用(0,0)表示左下角,所以坐标和一般的图片坐标发生了镜像翻转,所以加载纹理的时候只要也镜像翻转一下,问题就解决了。
好了,纹理加载好了,我们要如何告诉OpenGL有这么一个纹理呢?
很简单,绘制图像之前bind一下,绘制完毕release就行了。
也就是这样:
在这里插入图片描述
在这里,我们绘制的矩形的参数不是之前绘制三角形时的GL_TRIANGLES,0,3,而是GL_TRIANGLE_FAN,0,4。其中,GL_TRIANGLE_FAN就是之前提到的另一种绘制三角形的方法,它允许我们逆时针定义四个顶点,然后绘制出来两个三角形组成一个矩形。(当然,实际上能不能组成一个矩形OpenGL并不关心,能不能组成矩形是用户定义的顶点有关,和OpenGL没关系。甚至它实际上并不是用来绘制矩形的,仅仅是一种可以快速的绘制多个三角形的方法罢了,我们只是利用了它的特性而已)

好了,让我们运行一下程序吧:
在这里插入图片描述
恭喜!你成功了!(虽然图片的比例错了,但是你成功的将纹理中的图片绘制到窗口中了)

此时,聪明的你一定发现了一件奇怪的事情。
我们在Shader中定义了一个uniform sampler2D uTexture;用来表示纹理。
但是自始至终,我们都没有给这个输入变量赋值。那OpenGL又是怎么能将我们加载的纹理传到Shader中呢?
因为OpenGL的纹理系统有一个默认设定,那就是,使用纹理的时候(调用bind函数的时候),会被自动的对应到当前激活的纹理,默认激活的纹理就是纹理0,而我们第一个定义的纹理变量,就会被当作纹理0。这样,我们使用的纹理就自动的和Shader中定义的纹理对应上了,完美。
在这里插入图片描述
注意Qt文档中的说明,除了默认的bind函数,还有一个可以指定将纹理bind到那个索引的重载函数。

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

  • 14
    点赞
  • 63
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
要在QT使用OpenGL渲染一个OBJ模型,可以按照以下步骤进行: 1. 创建一个QT项目,选择OpenGL窗口模板。 2. 在OpenGL窗口初始化OpenGLQT的集成,可以使用以下代码: ```c++ void GLWidget::initializeGL() { initializeOpenGLFunctions(); glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glEnable(GL_DEPTH_TEST); glEnable(GL_LIGHTING); glEnable(GL_LIGHT0); glEnable(GL_COLOR_MATERIAL); } void GLWidget::resizeGL(int w, int h) { glViewport(0, 0, w, h); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(45.0, (double)w / (double)h, 0.01, 100.0); glMatrixMode(GL_MODELVIEW); } void GLWidget::paintGL() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); glTranslatef(0.0f, 0.0f, -5.0f); glRotatef(rotationX, 1.0, 0.0, 0.0); glRotatef(rotationY, 0.0, 1.0, 0.0); drawModel(); } ``` 3. 加载OBJ模型,并将其渲染到OpenGL窗口,可以使用以下代码: ```c++ void GLWidget::loadModel(QString filename) { model = glmReadOBJ(filename.toStdString().c_str()); glmUnitize(model); glmFacetNormals(model); glmVertexNormals(model, 90); } void GLWidget::drawModel() { if (model != NULL) { glmDraw(model, GLM_SMOOTH); } } ``` 4. 在QT的主窗口添加一个QPushButton,点击按钮后调用loadModel()函数加载OBJ模型。 ```c++ void MainWindow::on_btnLoad_clicked() { QString filename = QFileDialog::getOpenFileName(this, tr("Open File"), ".", tr("OBJ Files (*.obj)")); if (!filename.isEmpty()) { ui->glWidget->loadModel(filename); } } ``` 5. 运行程序,点击按钮加载OBJ模型并在OpenGL窗口显示。 以上就是在QT使用OpenGL渲染OBJ模型的基本步骤,需要注意的是,这里使用OpenGL Utility Toolkit(GLUT)和OpenGL Mathematics(GLM)库来帮助加载OBJ模型和进行矩阵变换等操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值