HDR (automatic exposure control + Tonemapping + Bloom)
一般我們說的HDR其實是代指,automatic exposure control + Tonemapping + 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計算亮度的公式是這個:
- 1
這個公式其實是色彩空間轉換的一部分,在CIE XYZ色彩空間中,Y分量代表的就是亮度,而完整的sRGB轉換成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次也沒什么問題,這樣的話亮度計算耗時基本可以忽略,進一步提高了性能。
原圖↑
自動曝光+ToneMapping↑
HDR↑
從前往后數第二張桌子的Bloom效果最明顯了,有種在發光的感覺,其實黑色環境里的燈的Bloom效果是最明顯的,可惜外網就只有這個場景。
總結
以上是生活随笔為你收集整理的HDR (automatic exposure control + Tonemapping + Bloom)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Ubuntu 16.04设置IP、网关、
- 下一篇: ubuntu下docker使用GPU