1. 程式人生 > >【Unity Shader程式設計】之十五 螢幕高斯模糊(Gaussian Blur)後期特效的實現

【Unity Shader程式設計】之十五 螢幕高斯模糊(Gaussian Blur)後期特效的實現

本篇文章將分析如何在Unity中基於Shader實現高斯模糊屏幕後期特效。

首先放出最終的實現效果。如下幾幅圖,是在Unity中使用本文所實現的Shader得到的高斯模糊屏幕後期特效與原始圖的效果對比圖。

卡通風格的效果測試:


寫實風格的效果測試:



OK,下面我們開始分析如何在Unity中實現上述的高斯模糊特效。

一、降取樣與高斯模糊的原理

1.1 關於影象的降取樣

降取樣(Downsample)也稱下采樣(Subsample),按字面意思理解即是降低取樣頻率。對於一幅N*M的影象來說,如果降取樣係數為k,則降取樣即是在原圖中每行每列每隔k個點取一個點組成一幅影象的一個過程。

不難得出,降取樣係數K值越大,則需要處理的畫素點越少,執行速度越快。

1.2 高斯模糊的原理

高斯模糊(Gaussian Blur),也叫高斯平滑,高斯濾波,其通常用它來減少影象噪聲以及降低細節層次,常常也被用於對影象進行模糊。

通俗的講,高斯模糊就是對整幅影象進行加權平均的過程,每一個畫素點的值,都由其本身和鄰域內的其他畫素值經過加權平均後得到。高斯模糊的具體操作是:用一個模板(或稱卷積、掩模)掃描影象中的每一個畫素,用模板確定的鄰域內畫素的加權平均灰度值去替代模板中心畫素點的值。

高斯分佈的數學表示如下:

其中,x為到畫素中心的距離,σ為標準差。

 

高斯分佈(正態分佈曲線)

分條來說明一下高斯模糊的幾個要點:

  • 從數學的角度來看,影象的高斯模糊過程就是影象與正態分佈做卷積。
  • 由於正態分佈又叫作高斯分佈,所以這項技術就叫作高斯模糊。
  • 高斯模糊能夠把某一點周圍的畫素色值按高斯曲線統計起來,採用數學上加權平均的計算方法得到這條曲線的色值
  • 所謂"模糊",可以理解成每一個畫素都取周邊畫素的平均值。
  • 影象與圓形方框模糊做卷積將會生成更加精確的焦外成像效果。由於高斯函式的傅立葉變換是另外一個高斯函式,所以高斯模糊對於影象來說就是一個低通濾波器。

高斯模糊的原理大致如此。若各位還想進一步瞭解,可以參考高斯模糊的wiki,以及《Real-Time Rendering 3rd》,或各種影象處理的書籍。相關參考內容見附錄中的reference。

下面主要來一起看一下高斯模糊特效在Unity中的實現。

二、高斯模糊特效在Unity中的實現

Unity中的螢幕特效,通常分為兩部分來實現:

    • Shader程式碼實現部分
    • C#/javascript程式碼實現部分

 上述兩者結合起來,便可以在Unity中實現具有很強可控性和靈活性的屏幕後期特效。

下面即是從這兩個方面對高斯模糊的特效進行實現。其實現思路類似Standard Assets/Image Effect中的Blur,但是本文的實現更簡潔,有更大的可控性。

2.1 Shader程式碼部分

本次的高斯模糊Shader包含逐行註釋後約200多行。

書寫思路方面,採用了3個通道(Pass)各司其職,他們分別是:

  • 通道0:降取樣通道。
  • 通道1:垂直方向模糊處理通道。
  • 通道2:水平方向模糊處理通道。

而三個通道中共用的變數、函式和結構體的程式碼位於CGINCLUDE和ENDCG之間。

以下貼出經過詳細註釋的Shader原始碼:

