Unity Shader 屏幕后處理-高斯模糊

Unity Shader系列文章:Unity Shader目錄-初級篇

Unity Shader系列文章:Unity Shader目錄-中級篇

效果:
左圖:原效果。右圖:高斯模糊后的效果

卷積的另一個常見應(yīng)用一高斯模糊。模糊的實現(xiàn)有很多方法,例如均值模糊和中值模糊。均值模糊同樣使用了卷積操作,它使用的卷積核中的各個元素值都相等,且相加等于1, 也就是說,卷積后得到的像素值是其鄰域內(nèi)各個像素值的平均值。而中值模糊則是選擇鄰域內(nèi)對所有像素排序后的中值替換掉原顏色。 一個更高級的模糊方法是高斯模糊。

高斯模糊同樣利用了卷積計算 ,它使用的卷積核名為高斯核 。高斯核是一個正方形大小的濾波核(如下圖),其中每個元素的計算都是基于下面的高斯方程:

G_(x,y) ={ \frac{1}{2\pi\alpha^2} e}^{-\frac{x^2+y^2}{2\alpha^2}}

其中, \alpha是標(biāo)準(zhǔn)方差(一般取值為 1), 分別對應(yīng)了當(dāng)前位置到卷積核中心的整數(shù)距離。要構(gòu)建一個高斯核 ,我們只需要計算高斯核中各個位置對應(yīng)的高斯值 為了保證濾波后的圖像不會變暗,我 需要對高斯核中的權(quán)重進行歸 化,即讓每個權(quán)重除以所有權(quán)重的和,這樣可以保證所有權(quán)重的和為 。因此 高斯函數(shù)中 前面的系數(shù)實際不會對結(jié)果有任何影響。

高斯方程很好地模擬了鄰域每個像素對當(dāng)前處理像素的影響程度一距離越近,影響越大。高斯核的維數(shù)越高,模糊程度越大。使用一個 NxN 的高斯核對圖像進行卷積濾波,就需要 NxNxWxH(W和H分別是圖像的寬和高)次紋理采樣。當(dāng)N的大小不斷增加時,采樣次數(shù)會變得非常巨大幸運的是,我們可以把這個二維高斯函數(shù)拆分成兩個一維函數(shù)。也就是說,我們可以使用兩個一維的高斯核(如圖)先后對圖像進行濾波,它們得到的結(jié)果和直接使用二維高斯核是一樣的,但采樣次數(shù)只需要 2xNxWxH 。我們可以進一步觀察到,兩個一維高斯核中包含了很多重復(fù)的權(quán)重。對于一個大小為 的一維高斯核,我們實際只需要記錄3個權(quán)重值即可。

一個5×5大小的高斯核。左圖顯示了標(biāo)準(zhǔn)方差為1的高斯核的權(quán)重分布。我們可以把這個二維高斯核拆分成兩個一維的高斯核(右圖)
具體實現(xiàn)思路:

將先后調(diào)用兩個 Pass,第一個 Pass 將會使用豎直方向的一維高斯核對圖像進行濾波,第二個 Pass 再使用水平方向的維高斯核對圖像進行濾波,得到最終的目標(biāo)圖像。在實現(xiàn)中,還將利用圖像縮放來進一步提高性能,并通過調(diào)整高斯濾波的應(yīng)用次數(shù)來控制模糊程度(次數(shù)越多,圖像越模糊)。

ScreenPostEffectsBase基類代碼:

using UnityEngine;

/// <summary>
/// 屏幕后處理效果基類
/// </summary>
[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]
public class ScreenPostEffectsBase : MonoBehaviour
{
    public Shader Shader;
    public Material Material
    {
        get
        {
            return CheckAndCreateMaterial();
        }
    }
    private Material _material;

    protected void Start()
    {
        CheckResources();
    }

    /// <summary>
    /// 檢查資源
    /// </summary>
    protected void CheckResources()
    {
        if (!CheckSupport())
        {
            NotSupported();
        }
    }

    /// <summary>
    /// 檢查支持
    /// </summary>
    /// <returns></returns>
    protected bool CheckSupport()
    {
        bool isSupported = SystemInfo.supportsImageEffects;
        return isSupported;
    }

    /// <summary>
    /// 不支持
    /// </summary>
    protected void NotSupported()
    {
        enabled = false;
    }

    /// <summary>
    /// 檢查和創(chuàng)建Material
    /// </summary>
    /// <returns></returns>
    protected Material CheckAndCreateMaterial()
    {
        if (!Shader || !Shader.isSupported)
        {
            return null;
        }

        if (_material && _material.shader == Shader)
        {
            return _material;
        }

        _material = new Material(Shader);
        _material.hideFlags = HideFlags.DontSave;
        return _material;
    }
}

ScreenGaussianBlur派生類代碼:

