LearnOpenGL笔记——五、高级光照:“HDR”和“泛光”

LearnOpenGL笔记——五、高级光照:“HDR”和“泛光”

5.6 HDR

  • 一般来说,当存储在帧缓冲(Framebuffer)中时,亮度和颜色的值是默认被限制在0.0到1.0之间的。
  • 这个看起来无辜的语句使我们一直将亮度与颜色的值设置在这个范围内,尝试着与场景契合。
  • 这样是能够运行的,也能给出还不错的效果。
  • 但是如果我们遇上了一个特定的区域,其中有多个亮光源使这些数值总和超过了1.0,又会发生什么呢?
    • 答案是这些片段中超过1.0的亮度或者颜色值会被约束在1.0,从而导致场景混成一片,难以分辨
  • 由于大量片段的颜色值都非常接近1.0,在很大一个区域内每一个亮的片段都有相同的白色。这损失了很多的细节,使场景看起来非常假。
  • 解决这个问题的一个方案是减小光源的强度从而保证场景内没有一个片段亮于1.0。
    • 然而这并不是一个好的方案,因为你需要使用不切实际的光照参数。
    • 一个更好的方案是让颜色暂时超过1.0,然后将其转换至0.0到1.0的区间内,从而防止损失细节。
  • 显示器被限制为只能显示值为0.0到1.0间的颜色,但是在光照方程中却没有这个限制。
  • 通过使片段的颜色超过1.0,我们有了一个更大的颜色范围,这也被称作HDR(High Dynamic Range, 高动态范围)
  • 有了HDR,亮的东西可以变得非常亮,暗的东西可以变得非常暗,而且充满细节。
  • HDR原本只是被运用在摄影上,摄影师对同一个场景采取不同曝光拍多张照片,捕捉大范围的色彩值。
  • 这些图片被合成为HDR图片,从而综合不同的曝光等级使得大范围的细节可见。
  • 看下面这个例子,左边这张图片在被光照亮的区域充满细节,但是在黑暗的区域就什么都看不见了;但是右边这张图的高曝光却可以让之前看不出来的黑暗区域显现出来。
    在这里插入图片描述
  • 这与我们眼睛工作的原理非常相似,也是HDR渲染的基础。
  • HDR渲染和其很相似,我们允许用更大范围的颜色值渲染从而获取大范围的黑暗与明亮的场景细节,最后将所有HDR值转换成在[0.0, 1.0]范围的LDR(Low Dynamic Range,低动态范围)
  • 转换HDR值到LDR值得过程叫做色调映射(Tone Mapping),现在现存有很多的色调映射算法,这些算法致力于在转换过程中保留尽可能多的HDR细节。
    • 这些色调映射算法经常会包含一个选择性倾向黑暗或者明亮区域的参数。
    • 在实时渲染中,HDR不仅允许我们超过LDR的范围[0.0, 1.0]与保留更多的细节,同时还让我们能够根据光源的真实强度指定它的强度
    • 比如太阳有比闪光灯之类的东西更高的强度,那么我们为什么不这样子设置呢?(比如说设置一个10.0的漫亮度)
    • 这允许我们用更现实的光照参数恰当地配置一个场景的光照,而这在LDR渲染中是不能实现的,因为他们会被上限约束在1.0。
  • 因为显示器只能显示在0.0到1.0范围之内的颜色,我们肯定要做一些转换从而使得当前的HDR颜色值符合显示器的范围。
  • 简单地取平均值重新转换这些颜色值并不能很好的解决这个问题,因为明亮的地方会显得更加显著。
  • 我们能做的是用一个不同的方程与/或曲线来转换这些HDR值到LDR值,从而给我们对于场景的亮度完全掌控,这就是之前说的色调变换,也是HDR渲染的最终步骤。