Shader "Learning Unity Shader/Lecture 15/RapidBlurEffect"
{
	//-----------------------------------【屬性 || Properties】------------------------------------------  
	Properties
	{
		//主紋理
		_MainTex("Base (RGB)", 2D) = "white" {}
	}

	//----------------------------------【子著色器 || SubShader】---------------------------------------  
	SubShader
	{
		ZWrite Off
		Blend Off

		//---------------------------------------【通道0 || Pass 0】------------------------------------
		//通道0:降取樣通道 ||Pass 0: Down Sample Pass
		Pass
		{
			ZTest Off
			Cull Off

			CGPROGRAM

			//指定此通道的頂點著色器為vert_DownSmpl
			#pragma vertex vert_DownSmpl
			//指定此通道的畫素著色器為frag_DownSmpl
			#pragma fragment frag_DownSmpl

			ENDCG

		}

		//---------------------------------------【通道1 || Pass 1】------------------------------------
		//通道1:垂直方向模糊處理通道 ||Pass 1: Vertical Pass
		Pass
		{
			ZTest Always
			Cull Off

			CGPROGRAM

			//指定此通道的頂點著色器為vert_BlurVertical
			#pragma vertex vert_BlurVertical
			//指定此通道的畫素著色器為frag_Blur
			#pragma fragment frag_Blur

			ENDCG
		}

		//---------------------------------------【通道2 || Pass 2】------------------------------------
		//通道2:水平方向模糊處理通道 ||Pass 2: Horizontal Pass
		Pass
		{
			ZTest Always
			Cull Off

			CGPROGRAM

			//指定此通道的頂點著色器為vert_BlurHorizontal
			#pragma vertex vert_BlurHorizontal
			//指定此通道的畫素著色器為frag_Blur
			#pragma fragment frag_Blur

			ENDCG
		}
	}


	//-------------------------CG著色語言宣告部分 || Begin CG Include Part----------------------  
	CGINCLUDE

	//【1】標頭檔案包含 || include
	#include "UnityCG.cginc"

	//【2】變數宣告 || Variable Declaration
	sampler2D _MainTex;
	//UnityCG.cginc中內建的變數,紋理中的單畫素尺寸|| it is the size of a texel of the texture
	uniform half4 _MainTex_TexelSize;
	//C#指令碼控制的變數 || Parameter
	uniform half _DownSampleValue;

	//【3】頂點輸入結構體 || Vertex Input Struct
	struct VertexInput
	{
		//頂點位置座標
		float4 vertex : POSITION;
		//一級紋理座標
		half2 texcoord : TEXCOORD0;
	};

	//【4】降取樣輸出結構體 || Vertex Input Struct
	struct VertexOutput_DownSmpl
	{
		//畫素位置座標
		float4 pos : SV_POSITION;
		//一級紋理座標(右上)
		half2 uv20 : TEXCOORD0;
		//二級紋理座標(左下)
		half2 uv21 : TEXCOORD1;
		//三級紋理座標(右下)
		half2 uv22 : TEXCOORD2;
		//四級紋理座標(左上)
		half2 uv23 : TEXCOORD3;
	};


	//【5】準備高斯模糊權重矩陣引數7x4的矩陣 ||  Gauss Weight
	static const half4 GaussWeight[7] =
	{
		half4(0.0205,0.0205,0.0205,0),
		half4(0.0855,0.0855,0.0855,0),
		half4(0.232,0.232,0.232,0),
		half4(0.324,0.324,0.324,1),
		half4(0.232,0.232,0.232,0),
		half4(0.0855,0.0855,0.0855,0),
		half4(0.0205,0.0205,0.0205,0)
	};


	//【6】頂點著色函式 || Vertex Shader Function
	VertexOutput_DownSmpl vert_DownSmpl(VertexInput v)
	{
		//【6.1】例項化一個降取樣輸出結構
		VertexOutput_DownSmpl o;

		//【6.2】填充輸出結構
		//將三維空間中的座標投影到二維視窗  
		o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
		//對影象的降取樣:取畫素上下左右周圍的點,分別存於四級紋理座標中
		o.uv20 = v.texcoord + _MainTex_TexelSize.xy* half2(0.5h, 0.5h);;
		o.uv21 = v.texcoord + _MainTex_TexelSize.xy * half2(-0.5h, -0.5h);
		o.uv22 = v.texcoord + _MainTex_TexelSize.xy * half2(0.5h, -0.5h);
		o.uv23 = v.texcoord + _MainTex_TexelSize.xy * half2(-0.5h, 0.5h);

		//【6.3】返回最終的輸出結果
		return o;
	}

	//【7】片段著色函式 || Fragment Shader Function
	fixed4 frag_DownSmpl(VertexOutput_DownSmpl i) : SV_Target
	{
		//【7.1】定義一個臨時的顏色值
		fixed4 color = (0,0,0,0);

		//【7.2】四個相鄰畫素點處的紋理值相加
		color += tex2D(_MainTex, i.uv20);
		color += tex2D(_MainTex, i.uv21);
		color += tex2D(_MainTex, i.uv22);
		color += tex2D(_MainTex, i.uv23);

		//【7.3】返回最終的平均值
		return color / 4;
	}

	//【8】頂點輸入結構體 || Vertex Input Struct
	struct VertexOutput_Blur
	{
		//畫素座標
		float4 pos : SV_POSITION;
		//一級紋理(紋理座標)
		half4 uv : TEXCOORD0;
		//二級紋理(偏移量)
		half2 offset : TEXCOORD1;
	};

	//【9】頂點著色函式 || Vertex Shader Function
	VertexOutput_Blur vert_BlurHorizontal(VertexInput v)
	{
		//【9.1】例項化一個輸出結構
		VertexOutput_Blur o;

		//【9.2】填充輸出結構
		//將三維空間中的座標投影到二維視窗  
		o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
		//紋理座標
		o.uv = half4(v.texcoord.xy, 1, 1);
		//計算X方向的偏移量
		o.offset = _MainTex_TexelSize.xy * half2(1.0, 0.0) * _DownSampleValue;

		//【9.3】返回最終的輸出結果
		return o;
	}

	//【10】頂點著色函式 || Vertex Shader Function
	VertexOutput_Blur vert_BlurVertical(VertexInput v)
	{
		//【10.1】例項化一個輸出結構
		VertexOutput_Blur o;

		//【10.2】填充輸出結構
		//將三維空間中的座標投影到二維視窗  
		o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
		//紋理座標
		o.uv = half4(v.texcoord.xy, 1, 1);
		//計算Y方向的偏移量
		o.offset = _MainTex_TexelSize.xy * half2(0.0, 1.0) * _DownSampleValue;

		//【10.3】返回最終的輸出結果
		return o;
	}

	//【11】片段著色函式 || Fragment Shader Function
	half4 frag_Blur(VertexOutput_Blur i) : SV_Target
	{
		//【11.1】獲取原始的uv座標
		half2 uv = i.uv.xy;

		//【11.2】獲取偏移量
		half2 OffsetWidth = i.offset;
		//從中心點偏移3個間隔,從最左或最上開始加權累加
		half2 uv_withOffset = uv - OffsetWidth * 3.0;

		//【11.3】迴圈獲取加權後的顏色值
		half4 color = 0;
		for (int j = 0; j< 7; j++)
		{
			//偏移後的畫素紋理值
			half4 texCol = tex2D(_MainTex, uv_withOffset);
			//待輸出顏色值+=偏移後的畫素紋理值 x 高斯權重
			color += texCol * GaussWeight[j];
			//移到下一個畫素處,準備下一次迴圈加權
			uv_withOffset += OffsetWidth;
		}

		//【11.4】返回最終的顏色值
		return color;
	}

	//-------------------結束CG著色語言宣告部分  || End CG Programming Part------------------  			
	ENDCG

	FallBack Off
}

