溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Unity Shader后處理中如何實現(xiàn)高斯模糊

發(fā)布時間:2022-01-05 15:15:54 來源:億速云 閱讀:195 作者:小新 欄目:大數(shù)據(jù)

小編給大家分享一下Unity Shader后處理中如何實現(xiàn)高斯模糊,希望大家閱讀完這篇文章之后都有所收獲,下面讓我們一起去探討吧!

一.簡介

之前學習了模糊的原理以及基本的模糊實現(xiàn),對于清晰和模糊這個定義感覺還是比較說明問題,這里再貼出一下:“清晰的圖片,各個像素之間會有明顯的過渡,而如果各個像素之間的差距不是很大,那么圖像就會模糊了”。鑒于這個定義,我們就可以通過代碼來實現(xiàn)模糊的效果。之前Unity Shader-后處理:均值模糊中實現(xiàn)了一個基本的均值模糊,也就是將一個像素和其周圍的像素取平均值從而進行模糊,并且通過迭代處理的方式,增強了模糊的效果。但是,均值模糊由于采樣次數(shù)較少,每個像素以及其周圍像素的權(quán)值是相同的,模糊出來的效果不佳,而多次迭代處理雖然可以增強模糊效果,但是迭代大大地增加了性能的消耗,雖然在學習時可以用迭代來達到效果,但是要實際使用的時候,效率就不得不成為我們考慮的重要因素。所以,這一次,我們來學習一下更加高級的模糊效果-高斯模糊。

高斯模糊(Gaussian Blur),又叫做高斯平滑。高斯模糊主要的功能是對圖片進行加權(quán)平均的過程,與均值模糊中周圍像素取平均值不同,高斯模糊進行的是一個加權(quán)平均操作,每個像素的顏色值都是由其本身和相鄰像素的顏色值進行加權(quán)平均得到的,越靠近像素本身,權(quán)值越高,越偏離像素的,權(quán)值越低。而這種權(quán)值符合我們比較熟悉的一種數(shù)學分布-正態(tài)分布,又叫高斯分布,所以這種模糊就是高斯模糊啦。

二.概念介紹

1.正態(tài)分布

先來復習一下正太分布,上一次聽說這個詞兒應該還是大二時的《概率與數(shù)理統(tǒng)計》課上。正太分布,又名高斯分布,這個分布函數(shù)具有很多非常漂亮的性質(zhì),使得其在諸多領(lǐng)域都有非常重要的影響力。而且,正態(tài)分布是一個比較自然的分布,在特定條件下,大量統(tǒng)計獨立的隨機變量的平局值的分布趨近于正態(tài)分布,這也就是傳說中的中心極限定理。說了這么多,歸結(jié)起來就是一句話,高斯分布比較好看,所以我們就用高斯分布作為我們進行加權(quán)計算時的權(quán)值參考。

高斯分布的定義如下:

Unity Shader后處理中如何實現(xiàn)高斯模糊

其中μ是正態(tài)分布隨機變量的均值,也就是期望值,也就是下圖中x軸上最中間的位置,隨機變量在μ兩側(cè)對稱分布,而第二個參數(shù)σ^2是這個隨機變量的方差,因而正態(tài)分布記為N(μ,σ2 ),符合正態(tài)分布的隨機變量越靠近μ則概率越大,而越遠離μ概率越小,σ越小,分布越集中在μ附近,σ越大,分布越分散。而當μ=0,σ^2=1時,稱為標準正態(tài)分布,記為N(0,1)。正態(tài)分布的圖像如下圖所示:

                                                      Unity Shader后處理中如何實現(xiàn)高斯模糊

那么,這個分布和我們的高斯模糊有什么關(guān)系呢?簡單來說,我們需要讓我們的采樣符合高斯分布。那么,我們處理每個像素的時候,像素本身的點就對應著μ對應的權(quán)值,而我們要在像素周圍采樣,這個采樣的范圍就可以用σ表示,比如我們的方差為1,那么我們直接向外采一個像素的值,基本就可以達到模糊的效果了;而如果方差為0,那么μ點的權(quán)值最大,加權(quán)平均后仍然為原像素值,等于沒模糊;而方差很大,那么采樣的范圍就很廣,就會更加模糊。

