1. 程式人生 > >【Unity3D Shader程式設計】之十 深入理解Unity5中的Standard Shader(二)&螢幕油畫特效的實現

【Unity3D Shader程式設計】之十 深入理解Unity5中的Standard Shader(二)&螢幕油畫特效的實現

本系列文章由出品,轉載請註明出處。 

本文工程使用的Unity3D版本:5.2.1 

概要:本文講解了Unity中著色器編譯多樣化的思路,並對Standard Shader中正向基礎渲染通道的原始碼進行了分析,以及對螢幕油畫特效進行了實現。

眾所周知,Unity官方文件對Shader進階內容的講解是非常匱乏的。本文中對Stardard Shader原始碼的一些分析,全是淺墨自己通過對Shader原始碼的理解,以及Google之後理解與分析而來。如有解釋不妥當之處,還請各位及時指出。

依然是附上一組本文配套工程的執行截圖之後,便開始我們的正文。本次的選用了新的場景,正如下圖中所展示的。

城鎮入口(with 螢幕油畫特效):


城鎮入口(原始圖):


 圖依然是貼這兩張。文章末尾有更多的執行截圖,並提供了源工程的下載。先放出可執行的exe下載,如下:

提示:在此遊戲場景中按F鍵可以開關螢幕特效。

著色器編譯多樣化算是Unity5中Shder書寫的新特性,標準著色器之所以能獨當一面,正是得益於這種特性,在這裡先對此特性進行一個簡單的說明與講解。

一、關於著色器編譯多樣化

Unity5中使用了一種被稱為著色器編譯多樣化(Multiple shader program variants)的新技術,常被稱為“megashaders”或“uber shaders”,並通過為每種情況提供不同的預處理指令來讓著色器程式碼多次被編譯來實現。

在Unity中,這可以通過#pragmamulti_compile或者#pragma shader_feature指令來在著色器程式碼段中實現。這種做法對錶面著色器也可行。

在執行時,相應的著色器變體是從材質的關鍵詞中取得的(Material.EnableKeyword和 DisableKeyword),或者全域性著色器關鍵詞(Shader.EnableKeyword和 DisableKeyword)。

1.1 multi_compile的用法簡析

若我們定義如下指令:

#pragma multi_compile FANCY_STUFF_OFFFANCY_STUFF_ON

