http://blog.csdn.net/u011047171/article/details/48522073
Bloom特效
概述
Bloom,又稱“全屏泛光”,是游戲中常用的一種鏡頭效果,是一種比較廉價的“偽HDR”效果(如下右圖);使用了Bloom效果后,畫面的對比會得到增強,亮的地方曝光也會得到加強,畫面也會呈現一種朦朧,夢幻的效果,婚紗攝影中照片處理經常用到這種類似處理效果。Bloom效果一般用來近似模擬HDR效果,效果也比較相向,但實現原理卻完全不同。本例將實現一個適合移動平台使用的bloom屏幕特效。


Bloom特效與HDR特效的異同
要比較兩者的異同,得先搞清楚HDR特效是什么;HDR,本身是High-Dynamic Range(高動態范圍)的縮寫,這本來是一個CG概念。HDR的含義,簡單說,就是超越普通的光照的顏色和強度的光照。計算機在表示圖象的時候是用8bit(256)級或16bit(65536)級來區分圖象的亮度的,但這區區幾百或幾萬無法再現真實自然的光照情況。因此普通情況下,無法同時顯示亮部和暗部的所有細節。
現實中,當人由黑暗地方走到光亮地方,眼睛會自動眯起來。人在黑暗的地方,為了看清楚對象,瞳孔會很大張開,以吸收更多光線。當突然走到光亮地方,瞳孔來不及收縮,所以唯有眯上眼睛,保護視網膜上的視神經。而電腦是死物,唯有靠HDR技術模擬這效果——人眼自動適應光線變化的能力。方法是快速將光線渲染得非常光亮,然后將亮度逐漸降低。而HDR的最終效果是亮處的效果是鮮亮,而黑暗處的效果是能分辨物體的輪廓和深度,而不是以往的一團黑。。
想要實現HDR特效,首先,游戲開發者要在游戲開發過程中,利用開發工具(就是游戲引擎)將實際場景用HDRI記錄下來,當然開發技術強的開發組會直接用小開發工具(比如3D MAX的某些特效插件)創造HDRI圖像;其次,我們的顯卡必須支持顯示HDR特效,nVIDIA的顯卡必須是GeForce 6系列或更高,ATI顯卡至少是Radeon 9550或以上。

第一,HDR效果就是超亮的光照與超暗的黑暗的某種結合,這個效果是光照產生的,強度、顏色等方面是游戲程序可動態控制的,是一種即時動態光影;bloom效果則是物體本身發出的光照,僅僅是將光照范圍調高到過飽和,是游戲程序無法動態控制的,是一種全屏泛光。
第二,bloom效果無需HDR就可以實現,但是bloom效果是很受限的,它只支持8位RGBA,而HDR最高支持到32位RGBA。
第三,bloom效果的實現很簡單,比如《半條命2》的MOD就是一個很小的很簡單的MOD,而且bloom效果不受顯卡的規格的限制,你甚至可以在TNT顯卡上實現bloom效果(當然效果很差)!而HDR,必須是6XXX以上的顯卡才能夠實現,這里的HDR是指nVIDIA的HDR。這時有必要談nVIDIA和ATI的顯卡所實現的HDR,兩者還是有區別的,具體區別就很專業了,總之從真實性表現來看,nVIDIA的顯卡實現的HDR更好一些。HDR是nVIDIA提出的概念,從技術上來講,ATI當然無法嚴格克隆nVIDIA的技術,所以ATI的HDR是另一種途徑實現的盡可能接近的HDR,不能算“真”HDR,據傳ATI的R520能夠真正實現FP16 HDR。