using UnityEngine;

/// <summary>
/// 屏幕后處理效果:高斯模糊
/// </summary>
public class ScreenGaussianBlur : ScreenPostEffectsBase
{
    /// <summary>
    /// 迭代此時
    /// </summary>
    [Range(0, 4)]
    public int Iterations = 2;

    /// <summary>
    /// 模糊擴散
    /// </summary>
    [Range(0.2f, 3)]
    public float BlurSpread = 1;

    /// <summary>
    /// 縮放系數(shù)
    /// </summary>
    [Range(1, 8)]
    public int DownSample = 2;

    // // 第一個版本,最簡單的OnRenderlmage實現(xiàn):
    // private void OnRenderImage(RenderTexture src, RenderTexture dest)
    // {
    //     if (Material != null)
    //     {
    //         int rtW = src.width;
    //         int rtH = src.height;        
    //         // 利用 RenderTexture GetTemporary 函數(shù)分配了一塊與屏幕圖像大小相同的緩沖區(qū)。
    //         // 這是因為 高斯模糊需要調(diào)用兩個Pass, 我們需要使用一塊中間緩存來存儲第一個Pass執(zhí)行完畢后得到的模糊結(jié)果。
    //         RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);

    //         // 使用 Shader 中的第1個Pass (即使用豎直方向的一維高斯核進行濾波)對src進行處理,并將結(jié)果存儲在了buffer中。
    //         Graphics.Blit(src, buffer, Material, 0);

    //         // 然后,再使用Shader中的第2個Pass (即使用水平方向的一維高斯核進行濾波)對buffer進行處理,返回最終屏幕圖像。
    //         Graphics.Blit(buffer, dest, Material, 1);

    //         // 最后 還需要釋放之前分配的緩存。
    //         RenderTexture.ReleaseTemporary(buffer);
    //     }
    //     else
    //     {
    //         Graphics.Blit(src, dest);
    //     }
    // }

    // // 第二個版本實現(xiàn):利用縮放對圖像進行降采樣,從而減少需要處理的像素 提高性能。
    // private void OnRenderImage(RenderTexture src, RenderTexture dest)
    // {
    //     if (Material)
    //     {
    //         int rtw = src.width / DownSample;
    //         int rtH = src.height / DownSample;
    //         // 使用了小于原屏幕分辨率的尺寸
    //         RenderTexture buffer = RenderTexture.GetTemporary(rtw, rtH, 0);
    //         // 并將該臨時渲染紋理的濾波模式設(shè)置為雙線性
    //         buffer.filterMode = FilterMode.Bilinear;
    //         // 這樣,在調(diào)用第一個 Pass 時,我們需要處理的像素個數(shù)就是原來的幾分之一。
    //         // 對圖像進行降采樣不僅可以減少需要處理的像素個數(shù),提高性能,而且適當(dāng)?shù)慕挡蓸油€可以得到更好的模糊效果。
    //         // 盡管 downSample 值越大,性能越好,但過大的 downSample 可能會造成圖像像素化。

    //         Graphics.Blit(src, buffer, Material, 0);

    //         Graphics.Blit(buffer, dest, Material, 1);

    //         RenderTexture.ReleaseTemporary(buffer);
    //     }
    //     else
    //     {
    //         Graphics.Blit(src, dest);
    //     }
    // }

    // 第三個版本實現(xiàn):考慮了高斯模糊的迭代次數(shù),何利用兩個臨時緩存在迭代之間進行交替的過程。
    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (Material)
        {
            int rtW = src.width / DownSample;
            int rtH = src.height / DownSample;

            // 定義了第一個緩存 bufferO, 并把 src 中的圖像縮放后存儲到 bufferO 中
            RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
            buffer0.filterMode = FilterMode.Bilinear;

            Graphics.Blit(src, buffer0);

            for (var i = 0; i < Iterations; i++)
            {
                Material.SetFloat("_BlurSize", 1 + i * BlurSpread);
                // 定義了第二個緩存 bufferl
                RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);

                // 執(zhí)行第一個 Pass 時,輸入 bufferO, 輸出是 bufferl
                Graphics.Blit(buffer0, buffer1, Material, 0);

                // 完畢后首先把 bufferO 釋放,再把結(jié)果值 buffer 存儲到 bufferO 中,重新分配 bufferl
                RenderTexture.ReleaseTemporary(buffer0);
                buffer0 = buffer1;
                buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);

                // 調(diào)用第二個Pass, 重復(fù)上述過程
                Graphics.Blit(buffer0, buffer1, Material, 1);

