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

分類:IT技術 時間:2016-10-16



本系列文章由@淺墨_毛星雲 出品,轉載請註明出處。  
文章鏈接: http://blog.csdn.net/poem_qianmo/article/details/51871531
作者:毛星雲(淺墨)    微博:http://weibo.com/u/1723155442
本文工程使用的Unity3D版本: 5.2.1 


 

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

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

 

 

卡通風格的效果測試:



 


寫實風格的效果測試:






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

 




 

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


 

首先梳理一下在Unity中實現高斯模糊效果需用到的幾個圖像處理的知識點,說起來也很巧,正好和之前我寫過一個關於OpenCV的系列博客裏的這篇文章(http://blog.csdn.net/poem_qianmo/article/details/22745559)涉及的知識點類似。

 

 



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#代碼部分



C#腳本文件的代碼可以從我們之前的幾篇分析屏幕特效實現的文章中重用(如這篇實現屏幕油畫特效的文章:http://blog.csdn.net/poem_qianmo/article/details/49719247),只用稍微改一點細節即可。 

貼出詳細註釋的配合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 = http://blog.csdn.net/poem_qianmo/article/details/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、本文配套源碼下載鏈接


  【Github】本文Shader源碼

 


附2、Reference

 

[1] https://en.wikipedia.org/wiki/Gaussian_blur

[2] http://www.cnblogs.com/foxianmo/p/4931507.html

[3]《Real-Time Rendering 3rd》,p467-p473.

 


Tags:

文章來源:


ads
ads

相關文章
ads

相關文章

ad