浮点帧缓冲

  • 当一个帧缓冲的颜色缓冲的内部格式被设定成了GL_RGB16F, GL_RGBA16F, GL_RGB32F 或者GL_RGBA32F时,这些帧缓冲被叫做浮点帧缓冲(Floating Point Framebuffer),浮点帧缓冲可以存储超过0.0到1.0范围的浮点值,所以非常适合HDR渲染。
  • 想要创建一个浮点帧缓冲,我们只需要改变颜色缓冲的内部格式参数就行了(注意GL_FLOAT参数):
    glBindTexture(GL_TEXTURE_2D, colorBuffer);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL);  
    
  • 默认的帧缓冲默认一个颜色分量只占用8位(bits)。当使用一个使用32位每颜色分量的浮点帧缓冲时(使用GL_RGB32F 或者GL_RGBA32F),我们需要四倍的内存来存储这些颜色。
  • 所以除非你需要一个非常高的精确度,32位不是必须的,使用GLRGB16F就足够了
  • 有了一个带有浮点颜色缓冲的帧缓冲,我们可以放心渲染场景到这个帧缓冲中。
  • 在这个教程的例子当中,我们先渲染一个光照的场景到浮点帧缓冲中,之后再在一个铺屏四边形(Screen-filling Quad)上应用这个帧缓冲的颜色缓冲。
  • 这里场景的颜色值存在一个可以包含任意颜色值的浮点颜色缓冲中,值可能是超过1.0的。
  • 这个简单的演示中,场景被创建为一个被拉伸的立方体通道和四个点光源,其中一个非常亮的在隧道的尽头
  • 渲染至浮点帧缓冲和渲染至一个普通的帧缓冲是一样的。新的东西就是这个的hdrShader的片段着色器,用来渲染最终拥有浮点颜色缓冲纹理的2D四边形。
  • 目前为止,因为我们直接转换HDR值到LDR值,这就像我们根本就没有应用HDR一样。
  • 为了修复这个问题我们需要做的是无损转化所有浮点颜色值回0.0-1.0范围中。我们需要应用到色调映射

色调映射

  • **色调映射(Tone Mapping)**是一个损失很小的转换浮点颜色值至我们所需的LDR[0.0, 1.0]范围内的过程,通常会伴有特定的风格的色平衡(Stylistic Color Balance)。
  • 最简单的色调映射算法是Reinhard色调映射,它涉及到分散整个HDR颜色值到LDR颜色值上,所有的值都有对应。
  • Reinhard色调映射算法平均地将所有亮度值分散到LDR上
  • 我们将Reinhard色调映射应用到之前的片段着色器上,并且为了更好的测量加上一个Gamma校正过滤(包括SRGB纹理的使用):
    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);
    }   
    
  • 有了Reinhard色调映射的应用,我们不再会在场景明亮的地方损失细节。
  • 当然,这个算法是倾向明亮的区域的,暗的区域会不那么精细也不那么有区分度。
  • 现在你可以看到在隧道的尽头木头纹理变得可见了
  • 用了这个非常简单地色调映射算法,我们可以合适的看到存在浮点帧缓冲中整个范围的HDR值,使我们能在不丢失细节的前提下,对场景光照有精确的控制。
  • 另一个有趣的色调映射应用是曝光(Exposure)参数的使用
  • 你可能还记得之前我们在介绍里讲到的,HDR图片包含在不同曝光等级的细节。
  • 如果我们有一个场景要展现日夜交替,我们当然会在白天使用低曝光,在夜间使用高曝光,就像人眼调节方式一样。
  • 有了这个曝光参数,我们可以去设置可以同时在白天和夜晚不同光照条件工作的光照参数,我们只需要调整曝光参数就行了。
  • 一个简单的曝光色调映射算法会像这样:
    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);
    }  
    
  • 在这里我们将exposure定义为默认为1.0的uniform,从而允许我们更加精确设定我们是要注重黑暗还是明亮的区域的HDR颜色值。
  • 举例来说,高曝光值会使隧道的黑暗部分显示更多的细节,然而低曝光值会显著减少黑暗区域的细节,但允许我们看到更多明亮区域的细节。
  • 下面这组图片展示了在不同曝光值下的通道:
    在这里插入图片描述