                // 完畢后再把 bufferO 釋放,再把結(jié)果值 buffer 存儲到 bufferO 中
                RenderTexture.ReleaseTemporary(buffer0);
                buffer0 = buffer1;
            }

            // 迭代完成后 bufferO 將存儲最終的圖像,將結(jié)果顯示到屏幕上
            Graphics.Blit(buffer0, dest);
            // 釋放緩存
            RenderTexture.ReleaseTemporary(buffer0);
        }
        else
        {
            Graphics.Blit(src, dest);
        }
    }
}

Shader代碼:

// 高斯模糊
Shader "Custom/GaussianBlur"
{
    Properties
    {
        _MainTex ("MainTex", 2D) = "white" { }
        _BlurSize ("Blur Size", Float) = 1 // 控制采樣距離,在高斯核維數(shù)不變的情況下, BlurSize 越大,模糊程度越高
    }

    SubShader
    {

        CGINCLUDE

        #include "UnityCG.cginc"

        sampler2D _MainTex;
        // _MainTex紋理的紋素大小,例如521x512大小的紋理,值約為0.001953,即1/512
        half4 _MainTex_TexelSize;
        float _BlurSize;

        // 頂點著色器傳遞給片元著色器的數(shù)據(jù)
        struct v2f
        {
            float4 pos: SV_POSITION;  // 裁剪空間下的頂點坐標(biāo)
            half2 uv[5]: TEXCOORD0; // 數(shù)組的第一個坐標(biāo)存儲了當(dāng)前的采樣紋理,而剩余的四個坐標(biāo)則是高斯模糊中對鄰域采樣時使用的紋理坐標(biāo)
        };

        // 頂點著色器,豎直模糊處理
        v2f vertBlurVertical(appdata_img v)
        {
            v2f o;

            // 將頂點坐標(biāo)從模型空間變換到裁剪空間
            o.pos = UnityObjectToClipPos(v.vertex);

            half2 uv = v.texcoord;

            // 豎直的5個相鄰的像素
            o.uv[0] = uv;
            o.uv[1] = uv + float2(0, _MainTex_TexelSize.y * 1) * _BlurSize;
            o.uv[2] = uv - float2(0, _MainTex_TexelSize.y * 1) * _BlurSize;
            o.uv[3] = uv + float2(0, _MainTex_TexelSize.y * 2) * _BlurSize;
            o.uv[4] = uv - float2(0, _MainTex_TexelSize.y * 2) * _BlurSize;

            return o;
        }

        // 頂點著色器,水平模糊處理
        v2f vertBlurHorizontal(appdata_img v)
        {
            v2f o;

            // 將頂點坐標(biāo)從模型空間變換到裁剪空間
            o.pos = UnityObjectToClipPos(v.vertex);

            half2 uv = v.texcoord;

            // 水平的5個相鄰的像素
            o.uv[0] = uv;
            o.uv[1] = uv + float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
            o.uv[2] = uv - float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
            o.uv[3] = uv + float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
            o.uv[4] = uv - float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
            
            return o;
        }

        // 片元著色器,模糊處理
        fixed4 fragBlur(v2f i): SV_Target
        {
            // 一個 sxs 的二維高斯核可以拆分成兩個大小為 5 的一維高斯核, 并且由千它的對稱性,我們只需要記錄3個高斯權(quán)重,
            float weight[3] = {
                0.4026, 0.2442, 0.0545
            };
            
            // 將結(jié)果值 sum 初始化為當(dāng)前的像素值乘以它的權(quán)重值
            fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
            
            // 根據(jù)對稱性,進行了兩次迭代,每次迭代包含了兩次紋理采樣,并把像素值和權(quán)重相乘后的結(jié)果疊加到 sum 中
            for (int it = 1; it < 3; it++)
            {
                sum += tex2D(_MainTex, i.uv[it * 2 - 1]).rgb * weight[it];
                sum += tex2D(_MainTex, i.uv[it * 2]).rgb * weight[it];
            }
            
            return fixed4(sum, 1.0);
        }

        ENDCG

        ZTest Always Cull Off ZWrite Off
        
        Pass
        {
            // Pass名字,這樣別的Shader可以通過此名字調(diào)用此Pass
            // 如: UsePass "Cusstom/GaussianBlur/GAUSSIAN_BLUR_VERTICAL"
            NAME "GAUSSIAN_BLUR_VERTICAL"
            
            CGPROGRAM

            #pragma vertex vertBlurVertical
            #pragma fragment fragBlur
            
            ENDCG

        }
        
        Pass
        {
            // Pass名字,這樣別的Shader可以通過此名字調(diào)用此Pass
            // 如: UsePass "Cusstom/GaussianBlur/GAUSSIAN_BLUR_HORIZONTAL"
            NAME "GAUSSIAN_BLUR_HORIZONTAL"
            
            CGPROGRAM

            #pragma vertex vertBlurHorizontal
            #pragma fragment fragBlur
            
            ENDCG

        }
    }

    FallBack "Diffuse"
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容