上一次實作了法線與視差貼圖,也玩了一遍TBN矩陣,這次接著實作HDR與Bloom
Advanced Lighting - HDR
float point frame buffer
- 一般的螢幕,每個像素的顏色範圍只能介於0到1之間,這個範圍又稱為LDR(low dynamic range)。
- 在做光照時的各種計算時,實際上並不期待輸出一定會介於0到1之間,我們需要更大的範圍的值才能表達出不同類型的光照強度(尤其是到了PBR時代)。這時候就會需要讓畫面能在HDR(high dynamic range)的範圍內做計算,
- 首先要先讓Pixel Shader輸出的值能超過0~1,所以要從frame buffer下手,預設的GL_RGBA,每個channel有8bit,範圍為0-1,將internal format改成GL_RGBA16F或32F就可以將這個frame buffer視為float point framebuffer,這樣用pixel shader渲染到該frame buffer時,就可以儲存HDR範圍的值了,
改完之後立刻來實驗看看 :
設定8個光源,各自輸出一個隨機顏色,每個顏色的channel值介於0~3之間

除了前排(左2)之外,幾乎都是白色的,這是因為雖然frame buffer已經改為可以HDR範圍,但是螢幕本身還是LDR的,
- 單單只是改Frame buffer並不夠,改成float point frame buffer讓pixel shader能在輸出HDR範圍的顏色後,要在顯示到畫面前透過Tone Mapping的方式將整個HDR的畫面重新映射到LDR的範圍內,再顯示到螢幕上。
Tone Mapping
Tone Mapping的工作就是把HDR的數值重新映射回0到1的範圍內,映射的公式有很多種。目前這個小專案就是照抄LearnOpenGL的Code,
最後拿上面八個光源的場景再測試一次,可以看出現在能夠看出不同光源間各自不同的顏色了。

所以簡單來說渲染一個HDR畫面的流程就是
pixel shader輸出到一個支援HDR的frame buffer
→
回到default frambuffer後HDR frame buffer當成貼圖,貼到一個全螢幕的quad上做Tone Mapping(映射回0~1範圍)
→
Gamma Correction
→
螢幕顯示。
嘗試不同的Tone Mapping
UE預設有Filmic ToneMapping,Unity預設則是有兩種(Neutral跟ACES)可以選擇(這兩種我都不太不喜歡…),有公式的話其實就是一個post effect的效果,要自己加進專案是很容易的。之前在Unity做卡通渲染有試過跑車浪滿旅使用的曲線(已經有人實作進Unity → https://github.com/yaoling1997/GT-ToneMapping),效果還不錯。
雖然筆記短短的,不過Tone Mapping是現在渲染遊戲時非常非常重要的一環,適合的Tone Mapping可以讓畫面提升不只一個檔次,一個醜醜的Unity專案配上適合的LUT跟Tone Mapping其實會瞬間讓畫面品質提升非常多,對馬戰鬼的開發者在SIGGRAPH簡報中也有介紹他們針對場景環境做出不同的Tone Mapping與LUT組合
Advanced Lighting - Bloom(泛光)
LearnOpenGL實作完HDR,接著實做過去被濫用到爆開的Bloom後處理,Bloom透過將高亮度物體的顏色向外暈開做出光暈,視覺上就會產生這個物體會更加明亮的感覺。

UE的bloom
Bloom這個效果並不跟HDR綁定,也就是說你可以完全不用float point frame buffer的同時來實做bloom,但是Bloom很適合跟HDR一起用,HDR畫面→開Bloom→Tone Mapping,就可以防止過曝以及過曝區域過大的發生。
實做
Bloom的原理超簡單,其實就是把需要有光暈的部分輸出成另一張貼圖,然後對這貼圖做模糊,接著將模糊後的貼圖疊加到畫面上。流程如下圖

實做bloom有兩個地方的code要特別處理,繪製高亮區mask以及實作模糊。
這章節在繪製mask的部分使用MRT(Multiple Render Targets),直接讓frame buffer object綁定兩個輸出的渲染對象,這樣就不需要再寫額外的pass去畫mask了,在bind這個frame buffer後,pixel shader內可以定義多個out變數來將畫出的結果輸出到不同的渲染對象中。
MRT
OpenGL的code
Pixel Shader中可以定義多個out
最後把畫面上的RGB顏色轉換成Luminance來當成是否輸入到Mask的依據
這裡的公式可以在wiki上找到說明,手工一點的話也可以用自定義的,比如將mask值存在vertex color或者貼圖中來指定bloom的範圍。
在Unity URP中,有個ConfigureTarget可以塞多個render target給它,來達成MRT
Blur Pass
模糊這種效果基本上會用到影像處理中的convolution kernal,
在繪製每個pixel時,對四周的pixel(根據kernal大小)各做一次採樣,並且將每個採樣的結果乘上kernal上對應的權重在全部相加,結果仍然會等於一,下面這張是一個隨便找的kernal範例XD

高斯模糊使用的就是…高斯模糊的kernal,照抄網路上數值就行,另外高斯模糊的kernal是可以拆成一維的,比如說今天有一個5x5的kernal,如果在一個shader draw內就要完成,每個pixel要計算5*5次,而如果將模糊這個步驟拆成兩個pass,一個pass只負責kernal上橫向的採樣,另一個pass負責縱向的,這樣對一次完整的模糊來說,每個pixel合計只要計算5+5次,可以節省非常多效能。
對GPU來說這種2 pass的做法,是很常見的技巧,因為比起一個肥厚的for loop,有時候將計算拆成多個pass處理更符合GPU的特性
把那張bloom mask跑完上面這個for loop之後就可以取得模糊後的結果了。
如果是HDR的,只要再配合Tone Mapping整個畫面就算大功告成了。

結論
可以看出影響bloom效能與結果的關鍵就是模糊的方式了,模糊的方法很多,可以參考毛星雲大大整理的這篇。另外近年UE4有推出基於FFT的bloom,甚至可以控制光暈的形狀,滿厲害der。
最後,緬懷一下bloom被濫用的年代的遊戲節圖 (Need For Speed : Most Wanted 2005)

參考資料