也就表示定義了兩個變體:FANCY_STUFF_OFF和FANCY_STUFF_ON。在執行時,其中的一個將被啟用,根據材質或者全域性著色器關鍵詞(#ifdef FANCY_STUFF_OFF之類的巨集命令也可以)來確定啟用哪個。若兩個關鍵詞都沒有啟用,那麼將預設使用前一個選項,也就是關閉(OFF)的選項FANCY_STUFF_OFF。

需要注意,也可以存在超過兩個關鍵字的multi_compile編譯選項,比如,如下程式碼將產生4種著色器的變體:

#pragma multi_compile SIMPLE_SHADINGBETTER_SHADING GOOD_SHADING BEST_SHADING

當#pragma multi_compile中存在所有名字都是下劃線的一個指定段時,就表示需在沒有預處理巨集的情況下產生一個空的著色器變種。這種做法在著色器編寫中比較常見,因為這樣可以在不影響使用的情況下,避免使用兩個關鍵詞,這樣就節省了一個變數個數的佔用(下面會提到,Unity中關鍵詞個數是有129個的數量限制的)。例如,下面的指令將產生兩個著色器變體;第一個沒有定義,第二個定義為FOO_ON:

#pragma multi_compile __ FOO_ON

這樣就省去了一個本來需要定義出來的 FOO_OFF(FOO_OFF沒有定義,自然也不能使用),節省了一個關鍵詞個數的佔用。

若Shader中有如上定義,則可以使用#ifdef來進行判斷:

#ifdef FOO_ON
//程式碼段1
#endif

根據上面已經定義過的FOO_ON,此#ifdef判斷的結果為真,程式碼段1部分的程式碼就會被執行到。反之,若#pragma multi_compile __FOO_ON一句程式碼沒有交代出來,那麼程式碼段1部分的程式碼就不會被執行。

這就是著色器編譯多樣化的實現方式,其實理解起來很容易,對吧。

1.2 shader_feature和multi_compile之間的區別

#pragma shader_feature 和#pragma multi_compile非常相似,唯一的區別在於採用了#pragmashader_feature語義的shader,在遇到不被使用的變體的時候,就不會將其編譯到遊戲中。所以,shader_feature中使得所有的設定到材質中的關鍵詞都是有效的,而multi_compile指令將從全域性程式碼裡設定關鍵詞。

另外,shader_feature還有一個僅僅含有一個關鍵字的快捷表達方式,例如:

#pragma shader_feature FANCY_STUFF

此為#pragma shader_feature _ FANCY_STUFF的一個簡寫形式,其擴展出了兩個著色器變體,第一種變體自然為不定此FANCY_STUFF變數(那麼若在稍後的Shader程式碼中進行#ifdef FANCY_STUFF的判斷,則結果為假),第二種變體為定義此FANCY_STUFF變數(此情況下#ifdef FANCY_STUFF的判斷結果為真)。

1.3 多個multi_compile連用會造成指數型增長

可以提供多個multi_compile流水線,然後著色器的結果可以被編譯為幾個流水線的排列組合,比如:

#pragma multi_compile A B C
#pragma multi_compile D E

第一行中有3種選項,第二行中有兩種選項,那麼進行排列組合,總共就會有六種選項(A+D, B+D, C+D, A+E, B+E, C+E)。

容易想到,一般每以個multi_compile流水線,都控制著著色器中某一單一的特性。請注意,著色器總量的增長速度是非常快的。

比如,10條包含兩個特性的multi_compil指令,會得到2的10次方,也就是1024種不同的著色器變體。

1.4 關於Unity中的關鍵詞限制Keyword limit

當使用著色變數時,我們應該記住,Unity中將關鍵詞的數量限制在了128個之內(著色變數算作關鍵字),且其中有一些已經被Unity內建使用了,因此,我們真正可以自定義使用關鍵詞的數量以及是小於128個的。同時,關鍵詞是在單個Unity專案中全域性使用並計數的,所以我們要千萬小心,在同一專案中存在的但沒用到Shader也要考慮在內,千萬不要合起來在數量上超出Unity的關鍵詞數量限制了。

1.5 Unity內建的快捷multi_compile指令

如下有Unity內建的幾個著色器變體的快捷多編譯指令,他們大多是應對Unity中不同的光線,陰影和光照貼圖型別。詳情見rendering pipeline

  • multi_compile_fwdbase - 此指令表示,編譯正向基礎渲染通道(用於正向渲染中,應用環境光照、主方向光照和頂點/球面調和光照(Spherical Harmonic Lighting))所需的所有變體。這些變體用於處理不同的光照貼圖型別、主要方向光源的陰影選項的開關與否。
  • multi_compile_fwdadd - 此指令表示, 編譯正向附加渲染通道(用於正向渲染中;以每個光照一個通道的方式應用附加的逐畫素光照)所需的所有變體。這些變體用於處理光源的型別(方向光源、聚光燈或者點光源),且這些變種都包含紋理cookie。
  • multi_compile_fwdadd_fullshadows – 此指令和上面的正向渲染附加通道基本一致,但同時為上述通道的處理賦予了光照實時陰影的能力。
  • multi_compile_fog - 此指令表示,編譯出幾個不同的Shader變體來處理不同型別的霧效(關閉/線性/指數/二階指數)(off/linear/exp/exp2).

1.6 使用指令跳過某些變體的編譯

大多數內建的快捷指令導致了很多著色的變體。若我們熟悉他們且知道有些並非所需,可以使用#pragmaskip_variants語句跳過其中一些的編譯。例如:

#pragma multi_compile_fwdadd
// 將跳過所有使用"POINT"或 "POINT_COOKIE"的變體
#pragma skip_variants POINT POINT_COOKIE

OK,通過上面經過翻譯&理解過後的官方文件材料,應該對Unity中的著色器編譯多樣化有了一個理解。說白了,著色器變體的定義和使用與巨集定義很類似。

1.7 對知識的提煉

上面交代了這麼多,看不懂沒關係,我們提煉一下,看懂這段提煉,關於著色器變體的意義與使用方式,也就懂了大半了。

若我們在著色器中定義了這一句:

#pragma shader_feature _THIS_IS_A_SAMPLE

這句程式碼理解起來,也就是_THIS_IS_A_SAMPLE被我們定義過了,它是存在的,以後我們如果判斷#ifdef _THIS_IS_A_SAMPLE,那就是真了。我們可以在這個判斷的#ifdef…… #endif塊裡面實現自己需要的實現程式碼X,這段實現程式碼X,只會在你用#pragma multi_compile 或#pragmashader_feature定義了_THIS_IS_A_SAMPLE這個“巨集”的時候會被執行,否則,它就不會被執行到。

實現程式碼X的執行與不執行,全靠你對變體的定義與否。這就是著色器編譯多樣化的實現方式,一個著色器+多個CG標頭檔案的小團隊(如標準著色器),可以獨當一面,一個打一群,可以取代一大堆獨立實現的Shader的原因所在。

二、Standard Shader中正向基礎渲染通道原始碼分析

這一節主要用來解析Standard Shader中正向基礎渲染通道的原始碼。

先上Standard Shader正向渲染基礎通道(Shader Model 3.0版)的Shader原始碼:

	//------------------------------------【子著色器1】------------------------------------
	// 此子著色器用於Shader Model 3.0
	//----------------------------------------------------------------------------------------
	SubShader
	{
		//渲染型別設定:不透明
		Tags { "RenderType"="Opaque" "PerformanceChecks"="False" }

		//細節層次設為:300
		LOD 300
		
		//--------------------------------通道1-------------------------------
		// 正向基礎渲染通道(Base forward pass)
		// 處理方向光,自發光,光照貼圖等 ...
		Pass
		{
			//設定通道名稱
			Name "FORWARD" 

			//於通道標籤中設定光照模型為ForwardBase,正向渲染基礎通道
			Tags { "LightMode" = "ForwardBase" }

			//混合操作:源混合乘以目標混合
			Blend [_SrcBlend] [_DstBlend]
			// 根據_ZWrite引數,設定深度寫入模式開關與否
			ZWrite [_ZWrite]

			//===========開啟CG著色器語言編寫模組===========
			CGPROGRAM

			//著色器編譯目標:Model 3.0
			#pragma target 3.0

			//編譯指令:不使用GLES渲染器編譯
			#pragma exclude_renderers gles
			
			// ---------編譯指令:著色器編譯多樣化--------
			#pragma shader_feature _NORMALMAP
			#pragma shader_feature _ _ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON
			#pragma shader_feature _EMISSION
			#pragma shader_feature _METALLICGLOSSMAP 
			#pragma shader_feature ___ _DETAIL_MULX2
			#pragma shader_feature _PARALLAXMAP
			
			//--------著色器編譯多樣化快捷指令------------
			//編譯指令:編譯正向渲染基礎通道(用於正向渲染中,應用環境光照、主方向光照和頂點/球面調和光照)所需的所有變體。
			//這些變體用於處理不同的光照貼圖型別、主要方向光源的陰影選項的開關與否
			#pragma multi_compile_fwdbase
			//編譯指令:編譯幾個不同變種來處理不同型別的霧效(關閉/線性/指數/二階指數/)
			#pragma multi_compile_fog

			//編譯指令:告知編譯器頂點和片段著色函式的名稱
			#pragma vertex vertForwardBase
			#pragma fragment fragForwardBase

			//包含輔助CG標頭檔案
			#include "UnityStandardCore.cginc"

			//===========結束CG著色器語言編寫模組===========
			ENDCG
		}
	……
	}

OK,一起來稍微分析一下上述程式碼。基本上是逐行註釋,所以找幾個容易疑惑的點來提一下。

第一處,著色器編譯多樣化部分,程式碼如下:

// ---------編譯指令:著色器編譯多樣化--------
#pragma shader_feature _NORMALMAP
#pragma shader_feature _ _ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON
#pragma shader_feature _EMISSION
#pragma shader_feature _METALLICGLOSSMAP
#pragma shader_feature ___ _DETAIL_MULX2
#pragma shader_feature _PARALLAXMAP

上文剛講過著色器編譯多樣化的一些理解,理解起來就是這樣,這邊定義了很多的“巨集”、 _NORMALMAP、_ALPHATEST_ON、_ALPHABLEND_ON、_EMISSION、_METALLICGLOSSMAP、_DETAIL_MULX2、_PARALLAXMAP,在頂點和片段著色器實現部分,可以用#ifdef _EMISSION類似的巨集命令來對不同情況下的實現進行區別對待。

第二處,著色器編譯多樣化快捷指令部分,上文的講解部分也有分別提到,這裡程式碼註釋已經很詳細,如下:

//--------著色器編譯多樣化快捷指令------------
//編譯指令:編譯正向渲染基礎通道(用於正向渲染中,應用環境光照、主方向光照和頂點/球面調和光照)所需的所有變體。
//這些變體用於處理不同的光照貼圖型別、主要方向光源的陰影選項的開關與否
#pragma multi_compile_fwdbase
 
//編譯指令:編譯幾個不同變種來處理不同型別的霧效(關閉/線性/指數/二階指數/)
#pragma multi_compile_fog

第三處,頂點著色函式和片段著色函式宣告部分,程式碼如下:

//編譯指令:告知編譯器頂點和片段著色函式的名稱
#pragma vertex vertForwardBase
#pragma fragment fragForwardBase

這裡比較關鍵,指明瞭這個pass中頂點著色函式和片段著色函式分別是名為vertForwardBase和fragForwardBase的函式。而這兩個函式定義於何處?看包含標頭檔案是什麼即可。一起來看一下第四處。

第四處,CG標頭檔案包含部分,程式碼如下:

//包含輔助CG標頭檔案
#include"UnityStandardCore.cginc"

很簡單的一句話,但卻像一切程式語言中標頭檔案的包含一樣,非常關鍵,不能缺少。vertForwardBase和       fragForwardBase的函式全都定義於此“UnityStandardCore.cginc”標頭檔案中。

OK,我們轉到“UnityStandardCore.cginc”標頭檔案,繼續分析下去。先從vertForwardBase函式開始。

1.頂點著色函式——vertForwardBase

vertForwardBase函式也已詳細註釋好,程式碼如下:

//-----------------------------------【vertForwardBase函式】----------------------------------------
//  用途:正向渲染基礎通道的頂點著色函式
//  說明:例項化一個VertexOutputForwardBase結構體物件,並進行相應的填充
//  輸入:VertexInput結構體
//  輸出:VertexOutputForwardBase結構體
//  附:VertexInput結構體原型:
/*
struct VertexInput
{
	float4 vertex	: POSITION;
	half3 normal	: NORMAL;
	float2 uv0		: TEXCOORD0;
	float2 uv1		: TEXCOORD1;
	#if defined(DYNAMICLIGHTMAP_ON) || defined(UNITY_PASS_META)
	float2 uv2		: TEXCOORD2;
	#endif
	#ifdef _TANGENT_TO_WORLD
	half4 tangent	: TANGENT;
	#endif
};
*/
//---------------------------------------------------------------------------------------------------------
VertexOutputForwardBase vertForwardBase (VertexInput v)
{
	//【1】例項化一個VertexOutputForwardBase結構體物件
	VertexOutputForwardBase o;
	//用Unity內建的巨集初始化引數
	UNITY_INITIALIZE_OUTPUT(VertexOutputForwardBase, o);

	//【2】通過物體座標系到世界座標系的變換矩陣乘以物體的頂點位置,得到物件在世界座標系中的位置
	float4 posWorld = mul(_Object2World, v.vertex);
	
	//【3】若定義了鏡面立方體投影巨集,將計算得到的世界座標系的xyz座標作為輸出引數的世界座標值
	#if UNITY_SPECCUBE_BOX_PROJECTION
		o.posWorld = posWorld.xyz;
	#endif

	//【4】輸出的頂點位置(畫素位置)為模型檢視投影矩陣乘以頂點位置,也就是將三維空間中的座標投影到了二維視窗
	o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
	//【5】計算紋理座標,使用UnityStandardInput.cginc標頭檔案中的輔助函式。
	o.tex = TexCoords(v);
	//【6】視線的方向= 物件在世界座標系中的位置減去攝像機的世界空間位置,並進行逐頂點歸一化
	o.eyeVec = NormalizePerVertexNormal(posWorld.xyz - _WorldSpaceCameraPos);

	//【7】計算物體在世界空間中的法線座標
	float3 normalWorld = UnityObjectToWorldNormal(v.normal);

	//【8】進行世界空間中的切線相關引數的計算與賦值
	//若定義了_TANGENT_TO_WORLD
	#ifdef _TANGENT_TO_WORLD
		//世界空間中的物體的法線值
		float4 tangentWorld = float4(UnityObjectToWorldDir(v.tangent.xyz), v.tangent.w);
		//在世界空間中為每個頂點建立切線
		float3x3 tangentToWorld = CreateTangentToWorldPerVertex(normalWorld, tangentWorld.xyz, tangentWorld.w);
		//分別為3個分量賦值
		o.tangentToWorldAndParallax[0].xyz = tangentToWorld[0];
		o.tangentToWorldAndParallax[1].xyz = tangentToWorld[1];
		o.tangentToWorldAndParallax[2].xyz = tangentToWorld[2];
	//否則,三個分量直接取為0,0和上面計算得到的normalWorld
	#else
		o.tangentToWorldAndParallax[0].xyz = 0;
		o.tangentToWorldAndParallax[1].xyz = 0;
		o.tangentToWorldAndParallax[2].xyz = normalWorld;
	#endif

	//【9】陰影的獲取
	TRANSFER_SHADOW(o);

	//【10】進行頂點正向相關的全域性光照操作
	o.ambientOrLightmapUV = VertexGIForward(v, posWorld, normalWorld);

	//【11】若定義了_PARALLAXMAP巨集,則計算視差的視角方向並賦值
	#ifdef _PARALLAXMAP
		//宣告一個由切線空間的基組成的3x3矩陣“rotation” 
		TANGENT_SPACE_ROTATION;
		//計算視差的視角方向
		half3 viewDirForParallax = mul (rotation, ObjSpaceViewDir(v.vertex));
		//分別將三個分量賦值給VertexOutputForwardBase結構體物件o的tangentToWorldAndParallax的三個分量
		o.tangentToWorldAndParallax[0].w = viewDirForParallax.x;
		o.tangentToWorldAndParallax[1].w = viewDirForParallax.y;
		o.tangentToWorldAndParallax[2].w = viewDirForParallax.z;
	#endif

	//【12】若定義了UNITY_OPTIMIZE_TEXCUBELOD,便計算反射光方向向量並賦值
	#if UNITY_OPTIMIZE_TEXCUBELOD
		//使用CG語言內建函式reflect計算反射光方向向量
		o.reflUVW 		= reflect(o.eyeVec, normalWorld);
	#endif

	//【13】從頂點中輸出霧資料
	UNITY_TRANSFER_FOG(o,o.pos);

	//【14】返回已經附好值的VertexOutputForwardBase型別的物件
	return o;
}


基本步驟已經在程式碼註釋中用序號列出,以下將對其中的主要知識點進行講解。首先看一下函式的輸出引數——VertexInput。

2.頂點輸入結構體——VertexInput

此結構體定義於UnityStandardInput.cginc標頭檔案中,是頂點著色函式vertForwardBase的輸入引數,相關程式碼如下所示:

//頂點輸入結構體
struct VertexInput
{
	float4 vertex	: POSITION;//位置座標
	half3 normal	: NORMAL;//法線向量
	float2 uv0		: TEXCOORD0;//一級紋理座標
	float2 uv1		: TEXCOORD1;//二級紋理座標
	//若DYNAMICLIGHTMAP_ON或者UNITY_PASS_META選項為開,則還定義一個三級紋理
#if defined(DYNAMICLIGHTMAP_ON) || defined(UNITY_PASS_META)
	float2 uv2		: TEXCOORD2;//三級紋理
#endif
#ifdef _TANGENT_TO_WORLD
	half4 tangent	: TANGENT;//切線向量
#endif
};

此結構體比較通用,不僅僅是用於正向基礎渲染通道,畢竟是定義在UnityStandardInput.cginc標頭檔案中的。

各個變數的含義,註釋中已經寫到了,好像沒有什麼值得多說的,再來看下頂點輸出結構體。

3.頂點輸出結構體——VertexOutputForwardBase

顧名思義,VertexOutputForwardBase結構體就是正向基礎渲染通道特有的輸出結構體,定義於UnityStandardCore.cginc標頭檔案中,註釋後的程式碼如下:

//正向渲染基礎通道的輸出結構體
struct VertexOutputForwardBase
{
	float4 pos							: SV_POSITION;//畫素座標
	float4 tex							: TEXCOORD0;//一級紋理
	half3 eyeVec 						: TEXCOORD1;//二級紋理(視線向量)
	half4 tangentToWorldAndParallax[3]	: TEXCOORD2;	//3x3為切線到世界矩陣的值,1x3為視差方向的值
	half4 ambientOrLightmapUV			: TEXCOORD5;	// 球諧函式(Spherical harmonics)或光照貼圖的UV座標
	SHADOW_COORDS(6)//陰影座標
	UNITY_FOG_COORDS(7)//霧效座標

	//若定義了鏡面立方體投影巨集,定義一個posWorld 
	#if UNITY_SPECCUBE_BOX_PROJECTION
		float3 posWorld					: TEXCOORD8;
	#endif
	//若定義了優化紋理的立方體LOD巨集,還將定義如下的引數reflUVW
	#if UNITY_OPTIMIZE_TEXCUBELOD
		#if UNITY_SPECCUBE_BOX_PROJECTION
			half3 reflUVW				: TEXCOORD9;
		#else
			half3 reflUVW				: TEXCOORD8;
		#endif
	#endif
};

從這裡開始,做一個規定,為了方便對照和理解,以下貼出程式碼中也會貼出原始的英文註釋——先翻譯為中文,以 || 結束,在 || 後附上原始的英文。

就像這樣:

 //最終的二次多項式 ||  Final quadraticpolynomial

OK,我們繼續,vertForwardBase函式中有很多知識點值得拿出來講一講的。

4. UNITY_INITIALIZE_OUTPUT巨集

UNITY_INITIALIZE_OUTPUT(type,name) –此巨集用於將給定型別的名稱變數初始化為零。在使用舊版標準所寫的Shader時,經常會報錯“Try adding UNITY_INITIALIZE_OUTPUT(Input,o); like this in your vertfunction.”之類的錯誤,加上這句就不會報錯了。

5._Object2World矩陣

_Object2World,Unity的內建矩陣,世界座標系到物件座標系的變換矩陣,簡稱“世界-物件矩陣”。

6.UNITY_MATRIX_MVP矩陣

UNITY_MATRIX_MVP為當前的模型矩陣x檢視矩陣x投影矩陣,簡稱“模型-檢視-投影矩陣”。其常用於在頂點著色函式中,通過將它和頂點位置相乘,從而可以把頂點位置從模型空間轉換到裁剪空間(clip space)中。也就是通過此矩陣,將三維空間中的座標投影到了二維視窗中。

7.TexCoords函式

TexCoords函式用於獲取紋理座標,定義UnityStandardInput.cginc標頭檔案中,相關程式碼如下:

float4 TexCoords(VertexInput v)
{
	float4 texcoord;
	texcoord.xy = TRANSFORM_TEX(v.uv0, _MainTex); // Always source from uv0
	texcoord.zw = TRANSFORM_TEX(((_UVSec == 0) ? v.uv0 : v.uv1), _DetailAlbedoMap);
	return texcoord;
}

函式實現程式碼中的_MainTex、_UVSec、_DetailAlbedoMap都是此標頭檔案定義的全域性的變數。

其中還涉及到了一個TRANSFORM_TEX巨集,在這邊也提一下,它定義於UnityCG.cginc標頭檔案中,相關程式碼如下:

// 按比例和偏移進行二維UV座標的變換
#define TRANSFORM_TEX(tex,name) (tex.xy *name##_ST.xy + name##_ST.zw)

8. NormalizePerVertexNormal函式

此函式位於unitystandardcore.cginc標頭檔案中,原型和註釋如下:

//--------------------------【函式NormalizePerVertexNormal】-----------------------------
// 用途:歸一化每頂點法線
// 說明:若滿足特定條件,便歸一化每頂點法線並返回,否則,直接返回原始值
// 輸入:half3型別的法線座標
// 輸出:若滿足判斷條件,返回half3型別的、經過歸一化後的法線座標,否則返回輸入的值
//----------------------------------------------------------------------------------------------- 
half3 NormalizePerVertexNormal (half3 n)
{
	//滿足著色目標模型的版本小於Shader Model 3.0,或者定義了UNITY_STANDARD_SIMPLE巨集,返回歸一化後的值
	#if (SHADER_TARGET < 30) || UNITY_STANDARD_SIMPLE
		return normalize(n);
	//否則,直接返回輸入的引數,後續應該會進行逐畫素的歸一化
	#else
		return n; 
	#endif
}

其中,SHADER_TARGET巨集代表的值為和著色器的目標編譯模型(shader model)相關的一個數值。

例如,當著色器編譯成Shader Model 3.0時,SHADER_TARGET 便為30。我們可以在shader程式碼中由此來進行條件判斷。相關程式碼如下:

#if SHADER_TARGET < 30
//實現程式碼A
#else   
//實現程式碼B
#endif

9. UnityObjectToWorldNormal函式

UnityObjectToWorldNormal是Unity內建的函式,可以將法線從模型空間變換到世界空間中,定義於UnityCG.cginc標頭檔案中,相關程式碼如下:

//將法線從模型空間變換到世界空間
inline float3 UnityObjectToWorldNormal( in float3 norm )
{
	// 將分量分別相乘,並進行歸一化
	//Multiply by transposed inverse matrix, actually using transpose() generates badly optimized code
	return normalize(_World2Object[0].xyz * norm.x + _World2Object[1].xyz * norm.y + _World2Object[2].xyz * norm.z);
}

而其中的normalize( )函式太常見不過了,是來自CG語言中的函式,作用是歸一化向量。

10.UnityObjectToWorldDir函式

UnityObjectToWorldDir函式用於方向值從物體空間切換到世界空間,也定義於UnityCG.cginc標頭檔案中,相關程式碼如下:

//將方向值從物體空間切換到世界空間
inline float3 UnityObjectToWorldDir( in float3 dir )
{
	return normalize(mul((float3x3)_Object2World, dir));
}

可以看到,就是返回一個世界-物件矩陣乘以方向值歸一化後的結果,比較好理解。

11. CreateTangentToWorldPerVertex函式

CreateTangentToWorldPerVertex函式用於在世界空間中為每個頂點建立切線,定義於UnityStandardUtils.cginc標頭檔案中,相關程式碼如下:

half3x3 CreateTangentToWorldPerVertex(half3 normal, half3 tangent, half tangentSign)
{
	//對於奇數負比例變換,我們需要將符號反向||For odd-negative scale transforms we need to flip the sign
	half sign = tangentSign * unity_WorldTransformParams.w;
	half3 binormal = cross(normal, tangent) * sign;
	return half3x3(tangent, binormal, normal);
}

其中的unity_WorldTransformParams是UnityShaderVariables.cginc標頭檔案中定義的一個uniform float4型的變數,其w分量用於標定奇數負比例變換(odd-negativescale transforms),通常取值為1.0或者-1.0。

12.TRANSFER_SHADOW(a)巨集

此巨集用於進行陰影在各種空間中的轉換,定義於AutoLight.cginc中。在不同的情況下,此巨集代表的意義並不相同。下面簡單進行下展開分析。

1)對於螢幕空間中的陰影(Screen space shadows)