2.2 C#程式碼部分

貼出詳細註釋的配合Shader實現此特效的C#指令碼:

using UnityEngine;
using System.Collections;

//設定在編輯模式下也執行該指令碼
[ExecuteInEditMode]
//新增選項到選單中
[AddComponentMenu("Learning Unity Shader/Lecture 15/RapidBlurEffect")]
public class RapidBlurEffect : MonoBehaviour
{
    //-------------------變數宣告部分-------------------
    #region Variables
    
    //指定Shader名稱
    private string ShaderName = "Learning Unity Shader/Lecture 15/RapidBlurEffect";

    //著色器和材質例項
    public Shader CurShader;
    private Material CurMaterial;

    //幾個用於調節引數的中間變數
    public static int ChangeValue;
    public static float ChangeValue2;
    public static int ChangeValue3;

    //降取樣次數
    [Range(0, 6), Tooltip("[降取樣次數]向下取樣的次數。此值越大,則取樣間隔越大,需要處理的畫素點越少,執行速度越快。")]
    public int DownSampleNum = 2;
    //模糊擴散度
    [Range(0.0f, 20.0f), Tooltip("[模糊擴散度]進行高斯模糊時,相鄰畫素點的間隔。此值越大相鄰畫素間隔越遠,影象越模糊。但過大的值會導致失真。")]
    public float BlurSpreadSize = 3.0f;
    //迭代次數
    [Range(0, 8), Tooltip("[迭代次數]此值越大,則模糊操作的迭代次數越多,模糊效果越好,但消耗越大。")]
    public int BlurIterations = 3;