HDR拓展

  • 在这里展示的两个色调映射算法仅仅是大量(更先进)的色调映射算法中的一小部分,这些算法各有长短。
  • 一些色调映射算法倾向于特定的某种颜色/强度,也有一些算法同时显示低高曝光颜色从而能够显示更加多彩和精细的图像。
  • 也有一些技巧被称作自动曝光调整(Automatic Exposure Adjustment)或者叫人眼适应(Eye Adaptation)技术,它能够检测前一帧场景的亮度并且缓慢调整曝光参数模仿人眼使得场景在黑暗区域逐渐变亮或者在明亮区域逐渐变暗。
  • HDR渲染的真正优点在庞大和复杂的场景中应用复杂光照算法会被显示出来,但是出于教学目的创建这样复杂的演示场景是很困难的,这个教程用的场景是很小的,而且缺乏细节。
  • 但是如此简单的演示也是能够显示出HDR渲染的一些优点:
    • 在明亮和黑暗区域无细节损失,因为它们可以通过色调映射重新获得;
    • 多个光照的叠加不会导致亮度被截断的区域的出现,光照可以被设定为它们原来的亮度值而不是被LDR值限制。
  • 而且,HDR渲染也使一些有趣的效果更加可行和真实; 其中一个效果叫做泛光(Bloom),我们将在下一节讨论。

5.7 泛光

  • 明亮的光源和区域经常很难向观察者表达出来,因为监视器的亮度范围是有限的。
  • 一种区分明亮光源的方式是使它们在监视器上发出光芒,光源的光芒向四周发散。这样观察者就会产生光源或亮区的确是强光区。
  • 光晕效果可以使用一个后处理特效泛光来实现。泛光使所有明亮区域产生光晕效果。下面是一个使用了和没有使用光晕的对比(图片生成自虚幻引擎):
    在这里插入图片描述
  • Bloom是我们能够注意到一个明亮的物体真的有种明亮的感觉。泛光可以极大提升场景中的光照效果,并提供了极大的效果提升,尽管做到这一切只需一点改变。
  • Bloom和HDR结合使用效果很好。
  • 常见的一个误解是HDR和泛光是一样的,很多人认为两种技术是可以互换的。
  • 但是它们是两种不同的技术,用于各自不同的目的上
  • 可以使用默认的8位精确度的帧缓冲,也可以在不使用泛光效果的时候,使用HDR。只不过在有了HDR之后再实现泛光就更简单了。
  • 为实现泛光,我们像平时那样渲染一个有光场景,提取出场景的HDR颜色缓冲以及只有这个场景明亮区域可见的图片。
  • 被提取的带有亮度的图片接着被模糊,结果被添加到HDR场景上面。
  • 我们来一步一步解释这个处理过程。
    • 一、我们在场景中渲染一个带有4个立方体形式不同颜色的明亮的光源。带有颜色的发光立方体的亮度在1.5到15.0之间。
    • 二、我们得到这个HDR颜色缓冲纹理,提取所有超出一定亮度的fragment。这样我们就会获得一个只有fragment超过了一定阈限的颜色区域
    • 三、我们将这个超过一定亮度阈限的纹理进行模糊。泛光效果的强度很大程度上是由被模糊过滤器的范围和强度所决定。
    • 四、最终的被模糊化的纹理就是我们用来获得发出光晕效果的东西。这个已模糊的纹理要添加到原来的HDR场景纹理之上。因为模糊过滤器的应用明亮区域发出光晕,所以明亮区域在长和宽上都有所扩展。
  • 泛光本身并不是个复杂的技术,但很难获得正确的效果。
  • 它的品质很大程度上取决于所用的模糊过滤器的质量和类型
  • 简单地改改模糊过滤器就会极大的改变泛光效果的品质。
  • 首先我们需要根据一定的阈限提取所有明亮的颜色。我们先来做这件事。

