QT with OpenGL(HDR)

借鉴自 https://www.bilibili.com/read/cv11765941/

为什么引入HDR(High Dynamic Range)

一般来说,当存储在帧缓冲(Framebuffer)中时,亮度和颜色的值是默认被限制在0.0到1.0之间的。

显示器被限制为只能显示值为0.0到1.0间的颜色,但是在光照方程中却没有这个限制。通过使片段的颜色超过1.0,我们有了一个更大的颜色范围,这也被称作HDR(High Dynamic Range, 高动态范围)。有了HDR,亮的东西可以变得非常亮,暗的东西可以变得非常暗,而且充满细节。

HDR渲染允许用更大范围的颜色值渲染从而获取大范围的黑暗与明亮的场景细节,最后将所有HDR值转换成在[0.0, 1.0]范围的LDR(Low Dynamic Range,低动态范围)。转换HDR值到LDR值得过程叫做色调映射(Tone Mapping),现在现存有很多的色调映射算法,这些算法致力于在转换过程中保留尽可能多的HDR细节。这些色调映射算法经常会包含一个选择性倾向黑暗或者明亮区域的参数。

如何使用更大范围的帧缓存

在Qt中使用帧缓存对象

Qt中的帧缓存对象为:QOpenGLFramebufferObject

与之前三个对象的使用方法不同,之前的对象需要我们调用create向显存申请创建对象,而帧缓存对象不同,为什么呢?

因为通过查阅源码明显能看出帧缓存对象与之前的三个对象不是一个人写的,不得不说QOpenGLFramebufferObject的代码质量是很高的,以我目前的使用情况来看,使用Qt封装的帧缓存对象基本已经可以完成任何操作了,并且Qt的封装免去了RenderObject的创建,这让帧缓存的使用体验非常友好。

创建帧缓存对象的时候,需要要求有当前OpenGL上下文(即是调用了QOpenGLWidget::makeCurrent()之后),所以我们不能再像之前那样,创建 成员变量实例,(因为在QOpenGLWidget的构造函数调用之前没有调用makeCurrent),因此,我们一般创建QOpenGLFramebufferObject的指针作为成员变量,在initializeGL()函数中new一个出来

关于makeCurrent,很多小伙伴可能比较烦,为什么每次在窗口外部写OpenGL代码都需要调用,之所以这么做,是为了能够兼容多个OpenGL窗口的使用,由于OpenGL本身是以状态机的方式进行编程的,与我们熟知的面对对象不同,为了让OpenGLFunction知道自己应该对哪个QOpenGLWidget进行操作,开发者必须手动调用makeCurrent()将实际需要操作的窗口绑定为当前上下文。这些问题在单窗口开发的时候并不明显,当涉及到多窗口的时候,请读者务必留心。

构造FBO

以参数最全的构造函数为例:

QOpenGLFramebufferObject::QOpenGLFramebufferObject(
        int width,
        int height,
        QOpenGLFramebufferObject::Attachment attachment,
        GLenum target = GL_TEXTURE_2D,
        GLenum internalFormat = 0)
  • widthheight很明显,是帧缓存的尺寸,这个尺寸一旦创建之后就无法改变,你可能会好奇,默认的帧缓存这么不是固定尺寸的,其实是一样的,只不过Qt在resizeEvent中已经重新创建了一个新的帧缓存。

  • attachment:可以是下面三个值,指定缓冲区是否要附带一些常用附件

    • NoAttachment:不添加任何附件。需要注意的是,帧缓存如果不存在深度缓存附件或者模板缓存附件,那么深度测试和模板测试将无法正常工作。

    • CombinedDepthStencil:如果存在GL_EXT_packed_depth_stencil扩展,则附加一个深度和模板缓冲区的组合。如果扩展不存在,则只附加一个深度缓冲区。

    • Depth:附加一个深度缓存区

  • target:指定纹理的格式,默认的是GL_TEXTURE_2D,与之前的纹理格式QOpenGLTexture::Target2D其本质就是将纹理绑定到GL_TEXTURE_2D上,我们常用的就是它,如果需要重采样的话,可以自行设置。

  • internalFormat :参数名称如果叫做pixelFormat相信大家就很容易理解了,这个参数指的是帧缓存纹理的像素格式,更多时候我们比较在意通道数,如果我们只需要像素的一个通道,那么就可以使用GL_RED,其他以此类推,其中支持的值如下,默认为GL_RGBA

GL_RED
GL_RG
GL_RGB
GL_BGR
GL_RGBA
GL_BGRA
GL_RED_INTEGER
GL_RG_INTEGER
GL_RGB_INTEGER
GL_BGR_INTEGER
GL_RGBA_INTEGER
GL_BGRA_INTEGER
GL_STENCIL_INDEX
GL_DEPTH_COMPONENT
GL_DEPTH_STENCIL 

需要注意的是:上述的格式会将输出的数值限制到[0,1],正常情况我们输出颜色是没有问题的,但是如果我们想利用帧缓存存储不限制大小的数据时,就需要用到浮点纹理,用法也很简单,只需要在格式后面加F即可

GL_R16F
GL_RG16F
GL_RGB16F
GL_R32F
... 

纹理附件(多纹理绑定及渲染)

之前的章节我们在片段着色器中使用gl_FragColor来输出颜色,上一节中说到这个关键字已经被废弃,需要自己定义输出变量。之所以要这么做,是为了能够实现多渲染目标技术,即纹理附件。现代OpenGL允许我们定义多个输出变量,每个输出变量会存储到相应的颜色附件当中。

使用纹理附件主要有两个目的:

  • 只使用一条流水线,在片段着色器中做一些简单调整就能得到多个输出。
  • 默认的颜色附件(RGBA四通道)不足以存储所有的输出数据。