    #endregion

    //-------------------------材質的get&set----------------------------
    #region MaterialGetAndSet
    Material material
    {
        get
        {
            if (CurMaterial == null)
            {
                CurMaterial = new Material(CurShader);
                CurMaterial.hideFlags = HideFlags.HideAndDontSave;
            }
            return CurMaterial;
        }
    }
    #endregion

    #region Functions
    //-----------------------------------------【Start()函式】---------------------------------------------  
    // 說明:此函式僅在Update函式第一次被呼叫前被呼叫
    //--------------------------------------------------------------------------------------------------------
    void Start()
    {
        //依次賦值
        ChangeValue = DownSampleNum;
        ChangeValue2 = BlurSpreadSize;
        ChangeValue3 = BlurIterations;

        //找到當前的Shader檔案
        CurShader = Shader.Find(ShaderName);

        //判斷當前裝置是否支援螢幕特效
        if (!SystemInfo.supportsImageEffects)
        {
            enabled = false;
            return;
        }
    }

    //-------------------------------------【OnRenderImage()函式】------------------------------------  
    // 說明:此函式在當完成所有渲染圖片後被呼叫,用來渲染圖片後期效果
    //--------------------------------------------------------------------------------------------------------
    void OnRenderImage(RenderTexture sourceTexture, RenderTexture destTexture)
    {
        //著色器例項不為空,就進行引數設定
        if (CurShader != null)
        {
            //【0】引數準備
            //根據向下取樣的次數確定寬度係數。用於控制降取樣後相鄰畫素的間隔
            float widthMod = 1.0f / (1.0f * (1 << DownSampleNum));
            //Shader的降取樣引數賦值
            material.SetFloat("_DownSampleValue", BlurSpreadSize * widthMod);
            //設定渲染模式:雙線性
            sourceTexture.filterMode = FilterMode.Bilinear;
            //通過右移,準備長、寬引數值
            int renderWidth = sourceTexture.width >> DownSampleNum;
            int renderHeight = sourceTexture.height >> DownSampleNum;

            // 【1】處理Shader的通道0,用於降取樣 ||Pass 0,for down sample
            //準備一個快取renderBuffer,用於準備存放最終資料
            RenderTexture renderBuffer = RenderTexture.GetTemporary(renderWidth, renderHeight, 0, sourceTexture.format);
            //設定渲染模式:雙線性
            renderBuffer.filterMode = FilterMode.Bilinear;
            //拷貝sourceTexture中的渲染資料到renderBuffer,並僅繪製指定的pass0的紋理資料
            Graphics.Blit(sourceTexture, renderBuffer, material, 0);

            //【2】根據BlurIterations(迭代次數),來進行指定次數的迭代操作
            for (int i = 0; i < BlurIterations; i++)
            {
                //【2.1】Shader引數賦值
                //迭代偏移量引數
                float iterationOffs = (i * 1.0f);
                //Shader的降取樣引數賦值
                material.SetFloat("_DownSampleValue", BlurSpreadSize * widthMod + iterationOffs);

                // 【2.2】處理Shader的通道1,垂直方向模糊處理 || Pass1,for vertical blur
                // 定義一個臨時渲染的快取tempBuffer
                RenderTexture tempBuffer = RenderTexture.GetTemporary(renderWidth, renderHeight, 0, sourceTexture.format);
                // 拷貝renderBuffer中的渲染資料到tempBuffer,並僅繪製指定的pass1的紋理資料
                Graphics.Blit(renderBuffer, tempBuffer, material, 1);
                //  清空renderBuffer
                RenderTexture.ReleaseTemporary(renderBuffer);
                // 將tempBuffer賦給renderBuffer,此時renderBuffer裡面pass0和pass1的資料已經準備好
                 renderBuffer = tempBuffer;

                // 【2.3】處理Shader的通道2,豎直方向模糊處理 || Pass2,for horizontal blur
                // 獲取臨時渲染紋理
                tempBuffer = RenderTexture.GetTemporary(renderWidth, renderHeight, 0, sourceTexture.format);
                // 拷貝renderBuffer中的渲染資料到tempBuffer,並僅繪製指定的pass2的紋理資料
                Graphics.Blit(renderBuffer, tempBuffer, CurMaterial, 2);

                //【2.4】得到pass0、pass1和pass2的資料都已經準備好的renderBuffer
                // 再次清空renderBuffer
                RenderTexture.ReleaseTemporary(renderBuffer);
                // 再次將tempBuffer賦給renderBuffer,此時renderBuffer裡面pass0、pass1和pass2的資料都已經準備好
                renderBuffer = tempBuffer;
            }

            //拷貝最終的renderBuffer到目標紋理,並繪製所有通道的紋理到螢幕
            Graphics.Blit(renderBuffer, destTexture);
            //清空renderBuffer
            RenderTexture.ReleaseTemporary(renderBuffer);

        }

        //著色器例項為空,直接拷貝螢幕上的效果。此情況下是沒有實現螢幕特效的
        else
        {
            //直接拷貝源紋理到目標渲染紋理
            Graphics.Blit(sourceTexture, destTexture);
        }
    }