對應於螢幕空間中的陰影,也就是#if defined (SHADOWS_SCREEN),其相關程式碼如下:

#if defined (SHADOWS_SCREEN)
……
#if defined(UNITY_NO_SCREENSPACE_SHADOWS)
#define TRANSFER_SHADOW(a) a._ShadowCoord = mul( unity_World2Shadow[0], mul( _Object2World, v.vertex ) );

#else // not UNITY_NO_SCREENSPACE_SHADOWS

#define TRANSFER_SHADOW(a) a._ShadowCoord = ComputeScreenPos(a.pos);
……
#endif

也就是說,這種情況下的TRANSFER_SHADOW(a)巨集,代表了一句程式碼,這句程式碼就是a._ShadowCoord = mul (unity_World2Shadow[0],mul(_Object2World,v.vertex));

此句程式碼的含義是:將世界-陰影座標乘以世界-模型座標和物體頂點座標的積,也就是先將物體座標轉換成世界座標,再將世界座標轉換成陰影座標,並將結果存放於a._ShadowCoord中。

2)對於聚光燈陰影(Spot light shadows)

而對於聚光燈的陰影,也就是#if defined (SHADOWS_DEPTH)&& defined (SPOT)

有如下定義:

#if defined (SHADOWS_DEPTH) && defined (SPOT)
#define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_World2Shadow[0], mul(_Object2World,v.vertex));
……
#endif

