HDR (automatic exposure control + Tonemapping + Bloom)

							            <div class="markdown_views">
						<!-- flowchart 箭头图标 勿删 -->
						<svg xmlns="http://www.w3.org/2000/svg" style="display: none;"><path stroke-linecap="round" d="M5,0 0,2.5 5,5z" id="raphael-marker-block" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path></svg>
						<p>前段时间在VR项目里实现了PCF+VSM之后,为了让阳光照射的区域看起来亮一些,降低了阴影区域的亮度,明暗区域的对比度确实高了,但是室内就有些过于暗了,于是寻思着得把HDR模块加上了。</p>

一般我们说的HDR其实是代指,automatic exposure control + Tonemapping + Bloom, 先根据场景的一帧计算出平均亮度,如果偏暗就加亮一些,反之亦然,调整好亮度之后再调整灰度,让明部跟暗部保持更多的细节,最后对高光部分做个Bloom,看起来更真实。

Bloom

在现实世界中 ,很亮的灯周围都会有一圈模糊的光晕,Bloom就是指这个光晕。我的HDR处理是在计算完光照跟阴影之后进行的,光照着色的输出为一张RGBA16F格式的纹理,注意这里不能再用RGBA8了,用这张纹理作为HDR的输入。

首先进行计算的是平均亮度,这里有两种计算方式,一是利用RTT的方式,渲染到一张一半长宽的纹理里去,循环这个操作,直到纹理只剩1x1像素,这个像素就是场景所有像素的平均了,这里要注意的一点是,RTT的时候,原纹理的过滤方式一定要设为Linear,这样渲染出来的半尺寸纹理才是原纹理的平均。方式二是利用ComputeShader,这个实现跟RTT类似,但是速度会快上一倍左右,因为VR程序非常非常考验性能,我选择的就是这种方式,这里重点说下这个。

第一个问题就是原纹理的尺寸不是2的N次幂,而且会根据用户设定变化,这种无规律的尺寸在ComputeShader里面处理起来比较麻烦,我们需要先把它规格化一下,一般的方案是RTT到一张长宽分别是 离原纹理的一半最近的2的N次幂的纹理上,比如1733*1733就应该规格化到 1024*1024的纹理上。尺寸规范化之后,就可以很方便的用ComputeShader计算平均亮度了,这里面有个技巧,就是可以利用GPU的并行操作进一步加快计算过程,具体参见这里:《AVERAGE LUMINANCE CALCULATION USING A COMPUTE SHADER》
使用sRGB计算亮度的公式是这个:

float lum = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b;
  
  
  • 1

这个公式其实是色彩空间转换的一部分,在CIE XYZ色彩空间中,Y分量代表的就是亮度,而完整的sRGB转换成CIE XYZ的公式是张这样的:
sRGB to CIE XYZ
ComputeShader中的第一步,就是计算出这个亮度,然后再对这个亮度做平均。其实亮度并不是线性的,对非线性的值直接做平均,最后得到的平均值并不准确,可以通过ln(lum)把亮度转化成线性的,然后对最后的全场景平均值再用exp(lum)转化回来,结果会更精确,不过我实际测试的结果来说,做不做这一步并没有明显的区别。另外一点需要特别提出来的,是场景画面中央的部分应该具有高权重,而边缘应该低些,总权重保持为1.我是在场景计算成16*16的时候引入的权重,中央4*4个像素具有更高的权重。

上一步执行完毕之后,我们会拥有一张1*1像素的平均亮度纹理,这个值别读到CPU端来,因为读取会造成渲染循环阻塞,非常的影响性能。只需要在需要使用的时候,用采样器采样就行了,采样坐标注意要设为(0.5,0.5)。有了当前场景的平均亮度,我们要计算一下适配亮度,人眼适应光线变化是一个渐进的过程,所以我们不能单纯的根据当帧的平均亮度来决定场景的亮度,需要引入上一帧的平均亮度,在两个亮度之间做一个插值。计算公式如下:

 float adaptedLum = lastLum + (currentLum - lastLum) * (1.0-pow(0.98, 30*elapesdTime));
  
  
  • 1

其中,elapesdTime是上一帧的耗时, 30是个经验参数,希望亮度变化更快可以加大这个值。另外一个就是需要加上一句类似如下功能的代码防止极端情况发生:

 adaptedLum = clamp(adaptedLum, 0.3, 0.7);
  
  
  • 1