    //-----------------------------------------【OnValidate()函式】--------------------------------------  
    // 說明:此函式在編輯器中該指令碼的某個值發生了改變後被呼叫
    //--------------------------------------------------------------------------------------------------------
    void OnValidate()
    {
        //將編輯器中的值賦值回來,確保在編輯器中值的改變立刻讓結果生效
        ChangeValue = DownSampleNum;
        ChangeValue2 = BlurSpreadSize;
        ChangeValue3 = BlurIterations;
    }

    //-----------------------------------------【Update()函式】--------------------------------------  
    // 說明:此函式每幀都會被呼叫
    //--------------------------------------------------------------------------------------------------------
    void Update()
    {
        //若程式在執行,進行賦值
        if (Application.isPlaying)
        {
            //賦值
            DownSampleNum = ChangeValue;
            BlurSpreadSize = ChangeValue2;
            BlurIterations = ChangeValue3;
        }
        //若程式沒有在執行,去尋找對應的Shader檔案
#if UNITY_EDITOR
        if (Application.isPlaying != true)
        {
            CurShader = Shader.Find(ShaderName);
        }
#endif

    }

    //-----------------------------------------【OnDisable()函式】---------------------------------------  
    // 說明:當物件變為不可用或非啟用狀態時此函式便被呼叫  
    //--------------------------------------------------------------------------------------------------------
    void OnDisable()
    {
        if (CurMaterial)
        {
            //立即銷燬材質例項
            DestroyImmediate(CurMaterial);
        }

    }

 #endregion

}

將此C#程式碼拖拽到場景的主攝像機之上, 且你的工程中也存在2.1節中貼出的Shader程式碼,那麼就可以在Game視窗中看到經過了螢幕模糊特效的處理後的鏡頭效果。

而Inspector中可得到如下所示的指令碼選項。

 

其中,有3個選項可以調節,他們分別是:

  • [Down Sample Num] – 降取樣的次數。此值越大,則取樣間隔越大,需要處理的畫素點越少,執行速度越快。
  • [Blur Speread Size] -模糊擴散度。進行高斯模糊時,相鄰畫素點的間隔。此值越大相鄰畫素間隔越遠,影象越模糊。但過大的值會導致失真。
  • [Blur Iterations] -迭代次數。此值越大,則模糊操作的迭代次數越多,模糊效果越好,但消耗越大。

調節這三個引數,便可以在場景中定製出自己需要的模糊特效。

2.3 推薦幾組引數設定

這邊推薦幾組效果出色較為出色的引數預設,方便有需要的朋友定製出適合自己的效果。

 





三、最終實現的效果圖示

3.1 Low Poly風格的效果測試




3.2 卡通風格效果測試



3.3 寫實風格的效果測試

 



附1、本文配套原始碼下載連結

附2、Reference