可以發現,這種情況下的TRANSFER_SHADOW(a)巨集代表的語句也是a._ShadowCoord = mul (unity_World2Shadow[0],mul(_Object2World,v.vertex));

同上,用途就是先將物體座標轉換成世界座標,再將世界座標轉換成陰影座標,並將結果存放於a._ShadowCoord中。

3)對於點光源陰影(Point light shadows)

而對於點光源的陰影,也就是#if defined (SHADOWS_CUBE),有如下定義:

#if defined (SHADOWS_CUBE)
	#define TRANSFER_SHADOW(a) a._ShadowCoord = mul(_Object2World, v.vertex).xyz - _LightPositionRange.xyz;
……
#endif

也就是說,這種情況下的TRANSFER_SHADOW(a)巨集代表語句a._ShadowCoord = mul(_Object2World, v.vertex).xyz -_LightPositionRange.xyz;

想了解此程式碼的含義,先要知道_LightPositionRange變數的含義。

這個變數是UnityShaderVariables.cginc標頭檔案中定義的一個全域性變數:

uniform float4 _LightPositionRange; // xyz= pos, w = 1/range 

從英文註釋可以發現,此引數的x,y,z分量表示世界空間下光源的座標,而w為世界空間下範圍的倒數。

那麼此句程式碼的含義,也就是先將物體-世界矩陣乘以物體頂點座標,得到物體的世界空間座標,然後取座標的xyz分量,與光源的座標相減,並將結果賦給a._ShadowCoord。