QOpenGLFrameBufferObject默认带有一个颜色附件(ColorAttachment0),片段着色器中的第一个输出变量将输出到该附件中。

如果需要额外的颜色附件,调用QOpenGLFrameBufferObject::addColorAttachment即可,正常使用的操作步骤如下:

frameBufferObject.bind();
//添加一个默认大小,只带有Red通道的纹理附件
frameBufferObject.addColorAttachment(frameBufferObject.size(),GL_RED);
frameBufferObject.release();

拥有多个纹理附件的着色器这么处理:

#version 450 core
layout(location = 0)out vec4 FragColor;			//输出到颜色附件0
layout(location = 1)out float RedBuffer;		//输出到颜色附件1,前面的数据类型取决于附件纹理的通道数
void main(){
	//...
}

还没完,在绘制多渲染目标的帧时,我们还需要手动指定drawBuffer

GLenum buffers[2]={ GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
glDrawBuffers(2,buffers);

glDrawArray(...);

接下来绘制时OpenGL将根据我们在着色器中指定的location把数据输出到对应的buffer里面。

客户端中获取附件的纹理ID,尺寸等:

frameBufferObject->textures();  //texture()等价于[0]
frameBufferObject->sizes();     //size()等价于[0] 

数据块传输:

数据块传输用于将一个帧缓存部分的图像拷贝到另一个帧缓存对象中。Qt中对应的函数为:

static void QOpenGLFramebufferObject::blitFramebuffer

HDR(高动态范围)

网上关于高动态范围的讲解比较复杂,为了方便理解,笔者会通过自己的见解进行讲述,如果有不对的地方,欢迎大家指正。
在这里插入图片描述

HDR的原理

我们最后还要通过一个叫色调映射(Tone Mapping) 的过程,将[0,+∞)的颜色值映射回[0.0,1.0],这样才能正常被显示器显示。

关于色调映射算法,我在LearnOpenGL教程中了解到了两个:

  • ReinHard算法 HDR=Color/(Color+1);
  • 曝光色调映射算法 HDR=1-exp(-Color*exposure);

观察上面那个函数,你会发现它们在[0,+∞)的值域为[0,1],这也解释了色调映射算法是如何工作的。

由于我们接下来要处理的图像颜色值一开始并没有超过1.0,因此将采样曝光色调映射算法。

如何在QT中使用HDR

首先创建浮点帧缓冲

当帧缓冲使用了一个标准化的定点格式(像GL_RGB)为其颜色缓冲的内部格式,OpenGL会在将这些值存入帧缓冲前自动将其约束到0.0到1.0之间。这一操作对大部分帧缓冲格式都是成立的,除了专门用来存放被拓展范围值的浮点格式

当一个帧缓冲的颜色缓冲的内部格式被设定成了GL_RGB16F, GL_RGBA16F, GL_RGB32F 或者GL_RGBA32F时,这些帧缓冲被叫做浮点帧缓冲(Floating Point Framebuffer),浮点帧缓冲可以存储超过0.0到1.0范围的浮点值,所以非常适合HDR渲染。

想要创建一个浮点帧缓冲,我们只需要改变颜色缓冲的内部格式参数就行了

QOpenGLFramebufferObject* FBOHDR = new QOpenGLFramebufferObject(
        width(),
        height(),
        QOpenGLFramebufferObject::CombinedDepthStencil,
        GL_TEXTURE_2D,
        GL_RGB16F
        )

默认的帧缓冲默认一个颜色分量只占用8位(bits)。当使用一个使用32位每颜色分量的浮点帧缓冲时(使用GL_RGB32F 或者GL_RGBA32F),我们需要四倍的内存来存储这些颜色。所以除非你需要一个非常高的精确度,32位不是必须的,使用GLRGB16F就足够了。

将场景渲染到HDR帧缓存

FBOHDR->bind();
DrawScene();

将帧缓存渲染到屏幕

QT如何导出帧缓存纹理看这里

也可以使用位传输blitFramebuffer,但无法将HFD色调映射到LDR。

QOpenGLFramebufferObject::blitFramebuffer(
								nullptr,
								rect(),
								FBOHDR,
								QRect(0,0,fbo->width(),fbo->height()));    
								//nullptr为默认缓存区

色调映射

shader需要使用色调映射,将颜色限制在【0,1】

  • Reinhard
void main()
{             
    const float gamma = 2.2;
    vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;

    // Reinhard色调映射
    vec3 mapped = hdrColor / (hdrColor + vec3(1.0));
    // Gamma校正
    mapped = pow(mapped, vec3(1.0 / gamma));

    color = vec4(mapped, 1.0);
}   
  • 曝光色调映射
uniform float exposure;

void main()
{             
    const float gamma = 2.2;
    vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;

    // 曝光色调映射
    vec3 mapped = vec3(1.0) - exp(-hdrColor * exposure);
    // Gamma校正 
    mapped = pow(mapped, vec3(1.0 / gamma));

    color = vec4(mapped, 1.0);
}  

我的想法

按照我的理解,不需要额外使用帧缓存,直接在窗口的shader中将HDR通过色调映射变为LDR即可。

  • 不开启HDR(超过1.0的部分将显示1.0);
    在这里插入图片描述
  • 使用Reinhard色调映射
   //色调映射
   if(toneMapping>0.0f){
       result.rgb = result.rgb /(result.rgb+1.0);
   }

在这里插入图片描述

  • 使用曝光色调映射
    曝光度0.1
    在这里插入图片描述
    曝光度0.5
    在这里插入图片描述
    曝光度1.0
    在这里插入图片描述
    曝光度2.0
    在这里插入图片描述

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片