前言

在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(四)

在Qt中使用OpenGL(三)相关推荐

  1. QT中使用OpenGL绘制图形

    Qt Creator中的3D绘图及动画教程(参照NeHe) 刚刚学习了Qt Creator,发现Qt提供了QtOpenGL模块,对OpenGL做了不错的封装,这使得我们可以很轻松地在Qt程序中使用Op ...

  2. 在Qt中使用OpenGL(二)

    前言 在Qt中使用OpenGL(一) 在上一篇文章中,我们结合了一个实际的例子了解了在Qt中使用OpenGL的全部过程.但是肯定对于初次接触的人来说哪怕知道了整个过程,依旧是两眼一抹黑的搞不懂到底要怎 ...

  3. 在Qt中使用OpenGL(四)

    前言 在Qt中使用OpenGL(一) 在Qt中使用OpenGL(二) 在Qt中使用OpenGL(三) 在之前的文章中,我们通过一个最简单的例子完成了在Qt中使用OpenGL绘图的全过程,然后又使用了纹 ...

  4. Qt中的OpenGL

    Qt还是本人可移植GUI程序开发的首选,不过Qt开发普通的应用程序是行,但是据说效率太低,以至于像某些人说的那种刷新看得到一条条横线?这点我比较纳闷,就我使用的感觉,虽然Qt不以效率著称,但是事实上有 ...

  5. Qt中使用OpenGL进行绘图

    Qt Creator中的3D绘图及动画教程(参照NeHe) 刚刚学习了Qt Creator,发现Qt提供了QtOpenGL模块,对OpenGL做了不错的封装,这使得我们可以很轻松地在Qt程序中使用Op ...

  6. qt 中 使用 opengl 上下文 (context) 相关的注意事项

    qt 中 使用 opengl 相关的注意事项 本人移植了一个glut到qt的项目,前期没有注意相关的上下文的使用,导致相关的显示混乱. 解决方案 makeCurrent();在每一个类函数中加上这一句 ...

  7. Qt中使用OpenGL渲染视频

    Qt5.4之后,OpenGL在Qt中可以通过QOpenGLWidget和QOpenGLFunctions来实现,以下Demo(只展示OpenGL相关部分)解码出AVFrame后对其进行渲染. //顶点 ...

  8. 在qt中使用opengl绘制图形动画

    可以使用Qt OpenGL模块的功能实现图形的绘制,实现3d动画效果,以下例子介绍如何使用Qt OpenGL相关功能. 1. 2D图像绘制:该示例使用QPainter和QGLWidget展示一个动态的 ...

  9. qt中opengl窗口的创建

    该笔记借鉴自 : "懂deeee珍惜"的 现代OpenGL+Qt学习笔记之二:程序框架 "爱种鱼的猫"的 QT中使用OpenGL(0)--创建一个窗口 引用引自 ...

最新文章

  1. 链表题目总结(第一篇)
  2. java mvc httpget怎么使用_springMVC正确使用GET POST PUT和DELETE方法,如何传递参数
  3. java getSource()和 getActionCommand()
  4. android中在java代码中设置Button按钮的背景颜色
  5. AR引擎vuforia源码分析、中文注释(2)用手势控制来与模型简单交互
  6. 最近碰到了一个病毒木马:virus.win32.ramnit.B
  7. 硬盘接口的分类和硬盘的分类
  8. 李宏毅机器学习——循环神经网络(一)
  9. 分布式开源调度框架TBSchedule原理与应用
  10. wincc mysql_Wincc操作数据库SQLSERVER
  11. 1982年图灵奖--斯蒂芬·库克简介
  12. 苏超 计算机系 南京大学,Ni-Ti基合金薄膜相变行为及其力学特性研究
  13. 一日精通python编程_爱上Python 一日精通Python编程 [Learn Python in One Day and Learn it Well ]...
  14. 小米air2se耳机只有一边有声音怎么办_小米推出与Air2系列的第三款Air2 SE拼性价比意义何在?...
  15. StormMedia: 一个关于暴风影音的文件夹
  16. 每日力扣009——575. 分糖果(OnO1)
  17. 腾讯海外游戏直播Android开发面经
  18. three.js 设置雾化效果(Fog)
  19. pcie总线与cpci总线_一种基于CPCI与CPCIE总线的多功能背板_2010205852433_说明书_专利查询_专利网_钻瓜专利网...
  20. 【基金量化研究系列】基金绩效归因模型(三)——基于CAPM、T-M、H-M、C-L模型的基金绩效归因研究

热门文章

  1. SpringBoot实现微信扫码登录功能让网站支持使用微信登录demo
  2. LeetCode1619删除某些元素后的数组均值(java)
  3. dw建站404问题,dw 404
  4. 学习笔记14--环境感知传感器技术之毫米波雷达
  5. Android:高德定位及搜索周边地址
  6. Rust - Pin | Unpin | PhantomPinned
  7. cocos2dx ipv6处理
  8. STC8H开发(十): SPI驱动Nokia5110 LCD(PCD8544)
  9. git push 报错 error: failed to push some refs to ‘git@xxx/xx.git‘
  10. 巧萌易携之ROS2Go的不完全教研攻略