4)對於關閉陰影(Shadows off)的情況

而對於關閉陰影的情況,也就是#if !defined (SHADOWS_SCREEN)&& !defined (SHADOWS_DEPTH) && !defined (SHADOWS_CUBE),有如下定義:

#if !defined (SHADOWS_SCREEN) && !defined (SHADOWS_DEPTH) && !defined (SHADOWS_CUBE)
	#define TRANSFER_SHADOW(a)
……
#endif

這種情況下的TRANSFER_SHADOW(a)巨集代表的是空白,並沒有什麼用。

13. VertexGIForward函式

定義於UnityStandardCore.cginc標頭檔案中。詳細註釋後的程式碼如下:

//頂點正向全域性光照函式
inline half4 VertexGIForward(VertexInput v, float3 posWorld, half3 normalWorld)
{
	//【1】定義一個half4型的ambientOrLightmapUV變數,並將四個分量都置為0
	half4 ambientOrLightmapUV = 0;

	//【2】對ambientOrLightmapUV變數的四個分量賦值
	// 【2-1】若沒有定義LIGHTMAP_OFF(關閉光照貼圖)巨集,也就是此情況下啟用靜態的光照貼圖,則計算對應的光照貼圖座標
	//static lightmap
	#ifndef LIGHTMAP_OFF
		ambientOrLightmapUV.xy = v.uv1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
		ambientOrLightmapUV.zw = 0;

	//【2-2】若定義了UNITY_SHOULD_SAMPLE_SH巨集,則表示對動態的物件取樣(不對靜態或者動態的光照貼圖取樣)
	// || Sample light probe for Dynamic objects only (no static or dynamic lightmaps)
	#elif UNITY_SHOULD_SAMPLE_SH

		//【2-2-1】若定義瞭如下的UNITY_SAMPLE_FULL_SH_PER_PIXEL巨集(即取樣計算全部的每畫素球面調和光照),便給ambientOrLightmapUV.rgb賦值為0		
		#if UNITY_SAMPLE_FULL_SH_PER_PIXEL 
			ambientOrLightmapUV.rgb = 0;

		//【2-2-2】若滿足著色目標模型的版本小於Shader Model 3.0,或者定義了UNITY_STANDARD_SIMPLE巨集
		//便使用球面調和函式ShadeSH9給ambientOrLightmapUV.rgb賦值
		#elif (SHADER_TARGET < 30) || UNITY_STANDARD_SIMPLE
			ambientOrLightmapUV.rgb = ShadeSH9(half4(normalWorld, 1.0));

		//【2-2-3】否則,使用三序球面調和函式ShadeSH3Order給ambientOrLightmapUV.rgb賦值
		#else
			//優化操作:光源L0、L1逐畫素,光源L2逐頂點 ||  Optimization: L2 per-vertex, L0..L1 per-pixel
			ambientOrLightmapUV.rgb = ShadeSH3Order(half4(normalWorld, 1.0));
		#endif

		//【2-2-4】 從非重要的點光源中新增近似的照明 || Add approximated illumination from non-important point lights	
		//若定義瞭如下的VERTEXLIGHT_ON巨集(即開啟頂點光照),便使用Shade4PointLights函式給ambientOrLightmapUV.rgb賦值,新增環境光
		#ifdef VERTEXLIGHT_ON
			//	Shade4PointLights為Unity內建的逐頂點光照處理函式,定義於unityCG.cginc標頭檔案中
			ambientOrLightmapUV.rgb += Shade4PointLights (
				unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
				unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,
				unity_4LightAtten0, posWorld, normalWorld);
		#endif
	#endif

	//【2-3】若定義瞭如下的VERTEXLIGHT_ONDYNAMICLIGHTMAP_ON巨集(即開啟動態光照貼圖),則給變數的zw分量賦值
	#ifdef DYNAMICLIGHTMAP_ON
		ambientOrLightmapUV.zw = v.uv2.xy * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw;
	#endif
	//【3】返回ambientOrLightmapUV變數的值
	return ambientOrLightmapUV;
}

