(二十九)unity shader之——————着色器的組織和複用(cginc文件複用代碼、宏定義和shader變體、自定義材質編輯器)

一、cginc文件

1.1 unity的UnityCG.cginc文件

我們經常使用#include 指令包含UnityCG.cginc。這個文件中包含了unity預定義的大量結構和函數,通過#include指令可以複用這些結構和函數,而不必每次都重新定義它們。

1.2定義自己的cginc文件

我們可以定義自己的cginc文件,然後用#include指令包含該文件,實現着色器代碼的複用。雖然包含了整個cginc文件,但unity只會在實際代碼中包含被用到的cginc文件中的那部分代碼。

二、通過UsePass來複用通道

2.1定義自己要複用的通道

我們可以爲定義的Pass聲明名字,以便外部引用,名字要大寫,因爲在unity引擎內部,所有通道名都是大寫的

2.2複用這些通道

複用格式如下:

UsePass "Tut/Organize/UsePass/MyPasses/RED"

三、定義着色器的關鍵字

3.1使用關鍵字改變着色器的行爲

我們可以自定義着色器關鍵字,用它們組織着色器的框架,讓他們可以在不同的平臺上實現預期效果。Unity4.x版本中,整個工程中着色器關鍵字最多不能超過64個(包括unity自己內置關鍵字),在unity5.0版本中,關鍵字數量會增加到128個。一般檢測方法可以用 #if defined(XX)、 #ifdef XX、 #ifndef XX,然後#endif。

3.2 自定義着色器關鍵字

Shader "Tut/Organize/KeyWords/KeyWord_1" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		pass{
		CGPROGRAM
		#define MY_Condition_1    //定義了一個關鍵字
		#pragma vertex vert
		#pragma fragment frag
		#include "UnityCG.cginc"
		struct vertOut{
			float4 pos:SV_POSITION;
		};
		vertOut vert(appdata_base v)
		{
			vertOut o;
			o.pos=UnityObjectToClipPos(v.vertex);
			return o;
		}
		float4 frag(vertOut i):COLOR
		{
			float4 c=float4(0,0,0,0);
			#if defined(MY_Condition_1)       //檢測此關鍵字是否被定義,並執行不同的運算
			c=float4(1,0,0,0);
			#endif
			return c;
		}
		ENDCG
		}//end pass
	} 
}

但是這樣做仍然是靜態的,只會在着色器編譯時起作用,如果想在運行時通過關鍵字改變着色器的行爲呢?

四、ShaderVariant shader變體介紹

4.1 ShaderVariant

舉個例子,對於一個支持法線貼圖的Shader來說,用戶肯定希望無論是否爲材質提供法線貼圖它的Shader都能正確的進行渲染處理。一般有兩種方法來保證這種需求:

  1.在底層shader(GLSL,HLSL等)定義一個由外部傳進來的變量(如int),有沒有提供法線貼圖由外部來判斷並給這個shader傳參,若是有則傳0,否則傳1,在Shader用if對這個變量進行判斷,然後在兩個分支中進行對應的處理。

  2.對底層shader封裝,如Unity的ShaderLab就是這種,然後在上層爲用戶提供定義宏的功能,並決定宏在被定義和未被定義下如何處理。最終編譯時,根據上層的宏定義,根據不同的組合編譯出多套底層shader.

  上述兩種方法,各有利弊,對於前者由於引入了條件判斷,會影響最終shader在GPU上的執行效率。而後者則會導致生成的shader源碼(或二進制文件)變大。Unity中內置的Shader往往採取的是後者,所以這裏只討論這種情況。   

  Unity的Shader中通過multi_compile和shader_feature來定義宏(keyword)。最終編譯的時候也是根據這些宏來編譯成多種組合形式的Shader源碼,其中每一種組合就是這個Uniy Shader的一個Variant。unity內置宏可以用#pragma skip_variants XX來屏蔽,這樣對減少shaderlab內存大小應該也有幫助。

4.2MaterialShaderVariant的關係

 一個Material同一時刻只能對應它所使用的Shader的一個variant。進行切換的要使用Material.EnableKeyword()和Material.DisableKeyword()來開關對應的宏,然後Unity會根據你設定的組合來匹配響應的shader variant進行渲染。如果你是在編輯器非運行模式下進行的修改那麼這些keyword的設置會被保存到材質的.mat文件中,嘗試用NotePad++打開.mat文件,你應該會看到類似於下面的一段內容(需要在編輯器設置裏把AssetSerializationMode設置爲Force Text):

%YAML 1.1

%TAG !u! tag:unity3d.com,2011:

--- !u!21 &2100000

Material:

  serializedVersion: 6

  m_ObjectHideFlags: 0

  m_PrefabParentObject: {fileID: 0}

  m_PrefabInternal: {fileID: 0}

  m_Name: New Material

  m_Shader: {fileID: 4800000, guid: 3e0be7fac8c0b7c4599935fa92c842a4, type: 3}

  m_ShaderKeywords: _B

  m_LightmapFlags: 1

  m_CustomRenderQueue: -1

  …

