分享

shader复习与深入:HDR(高动态范围)

 LVADDIE 2015-07-09

HDR(high dynamic range)应该算是很常见的图形后处理手法了。通俗点,就是让场景中光亮的部分更加光亮,暗的地方更加暗,在计算机的常态亮度范围(0-255)上模拟高光效果。如果这个高光还带点眩晕效果,那就颇绚颇“HDR”了。这些本想在09年末来写的,奈何不觉又穿越了那么久了。——ZwqXin.com

本文来源于 ZwqXin (http://www./), 转载请注明
      原文地址:http://www./archives/shaderglsl/review-high-dynamic-range.html

某个拍照的时候,通过快门光圈造成的过度曝光,可能会让照片更加有魅力;隧道出口处的光芒总是那么涣散人心。如果要在实时场景中实现这些效果,就要考虑两件事:怎么突出画面的明暗对比度,以及怎么伪造出那种光线给视觉造成的似隐若幻的模糊效果。我们常说的画面HDR其实(至少)是这两种效果的混合,前者是HDR的本意,映射高光的范围(最大亮度值大于255)到(0-255),同时做到不让人觉得太线性太均衡——所谓的tone-mapping;后者是HDR的“辅助技能”,其实也就是远在HDR之前就为人熟悉的Bloom——通过模糊画面造成柔化的效果。

shader复习与深入:HDR(高动态光照)

在当今图形学上,这是一种post-processing(后处理)。我们没必要去与光照打交道,而只不过是对每一个渲染帧做一些加工工作。跟shadow-map[Shadow Map阴影贴图技术之探Ⅰ] 类似,我们给原帧叠加上一层“HDR高光眩晕层”,而问题在于这个“高光眩晕层”怎么得出来。按上所述,这要通过两个主要步骤,按其先后顺序:bloom和tone-mapping。当然也可以先进行高光映射再模糊映射后的画面,但很多时候我们想模糊的只是那产生高光的部分,又为了让结果更加自然,所以先处理(提取)画面亮度信息,模糊,把tone-shading放在处理链的最后。

另外说一下,通常视觉的变化会导致场景中某一部分高光的突出。场景HDR导致该点过度曝光,譬如上图对比中,原图正上方位置的不明物体被高光“遮盖”了,这是因为视觉从上往下有一定偏角。而如果从别的角度看,譬如直视,高光就不会在该点太过集中,这样细节就出来了。这跟我们日常的情况相似:

shader复习与深入:HDR(高动态光照)

1.亮度信息

我们这里说的亮度可以直接用灰度来表达[基于亮度的图像二值化处理] 。这种转换是比较简单的,也适合shader来做:

IntenSity = 0.2990 * R + 0.5870 * G  + 0.1140 * B

我们待处理的原始帧图像的亮度必然是(0-255),在shader中表现为(0-1)。我们要映射出伪高光(亮的部分更加光亮,暗的地方更加暗),首先要知道的就是当前帧所有可见像素的一个亮度均衡值——简单点,就取平均亮度吧。怎么取平均亮度?当然是把总有亮度值加起来再除以总数啦。呵呵,先不说要预先另外准备一个pass,这计算量可不是盖的,尤其是大屏幕……于是我们可以投机一下,把原帧图像不断下采样(down-sample),以降低“分辨率”,甚至到最后采样到一个像素,直接取其亮度为平均亮度?这跟纹理的mapmap很类似嘛。理论上是这样的。但是呢,首先,汲取前人的失败经验,我们不要依赖mipmap了,也不要直接把帧图像纹理映射到小矩形上,好好地进行正规下采样处理——box-filtering(⑨领域滤波[图像处理里的空间域滤波] )吧。鉴于在shader中box-filtering的代价(新建pass啊多次纹理采样啊亲)也颇大的,所以建议不要做得太绝了(什么下采样到一个像素的)。

最开始的pass,把原帧渲染进一个纹理FBO(注意,作为输入的渲染帧的颜色值没必要规范化,也就是说允许计算的亮度值大于1,为了不让OpenGL实现自动把RGBA值Clamp到0-1区间,所以最好使用浮点纹理);第二个pass,给一个四分之一于原帧大小的屏幕矩形,shader里下采样一次,结果存入另一个FBO;第三个,再一次四分之一你懂的……这样你觉得“足够”了之后,就把当前FBO中的数据(4N分之一于原帧图像的下采样样本)取出来,变成亮度后加合平均一下啦(你问怎么取得数据,这个可随便啦。我一般是预先给该FBO中的纹理数据分配一个PBO去跟踪一下)。得到平均亮度后,tone-mapping前,模糊吧。

2.Bloom

提到模糊效果,很多千奇百怪的图像处理filter都可以拿出来了。但最实用简单的,我想,就是那高斯模糊(GaussionBlur)了吧。虽然一次纵向处理+一次横向处理两个pass也是花费不菲的……还有一点就是上面提及的,仅取高光部分来模糊(就是另用一个pass预先筛选亮度高于某个值的像素,再对之模糊),这个是可选了,视效果而定。再一点,就是如果实在觉得奢侈的话,可以利用上面那些4N分之一的采样样本作为输入源(还是那句,视效果而择好了),毕竟我们最后需要的也就渲染结果到一张纹理,这个bloom纹理。

3.Tone-mapping

最核心的部分。作为最后一个pass,现在我们准备一张原帧图像纹理,一张bloom纹理,一个平均亮度值AverageIntensity。tone-mapping本质上就是亮度映射:

Scaler = KeyIntenSity / AverageIntensity

这个Scaler就是一个比率,一头是实际亮度的均值AverageIntensity,另一头是我们的控制参数KeyIntenSity——它对应于我们可显示亮度范围的均值。这就好似角度转弧度时的“ PI / 180”,通过选取映射双方的系统某个特定值的比率,作为双方量域转化的比率。在大范围暗的场景中,我们可以把这个值设大一点,这样图像中占大部分的低亮度像素就可以映射在一个比较大的范围内,反之亦然。当然,这个值不一定需要规范到(0-1)区间的,但对于HDR而言,大部分常态场景,这个值以(0 - 0.5)之间为宜。这个值的算法基础详见《Photographic tone reproduction for digital images》这篇多年前的论文,该文称之为Key,一般场景以0.18为论(在伽马校正理论中,0.18经过校正后大概是0.5,也就是我们感官上的中等灰度级)。

以IntenSity是根据原帧图像纹理得的当前像素亮度值,则映射后的亮度值为:

ScaleredIntenSity = Scaler * IntenSity

这样,程序中就可以更自由控制“曝光”的程度了,而不是完全交由画面去决定。接下来是怎么把这个值规范化到(0-1):

IntenSityFin = ScaleredIntenSity / (1 + ScaleredIntenSity)

类似这样的规范化式子被称为tone-maping operator,作为最终决定范围映射结果的一步,不同的operator对于最终图像的细节反映程度有一定的影响。像上面这个operator(见于《Photographic tone reproduction for digital images》),对高光部分的影响(由未知大亮度趋向1.0)比暗部分(越接近0越不受限)的影响大,这样能尽量避免暗部细节的丢失的前提下,尽量把高光部分细节的层次感表达出来。针对不同情况选用不同的tone-mapping operator是比较英明的,但是这个就是很大很深的领域了。譬如最近比较有名的Filmic tone mapping(见此文章:Filmic Tonemapping Operators -http:///archives/75),对高亮和低亮的两端都给予一定的调和,这样留给暗部的“表达空间”增多些些,在一些有强烈光暗对比的场景上保留细节得更好一些。

最后采用类似specular-map的方式,把这个tone-shading结果值,与bloom纹理的采样值相加。输出屏幕。fragment-shader(用的上面公式所示operator):

GLSL代码 (fragment-shader)
  1. #version 130  
  2. #extension GL_EXT_gpu_shader4 : enable  
  3.   
  4. uniform sampler2D   basetex;  
  5. uniform sampler2D   bloomtex;  
  6.   
  7. uniform float fAvgLum;  
  8. uniform float fDimmer;  
  9.   
  10. varying vec2 varying_texcoord;  
  11.   
  12. out vec4 FragDataScene;  
  13.   
  14. void main(void)  
  15. {  
  16.    vec4 texCol = texture2D(basetex, varying_texcoord);    
  17.   
  18.    float vLum = 0.27 * texCol.r + 0.67 * texCol.g + 0.06 * texCol.b;  
  19.   
  20.    float vLumScaled = fDimmer * vLum / fAvgLum;  
  21.   
  22.    texCol = vLumScaled * texCol;  
  23.   
  24.    texCol = texCol / (vec4(1.0) + texCol);  
  25.    
  26.    vec4 texBloom = texture2D(bloomtex, varying_texcoord);    
  27.   
  28.    FragDataScene = texBloom + texCol;   
  29. }  

在这篇文章中有更详细的讲述:HDR渲染器的实现。里面还提及了通过计算亮度的自然对数后再取平均的,这个我就不太深入了。最后是一图流:

shader复习与深入:HDR(高动态光照)

具体来说,HDR的大体算法不算复杂,也应该比较容易理解。不过要动用那么多PASS那么多FBO那么多Shader什么的,果然还是一个很麻烦的效果。

本文来源于 ZwqXin (http://www./), 转载请注明
      原文地址:http://www./archives/shaderglsl/review-high-dynamic-range.html

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多