其中有一些小的點,這邊提出來講一下。

1)unity_LightmapST變數

unity_LightmapST變數型別為float4型,定義於UnityShaderVariables.cginc標頭檔案中,存放著光照貼圖操作的引數的值:

 float4 unity_LightmapST;

2)UNITY_SHOULD_SAMPLE_SH巨集

此巨集定義於UnityCG.cginc中,相關程式碼如下:

//包含間接漫反射的動態&靜態光照貼圖,所以忽略掉球面調和光照 ||  Dynamic & Static lightmaps contain indirect diffuse ligthing, thus ignore SH
#define UNITY_SHOULD_SAMPLE_SH ( defined (LIGHTMAP_OFF) && defined(DYNAMICLIGHTMAP_OFF) )

可以發現,這個巨集,其實就是將LIGHTMAP_OFF(關閉光照貼圖)巨集和DYNAMICLIGHTMAP_OFF(關閉動態光照貼圖)巨集的定義進行了封裝。

3)UNITY_SAMPLE_FULL_SH_PER_PIXEL巨集

UNITY_SAMPLE_FULL_SH_PER_PIXEL巨集定義於UnityStandardConfig.cginc標頭檔案中。其實也就是一個識別符號,用0標示UNITY_SAMPLE_FULL_SH_PER_PIXEL巨集是否已經定義。按字面上理解,啟用此巨集表示我們將取樣計算每畫素球面調和光照,而不是預設的逐頂點計算球面調和光照並且線性插值到每畫素中。其實現程式碼如下,非常簡單:

#ifndef UNITY_SAMPLE_FULL_SH_PER_PIXEL
#define UNITY_SAMPLE_FULL_SH_PER_PIXEL 0
#endif

4)ShadeSH9函式

ShadeSH9就是大家常說的球面調和函式,定義於UnityCG.cginc標頭檔案中,相關程式碼如下:

//球面調和函式
//法線需被初始化,w=1.0 || normal should be normalized, w=1.0
half3 ShadeSH9 (half4 normal)
{
	half3 x1, x2, x3;
	
	//線性+常數多項式  || Linear + constant polynomial terms
	x1.r = dot(unity_SHAr,normal);
	x1.g = dot(unity_SHAg,normal);
	x1.b = dot(unity_SHAb,normal);
	
	//二次多項式的四個引數 || 4 of the quadratic polynomials
	half4 vB = normal.xyzz * normal.yzzx;
	x2.r = dot(unity_SHBr,vB);
	x2.g = dot(unity_SHBg,vB);
	x2.b = dot(unity_SHBb,vB);
	
	//最終二次多項式 ||  Final quadratic polynomial
	half vC = normal.x*normal.x - normal.y*normal.y;
	x3 = unity_SHC.rgb * vC;
	return x2 + x3 + x1;
}

5)ShadeSH3Order函式

ShadeSH3Order函式,我將其翻譯為三序球面調和函式。定義於UnityCG.cginc標頭檔案中,相關程式碼如下:

//三序球面調和函式
//法線需被初始化,w=1.0 ||  normal should be normalized, w=1.0
half3 ShadeSH3Order(half4 normal)
{
	half3 x2, x3;
	//二次多項式的四個引數 || 4 of the quadratic polynomials
	half4 vB = normal.xyzz * normal.yzzx;
	x2.r = dot(unity_SHBr,vB);
	x2.g = dot(unity_SHBg,vB);
	x2.b = dot(unity_SHBb,vB);
	
	//最終的二次多項式 || Final quadratic polynomial
	half vC = normal.x*normal.x - normal.y*normal.y;
	x3 = unity_SHC.rgb * vC;

	return x2 + x3;
}
6)Shade4PointLights函式

Shade4PointLights為Unity為我們準備好的逐頂點光照處理函式,定義於unityCG.cginc標頭檔案中,相關程式碼如下:

//在正向基礎渲染通道中使用,根據4個不同的點光源計算出漫反射光照引數的rgb值|| Used in ForwardBase pass: Calculates diffuse lighting from 4 point lights, with data packed in a special way.
float3 Shade4PointLights (
	float4 lightPosX, float4 lightPosY, float4 lightPosZ,
	float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3,
	float4 lightAttenSq,
	float3 pos, float3 normal)
{
	// 【1】將輸入引數轉換為光照向量 || to light vectors
	float4 toLightX = lightPosX - pos.x;
	float4 toLightY = lightPosY - pos.y;
	float4 toLightZ = lightPosZ - pos.z;
	// 【2】計算平方的值 || squared lengths
	float4 lengthSq = 0;
	lengthSq += toLightX * toLightX;
	lengthSq += toLightY * toLightY;
	lengthSq += toLightZ * toLightZ;
	// 【3】法線方向點乘光線方向|| NdotL
	float4 ndotl = 0;
	ndotl += toLightX * normal.x;
	ndotl += toLightY * normal.y;
	ndotl += toLightZ * normal.z;
	// 【4】修正NdotL(法線方向點乘光線方向)的值 || correct NdotL
	float4 corr = rsqrt(lengthSq);
	ndotl = max (float4(0,0,0,0), ndotl * corr);
	// 【5】計算衰減係數 || attenuation
	float4 atten = 1.0 / (1.0 + lengthSq * lightAttenSq);
	float4 diff = ndotl * atten;
	// 【6】得到最終的顏色 || final color
	float3 col = 0;
	col += lightColor0 * diff.x;
	col += lightColor1 * diff.y;
	col += lightColor2 * diff.z;
	col += lightColor3 * diff.w;
	return col;
}


14. TANGENT_SPACE_ROTATION巨集

TANGENT_SPACE_ROTATION巨集定義於UnityCG.cginc中,作用是宣告一個由切線空間的基組成的3x3矩陣,相關程式碼如下:

//宣告一個由切線空間的基組成的3x3矩陣 || Declares 3x3 matrix 'rotation', filled with tangent space basis
#define TANGENT_SPACE_ROTATION \
	float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w; \
	float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )

也就是說,使用TANGENT_SPACE_ROTATION巨集也就表示定義了上述程式碼所示的float3 型別的binormal和float3x3型別的rotation兩個變數。且其中的rotation為3x3的矩陣,由切線空間的基組成。可以使用它把物體空間轉換到切線空間中。

15.UNITY_OPTIMIZE_TEXCUBELOD巨集

UNITY_OPTIMIZE_TEXCUBELOD巨集的定義非常簡單,就是用0標識是否開啟此功能,如下所示:

#ifndef UNITY_OPTIMIZE_TEXCUBELOD
	#define UNITY_OPTIMIZE_TEXCUBELOD 0
#endif

16.reflect函式

reflect函式是CG語言的內建函式。

reflect(I, N) 根據入射光方向向量I,和頂點法向量N,計算反射光方向向量。其中I 和N必須被歸一化,需要特別注意的是,這個I 是指向頂點的;且此函式只對三元向量有效。

17.UNITY_TRANSFER_FOG巨集

UNITY_TRANSFER_FOG巨集相關程式碼定義於UnityCG.Cginc標頭檔案中,用於的相關程式碼如下所示:

//【0】實現不同版本的UNITY_CALC_FOG_FACTOR巨集。
#if defined(FOG_LINEAR)
	// factor = (end-z)/(end-start) = z * (-1/(end-start)) + (end/(end-start))
	#define UNITY_CALC_FOG_FACTOR(coord) float unityFogFactor = (coord) * unity_FogParams.z + unity_FogParams.w
#elif defined(FOG_EXP)
	// factor = exp(-density*z)
	#define UNITY_CALC_FOG_FACTOR(coord) float unityFogFactor = unity_FogParams.y * (coord); unityFogFactor = exp2(-unityFogFactor)
#elif defined(FOG_EXP2)
	// factor = exp(-(density*z)^2)
	#define UNITY_CALC_FOG_FACTOR(coord) float unityFogFactor = unity_FogParams.x * (coord); unityFogFactor = exp2(-unityFogFactor*unityFogFactor)
#else
	#define UNITY_CALC_FOG_FACTOR(coord) float unityFogFactor = 0.0
#endif

//【1】若已經定義了FOG_LINEAR、FOG_EXP、FOG_EXP2巨集三者之中至少之一,便可以進行到此#if實現部分
#if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)

	//【1-1】定義UNITY_FOG_COORDS(idx)巨集
	#define UNITY_FOG_COORDS(idx) float fogCoord : TEXCOORD##idx;

	//【1-2】定義UNITY_TRANSFER_FOG(o,outpos)巨集
	//【1-2-1】若滿足著色目標模型的版本小於Shader Model 3.0,或者定義了SHADER_API_MOBILE巨集,便可以進行到此#if實現部分
//UNITY_CALC_FOG_FACTOR巨集的實現見上
	#if (SHADER_TARGET < 30) || defined(SHADER_API_MOBILE)
		// 移動平臺和Shader Model 2.0:計算每頂點的霧效因子 || mobile or SM2.0: calculate fog factor per-vertex
		#define UNITY_TRANSFER_FOG(o,outpos) UNITY_CALC_FOG_FACTOR((outpos).z); o.fogCoord = unityFogFactor
	//【1-2-2】否則
	#else
		// Shader Model 3.0和PC/遊戲主機平臺:計算每頂點的霧距離,以及每畫素霧效因子 || SM3.0 and PC/console: calculate fog distance per-vertex, and fog factor per-pixel
		#define UNITY_TRANSFER_FOG(o,outpos) o.fogCoord = (outpos).z
	#endif
//【2】否則,直接用UNITY_FOG_COORDS巨集計算霧效引數
#else
	#define UNITY_FOG_COORDS(idx)
	#define UNITY_TRANSFER_FOG(o,outpos)
#endif

可以發現,關於此巨集的定義,主要集中在如下幾句:

#if (SHADER_TARGET < 30) || defined(SHADER_API_MOBILE)
		// 移動平臺和Shader Model 2.0:計算每頂點的霧效因子 || mobile or SM2.0: calculate fog factor per-vertex
		#define UNITY_TRANSFER_FOG(o,outpos) UNITY_CALC_FOG_FACTOR((outpos).z); o.fogCoord = unityFogFactor
	//【1-2-2】否則
	#else
		// Shader Model 3.0和PC/遊戲主機平臺:計算每頂點的霧距離,以及每畫素霧效因子 || SM3.0 and PC/console: calculate fog distance per-vertex, and fog factor per-pixel
		#define UNITY_TRANSFER_FOG(o,outpos) o.fogCoord = (outpos).z
	#endif


而其中巨集定義依賴的UNITY_CALC_FOG_FACTOR巨集,定義於這段程式碼的一開頭,也根據不同的場合,計算方法分為了幾個版本。

OK,頂點著色器分析完篇幅都這麼多了,這一節就到這裡。

三、螢幕油畫特效的實現

之前的文章中提出,Unity中的螢幕特效通常分為兩部分來實現:

  • Shader實現部分
  • 指令碼實現部分

下面依舊是從這兩個方面對本次的特效進行實現。

3.1 Shader實現部分

依舊老規矩,先上註釋好的Shader程式碼。

//Reference:https://www.shadertoy.com/view/MsXSRN#