關(guān)于高斯模糊,還有一點,就是采樣的個數(shù),也就是傳說中的高斯模板(高斯核)的大小。也就是我們要取幾個采樣點的問題,μ對應像素點本身,μ±1σ,μ±2σ分別代表向外1個采樣點,兩個采樣點,當然由于越向外,對應的權(quán)值越小,所以后面的我們基本可以不予考慮,這里我們就采用一個μ,μ±1σ,μ±2σ,μ±3σ一共7個采樣點作為高斯高斯核。

關(guān)于高斯核的權(quán)值設定,本人看了好幾篇文章以及書籍,然而每個高斯模糊用的高斯核都不同,這里也沒有什么標準。正如某圖形學大牛說的:“圖形學這東西,看起來是對的,那就是對的”,不管怎么樣,畢竟最后都是給人看的,效果最好計算簡單比什么都強!下面代碼里面我自己設置了一套高斯權(quán)重,雖然看起來山寨了一點,不過滿足相加等于1就行啦。

2.卷積

卷積是一個神奇的概念,最近看圖像處理倒是經(jīng)??吹竭@個詞兒,想到上學的時候也沒有搞懂這個東西,于是強迫癥發(fā)作,決定查一查卷積到底怎么解釋。對于卷積,百度百科上是這么說的:卷積是通過兩個函數(shù)f和g生成第三個函數(shù)的一種數(shù)學算子。不過,高手在民間,知乎上對卷積的解釋更加通俗易懂,這里摘抄一小段個人認為最為精辟的:

比如說你的老板命令你干活,你卻到樓下打臺球去了,后來被老板發(fā)現(xiàn),他非常氣憤,扇了你一巴掌(注意,這就是輸入信號,脈沖),于是你的臉上會漸漸地(賤賤地)鼓起來一個包,你的臉就是一個系統(tǒng),而鼓起來的包就是你的臉對巴掌的響應,好,這樣就和信號系統(tǒng)建立起來意義對應的聯(lián)系。下面還需要一些假設來保證論證的嚴謹:假定你的臉是線性時不變系統(tǒng),也就是說,無論什么時候老板打你一巴掌,打在你臉的同一位置(這似乎要求你的臉足夠光滑,如果你說你長了很多青春痘,甚至整個臉皮處處連續(xù)處處不可導,那難度太大了,我就無話可說了哈哈),你的臉上總是會在相同的時間間隔內(nèi)鼓起來一個相同高度的包來,并且假定以鼓起來的包的大小作為系統(tǒng)輸出。好了,那么,下面可以進入核心內(nèi)容——卷積了!

如果你每天都到地下去打臺球,那么老板每天都要扇你一巴掌,不過當老板打你一巴掌后,你5分鐘就消腫了,所以時間長了,你甚至就適應這種生活了……如果有一天,老板忍無可忍,以0.5秒的間隔開始不間斷的扇你的過程,這樣問題就來了,第一次扇你鼓起來的包還沒消腫,第二個巴掌就來了,你臉上的包就可能鼓起來兩倍高,老板不斷扇你,脈沖不斷作用在你臉上,效果不斷疊加了,這樣這些效果就可以求和了,結(jié)果就是你臉上的包的高度隨時間變化的一個函數(shù)了(注意理解);如果老板再狠一點,頻率越來越高,以至于你都辨別不清時間間隔了,那么,求和就變成積分了??梢赃@樣理解,在這個過程中的某一固定的時刻,你的臉上的包的鼓起程度和什么有關(guān)呢?和之前每次打你都有關(guān)!但是各次的貢獻是不一樣的,越早打的巴掌,貢獻越小,所以這就是說,某一時刻的輸出是之前很多次輸入乘以各自的衰減系數(shù)之后的疊加而形成某一點的輸出,然后再把不同時刻的輸出點放在一起,形成一個函數(shù),這就是卷積,卷積之后的函數(shù)就是你臉上的包的大小隨時間變化的函數(shù)。本來你的包幾分鐘就可以消腫,可是如果連續(xù)打,幾個小時也消不了腫了,這難道不是一種平滑過程么?反映到劍橋大學的公式上,f(a)就是第a個巴掌,g(x-a)就是第a個巴掌在x時刻的作用程度,乘起來再疊加就ok了

 