其中的m_ShaderKeywords就保存了這個材質球使用了哪些宏(keyword).

  如果你手頭有built-in Shader的源碼可以打開裏面的StandardShaderGUI.cs看一下Unity自己事怎麼處理對於StandardShader的keyword設置的。

  另外Shader.EnableKeyword,和Shader.DisableKeyword是對Shader進行全局宏設置的,這裏不提了。

五、使用multi_Compile編譯着色器的多個版本

5.1 使用multi_compile實現多次編譯

unity爲我們提供了multi_compile選項,讓unity針對不同的定義條件或關鍵字編譯多次。然後在運行時,在腳本中開啓或關閉關鍵字,從而使用着色器不同條件下的代碼版本。下面是這種着色器的一個例子:

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Tut/Organize/KeyWords/Multi_Compile" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		pass{
		CGPROGRAM
		#pragma vertex vert
		#pragma fragment frag
		#pragma multi_compile MY_multi_1 MY_multi_2
		#include "UnityCG.cginc"
		struct vertOut{
			float4 pos:SV_POSITION;
		};
		vertOut vert(appdata_base v)
		{
			vertOut o;
			o.pos=UnityObjectToClipPos(v.vertex);
			return o;
		}
		float4 frag(vertOut i):COLOR
		{
			float4 c=float4(0,0,0,0);
			#ifdef MY_multi_1
			c=float4(0,1,0,0);
			#endif
			#ifdef MY_multi_2
			c=float4(0,0,1,0);
			#endif
			return c;
		}
		ENDCG
		}//end pass
		
	} 
	CustomEditor "ExamEditor_2"
}

5.2 在腳本中選擇着色器的版本

當使用multi_compile編譯出着色器的多個版本之後,就可以在腳本中通過着色器的類函數來啓用或者關閉全局全局的關鍵字,從而實現着色器的版本選擇。使用multi_compile似乎可以動態選擇着色器版本,但是該方法是以着色器的冗餘副本爲代價的。比如,有兩個multi_compile A B C、multi_compile D E,會實際產生(A+D,A+E,B+D,B+E,C+D,C+E)6個着色器的版本,因此使用multi_compile來實現動態着色器選擇是有代價的,慎用。

控制腳本例子如下:

 if (flip1)
            {
                Shader.EnableKeyword("MY_multi_1");
                Shader.DisableKeyword("MY_multi_2");
            }
            else
            {
                Shader.EnableKeyword("MY_multi_2");
                Shader.DisableKeyword("MY_multi_1");
            }

六、使用shader_feature編譯着色器的多個版本

6.1multi_compileshader_feature的區別

multi_compile是一直都有的,shader_feature是後來的unity版本中加入的關鍵字。

舉例介紹一下multi_compile和shader_feature:

1.如果你在shader中添加了

#pragma multi_compile  _A _B
#pragma multi_compile  _C _D

  那麼無論這些宏是否真的被用到,你的shader都會被Unity編譯成四個variant,分別包含了_A _C,_A _D, _B _C,_B _D四種keyword組合的代碼

2.如果是

#pragma shader_feature _A _B
#pragma shader_feature _C _D

  那麼你的shader只會保留生成被用到的keyword組合的variant,至於如何判定哪些組合被用到了,等後面提到Assetbundle時候再說。

6.2ShaderVariantAssetbundle的關係

我當時發現打成AB包之後shader_feature所定義的宏沒有被正確包含進去。

  上面說了multi_compile定義的keyword是一定能正確的生成對應的多種組合的shaderVariant,但shader_feature不盡然,Unity引入shader_feature就是爲了避免multi_compile那種完整編譯所導致組合爆炸,很多根本不會被使用的shader_variant也會被生成。Unity在處理shader_feature時會判斷相應的keyword組合是否被使用。需要區分一下幾種情況:

  1.如果shader沒有與使用它的材質打在一個AB中,那麼shader_feature的所有宏相關的代碼都不會被包含進AB包中(有一種例外,就是當shader_feature _A這種形式的時候是可以的),這個shader最終被程序從AB包中拿出來使用也會是錯誤的(粉紅色).

  2.把shader和使用它的材質放到一個AB包中,但是材質中沒有保存任何的keyword信息(你在編輯器中也是這種情況),shader_feature會默認的把第一個keyword也就是上面的_A和_C(即每個shader_feature的第一個)作爲你的選擇。而不會把_A _D,_B _C,_B _D這三種組合的代碼編譯到AB包中。

  3.把shader和使用它的材質放到一個AB包中,並且材質保存了keyword信息(_A _C)爲例,那麼這個AB包就只包含_A _C的shaderVariant.

  可以看到shader_feature所定義的keyword產生的ShaderVariant並不是全部被打包到AB中,特別是你想在遊戲運行時動態的通過EnableKeyWorld函數來進行動態修改材質使用的shaderVariant,如果一開始就沒有把對於variant放進AB包,自然也就找不到。