Shader "淺墨Shader程式設計/Volume10/OilPaintEffect" 
{
	//------------------------------------【屬性值】------------------------------------
	Properties
	{
		_MainTex("Base (RGB)", 2D) = "white" {}
		_Distortion("_Distortion", Range(0.0, 1.0)) = 0.3
		_ScreenResolution("_ScreenResolution", Vector) = (0., 0., 0., 0.)
		_ResolutionValue("_ResolutionValue", Range(0.0, 5.0)) = 1.0
		_Radius("_Radius", Range(0.0, 5.0)) = 2.0
	}

	//------------------------------------【唯一的子著色器】------------------------------------
	SubShader
	{
		//--------------------------------唯一的通道-------------------------------
		Pass
		{
			//設定深度測試模式:渲染所有畫素.等同於關閉透明度測試(AlphaTest Off)
			ZTest Always

			//===========開啟CG著色器語言編寫模組===========
			CGPROGRAM

			//編譯指令: 指定著色器編譯目標為Shader Model 3.0
			#pragma target 3.0

			//編譯指令:告知編譯器頂點和片段著色函式的名稱
			#pragma vertex vert
			#pragma fragment frag

			//包含輔助CG標頭檔案
			#include "UnityCG.cginc"

			//外部變數的宣告
			uniform sampler2D _MainTex;
			uniform float _Distortion;
			uniform float4 _ScreenResolution;
			uniform float _ResolutionValue;
			uniform int  _Radius;

			//頂點輸入結構
			struct vertexInput
			{
				float4 vertex : POSITION;//頂點位置
				float4 color : COLOR;//顏色值
				float2 texcoord : TEXCOORD0;//一級紋理座標
			};

			//頂點輸出結構
			struct vertexOutput
			{
				half2 texcoord : TEXCOORD0;//一級紋理座標
				float4 vertex : SV_POSITION;//畫素位置
				fixed4 color : COLOR;//顏色值
			};


			//--------------------------------【頂點著色函式】-----------------------------
			// 輸入:頂點輸入結構體
			// 輸出:頂點輸出結構體
			//---------------------------------------------------------------------------------
			vertexOutput vert(vertexInput Input)
			{
				//【1】宣告一個輸出結構物件
				vertexOutput Output;

				//【2】填充此輸出結構
				//輸出的頂點位置為模型檢視投影矩陣乘以頂點位置,也就是將三維空間中的座標投影到了二維視窗
				Output.vertex = mul(UNITY_MATRIX_MVP, Input.vertex);
				//輸出的紋理座標也就是輸入的紋理座標
				Output.texcoord = Input.texcoord;
				//輸出的顏色值也就是輸入的顏色值
				Output.color = Input.color;

				//【3】返回此輸出結構物件
				return Output;
			}

			//--------------------------------【片段著色函式】-----------------------------
			// 輸入:頂點輸出結構體
			// 輸出:float4型的顏色值
			//---------------------------------------------------------------------------------
			float4 frag(vertexOutput Input) : COLOR
			{
				//【1】根據設定的解析度比值,計算影象尺寸
				float2 src_size = float2(_ResolutionValue / _ScreenResolution.x, _ResolutionValue / _ScreenResolution.y);
				
				//【2】獲取座標值
				float2 uv = Input.texcoord.xy;

				//【3】根據半徑,計算出n的值
				float n = float((_Radius + 1) * (_Radius + 1));;

				//【4】定義一些引數
				float3 m0 = 0.0;  float3 m1 = 0.0;
				float3 s0 = 0.0;  float3 s1 = 0.0;
				float3 c;

				//【5】按半徑Radius的值,迭代計算m0和s0的值
				for (int j = -_Radius; j <= 0; ++j)
				{
					for (int i = -_Radius; i <= 0; ++i)
					{
						c = tex2D(_MainTex, uv + float2(i, j) * src_size).rgb; 
						m0 += c; 
						s0 += c * c;
					}
				}

				//【6】按半徑Radius的值,迭代計算m1和s1的值
				for (int j = 0; j <= _Radius; ++j)
				{
					for (int i = 0; i <= _Radius; ++i)
					{
						c = tex2D(_MainTex, uv + float2(i, j) * src_size).rgb; 
						m1 += c;
						s1 += c * c;
					}
				}

				//【7】定義引數,準備計算最終的顏色值
				float4 finalFragColor = 0.;
				float min_sigma2 = 1e+2;

				//【8】根據m0和s0,第一次計算finalFragColor的值
				m0 /= n;
				s0 = abs(s0 / n - m0 * m0);

				float sigma2 = s0.r + s0.g + s0.b;
				if (sigma2 < min_sigma2) 
				{
					min_sigma2 = sigma2;
					finalFragColor = float4(m0, 1.0);
				}

				//【9】根據m1和s1,第二次計算finalFragColor的值
				m1 /= n;
				s1 = abs(s1 / n - m1 * m1);

				sigma2 = s1.r + s1.g + s1.b;
				if (sigma2 < min_sigma2) 
				{
					min_sigma2 = sigma2;
					finalFragColor = float4(m1, 1.0);
				}

				//【10】返回最終的顏色值
				return finalFragColor;
			}

			ENDCG
		}

	}
}

此Shadertoy頁面貼出的基於GLSL的Shader程式碼的void mainImage( out vec4 fragColor,in vec2 fragCoord )函式對應於Unity 中Shader的片段著色器。本次Shader中片段著色函式中的實現方法基本由Shadertoy中的這個OilPaint shader優化和精簡而來,具體原理應該估計要翻國外的paper來寫,會花費不少的時間,精力有限,在這邊就暫且不細展開了。暫時只需知道這邊就是在片段著色器用類似濾波的操作計算出了不同的顏色值並輸出即可。

另外需要注意一點,此Shader的_Radius值越大,此Shader就越耗時,因為_Radius決定了雙層迴圈的次數,而且是指數級的決定關係。_Radius值約小,迴圈的次數就會越小,從而有更快的執行效率。

3.2 C#指令碼實現部分

C#指令碼檔案的程式碼幾乎可以從之前的幾個特效中重用,只用稍微改一點細節就可以。下面也是貼出詳細註釋的實現此特效的C#指令碼:

using UnityEngine;
using System.Collections;

//設定在編輯模式下也執行該指令碼
[ExecuteInEditMode]
//新增選項到選單中
[AddComponentMenu("淺墨Shader程式設計/Volume10/ScreenOilPaintEffect")]
public class ScreenOilPaintEffect : MonoBehaviour 
{
    //-------------------變數宣告部分-------------------
	#region Variables

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

    //兩個引數值
	[Range(0, 5),Tooltip("解析度比例值")]
    public float ResolutionValue = 0.9f;
    [Range(1, 30),Tooltip("半徑的值,決定了迭代的次數")]
    public int RadiusValue = 5;

    //兩個用於調節引數的中間變數
	public static float ChangeValue;
    public static int ChangeValue2;
	#endregion

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

    //-----------------------------------------【Start()函式】---------------------------------------------  
    // 說明:此函式僅在Update函式第一次被呼叫前被呼叫
    //--------------------------------------------------------------------------------------------------------
	void Start () 
	{
        //依次賦值
        ChangeValue = ResolutionValue;
        ChangeValue2 = RadiusValue;

        //找到當前的Shader檔案
        CurShader = Shader.Find("淺墨Shader程式設計/Volume10/ScreenOilPaintEffect");

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

    //-------------------------------------【OnRenderImage()函式】------------------------------------  
    // 說明:此函式在當完成所有渲染圖片後被呼叫,用來渲染圖片後期效果
    //--------------------------------------------------------------------------------------------------------
	void OnRenderImage (RenderTexture sourceTexture, RenderTexture destTexture)
	{
        //著色器例項不為空,就進行引數設定
		if(CurShader != null)
		{
            //給Shader中的外部變數賦值
            material.SetFloat("_ResolutionValue", ResolutionValue);
            material.SetInt("_Radius", RadiusValue);
            material.SetVector("_ScreenResolution", new Vector4(sourceTexture.width, sourceTexture.height, 0.0f, 0.0f));

            //拷貝源紋理到目標渲染紋理,加上我們的材質效果
			Graphics.Blit(sourceTexture, destTexture, material);
		}

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


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