大萌喵的Unity後期特效第一發---鏡頭炫光與光暈(Flare)

哈羅大家好, 我是大萌喵, 一隻對渲染與著色器技術十分著迷的學生黨. 我之所以突然間想辦一個專門講解著色器技術的專欄, 其一是因為國內的相關資料少之又少, 其二是作為一個學生, 我的不足之處還有很多, 通過寫文章可以排除很多模稜兩可的知識點, 其三是同時還能和知乎上的更多開發者交流, 相互促進 ~

鏡頭炫光和光暈是個啥?

我們都知道, 光在傳播的過程中經過兩種折射率不同的介質交界面時, 會發生折射和反射現象. 當折射和反射達成某種角度, 尤其是光比比較大時, 反射光和折射光可以交匯形成炫光. 在多數情況下, 這並不是一個好的現象, 因為這導致被觀察物體的表現形式失真. 現在大多數鏡頭的鏡片上都會有各種各樣的鍍膜, 為的就是減少炫光的出現.

如上圖, 這就是一個標準的鏡頭炫光的結果. 這種結果當然不會是我們想要的.

當然了, 如果使用得當, 鏡頭光暈也可以像這樣 ...

那麼, 我們為什麼要做一個鏡頭炫光的特效呢?

第一, 雖然鏡頭炫光是人造效果, 但是它能夠增加一張圖片的動態範圍, 使其更加直觀和清晰地闡述明亮度.

第二, 鏡頭炫光給人一種意境化的感覺, 使用得當可以讓人更加身臨其境.

第三, 鏡頭炫光看起來真的很 ...

所以說我們要幹啥?

根據一個RenderTexture(一般簡稱為RT)的顏色信息, 通過後期圖像特效(Image Effect)計算產生一張帶有其鏡頭光暈的RT, 然後將後產生的RT合併到原本的RT中通過屏幕輸出.

文章中介紹的做法不是基於物理的, 但是性能開銷非常廉價, 可以應用到移動端上. 效果也還可以.

想看懂這篇文章, 我得知道啥?

為了能夠完全讀懂大萌喵寫的東東, 您只需要知道Image Effect的基本原理就足夠了.

看完了這篇文章, 我能得到啥?

你將知道我是怎麼利用屏幕後期特效實現鏡頭光暈效果的. 在我講解的過程中可能會有些名詞或方法你之前並不了解, 我也會提供相應的輔助手段幫助你了解.

你也會獲得完整源代碼. 但是如果之前您沒實現過類似的屏幕特效, 那麼我個人強烈建議您在看完整源代碼前自己動手實現一遍.

最後, 如果您有任何話題想要和大萌喵交流, 大萌喵都是歡迎噠歡迎噠歡迎噠!

大萌喵.不正常模式.SetStatus(false);

大萌喵.SetFace (Face.嚴肅臉);

著色器特效處理流程

第一步, 根據一個閾值提取圖像中的所有明亮度高的像素, 並將結果適當降採樣(DownSample)以提升時間效率. 不了解降採樣的童鞋可以通過搜索引擎了解一下. Unity的Standard Assets中的後期特效中有降採樣的源代碼.

第二步, 基於第一步得到的RT(如果你不知道RT是什麼, ctrl+F一下就行), 計算出對應的鬼影位置.

第三步, 對第二步得到的RT進行高斯模糊, 而後進行星射線採樣處理, 得到真正的鏡頭炫光.

第四步, 將第三步的RT和原有ColorBuffer進行混合, 投射到屏幕上.

第一步: 降採樣和像素提取

降採樣是為了以犧牲圖像質量為代價來降低後續操作的性能開銷. 在後面的操作過程中我們涉及到了高斯模糊, 而顧名思義反正早晚要模糊, 那乾脆輸入一個本來就有點糊(注意, 我說是"有點"), 但是尺寸小很多的RT該多麼划算. 因此可以先用將原圖採樣到一個尺寸更小的圖片中的方式來壓縮原圖. 思路是使用一個長寬等比例縮小k倍的RT, 在著色器實現過程中每隔原圖的k個像素點採樣一次, 放到新的降採樣RT中. 具體到程序實現的話可以如此理解: 將降採樣RT"強行"拉伸到原圖的尺寸, 那麼相鄰兩個片元的UV坐標差距也就增加到了原來的K倍. 因此我們可以利用降採樣RT片元的UV坐標來採樣原圖. 為了保證降採樣後的效果, 我們將原圖對應部分的上下左右四個像素點求平均值進行輸出. (在Unity官方Shader中是採用對應部分像素點和其左邊, 右邊, 左下角共四個像素點求平均值, 在最終的視覺效果上感覺也沒什麼差異, 但是取上下左右似乎更符合"對稱"的強迫症思想).