6.3 ShaderVariantCollection

要正確的讓各種variant正確的在遊戲運行時正確處理,

最直接暴力的兩種方法:

1.把Shader放到在ProjectSetting->Graphics->Always Include Shaders列表裏,Unity就會編譯所有的組合變種。

2.把Shader放到Resources文件夾下,也會正確處理,我猜也應該是全部keyword組合都編譯。

  但是這兩種情況最大的問題就是組合爆炸的問題,如果keyword比較少還好,要是多了那真是不得了,比如你把standardShader放進去,由於它有大量的keyword,全部變種都生成的話大概有幾百兆。另外一個問題就是這種辦法沒法熱更新。自然不如放到AB包裏的好控制。

  放到AB包就又涉及到shader_feature的處理,爲了在運行時動態切換材質的shadervariant,可以在工程裏新建一堆材質,然後把每個材質設置成一種想要的keyword組合,把他們和shader放到一起打到一個AB中去,這樣雖然能讓shadervariant正確生成,但是這些Material是完全多餘的。

  爲了解決這種問題,Unity5.0以後引入了ShaderVariantCollection(下面簡稱SVC),這裏不講用法,只說問題,這個SVC文件可以讓我指定某個shader要編譯都要編譯帶有哪些keyword的變種。並且在ProjectSetting->Graphics界面新加了一個Preloaded Shaders列表,可以讓你把SVC文件放進去,編譯時指定的Shader就會按照SVC中的設置進行正確的variant生成,而不會像Always Include Shaders列表中的那樣全部變種都生成。

  但是它在AB中的表現可就不盡如人意了,要讓它起作用,就必須把它和對應的shader放在一個AB中,而且除了5.6以外版本,我試了幾個都不能正確使用,不是一個variant都沒生成,就是隻生成一個shadervariant(和放一個沒有設置keyword的材質效果一樣).你可以自己用UnityStudio打開查看一下生成的AB內容。

      應該正確的理解Unity提供multi_compile和shader_feature以及ShaderVariantCollection的意圖,根據自己的情況來選擇合理的解決方案。

七、使用自定義的材質編輯器

除了在腳本中選擇着色器版本外,還可以通過擴展MaterialEditor實現靜態的選擇。首先編寫腳本ExamEditor_2,在這個類中重寫OnInspectorGUI方法,代碼如下:

public class ExamEditor_2 : MaterialEditor 
{

	public override void OnInspectorGUI ()
	{
		base.OnInspectorGUI ();

		if (!isVisible)
				return;

		Material targetMat = target as Material;
		string [] keyWords = targetMat.shaderKeywords;
		bool switon = keyWords.Contains ("MY_multi_1");

		EditorGUI.BeginChangeCheck ();//GUI變動開始
		switon = EditorGUILayout.Toggle ("MY_multi_1",switon);
		if(EditorGUI.EndChangeCheck())//GUI變動結束
		{
			var keys=new List<string>{switon?"MY_multi_1":"MY_multi_2"};
			targetMat.shaderKeywords=keys.ToArray();
			EditorUtility.SetDirty(targetMat);
		}
	}
}

然後在shader裏添加:

CustomEditor "ExamEditor_2"

完成上述程序就可以在材質編輯器使用複選框來切換選項了,如下圖:

7.1 MaterialEditor

unity提供了豐富的類和接口,使我們能擴展Unity的編輯器功能。對於材質編輯,也提供了相關的類。MaterialEditor就是這樣一個 編輯器類,可以像擴展普通編輯器類一樣集成、重寫MaterialEditor中的方法。但是要放到Editor文件夾下才能生效。

下面編寫一個類,獲取當前材質編輯器中材質的一個range類型的浮點數、一個整數,並將其值展示出來:

public class ExamEditor_1 : MaterialEditor {
	
	public override void OnInspectorGUI ()
	{
		base.OnInspectorGUI ();
		
		if (!isVisible)
			return;
		
		Material targetMat = target as Material;//我們正在編輯的材質
		Shader shader = targetMat.shader;
		//第二個材質屬性
		string label1 = ShaderUtil.GetPropertyDescription(shader, 1);
		string propertyName1 = ShaderUtil.GetPropertyName(shader, 1);
		float val1 = targetMat.GetFloat (propertyName1);
		//第三個材質屬性
		string label2 = ShaderUtil.GetPropertyDescription(shader, 2);
		string propertyName2 = ShaderUtil.GetPropertyName(shader, 2);
		int val2 = targetMat.GetInt (propertyName2);

		//第1個浮點值的展示
		EditorGUILayout.LabelField(label1+"/"+propertyName1," "+val1);
		//第2個整型的展示
		EditorGUILayout.LabelField(label2+"/"+propertyName2," "+val2);


		EditorGUI.BeginChangeCheck ();//GUI變動開始

		if(EditorGUI.EndChangeCheck())//GUI變動結束
		{
			EditorUtility.SetDirty(targetMat);
		}
	}

下面是自定義材質在編輯器的顯示方式:

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章