未使用HDR圖像 使用HDR圖像
Bloom特效的實現流程
Bloom效果實現的流程與HDR的物理還原不同,它只是一種簡單的近似模擬:
- 第一步: 先獲取屏幕圖像,然后對每個像素進行亮度檢測,若大於某個閥值即保留原始顏色值,否則置為黑色;
- 第二步:對上一步獲取的圖像,做一個模糊,通常使用高斯模糊。
- 第三步:將模糊后的圖片和原圖片做一個加權和。
通過這三步就可以達到一個全屏泛光的效果。
Bloom特效的shader實現
本例在shader中實現大致和上面所述流程類似,只是在第一步做了少許改動,在這里我們將亮部的像素進行了擴展,關鍵代碼如下:
- struct v2f_withMaxCoords {
- half4 pos : SV_POSITION;
- half2 uv2[5] : TEXCOORD0;
- };
- //在vert函數里對uv坐標做了四次偏移,對原像素周圍臨近的像素采樣
- v2f_withMaxCoords vertMax (appdata_img v)
- {
- v2f_withMaxCoords o;
- o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
- o.uv2[0] = v.texcoord + _MainTex_TexelSize.xy * half2(1.5,1.5);
- o.uv2[1] = v.texcoord + _MainTex_TexelSize.xy * half2(-1.5,1.5);
- o.uv2[2] = v.texcoord + _MainTex_TexelSize.xy * half2(-1.5,-1.5);
- o.uv2[3] = v.texcoord + _MainTex_TexelSize.xy * half2(1.5,-1.5);
- o.uv2[4] = v.texcoord ;
- return o;
- }
- //Frag函數用偏移的uv坐標采樣,並且與原像素進行對比,如果亮度比原像素大,則取代原像素,因此亮部像素得到了擴展處理。這里ONE_MINUS_INTENSITY是由腳本傳遞過來的參數,用來控制bloom范圍,功能就是講低於這個值的像素設置為黑色。
- fixed4 fragMax ( v2f_withMaxCoords i ) : COLOR
- {
- fixed4 color = tex2D(_MainTex, i.uv2[4]);
- color = max(color, tex2D (_MainTex, i.uv2[0]));
- color = max(color, tex2D (_MainTex, i.uv2[1]));
- color = max(color, tex2D (_MainTex, i.uv2[2]));
- color = max(color, tex2D (_MainTex, i.uv2[3]));
- return saturate(color - ONE_MINUS_INTENSITY);
- }
流程的第二步就是講上一步的結果做模糊處理,在這里我們使用的上一個例子所使用的高斯模糊,因此不多做解釋,關鍵代碼如下面所示:
- struct v2f_withBlurCoordsSGX
- {
- float4 pos : SV_POSITION;
- half2 offs[7] : TEXCOORD0;
- };
- //水平方向的像素偏移,用來做水平模糊
- v2f_withBlurCoordsSGX vertBlurHorizontalSGX (appdata_img v)
- {
- v2f_withBlurCoordsSGX o;
- o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
- half2 netFilterWidth = _MainTex_TexelSize.xy * half2(1.0, 0.0) * _Parameter.x;
- o.offs[0] = v.texcoord + netFilterWidth;
- o.offs[1] = v.texcoord + netFilterWidth*2.0;
- o.offs[2] = v.texcoord + netFilterWidth*3.0;
- o.offs[3] = v.texcoord - netFilterWidth;
- o.offs[4] = v.texcoord - netFilterWidth*2.0;
- o.offs[5] = v.texcoord - netFilterWidth*3.0;
- o.offs[6] = v.texcoord;
- return o;
- }
- //垂直方向的像素偏移,用來做水平模糊
- v2f_withBlurCoordsSGX vertBlurVerticalSGX (appdata_img v)
- {
- v2f_withBlurCoordsSGX o;
- o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
- half2 netFilterWidth = _MainTex_TexelSize.xy * half2(0.0, 1.0) * _Parameter.x;
- o.offs[0] = v.texcoord + netFilterWidth;
- o.offs[1] = v.texcoord + netFilterWidth*2.0;
- o.offs[2] = v.texcoord + netFilterWidth*3.0;
- o.offs[3] = v.texcoord - netFilterWidth;
- o.offs[4] = v.texcoord - netFilterWidth*2.0;
- o.offs[5] = v.texcoord - netFilterWidth*3.0;
- o.offs[6] = v.texcoord;
- return o;
- }
- //用vert傳過來的uv坐標數組進行采樣,並乘以對應的權重進行疊加,其結果是個近似高斯模糊。
- fixed4 fragBlurSGX ( v2f_withBlurCoordsSGX i ) : COLOR
- {
- fixed4 color = tex2D(_MainTex, i.offs[6]) * curve[3];
- color += tex2D(_MainTex, i.offs[0])*curve[2];
- color += tex2D(_MainTex, i.offs[1])*curve[1];
- color += tex2D(_MainTex, i.offs[2])*curve[0];
- color += tex2D(_MainTex, i.offs[3])*curve[2];
- color += tex2D(_MainTex, i.offs[4])*curve[1];
- color += tex2D(_MainTex, i.offs[5])*curve[0];
- return color;
- }
流程的最后一步就非常簡單了,將上一步獲取的結果與原圖進行權重求和即可,就能得到一個bloom效果。在這里我們添加了從C#腳本傳遞過來的權重參數_Parameter.z和顏色參數_ColorMix,用來控制bloom的強度以及顏色傾向。
- struct v2f_simple {
- half4 pos : SV_POSITION;
- half4 uv : TEXCOORD0;
- };
- //考慮到D3D9的uv坐標Y軸是反轉的,因此需要做個判斷進行調整,防止圖像倒轉。
- v2f_simple vertBloom (appdata_img v)
- {
- v2f_simple o;
- o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
- o.uv = v.texcoord.xyxy;
- #if SHADER_API_D3D9
- if (_MainTex_TexelSize.y < 0.0)
- o.uv.w = 1.0 - o.uv.w;
- #endif
- return o;
- }
- fixed4 fragBloom ( v2f_simple i ) : COLOR
- {
- fixed4 color = tex2D(_MainTex, i.uv.xy);
- color += tex2D(_Bloom, i.uv.zw)*_Parameter.z*_ColorMix;
- return color;
- }
C#腳本
C#腳本相對而言比較簡單,和前面的的屏幕特效腳本類似,需要對shader的不同pass分別調用,並且開放了四個參數以方便效果的調節:Color Mix控制bloom特效的顏色傾向,Threshold控制bloom效果的范圍,Intensity控制bloom特效的強度,Blur Size控制模糊范圍以及模糊的質量。關鍵代碼如下;完整代碼請到文末放出的鏈接下載。
- void OnRenderImage (RenderTexture sourceTexture, RenderTexture destTexture)
- {
- #if UNITY_EDITOR
- FindShaders ();
- CheckSupport ();
- CreateMaterials ();
- #endif
- if(threshold != 0 && intensity != 0){
- int rtW = sourceTexture.width/4;
- int rtH = sourceTexture.height/4;
- BloomMaterial.SetColor ("_ColorMix", colorMix);
- BloomMaterial.SetVector ("_Parameter", new Vector4(BlurSize*1.5f, 0.0f, intensity,0.8f - threshold));
- // material.SetFloat("_blurSize",BlurSize);
- RenderTexture rtTempA = RenderTexture.GetTemporary (rtW, rtH, 0,rtFormat);
- rtTempA.filterMode = FilterMode.Bilinear;
- RenderTexture rtTempB = RenderTexture.GetTemporary (rtW, rtH, 0,rtFormat);
- rtTempA.filterMode = FilterMode.Bilinear;
- Graphics.Blit (sourceTexture, rtTempA,BloomMaterial,0);
- Graphics.Blit (rtTempA, rtTempB, BloomMaterial,1);
- RenderTexture.ReleaseTemporary(rtTempA);
- rtTempA = RenderTexture.GetTemporary (rtW, rtH, 0, rtFormat);
- rtTempB.filterMode = FilterMode.Bilinear;
- Graphics.Blit (rtTempB, rtTempA, BloomMaterial,2);
- BloomMaterial.SetTexture ("_Bloom", rtTempA);
- Graphics.Blit (sourceTexture, destTexture, BloomMaterial,3);
- RenderTexture.ReleaseTemporary(rtTempA);
- RenderTexture.ReleaseTemporary(rtTempB);
- }
- else{
- Graphics.Blit(sourceTexture, destTexture);
- }
- }
本例實現的效果如圖


總結
本例bloom效果是為移動平台開發,做了不少的優化,使之在移動平台上也有不錯的效率,當然本例效果還有進一步的優化空間,比如將第一步的像素擴展去掉,可以節省掉4次多余的采樣,第二步的高斯模糊同樣也可以降階,甚至也可以換成均值模糊,也能節省不少的計算。