簡單來說,卷積就是一個進行數(shù)學處理的一個算子。在圖像處理中,設圖像為f(x),模板g(x),然后圖像處理就是將模板g(x)在圖像f中移動,每移動到一個像素位置,就把f(x)與g(x)定義域相交的元素進行乘積并求和,得出新的圖像中的該像素點,當全部像素點操作完成后,就得到了卷積后的圖像,模板就是卷積核,上文中我們定義的高斯核就是一個卷積核。

上面我們說過高斯核,正常來看,我們應該是取像素為μ點,然后像素上下左右分別取一些像素點作為采樣點,然后根據(jù)距離μ點的距離分別乘以相應的權(quán)值,作為處理后的這一點的像素值。但是這樣做有一個弊端,就是我們在處理每個像素點的時候,需要進行大量的采樣計算,需要像素點以及像素點周圍幾圈的采樣點才能將中間像素周圍所有的像素進行加權(quán)平均。而這樣的操作是逐像素計算的,更可怕的是,這種效果是全屏幕后處理效果?。〖僭O屏幕分辨率是M*N,我們的高斯核大小是m*n,那么進行一次后處理的時間復雜度為O(M*N*m*n)。

有什么好辦法進行優(yōu)化嗎?

高斯模糊就是一個卷積操作,而這個操作是一個線性操作,換句話說這個系統(tǒng)是一個線性系統(tǒng)。所謂線性系統(tǒng)是一個系統(tǒng)的輸入和輸出是線性關(guān)系,就是說整個系統(tǒng)可以拆分成多個無關(guān)的獨立變化,而整個系統(tǒng)就是這些變化的累加。比如一個系統(tǒng),輸入x1(t)產(chǎn)生輸出y1(t),表示為x1(t)->y1(t),而另一個輸入x2(t)產(chǎn)生輸出y2(t)即x2(t)->y2(t)。這個系統(tǒng)是線性的,當且僅當x1(t)+x2(t)->y1(t)+y2(t)。

我畫了一個簡單的算平均數(shù)的圖示,希望可以解釋清楚這樣的問題:

        Unity Shader后處理中如何實現(xiàn)高斯模糊

從上面的圖中我們看出,直接計算整個的平均數(shù),和先計算橫向的平均,再計算豎向的平均,得到的結(jié)果是相同的,也就是說兩者是等價的(注意,本圖只是一個類比,并不代表高斯模糊的原理,證明線性系統(tǒng)可以拆分成多個獨立的操作,或者哪位高人有更好的證明方式也可以指點小弟一下)。

我們的高斯模糊操作,如果整個圖像進行采樣,那么會進行M*N*m*n次采樣操作,而如果是先橫向,再豎向,那么我們在橫向方向需要M*m*n次采樣操作,而在豎向方向需要N*m*n次采樣操作,總共的時間復雜度就是O((M+N)*m*n)。從M*N降到M+N,一般地,M和N為屏幕分辨率,比如1024*768,那么,這樣一個操作就大大降低了時間復雜度?。?!不過需要一點點空間作為中間結(jié)果的緩存,不過這點緩存對于性能的優(yōu)化還是很值得的。

三.高斯模糊的實現(xiàn)

扯了這么久的理論,終于要開始寫代碼了,這里不多說,直接上帶有注釋的代碼了。

shader部分:

