今天给各位分享QtOpenGL入门教程(六)—— 初识帧缓存对象(泛光滤镜)的知识,其中也会对QtOpenGL入门教程(六)—— 初识帧缓存对象(泛光滤镜)进行解释,如果能碰巧解决你现在面临的问题,别忘了关注本站,现在开始吧!
QtOpenGL入门教程(六)—— 初识帧缓存对象(泛光滤镜)的介绍就聊到这里吧,感谢你花时间阅读本站内容,更多关于QtOpenGL入门教程(六)—— 初识帧缓存对象(泛光滤镜)、QtOpenGL入门教程(六)—— 初识帧缓存对象(泛光滤镜)的信息别忘了在本站进行查找喔。
本文导读目录:
1、QtOpenGL入门教程(三)—— 初识OpenGL对象(顶点数组对象和缓存对象)
2、项目地址:https://github.com/Italink/QtOpenGL-Essential-Training/tree/main/Day03
3、QtOpenGL入门教程(六)—— 初识帧缓存对象(泛光滤镜)
上一节中我们使用几行代码,就成功的绘制出了一个矩形。很简单是吧,其实掌握了上一节的内容,你就已经学会了使用GPU绘图,但是别以为这么就完了,如果你只会那么点东西,能做的东西还是屈指可数,接下来我们所要学习的,是将OpenGL的绘图性能开发到极致!这样我们才能使用它制造出更复杂,更炫酷的图形效果! 现代OpenGL渲染使用的是核心渲染模式,应尽量使用顶点数组对象(Vertex Array Object)渲染图形。 原生OpenGL一般会使用以glGen***开头的一系列函数向GPU申请创建OpenGL对象,创建成功之后会返回对象的ID Qt对以上四种对象都有着很好的支持,把这些操作进行了简化,但是Qt中并不是创建好对象同时就在GPU端生产相应的对象,我们仍然需要调用这些对象的create()方法来向GPU申请创建(FBO除外) 下面我们来详细说说这些缓存对象都是用来做什么的。 作用:存储顶点数据的来源和解析格式 在上一节我们的代码中有一句: 除此之外,Qt还有其他两个方法设置顶点数据,分别是 还需要注意的是,VAO存储解析格式,除了需要【解析尺寸】【步长】,往往还要设置【偏移量】,比如下面这组数据: 每“行”的前三个表示顶点位置,后三个表示颜色,如果调用setAttributeArray设置解析格式: 需要注意的是,上面第二行中的vertices+3是将vertices偏移 上面是设置数据来源为CPU端的数组,下面我们设置数值来源为缓存对象(显存) : 这里需要我们指定数据类型【GL_FLOAT】,紧跟着后面有一个参数设置数据的【字节偏移量】,因此就不需要像setArray那样直接加减数据的指针进行偏移。 上面的操作中,我们的数据都是存储在一个原生数组中,这样处理起来会很麻烦,我们其实可以直接存储到QVector(或者std::vector)里面,填写数据指针时,只需要使用QVector::data()就能得到原生数组指针,而数组的字节大小一般可以使用下面的方式进行计算: sizeof(元素类型)*QVector::size() 不过我要说并不是QVector,而是一种更直观的数据存储方式——结构体 例如上面的数据,我们可以存储为一个这样的形式 调用setAttributeArray(): 唯一需要说明的就是offset参数,我们使用的是(byte*)vertices.data()+offsetof(Vertex,position) 首先我们将数据指针转换成(byte*),这样之后我们对指针加减的单位就是单个字节,之后我们再用操作符【offsetof】计算结构体Vertex中position的字节偏移(结果为0),二者相加则是【position的偏移量】,很简单是吧。 下面是调用setAttributeBuffer(省略了VAO,VBO的操作): Buffer的就更简单了,不用再对数组指针进行偏移了,直接给个参数就是,如果你非常熟悉C/C++的字节对齐也可以不用offsetof,自己计算,如果想了解他们,可以看看笔者的这篇文章:https://blog.csdn.net/qq_40946921/article/details/102933860 VAO的是用来存储解析格式及解析数据来源的,一定要清楚它的作用,否则在之后的开发中,很容易出现“非预期”的错误,很多小伙伴一开始不太清楚VAO,VBO的绑定原理,以为调用VBO.bind是将VBO绑定到当前VAO中,但实际上该操作只是将VBO绑定为当前缓存对象,真正将VBO的指针及VBO的解析格式存放到VAO中,是通过setAttributeBuffer执行的。这一点在很多教程中,包括learnOpenGL中都没有进行说明,大家请一定要注意!!! 看了上面对VAO的描述,不知道你有没有发现一个非常奇葩的问题——我们调用shaderProgram的方法居然是在设置VAO的值,没想到吧,我也没想明白Qt为什么要这样进行封装,笔者在这个地方真的是踩了很多坑,希望大家引以为鉴。 下面我们演示在Qt中创建并使用VAO,以上一节代码为例(这里我已经提前创建好项目了,读者可以自己重新实现一遍,当做回顾): paintGL每次刷新都调用setAttributeArray设置解析格式很显然是不明智的,我们可以把这项操作放到initGL中,但注意别忘了将顶点数组设置为成员变量,因为局部变量会在作用域结束的时候释放。不过先别急着改,它只是个小问题! 最大的问题是你有没有发现我们的矩形怎么变成三角形???这是怎么回事,难道我们做错了? 按理来说我们这样操作应该是没有问题,那问题问题究竟出在哪? 在OpenGLES3.0的官方文档对于函数glVertexAttribPointer有这么一句话: Client vertex arrays (a binding of zero to the target) are only valid in conjunction with the zero named vertex array object. This is provided for backwards compatibility with OpenGL ES 2.0. 翻译一下就是:setAttributeArray底层就是通过这个函数来实现的,因此,划红线了,请注意: 作用:一段可供使用的GPU内存 如果你学习过C/C++,可能对内存的概念会很熟悉,C/C++编程中经常会需要调用malloc(C)或者 new(C++)来分配一段可供使用的内存,它们一般是存储在客户端内存中,而缓存对象其实就是跟我们平常使用的内存一样,只不过它是存储在显存中,而我们在本地只记录缓存对象的ID,通过OpenGL提供一些的指令(以id为参数),可以间接的操控这段显存。 只要把缓存对象理解成一段可供操作的显存,相信你会很容易理解它的用法,使用缓存对象其实就跟平时使用内存一样,首先分配大小,然后可以这片内存区域进行读、写等操作,我们甚至可以通过函数(glCopyBufferSubData)在GPU端转移数据。以及(glBindBufferRange)绑定指定区域的缓存对象。 缓存对象的用法很多,下面我举两个例子: 总之,我们要知道:CPU传输数据到GPU,是一个非常耗时的操作。 我们使用缓存对象的目的,就是尽可能的想办法优化这个操作。 在之前说顶点数值对象的使用我们一直提到一个东西——VBO(Vertex Buffer Object),它其实就是一个缓存对象, 看名字应该能猜出功能:我们是将顶点数据存放到在缓存中。 接下来我直接演示如何使用VBO: 首先我们创建一个QOpenGLBuffer对象,在构造函数初始化为QOpenGLBuffer::VertexBuffer,如果不给参数的话,默认就是VertexBuffer类型的缓存对象,我这里写出来是为了告诉你:QOpenGLBuffer除了VBO还有其他“类型”的缓存对象。 这里类型打上引号,是因为它们本质上都是缓存对象,唯一不同的就是当调用它们的bind函数时,会将它们绑定到不同的缓存区(如VBO将绑定到GL_ARRAY_BUFFER上),因此,在我们绑定VBO之后,如果解绑不规范就可能会导致之后的代码出现问题。 接着我们就在initGL中先调用create()向GPU申请创建这个对象,然后调用bind进行绑定,之后再调用allocate申请分配一定字节大小的显存,并且把顶点数组【vertices】的值传递到显存之中。 这里调用allocate之前我们必须先绑定VBO,否则出错! 最后我们在initGL中设置顶点解析格式,这次我们调用的是shaderProgram.setAttributeBuffer():设置解析格式,并且自动设置数据来源为当前VBO。 设置完成,别忘记解绑缓存对象,之后再运行,会发现我们的图形已经能正常绘制了! 索引缓存对 总结:使用EBO能让我们重复使用顶点数组中的某些顶点。 总结:使用EBO能让我们只使用顶点数组中的一部分顶点。 说的这么神,那么EBO究竟是这么做到的呢? 假如我给定一个数组【array】: 再给定一个索引数组【element】 我们把这两个数组传递给GPU,再告诉GPU我们准备使用索引来绘制,那么接下来实际GPU将生成这样的数据: 现在应该能明白使用索引筛选数据是怎么回事了吧?而EBO的作用,就是存储索引数组的数据,之前我们已经做好了VBO,接下来我们使用EBO来筛选出三个顶点绘制一个三角形: EBO的创建和使用与VBO相似,我们只需创建一个缓存对象,并在构造函数中初始化为EBO,之后我们在initGL中设置索引数据(注意数据类型使用unsigned int),paintGL中改用drawElements就能绘制出三角形啦! 这里需要注意两个地方: https://github.com/Italink/QtOpenGL-Essential-Training/tree/main/Day02 这一节的内容有点多,理解有些困难,但是如果你能坚持看完,你一定会有很大的收获,因为我敢说,网上几乎没有教程能把缓存对象讲的这么细致(如果你非要说有,那就加个限定条件【Qt】,0.0,如果你还要说有,那...对,你说的对T.T) 包括笔者自己一开始学(通过Learn OpenGL网站学习),也是学的云里雾里,根本看不明白,后面是踩了很多坑,查阅源码,翻阅文档,才慢慢解决的,所以,这章请尽量认真一点,因为这章暗坑很多,稍有不慎,没理解透彻,在之后的学习和开发中会很容易出现一些不可名状的问题。(笔者经历的就多了,说多了都是泪。) 本来想着这一节中把纹理对象和帧缓存对象一并给说了,没想到写完前面这些就已经六千多字了,考虑到篇幅太大,不方便阅读,纹理对象和帧缓存对象我放到下一节中。 能够掌握了上一节中的知识(缓存对象和顶点缓存对象),之后的学习完全可以说是一马平川!因为基本图形绘制中出现的大多数问题,一般都是因为缓冲区绑定混乱导致的,因此,这里再强调一下,如果上一节中有什么地方漏看了,请务必再去回顾一下! 说简单一点,纹理就相当于一种“贴图”,我们可以把它“贴”到图形上(这里纹理充当颜色缓存的作用)。 本质上,纹理对象是一段特殊的存储区域(也是存储在显存中,客户端保留ID),它特殊在我们可以在着色器程序中使用一个名为采样器的东西,快速的从纹理中读取我们想要的数据(待会介绍采样器) 上面特定强调了数据两个字,而不是单单指颜色,是因为纹理有很多其他的用法,并不是只能用来填色,下面例举一些其他的用法,后面我们可能会讲到: 在OpenGL中,常见纹理类型有1D,2D,3D等,这些纹理可以使用对应的采样器(sampler)获取数据,以我们经常使用的2D纹理为例,2D纹理可以提供两个参数(可以理解为图片的x,y坐标)从采样器【sampler2D】中获取到数据。 这一步骤一般是在片段着色器中进行的,操作类似于下面这样: 与标准化坐标系不同的是,纹理坐标的范围为[0,1],下面是2D纹理坐标的示意图: 同理,3D纹理使用三维坐标中的[0,1]部分。 QOpenGLTexture的使用方式与QOpenGLBuffer类似,在创建纹理对象QOpenGLTexture的构造函数要求我们必须指定纹理的目标类型,以下是Qt支持的目标类型: 这个类型在底层决定当调用QOpenGLTexture::bind()时具体绑定到哪个缓存区。 创建纹理起始步骤跟使用VBO一样: 接下来我们一般需要调用纹理的一些设置方法设置纹理的格式,设置结束之后调用 一旦分配了存储,就不可能再更改这些属性。分配存储之后你可以调用setData()重载之一上传像素数据。 完整的创建方法就像是下面这样: 接下来我们认识一下纹理的一些采用设置 2D纹理坐标的范围通常是从(0, 0)到(1, 1),那如果我们把纹理坐标设置在范围之外会发生什么?OpenGL默认的行为是重复这个纹理图像(我们基本上忽略浮点纹理坐标的整数部分),但QOpenGLTexture提供了更多的选择: 当纹理坐标超出默认范围时,每个选项都有不同的视觉效果输出。我们来看看这些纹理图像的例子: 前面提到的每个选项都可以使用setWrapMode函数对单独的一个坐标轴设置(S、T(如果是使用3D纹理那么还有一个R)它们和x、y、z是等价的): 第一个参数指定了应用的纹理轴。 第二个参数我们传递一个环绕方式(Wrapping)。 如果我们选择QOpenGLTexture::ClampToBorder选项,我们还需要指定一个边缘的颜色。这需要使用setBorderColor函数,并且传递一个QColor作为边缘的颜色值: 纹理坐标不依赖于分辨率(Resolution),它可以是任意浮点值,所以OpenGL需要知道怎样将纹理像素(Texture Pixel,也叫Texel)映射到纹理坐标。当你有一个很大的物体但是纹理的分辨率很低的时候这就变得很重要了。你可能已经猜到了,QOpenGLTexutre也有对于纹理过滤(Texture Filtering)的选项。纹理过滤有很多个选项,但是现在我们只讨论最重要的两种:QOpenGLTexture::Nearest和QOpenGLTexture::Linear Texture Pixel也叫Texel,你可以想象你打开一张.jpg格式图片,不断放大你会发现它是由无数像素点组成的,这个点就是纹理像素;注意不要和纹理坐标搞混,纹理坐标的各个分量都已经标准化到【0,1】,像素坐标则是具体像素的的横纵坐标。 QOpenGLTexture::Nearest(也叫邻近过滤,Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。当设置为QOpenGLTexture::Nearest的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。下图中你可以看到四个像素,加号代表纹理坐标。左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色: QOpenGLTexture::Linear(也叫线性过滤,(Bi)linear Filtering)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。下图中你可以看到返回的颜色是邻近像素的混合色: 那么这两种纹理过滤方式有怎样的视觉效果呢?让我们看看在一个很大的物体上应用一张低分辨率的纹理会发生什么吧(纹理被放大了,每个纹理像素都能看到): QOpenGLTexture::Nearest产生了颗粒状的图案,我们能够清晰看到组成纹理的像素,而QOpenGLTexture::Linear能够产生更平滑的图案,很难看出单个的纹理像素。QOpenGLTexture::Linear可以产生更真实的输出,但有些开发者更喜欢8-bit风格,所以他们会用GL_NEAREST选项。 当进行放大(Magnify)和缩小(Minify)操作的时候可以设置纹理过滤的选项,比如你可以在纹理被缩小的时候使用邻近过滤,被放大时使用线性过滤。我们需要使用setMinMagFilters函数为放大和缩小指定过滤方式: 当然我们也可以直接调用setMagnificationFilter和 setMinificationFilter单独进行设置 想象一下,假设我们有一个包含着上千物体的大房间,每个物体上都有纹理。有些物体会很远,但其纹理会拥有与近处物体同样高的分辨率。由于远处的物体可能只产生很少的片段,OpenGL从高分辨率纹理中为这些片段获取正确的颜色值就很困难,因为它需要对一个跨过纹理很大部分的片段只拾取一个纹理颜色。在小物体上这会产生不真实的感觉,更不用说对它们使用高分辨率纹理浪费内存的问题了。 OpenGL使用一种叫做多级渐远纹理(Mipmap)的概念来解决这个问题,它简单来说就是一系列的纹理图像,后一个纹理图像是前一个的二分之一。多级渐远纹理背后的理念很简单:距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。由于距离远,解析度不高也不会被用户注意到。同时,多级渐远纹理另一加分之处是它的性能非常好。让我们看一下多级渐远纹理是什么样子的: 手工为每个纹理图像创建一系列多级渐远纹理很麻烦,幸好QOpenGLTexture有一个generateMipMaps()函数,在创建完一个纹理后调用它OpenGL就会承担接下来的所有工作了。 在渲染中切换多级渐远纹理级别(Level)时,OpenGL在两个不同级别的多级渐远纹理层之间会产生不真实的生硬边界。就像普通的纹理过滤一样,切换多级渐远纹理级别时你也可以在两个不同多级渐远纹理级别之间使用QOpenGLTexture::Nearest和QOpenGLTexture::Linear过滤。为了指定不同多级渐远纹理级别之间的过滤方式,你可以使用下面四个选项中的一个代替原有的过滤方式: 就像纹理过滤一样,我们可以使用glTexParameteri将过滤方式设置为前面四种提到的方法之一: 一个常见的错误是,将放大过滤的选项设置为多级渐远纹理过滤选项之一。这样没有任何效果,因为多级渐远纹理主要是使用在纹理被缩小的情况下的:纹理放大不会使用多级渐远纹理,为放大过滤设置多级渐远纹理的选项会产生一个GL_INVALID_ENUM错误代码。 在使用纹理之前,我们需要知道:着色器程序并不是直接从纹理对象中读取像素数据,而是通过一个名叫纹理单元的东西,它的目的是让我们在着色器中可以使用多个的纹理。 使用纹理首先需要将纹理对象绑定到一个纹理单元(默认是0),在通过着色器程序对象的setUniform设置着色器代码中的采样器从哪一个纹理单元读取数据。一般步骤如下: 片段着色器代码: 客户端代码: OpenGL至少保证有16个纹理单元供你使用,也就是说你可以绑定0-15,它们都是按顺序定义的。 下面我们直接开始操作吧! 在OpenGL中,绘制图片的思路很简单:我们只需要绘制一个矩形,除了之前传递的顶点位置数据,现在我们还需要传递纹理坐标到顶点着色器中。 这次也是使用上一节中的代码,你也可以重新实现一遍当做回顾,然后使用下面这张图片(浏览器右键另存为,笔者已经将它放到了项目目录下面了),废话不多说,我们直接开始操作吧 本次使用的着色器代码中使用了两个新关键字,它们的作用如下: 下一章中我们会详细讲解关于着色器的知识,如果现在不理解,可以暂时不用管。 还没完,你有没有发现我们绘制的图形为什么是倒的,为什么呢? 是因为OpenGL的采样顺序和QImage图片的存储顺序有细微差别:大多数图片都会以图片的左上角开始存储图形数据,而OpenGL的采样器是以左下角为原点进行绘制。要解决这个问题有两个方法: 1.修改纹理坐标为 2.调用QImage QImage::mirrored(bool horizontal = ..., bool vertical = ...) const &翻转图像 两种方法都能得到正确的结果: 鉴于之前的章节太过枯燥无畏,从这一章节开始,每个小节结束,我们会使用本节学到的东西,做一些有意思的扩展,这些扩展除了一些必要的说明之外,将全部以视频的方式演示,对此笔者真的很抱歉,因为时间有限,只能把核心放在基础重点上,不过我会尽量在项目代码中增加注释。 那么这一节要做什么呢? 我们要做一个显示时间的电子钟,就像是下面这样: 当然这一节我们制作的效果还没那么炫,只是简单的文字,泛光效果会在之后我们将帧缓存的时候进行实现。 通过它能能学到什么呢? 那么下面的话,就直接开始吧! 制作完成之后效果如下: 但是你会发现一个很严重的问题,那就是文字的大小是随着窗口改变的,这是因为我们使用的纹理坐标是整个窗口,而实际上文字的大小应该是我们用QFontMetrics量出来的,,窗口的大小明显大于纹理的大小,因此拉伸导致了变模糊,那有什么方法能解决这个问题呢?其核心就是调整矩形的大小跟纹理的大小一致,使用以下方法都能做到: 代码地址: https://github.com/Italink/QtOpenGL-Essential-Training/tree/main/Day03_Text2D 上面的代码虽然能完成2D文字的绘制,但是在以后的开发之中,我们总是要绘制纹理,文字等特定的图形,你会发现上面我们的代码很乱,根本无法复用,因此笔者简单封装一个静态工具库,将这些常用图形的绘制算法放在里面,代码地址: 该库为静态链接库,笔者使用MSVC2017 32位编译了Debug和Release,如果使用其他编译器的朋友可以手动编译一下,注意在QtCreator左侧的项目配置中取消勾选shadow build 利用这个工具我们重新完成2D文字的绘制,代码地址如下: 在入门教程中我们熟悉了三个OpenGL对象,分别是:缓存对象、顶点缓存对象和纹理对象。帧缓存对象跟他们一样,也是存储在显存中的一段内存,客户端中只保留帧缓存ID通过一些指令对其进行操作。 相信大家都清楚帧的概念:视频(或者动画)的本质是通过播放一系列连续的图像完成的,这其中的每一张图像我们都可以称它为作一个视频帧,而帧缓存,就是用来存储帧的数据。这有点跟纹理相似,区别在于帧缓存的纹理数据不是通过客户端上传的,而是绘制出来的。 其实我们在之前的章节之中,就已经使用过帧缓存对象,只不过我们不知道,什么地方用了呢? 调用glDraw函数,其本质是将图像绘制在当前的帧缓存中,同样glClear是清除当前帧缓存中的数据。我们其实并不是把图形直接绘制到了屏幕上,而是绘制在一个缓存区域上,最后再统一放到屏幕缓存上,这也就是双缓冲机制,能有效避免图像闪烁。 QOpenGLWidget拥有一个默认的帧缓存区,使用成员函数defaultFramebufferObject()能获取到它的帧缓存ID,Qt在调用paintGL之前会自动绑定帧缓存区,操作如下: 所以我们在paintGL之前调用glDraw和glClear其实都在对这个默认的帧缓冲区进行操作。 在一些帧缓存教程中,你可能经常会看到有关离屏渲染的说明,简单点说,离屏渲染就是不在屏幕上进行绘图。看似这个功能没什么用,但实际上它让图形渲染管线所能做的事情有了质的飞跃。很多图形渲染的核心技术:动态纹理、阴影生成、延迟着色、后期滤镜、图形点选等,都需要借助帧缓存才能完成。 而我们使用帧缓存,大多都是在干这么一件事:把一次图形渲染管线的输出,做为另一次图形渲染管线的输入(纹理)。 Qt中的帧缓存对象为:QOpenGLFramebufferObject 与之前三个对象的使用方法不同,之前的对象需要我们调用create向显存申请创建对象,而帧缓存对象不同,为什么呢? 因为通过查阅源码明显能看出帧缓存对象与之前的三个对象不是一个人写的,不得不说QOpenGLFramebufferObject的代码质量是很高的,以我目前的使用情况来看,使用Qt封装的帧缓存对象基本已经可以完成任何操作了,并且Qt的封装免去了RenderObject的创建,这让帧缓存的使用体验非常友好。 创建帧缓存对象的时候,需要要求有当前OpenGL上下文(即是调用了QOpenGLWidget::makeCurrent()之后),所以我们不能再像之前那样,创建 成员变量实例,(因为在QOpenGLWidget的构造函数调用之前没有调用makeCurrent),因此,我们一般创建QOpenGLFramebufferObject的指针作为成员变量,在initializeGL()函数中new一个出来。 关于makeCurrent,很多小伙伴可能比较烦,为什么每次在窗口外部写OpenGL代码都需要调用,之所以这么做,是为了能够兼容多个OpenGL窗口的使用,由于OpenGL本身是以状态机的方式进行编程的,与我们熟知的面对对象不同,为了让OpenGLFunction知道自己应该对哪个QOpenGLWidget进行操作,开发者必须手动调用makeCurrent()将实际需要操作的窗口绑定为当前上下文。这些问题在单窗口开发的时候并不明显,当涉及到多窗口的时候,请读者务必留心。 以参数最全的构造函数为例: 上述的格式会将输出的数值限制到[0,1],正常情况我们输出颜色是没有问题的,但是如果我们想利用帧缓存存储不限制大小的数据时,就需要用到浮点纹理,用法也很简单,只需要在格式后面加F即可 使用步骤: 知道glDraw,glClear的操作对象为当前帧缓存对象的话,你应该已经能猜出该怎么使用它了: 此外还有一些其他操作需要了解: 与之前的三个对象的id获取函数名称不同,Qt中使用handle()函数获取到帧缓存的id,一般我们无需使用这个ID,保留这个接口是为了能够兼容原生OpenGLAPI。 之前的章节我们在片段着色器中使用gl_FragColor来输出颜色,上一节中说到这个关键字已经被废弃,需要自己定义输出变量。之所以要这么做,是为了能够实现多渲染目标技术,即纹理附件。现代OpenGL允许我们定义多个输出变量,每个输出变量会存储到相应的颜色附件当中。 使用纹理附件主要有两个目的: QOpenGLFrameBufferObject默认带有一个颜色附件(ColorAttachment0),片段着色器中的第一个输出变量将输出到该附件中。 如果需要额外的颜色附件,调用QOpenGLFrameBufferObject::addColorAttachment即可,正常使用的操作步骤如下: 拥有多个纹理附件的着色器这么处理: 还没完,在绘制多渲染目标的帧时,我们还需要手动指定drawBuffer 接下来绘制时OpenGL将根据我们在着色器中指定的location把数据输出到对应的buffer里面。 客户端中获取附件的纹理ID,尺寸等: 数据块传输用于将一个帧缓存部分的图像拷贝到另一个帧缓存对象中。Qt中对应的函数为: static void QOpenGLFramebufferObject::blitFramebuffer 其使用细则请查询官方文档。 综上,便是帧缓存比较常用的内容。接下来我们将使用帧缓存做点什么东西。做点什么呢? 还记得之前第四节(纹理)教程中制作的2D文字时钟吗?我曾说过要给文字增加一点“特效”,使之变得炫酷一些。下面我们开始吧 这一节对应LearnOpenGL教程的帧缓存章节,本节完成后我们将实现类似如下效果: 2D文字时钟为第四节的内容,代码可以从这里获取(注意一下,近期UP把代码重新优化了一遍,变动较大,朋友们使用前请仔细阅读一下代码) https://github.com/Italink/QtOpenGL-Essential-Training/tree/main/Section4_2DTextByTool 首先我们来尝试使用一下帧缓存对象: 1.创建QOpenGLFramebufferObject* fbo; 成员变量。 2.在initializeGL函数中为fbo new一个实例。 3.在paintGL中绑定fbo,让2D时钟的图像绘制到该帧缓存对象中。 4.运行之后,你会发现屏幕上漆黑一片,接着我们需要把该帧缓存区的图像绘制到默认的帧缓存区中,实现有两种方法: (1)块传输(blit):只需在GLTool::drawText之后使用函数QOpenGLFramebufferObject::blitFramebuffer将fbo的数据传输到默认帧缓存区中: (2)纹理绘制:将fbo的图像当作纹理绘制到默认帧缓存区上: 完成之后你能得到下面的效果 5.问题:由于我们的帧缓存纹理尺寸是固定的,如果窗口大小变化 ,帧的显示就出问题了,想要解决这个问题,只需要在resizeGL中重新创建帧缓存对象即可。 虽然我们这么折腾一通,效果没什么变化,但却是非常有意义的,我们已经基本掌握帧缓存对象的使用流程,要做滤镜的话,只需在中间进行额外的操作就行。不过为了完成辉光滤镜,我们还需要掌握一些前置知识——高斯模糊和HDR 要想让图形看起来像是“光芒四射”需要借助模糊算法,常见的模糊算法有均值模糊,高斯模糊,方向模糊,动态模糊,运动模糊,径向模糊等。均值模糊和高斯模糊的使用目的基本一致,只是使用高斯模糊能得到相比均值模糊更好的效果。 均值模糊:如果你之前接触过图像处理,可能会知道高斯模糊和均值模糊的原理:图像就像是一个矩阵(二维数组),其中的每个元素对应的像素点的颜色。而均值模糊就是将其中的每个像素点与周围像素的颜色值进行平均(例如,对一个3*3的小矩阵中的9个元素求均值,把该值作为矩阵中心的值),范围越大,图像约模糊,但是性能消耗就越大。比如要对一张1000*1000大小的图形以半径为1(也就是3*3的核)进行模糊,每个像素点要进行9次取样,那么模糊整张图片就得取样1000*1000*9次,这个数据量是非常庞大的,但对于GPU来说,处理这点数据依旧是不费吹灰之力,就是半径再扩大个100倍,GPU一般也能承受。 为什么要用高斯模糊:很显然,开头已经说了——高斯模糊的效果更好。但为什么呢?是因为均值模糊中,比如3*3的核,其中每个元素的收益是相等的,”扩散“的效果并不明显,如果可以让离中心近的元素收益高一些,而远的元素收益低一些,下图是一个二维高斯函数的分布情况,从图中可以很好的体现”扩散“这一效果。 优化: 网上关于高动态范围的讲解比较复杂,为了方便理解,笔者会通过自己的见解进行讲述,如果有不对的地方,欢迎大家指正。 使用HDR有什么好处? 上面我加粗了两个字”临时“,是因为我们最后还要通过一个叫色调映射(Tone Mapping)的过程,将[0,+∞)的颜色值映射回[0.0,1.0],这样才能正常被显示器显示。 关于色调映射算法,我在LearnOpenGL教程中了解到了两个: 观察上面那个函数,你会发现它们在[0,+∞)的值域为[0,1],这也解释了色调映射算法是如何工作的。 由于我们接下来要处理的图像颜色值一开始并没有超过1.0,因此将采样曝光色调映射算法。 gamma矫正 关于伽马矫正,笔者也似懂非懂,大家可以看看知乎上的回答: https://www.zhihu.com/question/27467127/answer/37602200 使用帧缓存对象模拟水面的效果 这是之前的代码效果,最近代码进行了重写,Github地址为: https://github.com/Italink/QtOpenGL-Essential-Training/tree/main/WaveEffect 这个教程写到现在,收到两位小伙伴的充电,非常感谢=.=!虽然笔者现在身无分文,还在啃老,但做这个教程并非为了盈利,而是想通过分享自己学到东西,来结交更多志同道合的朋友,笔者今年本科刚毕业,很多东西是靠自己理解来做的,路子有点野,不对的地方还望不吝赐教,如果你也对图形渲染感兴趣,可以加笔者的微信(italink),一起讨论技术上的问题0.0QtOpenGL入门教程(六)—— 初识帧缓存对象(泛光滤镜)的介绍就聊到这里吧,感谢你花时间阅读本站内容,更多关于QtOpenGL入门教程(六)—— 初识帧缓存对象(泛光滤镜)、QtOpenGL入门教程(六)—— 初识帧缓存对象(泛光滤镜)的信息别忘了在本站进行查找喔。
未经允许不得转载! 作者:谁是谁的谁,转载或复制请以超链接形式并注明出处。
原文地址:http://96gps.cn/post/18619.html发布于:2026-02-23