提取亮色

  • 第一步我们要从渲染出来的场景中提取两张图片。
  • 我们可以渲染场景两次,每次使用一个不同的着色器渲染到不同的帧缓冲中,但我们可以使用一个叫做**MRT(Multiple Render Targets,多渲染目标)**的小技巧,这样我们就能指定多个像素着色器输出;
  • 有了它我们还能够在一个单独渲染处理中提取头两个图片。
  • 在像素着色器的输出前,我们指定一个布局location标识符,这样我们便可控制一个像素着色器写入到哪个颜色缓冲:
    layout (location = 0) out vec4 FragColor;
    layout (location = 1) out vec4 BrightColor;
    
  • 只有我们真的具有多个地方可写的时候这才能工作。使用多个像素着色器输出的必要条件是,有多个颜色缓冲附加到了当前绑定的帧缓冲对象上。
  • 当把一个纹理链接到帧缓冲的颜色缓冲上时,我们可以指定一个颜色附件。
  • 直到现在,我们一直使用着GL_COLOR_ATTACHMENT0,但通过使用GL_COLOR_ATTACHMENT1,我们可以得到一个附加了两个颜色缓冲的帧缓冲对象:
    // configure (floating point) framebuffers
        // ---------------------------------------
        unsigned int hdrFBO;
        glGenFramebuffers(1, &hdrFBO);
        glBindFramebuffer(GL_FRAMEBUFFER, hdrFBO);
        // create 2 floating point color buffers (1 for normal rendering, other for brightness threshold values)
        unsigned int colorBuffers[2];
        glGenTextures(2, colorBuffers);
        for (unsigned int i = 0; i < 2; i++)
        {
            glBindTexture(GL_TEXTURE_2D, colorBuffers[i]);
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, GL_FLOAT, NULL);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);  // we clamp to the edge as the blur filter would otherwise sample repeated texture values!
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
            // attach texture to framebuffer
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, colorBuffers[i], 0);
        }
        // create and attach depth buffer (renderbuffer)
        unsigned int rboDepth;
        glGenRenderbuffers(1, &rboDepth);
        glBindRenderbuffer(GL_RENDERBUFFER, rboDepth);
        glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, SCR_WIDTH, SCR_HEIGHT);
        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboDepth);
    
  • 我们需要显式告知OpenGL我们正在通过glDrawBuffers渲染到多个颜色缓冲,否则OpenGL只会渲染到帧缓冲的第一个颜色附件,而忽略所有其他的。
  • 我们可以通过传递多个颜色附件的枚举来做这件事,我们以下面的操作进行渲染:
        // tell OpenGL which color attachments we'll use (of this framebuffer) for rendering 
        unsigned int attachments[2] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 };
        glDrawBuffers(2, attachments);
    
  • 当渲染到这个帧缓冲的时候,一个着色器使用一个布局location修饰符,fragment就会写入对应的颜色缓冲。因为我们现在可以直接从将被渲染的fragment提取出它们:
        // check whether result is higher than some threshold, if so, output as bloom threshold color
        float brightness = dot(result, vec3(0.2126, 0.7152, 0.0722));
        if(brightness > 1.0)
            BrightColor = vec4(result, 1.0);
        else
            BrightColor = vec4(0.0, 0.0, 0.0, 1.0);
        FragColor = vec4(result, 1.0);
    
  • 这里我们先正常计算光照,将其传递给第一个像素着色器的输出变量FragColor。
  • 然后我们使用当前储存在FragColor的东西来决定它的亮度是否超过了一定阈限。
  • 我们通过恰当地将其转为灰度的方式计算一个fragment的亮度,如果它超过了一定阈限,我们就把颜色输出到第二个颜色缓冲,那里保存着所有亮部;渲染发光的立方体也是一样的。
  • 这也说明了为什么泛光在HDR基础上能够运行得很好。
    • 因为HDR中,我们可以将颜色值指定超过1.0这个默认的范围,我们能够得到对一个图像中的亮度的更好的控制权。
    • 没有HDR我们必须将阈限设置为小于1.0的数,虽然可行,但是亮部很容易变得很多,这就导致光晕效果过重。
  • 有了两个颜色缓冲,我们就有了一个正常场景的图像和一个提取出的亮区的图像;这些都在一个渲染步骤中完成。
    在这里插入图片描述
  • 有了一个提取出的亮区图像,我们现在就要把这个图像进行模糊处理。
  • 我们可以使用帧缓冲教程后处理部分的那个简单的盒子过滤器,但不过我们最好还是使用一个更高级的更漂亮的模糊过滤器:高斯模糊(Gaussian blur)

高斯模糊

  • 关于高斯模糊的具体实现参考代码即可
  • 此部分代码可以加深对于帧缓冲以及之前相关概念的理解
  • 这个教程我们只是用了一个相对简单的高斯模糊过滤器,它在每个方向上只有5个样本。
  • 通过沿着更大的半径或重复更多次数的模糊,进行采样我们就可以提升模糊的效果。
  • 因为模糊的质量与泛光效果的质量正相关,提升模糊效果就能够提升泛光效果。
  • 关于“提升高斯模糊来显著提升泛光效果”,请看官网附加资源
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值