Shader "Custom/GaussianBlur"  
{  
  
    Properties  
    {  
        _MainTex("Base (RGB)", 2D) = "white" {}  
    }  
  
    //通過CGINCLUDE我們可以預定義一些下面在Pass中用到的struct以及函數(shù),  
    //這樣在pass中只需要設置渲染狀態(tài)以及調(diào)用函數(shù),shader更加簡潔明了  
    CGINCLUDE  
    #include "UnityCG.cginc"  
  
    //blur結(jié)構(gòu)體,從blur的vert函數(shù)傳遞到frag函數(shù)的參數(shù)  
    struct v2f_blur  
    {  
        float4 pos : SV_POSITION;   //頂點位置  
        float2 uv  : TEXCOORD0;     //紋理坐標  
        float4 uv01 : TEXCOORD1;    //一個vector4存儲兩個紋理坐標  
        float4 uv23 : TEXCOORD2;    //一個vector4存儲兩個紋理坐標  
        float4 uv45 : TEXCOORD3;    //一個vector4存儲兩個紋理坐標  
    };  
  
    //shader中用到的參數(shù)  
    sampler2D _MainTex;  
    //XX_TexelSize,XX紋理的像素相關(guān)大小width,height對應紋理的分辨率,x = 1/width, y = 1/height, z = width, w = height  
    float4 _MainTex_TexelSize;  
    //給一個offset,這個offset可以在外面設置,是我們設置橫向和豎向blur的關(guān)鍵參數(shù)  
    float4 _offsets;  
  
    //vertex shader  
    v2f_blur vert_blur(appdata_img v)  
    {  
        v2f_blur o;  
        o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
        //uv坐標  
        o.uv = v.texcoord.xy;  
  
        //計算一個偏移值,offset可能是(0,1,0,0)也可能是(1,0,0,0)這樣就表示了橫向或者豎向取像素周圍的點  
        _offsets *= _MainTex_TexelSize.xyxy;  
          
        //由于uv可以存儲4個值,所以一個uv保存兩個vector坐標,_offsets.xyxy * float4(1,1,-1,-1)可能表示(0,1,0-1),表示像素上下兩個  
        //坐標,也可能是(1,0,-1,0),表示像素左右兩個像素點的坐標,下面*2.0,*3.0同理  
        o.uv01 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1);  
        o.uv23 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1) * 2.0;  
        o.uv45 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1) * 3.0;  
  
        return o;  
    }  
  
    //fragment shader  
    fixed4 frag_blur(v2f_blur i) : SV_Target  
    {  
        fixed4 color = fixed4(0,0,0,0);  
        //將像素本身以及像素左右(或者上下,取決于vertex shader傳進來的uv坐標)像素值的加權(quán)平均  
        color += 0.4 * tex2D(_MainTex, i.uv);  
        color += 0.15 * tex2D(_MainTex, i.uv01.xy);  
        color += 0.15 * tex2D(_MainTex, i.uv01.zw);  
        color += 0.10 * tex2D(_MainTex, i.uv23.xy);  
        color += 0.10 * tex2D(_MainTex, i.uv23.zw);  
        color += 0.05 * tex2D(_MainTex, i.uv45.xy);  
        color += 0.05 * tex2D(_MainTex, i.uv45.zw);  
        return color;  
    }  
  
    ENDCG  
  
    //開始SubShader  
    SubShader  
    {  
        //開始一個Pass  
        Pass  
        {  
            //后處理效果一般都是這幾個狀態(tài)  
            ZTest Always  
            Cull Off  
            ZWrite Off  
            Fog{ Mode Off }  
  
            //使用上面定義的vertex和fragment shader  
            CGPROGRAM  
            #pragma vertex vert_blur  
            #pragma fragment frag_blur  
            ENDCG  
        }  
  
    }  
//后處理效果一般不給fallback,如果不支持,不顯示后處理即可  
}

c#部分:

using UnityEngine;  
using System.Collections;  
  
//編輯狀態(tài)下也運行  
[ExecuteInEditMode]  
//繼承自PostEffectBase  
public class GaussianBlur : PostEffectBase  
{  
    //模糊半徑  
    public float BlurRadius = 1.0f;  
    //降分辨率  
    public int downSample = 2;  
    //迭代次數(shù)  
    public int iteration = 1;  
  