在降採樣後, 我們需要從中提取一些明亮度較高的像素. 因為我們只希望將圖片中特別明亮的部分進行鏡頭光暈化的處理. 具體做法就很簡單了, 降採樣後的顏色像素RGB超過閾值的部分提取出來.

struct v2f_DownSamplent{nttfloat4 pos : SV_POSITION;ntthalf2 uv20 : TEXCOORD0;ntthalf2 uv21 : TEXCOORD1;ntthalf2 uv22 : TEXCOORD2;ntthalf2 uv23 : TEXCOORD3;nt};nntv2f_DownSample vert_DownSample ( appdata_img v )nt{nttv2f_DownSample o;nntto.pos = mul ( UNITY_MATRIX_MVP, v.vertex );nntto.uv20 = UnityStereoScreenSpaceUVAdjust ( v.texcoord + _MainTex_TexelSize * half2 ( 0.5h, 0.5h ), _MainTex_ST );ntto.uv21 = UnityStereoScreenSpaceUVAdjust ( v.texcoord + _MainTex_TexelSize * half2 ( -0.5h, 0.5h ), _MainTex_ST );ntto.uv22 = UnityStereoScreenSpaceUVAdjust ( v.texcoord + _MainTex_TexelSize * half2 ( -0.5h, -0.5h ), _MainTex_ST );ntto.uv23 = UnityStereoScreenSpaceUVAdjust ( v.texcoord + _MainTex_TexelSize * half2 ( 0.5h, -0.5h ), _MainTex_ST );nnttreturn o;nt}nntfixed4 frag_DownSample ( v2f_DownSample i ) : COLORnt{nttfixed4 color = tex2D ( _MainTex, i.uv20 ) + tex2D ( _MainTex, i.uv21 ) + tex2D ( _MainTex, i.uv22 ) + tex2D ( _MainTex, i.uv23 );n treturn max ( 0.0, color / 4 - _Threshold ) * _Intensity;nt}n

降採樣部分的C#源碼:

int rtWidth = source.width >> downSampleNum;nint rtHright = source.height >> downSampleNum;nRenderTexture downSampleBuffer = RenderTexture.GetTemporary(rtWidth, rtHright, 0, source.format);ndownSampleBuffer.filterMode = FilterMode.Bilinear;n//Shader的第0個Pass對應的點元著色函數是vert_DownSample, 片元著色函數是frag_DownSamplenGraphics.Blit(source, downSampleBuffer, material, 0);n

成果如下:

(原圖)

(提取高亮顏色)

(降採樣後)

第二步: 計算鬼影

鬼影是啥? 聽起來好恐怖的樣子, 不會讓Unity崩潰吧?

簡單來說, 我們要實現的鬼影就是將第一步中得到的RT的明亮部分錯位重複渲染幾次, 達到模擬多層鏡片反射的效果. 在這裡我們默認對稱中心就是我們的原圖圖像中心(當然因此也就同時是降採樣後的RT中心).

OK, 我們先來個簡單的版本, 只渲染一個鬼影, 和原本的像素位置相對於圖片中心呈中心對稱. 實現上很簡單, 重新採樣一次然後和混合相加就好:

(請和第一步的圖二做對比)

這似乎有點太無聊了, 我們應該再加幾個鬼影才能實現模擬多次反射的效果. 但是我們都知道, 在真正的鏡頭中, 這種鬼影越趨近於反射中心越小, 越靠近鏡頭邊緣則越大, 相當於在中心對稱的過程中"又多了一小段距離". 因此我們應該引入一個鬼影發散率dis, 用以表示第i層鬼影相對於i-1層的位置和大小的偏離情況.

具體到實現上, 我們可以從當前像素點向畫面中心連一個向量v, 很容易發現v * dis * i就是當前鬼影的偏離向量. 採樣後相加, 我們就得到了這樣的結果:

程序如下:

v2f_Simple vert_Simple ( appdata_img v )nt{nttv2f_Simple o;nntto.pos = mul ( UNITY_MATRIX_MVP, v.vertex );nntto.uv = v.texcoord.xy;nnttreturn o;nt}nntfixed4 frag_Ghost ( v2f_Simple i ) : COLORnt{ntthalf2 newUV = half2 ( 1.0h, 1.0h ) - i.uv;ntthalf2 ghostVector = ( half2 ( 0.5h, 0.5h ) - newUV ) * _GhostDispersal;nttfixed4 finalColor = fixed4 ( 0, 0, 0, 0 );nttfor (int ii = 0;ii < _GhostNum;ii++)ntt{nttthalf2 offset = frac ( newUV + ghostVector * float ( ii ) );ntttfloat weight = length ( half2 ( 0.5h, 0.5h ) - offset ) / length ( half2 ( 0.5h, 0.5h ) );n ttweight = pow ( 1.0 - weight, 1.0 );n ttfinalColor += tex2D ( _MainTex, offset ) * weight;ntt}nttreturn finalColor * tex2D ( _Gradient, length ( half2 ( 0.5h, 0.5h ) - newUV ) / length ( half2 ( 0.5h, 0.5h ) ) );nt}n

第三步: 模糊

哇卡卡卡卡卡鬼影都計算完了! 讓我們趕快糊上去看看是什麼效果吧!

在辦專欄之前, 我認真看了專欄守則, 文章中不允許出現污言穢語的.

好吧, 那我就無法評論這個結果了, 我們繼續吧.

問題出在哪?

太寫實了! 我們要的是光斑, 光暈, 和炫光, 但是這個結果叫鏡面反射!

那麼我們接下來要做的事情就很明確了: 輸入一個稜角分明的RT, 輸出一個模模糊糊帶著光斑的RT.

很好, 我猜你也想到了高斯模糊(Gaussian Blur).

關於高斯模糊我不打算詳細介紹, 網上的資料已經足夠多了. 在這裡我貼上淺墨大大的CSDN博客, 大家想要詳細了解高斯模糊的話可以移步查閱, 淺墨大大講得可比我好多啦 ~

代碼我也就不貼了, 畢竟一大坨, 而且長得也和淺墨大大的代碼差不多. 成果如下:

第四步: 將第三步得到的RT和sourceRT混合, 輸出

我們將第三步得到的RT設置為著色器的最終鬼影紋理, 並單獨在一個pass中將原圖和鬼影進行混合.

fixed4 frag_Flare ( v2f_Simple i ) : COLORnt{nttreturn tex2D ( _MainTex, i.uv ) + tex2D ( _Flare, i.uv );nt}n

然後我們就得到了下面的結果:

雖然看起來有點奇怪, 不過相比於之前還是正常太多了.

但是, 我們不希望它看起來奇怪

幹嘛又是這張圖? 因為這張圖雖然丑, 但是它的光暈看起來很正常. 所以能給我們一點提示.

首先, 這個光暈受一種"星射線"的影響, 給人以一種朦朧, 顫抖的感覺. 因此, 我們可以考慮為模糊後的鬼影加上一層StarBurst採樣.

再者, 通過觀察我們發現, 光暈的藍色和黃色(其實是綠色)的部分集中在中央, 而紅色則擴散到了整個屏幕的各個角落. 這是因為鏡頭對不同波長的光有不同的折射效果. 為了實際地表現出這種效果, 我們需要做兩件事情:

第一, 在計算鬼影的過程中, 應該允許RGB各自以不同的縮放率進行採樣.

第二, 在鬼影計算完成後, 加入一個鏡頭徑向採樣紋理, 與鬼影相混合.

(徑向採樣紋理, 這張紋理意味著靠近屏幕中央的鬼影更加偏向紅色, 越向外越開始靠近綠色, 藍色, 依次往複類推)

為了做到第一步, 我們修改下第二步中鬼影的採樣即可.

finalColor += GetDistortedColor ( offset, normalize ( ghostVector ) ) * weight;n

fixed4 GetDistortedColor ( half2 uv, half2 dir)n {n treturn fixed4 (n tttex2D ( _MainTex, uv + dir * _ColorDistortion.r ).r,n tttex2D ( _MainTex, uv + dir * _ColorDistortion.g ).g,n tttex2D ( _MainTex, uv + dir * _ColorDistortion.b ).b,n tt1n t);n }n

最終成果如下:

後記

大萌喵會儘快將全部源代碼上傳到GitHub上滴. 我後續也會對光暈特效做出優化和改進 ~ 最慢一天哦!

今後打算長期在知乎混啦, 大家可以來監督我呦 ~ 每周至少保證兩更, 分享自己寫出來過的Shader ~ 逾期未更新可以在評論區抓我現行, 第一個舉報人獎勵人民幣10塊哈哈哈哈哈!

FIN

大萌喵是個學生黨, 非常熱切地希望能和諸位前輩們交流!

推薦閱讀:

(Unity) 為被 Lua 隔斷的 C# 實現添加 Profiler 支持
Unite 2016 針對移動設備端的Unity應用優化
從零開始學基於ARKit的Unity3d遊戲開發系列6
比預想的更複雜——動態斷肢實現
Lua性能優化(一):Lua內存優化

TAG:shader | 着色器 | Unity游戏引擎 |