同样,0.3跟0.7是经验参数;有了适配亮度之后,就可以进行场景的亮度校正了,我们希望场景不过暗,也不过亮,也就是希望亮度刚好在0.5左右,因此我们可以利用如下公式来计算曝光度:

float exposure = 0.5 /  adaptedLum;
  
  
  • 1

公式内的0.5可以弄成一个传入参数,以便根据用户期望来调整场景的亮度。将输入纹理的每一个像素都乘上exposure,自动曝光也就完成了。一般在这个shader里面,还会同时进行ToneMapping,这个比较简单,业内用的最多的就是Filmic Tone Mapping,效果是很不错的,计算方式如下:

float3 F(float3 x)
{
    const float A = 0.15f;
    const float B = 0.50f;
    const float C = 0.10f;
    const float D = 0.20f;
    const float E = 0.02f;
    const float F = 0.30f;
    return ((x * (A*x + C*B) + D*E) / (x * (A*x + B) + D*F)) - E/F;
}

float3 FilmicToneMapping(float3 color, float exposure)
{
    const float WHITE = 11.2f;
    return F(exposure * color) / F(WHITE);
}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

好了,只剩下Bloom了。传统的Bloom的流程是先利用场景的原始图片作为输入,提取出高光部分,然后做多次高斯模糊,再把模糊了的纹理叠加回去。这里有个示范:https://learnopengl.com/#!Advanced-Lighting/Bloom,这么做效果是很好的,不足之处就是性能影响有些大了,想要Bloom效果明显,需要以不同的Kernel尺寸进行多次高斯模糊,这个过程非常非常耗性能。在VR里面每一点性能都不能浪费,那么有没有效果不错,性能也好的方案呢? 有的:《How to do good bloom for HDR rendering》一文中,作者提到在以5×5, 11×11, 21×21, 41×41为Kernel的高斯模糊之后,Bloom才有了一个不错的效果,但是41*41的高斯模糊,想想都觉得可怕,于是提出了另一种方案,降低原图的分辨率,降低一倍原图分辨率就相当于Kernel加大一倍,由于分辨率降低,速度更是进一步提高,最后多张分辨率不一的图片叠加起来,达到的效果跟在原图上以不同Kernel模糊出来的效果是差不多的!

我这里以5*5为核心,sigma为4作为高斯模糊参数,对原始图片的1/2长宽的level0, 1/4的level1, 1/8的level2,1/16的level3,四层图片进行了模糊,然后用加法直接叠加在了原图上,实现了Bloom效果。最后这一步我有了些改进,直接加上去,高光区域的颜色会过饱和,看起来油油的,而且会导致亮部细节丢失,白茫茫一片什么也看不清。于是我引入了一个机制,亮度越高的像素,加入的Bloom分量越少。最后完美解决了这个问题,计算过程如下:

// vec3 sceneColor:做过自动曝光和ToneMapping之后的像素颜色
// vec3 bloomColor : Bloom颜色
float linsetp(float _min, float _max, float v)
{
    return clamp((v - _min) / (_max - _min), 0.0, 1.0);
}

vec3 LUMINANCE_VECTOR = vec3(0.2126, 0.7152 , 0.0722);
float lum = dot(sceneColor, LUMINANCE_VECTOR);
sceneColor += bloomColor * (1 - linstep(0.4, 0.8, lum));
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

高斯模糊的代码参见:《Fast and beautiful blur filter / shader recommendations?》中,JobLeonard 的回答,类似代码很多,毕竟高斯模糊是很通用的算法了。

我机器是I7-6700+GTX960的显卡,整个HDR过程耗时在1.6ms左右,场景的平均亮度我并不需要每帧都更新,每秒更新20次就足够了,就算是10次也没什么问题,这样的话亮度计算耗时基本可以忽略,进一步提高了性能。

原图
原图↑

无Bloom
自动曝光+ToneMapping↑

HDR
HDR↑

从前往后数第二张桌子的Bloom效果最明显了,有种在发光的感觉,其实黑色环境里的灯的Bloom效果是最明显的,可惜外网就只有这个场景。

猜你喜欢

转载自blog.csdn.net/Real_Myth/article/details/82906669
HDR
今日推荐