    void OnRenderImage(RenderTexture source, RenderTexture destination)  
    {  
        if (_Material)  
        {  
            //申請RenderTexture,RT的分辨率按照downSample降低  
            RenderTexture rt1 = RenderTexture.GetTemporary(source.width >> downSample, source.height >> downSample, 0, source.format);  
            RenderTexture rt2 = RenderTexture.GetTemporary(source.width >> downSample, source.height >> downSample, 0, source.format);  
  
            //直接將原圖拷貝到降分辨率的RT上  
            Graphics.Blit(source, rt1);  
  
            //進行迭代高斯模糊  
            for(int i = 0; i < iteration; i++)  
            {  
                //第一次高斯模糊,設置offsets,豎向模糊  
                _Material.SetVector("_offsets", new Vector4(0, BlurRadius, 0, 0));  
                Graphics.Blit(rt1, rt2, _Material);  
                //第二次高斯模糊,設置offsets,橫向模糊  
                _Material.SetVector("_offsets", new Vector4(BlurRadius, 0, 0, 0));  
                Graphics.Blit(rt2, rt1, _Material);  
            }  
  
            //將結(jié)果輸出  
            Graphics.Blit(rt1, destination);  
  
            //釋放申請的兩塊RenderBuffer內(nèi)容  
            RenderTexture.ReleaseTemporary(rt1);  
            RenderTexture.ReleaseTemporary(rt2);  
        }  
    }  
}

注意,這里的GaussianBlur繼承了PostEffectBase類。該類在之前的文章《UnityShader-后處理:簡單亮度對比度飽和度調(diào)整》中有完整的實現(xiàn),這里就不貼出代碼了。

其實GaussianBlur和上一篇文章中的SimpleBlurEffect基本一致,改變的地方就在于我們需要兩遍高斯模糊,這兩遍模糊分別是針對豎向和橫向進行模糊的,區(qū)別和設置就在于offset的設置。其實也可以分別使用兩個不同的pass分別作為horizontal和vertical方向的模糊,不過這樣代碼有冗余,所以還是用這種比較“優(yōu)雅”的方式實現(xiàn)了。

四.效果展示

我們在MainCamera上掛在GaussianBlur腳本,然后把GaussianBlur.shader賦給shader槽,就可以看到模糊的效果了。

首先看一下原圖效果,如果不需要后處理,直接關(guān)掉后處理控件最好,因為Graphic.Blit等操作也是很費的操作。不過這里為了演示,直接模糊半徑為0,不降分辨率,迭代1次,即可顯示清晰的原圖效果:

Unity Shader后處理中如何實現(xiàn)高斯模糊

當模糊半徑設為1,分辨率降低為1/2,迭代1次,輕微的模糊效果:

Unity Shader后處理中如何實現(xiàn)高斯模糊

模糊半徑為1,分辨率將為1/4,迭代一次,更加模糊的效果:

Unity Shader后處理中如何實現(xiàn)高斯模糊

模糊半徑為1,分辨率將為1/4,迭代兩次,可以達到下圖這種毛玻璃效果(PS,這種效果在最近新出的《天下》手游里面,點擊界面后,屏幕背景就會變成類似的效果):

Unity Shader后處理中如何實現(xiàn)高斯模糊

通過上面的圖片我們看到,其實降低分辨率對我們的高斯模糊效果有很大的提升。其實降分辨率的那個操作本身就是一個類似模糊的效果,這種效果在我們想要清晰的圖片時看起來會特別蛋疼,如下圖所示:

Unity Shader后處理中如何實現(xiàn)高斯模糊

但是,如果不想要清晰的圖片,降分辨率反倒讓我們的模糊效果更好。而且,最最重要的是,降分辨率之后,pixel shader采樣會大大降低,這對我們的效率會有很大的提升。

高斯模糊的效果更加平滑,沒有簡單的均值模糊那種"近視眼"或者像素塊的感覺,下面看一下高斯模糊和上一篇文章中的簡單均值模糊的對比:

Unity Shader后處理中如何實現(xiàn)高斯模糊

看完了這篇文章,相信你對“Unity Shader后處理中如何實現(xiàn)高斯模糊”有了一定的了解,如果想了解更多相關(guān)知識,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI