Unity Shader入門精要學習筆記 - 第5章 開始 Unity Shader 學習之旅

轉載自 馮樂樂 《Unity Shader 入門精要》

一個頂點/片元 着色器的結構大概如下:

 

Shader "MyShaderName"
{
	Properties
	{
		//屬性
	}
	SubShader
	{
		//針對顯卡A的SubShader
		Pass
		{
			//設置渲染狀態和標籤
			//開始CG代碼片段
			CGPROGRAM
			//該代碼的預編譯指令,例如:
			#pragma vertex vert
			#pragma fragment frag
			//CG代碼寫在這兒
			ENDCG
			//其他設置
		}
	}
	SubShader
	{
		//針對顯卡B的SubShader
	}
}


其中,最重要的部分是Pass語義塊,我們絕大部分的代碼都是寫在這個語義塊裏面的。下面我們來創建一個最簡單的頂點/片元着色器。

 

 

 

1)新建一個場景,命名爲Scene_5_2,如下:

可以看到,場景中已經包含了一個攝像機、一個平行光。而且場景的背景不是純色的,而是一個天空盒子。我們可以Window->Lighting->SkyBox,把該項設置爲空,去掉天空盒子。

2)新建一個Unity Shader,把它命名爲Chapter5-SimpleShader。

3)新建一個材質球,把它命名爲SimpleShaderMat。把第2步中新建的Unity Shader賦給它。

4)新建一個球體,拖拽它的位置以便在Game視圖中更可以合適地顯示出來。把第3步中新建的材質拖拽給它。

5)雙擊打開第2步中創建的Unity Shader。刪除裏面所有的代碼。把下面的代碼粘貼進去。

 

Shader "Unity Shaders Book/Chapter 5/Simple Shader"
{
	SubShader
	{
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			float4 vert(float4 v : POSITION) : SV_POSITION
			{
				return mul(UNITY_MATRIX_MVP, v);
			}
			
			fixed4 frag() : SV_Target
			{
				return fixed4(1.0,1.0,1.0,1.0);
			}
			ENDCG
		}
	}
}


保存並返回Unity查看結果,如下:

 

首先,代碼的第一行通過Shader語義定義了這個Unity Shader的名字——"Unity Shaders Book/Chapter 5/Simple Shader"。

需要注意的是,我們沒有用到Properties 語義塊。Properties 語義並不是必須的,我們可以選擇不聲明任何材質屬性。

然後,我們聲明瞭SubShader 和 Pass 語義塊。在本例中,我們不需要進行任何渲染設置和標籤設置,因此SubShader 將使用默認的渲染設置和標籤設置。在SubShader語義塊中,我們定義了一個Pass,在這個Pass中,我們同樣沒有進行任何自定義的渲染設置和標籤設置。

接着,就是由CGPROGRAM 和 ENDCG 所包圍的CG 代碼段。這是我們的重點。首先,我們遇到了兩行非常重要的編譯指令:

 

#pragma vertex vert
#pragma fragment frag

 

它們告訴Unity,哪個函數包含了頂點着色器的代碼,哪個函數包含了片元着色器的代碼。

 

更通用的編譯指令表示如下:

 

#pragma vertex name
#pragma fragment name

其中 name 就是我們指定的函數名,這兩個函數的名字不一定是vert 和 frag,它們可以是任何自定義的合法的函數名,但我們一般使用vert 和 frag 來定義這兩個函數,因爲它們很直觀。

 

 

接下來,我們看一下vert函數的定義:

 

float4 vert(float4 v : POSITION) : SV_POSITION
{
    return mul(UNITY_MATRIX_MVP, v);
}

 

這就是本例使用的頂點着色器代碼,它是逐頂點執行的。vert函數的輸入v包含了這個頂點的位置,這是通過POSITION 語義指定的。它的返回值是一個float4 類型的變量,它是該頂點在裁剪空間中的位置,POSITION 和 SV_POSITION 都是CG/HLSL中的語義,它們是不可省略的,這些語義將告訴系統用戶需要哪些輸入值,以及用戶的輸出是什麼。例如這裏,POSITION 將告訴Unity
,把模型的頂點座標填充到參數v中,SV_POSITION 將告訴Unity,頂點着色器的輸出是裁剪空間中的頂點座標。如果沒有這些語義來限定輸入和輸出的參數的話,渲染器就完全不知道用戶的輸入輸出是什麼,因此就會得到錯誤的效果。本例中的這一步,就是把頂點座標從模型空間轉換到裁剪空間中。UNITY_MATRIX_MVP 矩陣是unity 內置的模型·觀察·投影矩陣。

 

然後,我們來看下frag函數:

 

fixed4 frag() : SV_Target
{
	return fixed4(1.0,1.0,1.0,1.0);
}

 

在本例中,frag函數沒有任何輸入。它的輸出是一個fixed4 類型的變量,並且使用了SV_Target 語義進行限定。SV_Target  也是HLSL 中的一個系統語義,它等同於告訴渲染器,把用戶的輸出顏色存儲到一個渲染目標中,這裏將輸出到默認的幀緩存中。片元着色器中的代碼很簡單,返回了一個表示白色的fixed4類型的變量。片元着色器輸出的顏色的每個分量範圍在[0,1],其中(0,0,0)表示黑色,而(1,1,1)表示白色。

 

 

在上面的例子中,在頂點着色器我們使用了POSITION 語義得到了模型的頂點位置 。那麼,如果我們想要得到更多模型數據怎麼辦呢?

現在,我們想得到模型上每個頂點的紋理座標和法線方向。這個需求是很常見的,我們需要使用紋理座標來訪問紋理,而法線可用於計算光照。因此,我們需要爲頂點着色器定義一個新的輸入參數,這個參數不再是一個簡單的數據類型,而是一個結構體。修改後的代碼如下:

 

Shader "Unity Shaders Book/Chapter 5/Simple Shader"
{
	SubShader
	{
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			//使用一個結構體來定義頂點着色器的輸入
			struct a2v
			{
				//POSITION 語義告訴Unity,用模型空間的頂點座標填充vertext變量
				float4 vertex : POSITION;
				//NORMAL 語義告訴Unity,用模型空間的法線方向填充normal變量
				float3 normal : NORMAL;
				//TEXCOORD0 語義告訴Unity,用模型的第一套紋理座標填充texcoord變量
				float4 texcoord : TEXCOORD0;
			};
			
			float4 vert(a2v v) : SV_POSITION
			{
				//使用v.vertex來訪問模型空間的頂點座標
				return mul(UNITY_MATRIX_MVP, v.vertex);
			}
			
			fixed4 frag() : SV_Target
			{
				return fixed4(1.0,1.0,1.0,1.0);
			}
			ENDCG
		}
	}
}

 

在上面的代碼中,我們聲明瞭一個新的結構體a2v,它包含了頂點着色器需要的模型數據。在a2v的定義中,我們用到了更多Unity 支持的語義,如NORMAL 和 TEXTCOORD0,當它們作爲頂點着色器的輸入時都是有特定含義的,因爲Unity 會根據這些語義來填充這個結構體。對於頂點着色器的輸出,Unity支持的語義還有:POSITION,TANGENT,NORMAL,TEXTCOORD0,TEXTCOORD1,TEXTCOORD2,TEXTCOORD3,COLOR等。

 

爲了創建一個自定義的結構體,我們必須使用如下格式來定義它:

 

struct StructName
{
	Type Name : Semantic
	Type Name : Semantic
	......
};

 

其中,語義是不可以被省略的。

 

然後,我們修改了vert函數的輸入參數類型,把它設置爲我們新定義的結構體a2f。通過這種自定義結構體的方式,我們就可以在頂點着色器中訪問模型數據。

a2v 中 a 表示應用,v 表示頂點着色器。a2v的意思是把數據從應用階段傳遞到頂點着色中。

在Unity中,POSITION,TANGENT,NORMAL 等語義中的數據是由該材質的Mesh Render 組件提供的。在每幀調用Draw Call 的時候,Mesh Render 組件會把它負責渲染的模型數據發送給Unity Shader。我們知道,一個模型通常包含了一組三角面片,每個三角面片由3ge頂點構成,而每個頂點又包含了一些數據,例如頂點位置、法線、切線、紋理座標、頂點顏色等、通過上面的方法,我們就可以在頂點着色器中訪問頂點的這些模型數據。

在實踐中,我們往往希望從頂點着色去輸出一些數據。例如把模型的法線、紋理座標等傳遞給片元着色器。這就涉及頂點着色器和片元着色器之間的通信。
爲此,我們需要再定義一個新的結構體。修改後的代碼如下:

 

Shader "Unity Shaders Book/Chapter 5/Simple Shader"
{
	SubShader
	{
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			struct a2v
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
			};
			
			struct v2f
			{
				//SV_POSITION語義告訴Unity,pos 裏包含了頂點在裁剪空間中的位置信息
				float4 pos : SV_POSITION;
				//COLOR0 語義可以用於存儲顏色信息
				fixed3 color : COLOR0;
			};
			
			/*
			//原有的SV_POSITION的作用是將輸出的pos轉換到裁剪空間
			Unity5.3.6以下可用
			v2f vert(a2v v) : SV_POSITION
			{
				//聲明輸出結構
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				//v.normal 包含了頂點的法線方向,其分量範圍在[-1.0, 1.0]
				//下面的代碼把分量範圍映射到了[0.0, 1.0]
				//存儲到o.color 中傳遞給片元着色器
				o.color = v.normal * 0.5 + fixed3(0.5, 0.5, 0.5);
				return o;
			}
			*/

			float4 UnityObjectToClipPos(in float3 pos){  
				//先通過mul(_Object2World, float4(pos, 1.0)) 將頂點從模型空間轉換到世界空間
				//然後再通過UNITY_MATRIX_VP進行觀察和投影變換,轉換到裁剪空間
				return mul(UNITY_MATRIX_VP, mul(_Object2World, float4(pos, 1.0)));
			}
			//Unity5.3.6及以上可用
			v2f vert(a2v v)
			{
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.color = v.normal * 0.5 + fixed3(0.5,0.5,0.5);
				return o;
			}

			fixed4 frag(v2f i) : SV_Target
			{
				//將插值後的i.color顯示到屏幕
				return fixed4(i.color,1.0);
			}

			ENDCG
		}
	}
}

 

在上面的代碼中,我們聲明瞭一個新的結構體v2f。v2f用於在頂點着色器和片元着色器之間傳遞信息。同樣的,v2f中也需要制定每個變量的語義。在本例中,我們使用了SV_POSITION 和 COLOR0 語義。頂點着色器的輸出結構中,必須包含一個變量,它的語義是 SV_POSITION 。否則,渲染器將無法得到裁剪空間中的頂點座標,也就無法把頂點渲染到屏幕上。COLOR0  語義中的數據可以由用戶自行定義,但一般都是存儲顏色,例如逐頂點的漫反射顏色或逐頂點的高光反射顏色,類似的語義還有COLOR1等。

至此,我們就完成了頂點着色器和片元着色器之間的通信。需要注意的是,頂點着色器是逐頂點調用的,而片元着色器是逐片元調用的。片元着色器中的輸入實際上是把頂點着色器的輸出進行插值後得到的結果。


現在,我們有了新的需求,我們想要在材質面板顯示一個顏色拾取器,從而可以直接控制模型在屏幕上顯示的顏色。爲此,我們繼續修改上面的代碼。

Shader "Unity Shaders Book/Chapter 5/Simple Shader"
{
	Properties
	{
		//聲明一個Color 類型的屬性
		_Color ("Color Tint", Color) = (1.0, 1.0, 1.0, 1.0)
	}
	SubShader
	{
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			//在CG代碼中,我們需要定義一個與屬性名稱和類型都匹配的變量
			fixed4 _Color;
			
			struct a2v
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
			};
			
			struct v2f
			{
				float4 pos : SV_POSITION;
				fixed3 color : COLOR0;
			};
			
			v2f vert(a2v v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_VP, mul(_Object2World, v.vertex));
				o.color = v.normal * 0.5 + fixed3(0.5, 0.5, 0.5);
				return o;
			}
			
			fixed4 frag(v2f i) : SV_Target
			{
				fixed3 c = i.color;
				c *= _Color.rgb;
				return fixed4(c,1.0);
			}
			ENDCG
		}
	}
}

在上面的代碼中,我們首先添加了Properties語義塊中,並在其中聲明瞭一個屬性_Color,它的類型是Color,初始值是(1.0,1.0,1.0,1.0),對應白色。爲了在CG代碼中可以訪問它,我們還需要再CG代碼片段中提前定義一個新的變量,這個變量的名稱和類型必須與Properties語義塊中的屬性定義相匹配。

 

ShaderLab中屬性的類型和CG變量的類型之間的匹配關係如下表:


uniform 關鍵詞是CG中修飾變量和參數的一種修飾詞,它僅僅用於提供一些關於該變量的初始值是如何指定和存儲的相關信息。在Unity中,uniform關鍵詞是可以省略的。
 

內置文件

爲了方便開發者開發,Unity提供了很多內置文件。這些文件包含了很多提前定義的函數、變量和宏等。

包含文件是類似於C++中頭文件的一種文件。在Unity中,它的後綴是.cgnic。在編寫Shader時,我們可以使用#include指令把這些文件包含進來,這樣我們就可以使用Unity爲我們提供的一些非常有用的變量和幫助函數。例如:

 

CGPROGRAM
//...
#include "UnityCG.cgnic"
//...
ENDCG

 

我們可以在官網(https://unity3d.com/cn/get-unity/download/archive)上選擇下載->內置着色器來直接下載這些文件,下圖顯示了由官網壓縮包得到的文件。

 



從上圖可以看出,從官網上下載的文件中包含了許多文件夾。其中,CGIncludes 文件夾中包含了所有的內置包含文件;DefaultResouces 文件夾中包含了一些內置組件或功能所需要的Unity Shader,例如一些GUI元素使用的Shader;DefaultResourcesExtra 則包含了所有Unity中內置的Unity Shader;Editor 文件夾目前只包含了一個腳本文件,它用於定義Unity5 引入的 Standard Shader 所用的材質面板。這些文件都是非常好的參考資料,在我們想要學習內置着色器的實現或是尋找內置函數的實現時,都可以在這裏找到內部實現。但在這裏,我們只關注CGIncludes文件夾下的相關文件。

我們也可以從Unity應用程序中直接找到CGIncludes文件夾。在Windows上,它的位置是:

Unity的安裝路徑/Data/CGIncludes。

下表給出了CGIncludes 中主要包含文件以及它們的用處。

可以看出,有一些文件即使我們沒有包含進來,Unity也會幫我們自己包含。

UnityCG.cgnic文件是我們最常接觸的一個文件。下表給出了一些結構體的名稱和包含的變量。

強烈建議讀者找到UnityCG.cgnic文件並查看上述結構體的聲明,這樣的過程可以幫助我們更快理解Unity中一些內置變量的工作原理。

除了上述結構體外,UnityCG.cgnic也提供了一些常用的幫助函數。下表給出了一些函數名和它們的描述

 

Unity提供的CG/HLSL語義

語義實際上就是一個賦給Shader 輸入和輸出的字符串,這個字符串表達了這個參數的含義。通俗的講,這些語義可以讓Shader知道從哪裏讀取數據,並把數據輸出到哪裏,它們在CG/HLSL的流水線中是不可缺的。需要注意的是,Unity並沒有支持所有的語義。

通常情況下,這些輸入輸出變量並不需要有特別的意義,也就是說,我們可以自行決定這些變量的用途。例如在上面的代碼中,定點着色器的輸出結構體中,我們用COLOR0去描述color變量。color變量本身存儲了什麼,Shader流水線並不關心。

而Unity爲了方便對模型數據的傳輸,對一些語義進行了特別的含義規定。例如,在頂點着色器的輸入結構體 a2f 用TEXCOORD0來描述texcoord,Unity會識別TEXCOORD0 語義,以把模型的第一組紋理座標填充到texcoord中。需要注意的是,即便語義的名稱一樣,如果出現的位置不同,含義也不同。例如,TEXCOORD0即可用於描述頂點着色器的輸入結構體a2f,也可以用於描述輸出結構體v2f。但在輸入結構體a2f中,TEXCOORD0 有特別的含義,即把模型的第一組紋理座標存儲到該變量中,而在輸出結構體v2f中,TEXCOORD0修飾的變量含義就可以由我們來決定。

在DirectX以後,有了一種新的語義類型,就是系統數值語義。這類語義是以SV開頭的,SV代表的意思就是系統數值。這些語義在渲染流水線中有特殊的含義。例如在上面的代碼中,我們使用SV_POSITION語義去修飾頂點着色器的輸出變量pos,那麼就表示pos包含了可用於光柵化的變換後的頂點座標(即齊次裁剪空間中的座標)。用這些語義描述的變量是不可以隨便賦值的,因爲流水線需要使用它們完成特定的目的,例如渲染引擎會把用SV_POSITION修飾的變量經光柵化後顯示在屏幕上。讀者有時可能會看到同一個變量在不同的Shader裏面使用了不同的語義修飾。例如,一些Shader會使用POSITION而不是SV_POSITION來修飾頂點着色器的輸出。SV_POSTION是DirectX10中引入的系統數值語義,在絕大多數平臺上,它和POSITON是等價的。但在某些平臺,如PS4上,必須使用SV_POSTION來修飾頂點着色器的輸出,否則無法讓着色器正常工作。同樣的例子還有COLOR和SV_Target。因此,爲了讓我們的Shader有更好的跨平臺特性,對於這些有特殊含義的變量我們最好以SV開頭進行修飾。

下表總結了從應用階段傳遞模型數據給頂點着色器時Unity使用的常用語義。這些語義雖然沒有使用SV開頭,但Unity內部賦予它們特殊的含義。

其中TEXCOORDn中n的數目是和Shader Model 有關的,例如一般在Shader Model 2(即Unity默認編譯到的Shader Model版本)和Shader Model 3 中,n等於8,而在Shader Model 4 和 Shader Model 5 中,n等於16。通常情況下,一個模型的紋理座標數組一般不超過2,即我們往往只使用 TEXCOORD0 和 TEXCOORD1。在 Unity內置的數據結構體appdata_full中,它最多使用的6個座標紋理組。

下表總結了從頂點着色器階段到片元着色器階段Unity支持的常用語義。

上面的語義中,除了SV_POSITION有特別的含義之外,其他語義對變量的含義沒有明確要求,也就是說,我們可以存儲任意值到這些語義描述變量中。通常,如果我們需要把一些自定義的數據從頂點着色器傳遞給片元着色器,一般選用TEXCOORD0。

下表給出了Unity中支持的片元着色器的輸出語義。

上面提到的語義絕大部分用於描述標量或矢量類型的變量,例如fixed2、float、float4、fixed4 等。下面的代碼給出了一個使用語義來修飾不同類型變量的例子:

 

struct v2f
{
	float4 pos : SV_POSITION;
	fixed3 color0 : COLOR0;
	fixed4 color1 : COLOR1;
	half value0 : TEXCOORD0;
	float2 value1 : TEXCOORD1;
};

 

 

 

需要注意的是,一個語義可以使用的寄存器只能處理4個浮點值。因此,如果我們想要定義矩陣類型,如 float3×4、float4×4 等變量就需要使用更多的空間。一種方式是,把這些變量拆分成多個變量,例如對於float4×4 的矩陣類型,我們就可以拆分成4個float4類型的變量,每個變量存儲了矩陣中的一行數據。

 

 

 

Unity中對 Unity Shader 的兩種調式方法

假彩色圖像指的是用假彩色技術生成的一種圖像。與假彩色圖像對應的是照片這種真彩色。一張假彩色圖像可以用於可視化一些數據,那麼如何用它對Shader進行調試呢?

主要思想是,我們可以把需要調試的變量映射到[0,1]之間,把它們作爲顏色輸出到屏幕上,然後通過屏幕上顯示的像素顏色來判斷這個值是否正確。

需要注意的是,由於顏色的分量範圍在[0,1],因此我們需要小心處理需要調試的變量的範圍。如果我們已知它的值域範圍,可以先把它映射到[0,1]之間再進行輸出。如果你不知道一個變量的範圍,我們就只能不停的進行實驗。一個提示是,顏色分量中任何大於1的數值將會被設置爲1,而任何小於0的數值將會設置爲0。因此,我們可以嘗試使用不同的映射,直到發現顏色發生了變化。

如果我們要調試的數據是一個一維數據,那麼可以選擇一個單獨的顏色分量(如R分量)進行輸出,而把其他顏色分量置爲0。如果是多維數據,可以選擇對它的每一個分量單獨調試,或者選擇多個顏色分量進行輸出。

作爲實例,下面我們會使用假彩色圖像的方式來可視化一些模型數據,如法線,切線,紋理座標,頂點顏色,以及它們之間的運算結果等。代碼如下:

 

Shader "Unity Shader Book/Chapter 5/False Color"
{
	SubShader
	{
		Pass
		{
			CGPROGRAM
			
			#pragma vertex vert
			#pragma frament frag
			
			#include "UnityCG.cgnic"
			
			struct v2f
			{
				float4 pos : SV_POSITION;
				fixed4 color : COLOR0;
			};
			
			v2f vert(appdata_full v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
				//可視化法線方向
				o.color = fixed4(v.normal * 0.5 + fixed3(0.5, 0.5, 0.5), 1.0);
				//可視化切線方向
				o.color = fixed4(v.tangent * 0.5 + fixed3(0.5, 0.5, 0.5,), 1.0);
				//可視化副切線方向
				fixed3 binormal = cross(v.normal, v.tangent.xyz) * v.tangen.w;
				o.color = fixed4(binormal * 0.5 + fixed3(0.5, 0.5, 0.5,), 1.0);
				//可視化第一組紋理座標
				o.color = fixed4(v.texcoord.xy, 0.0, 1.0);
				//可視化第二組紋理座標
				o.color = fixed4(v.texcoord1.xy, 0.0, 1.0);
				//可視化第一組紋理座標的小數部分
				o.color = frac(v.texcoord);
				if(any(saturate(v.texcoord) - v.texcoord))
				{
					o.color.b = 0.5;
				}
				o.color.a = 1.0;
				//可視化第二組紋理座標的小數部分
				o.color = frac(v.texcoord1);
				if(any(saturate(v.texcoord1) - v.texcoord1))
				{
					o.color.b = 0.5;
				}
				o.color.a = 1.0;
				//可視化頂點顏色
				//o.color = v.color;
				return o;
			}
			
			fixed4 frag(v2f i) : SV_Target
			{
				return i.color;
			}
			
			ENDCG
		}
	}
}

在上面的代碼中,我們使用了Unity內置的一個結構體——appdata_full。我們可以在UnityCG.cgnic文件中找到它的定義:

 

struct appdata_full {
	float4 vertex : POSITION;
	float4 tangent : TANGENT;
	float3 normal : NORMAL;
	float4 texcoord : TEXCOORD0;
	float4 texcoord1 : TEXCOORD1;
	float4 texcoord2 : TEXCOORD2;
	float4 texcoord3 : TEXCOORD3;
#if defined(SHADER_API_XBOX360)
	half4 texcoord4 : TEXCOORD4;
	half4 texcoord5 : TEXCOORD5;
#endif
	fixed4 color : COLOR;
};

 

可以看出,appdata_full幾乎包含了所有的模型數據。

 

我們把計算得到的假彩色存儲到了頂點着色器的輸出結構體——v2f中的color變量裏,並且在片元着色器中輸出了這個顏色。讀者可以對其中的代碼添加或取消註釋,觀察不同運算和數據得到的效果。下圖給出了這些代碼運行的效果

爲了可以得到某點的顏色值,我們可以使用類似顏色拾取器的腳本得到屏幕上某點的RGBA值,從而推斷出改點的調試信息。

 

Visual Sutio 2012版本以上也提供了對Unity Shader 的調試功能——Graphics Debugger。

通過Graphics Debugger,我們不僅可以查看每個像素的最終顏色、位置信息等,還可以對頂點着色器和片元着色器進行單步調試。具體的安裝步驟參照Unity官網中的鏈接 https://docs.unity3d.com/Manual/SL-DebuggingD3D11ShadersWithVS.html

當然,本方法也有限制。例如,我們要保證Unity運行在DirectX 11 平臺上,而且Graphics Debugger 本身還存在一些bug。

Unity 5 中 還可以使用幀調試器。

要使用幀調試器,我們首先需要在Window -> Fragme Debugger 中打開幀調試窗口,如下圖所示

幀調試器可以用於查看渲染該幀時進行的各種渲染事件,這些事件包含了Draw Call序列,也包含了類似清空幀緩存等操作。幀調試器窗口大致可分爲3個部分:最上面的區域可以開啓/關閉幀調試功能,當開啓了幀調試時,通過移動窗口最上方的滑動條,我們可以重放這些渲染事件;左側的區域顯示了所有事件的樹狀圖,在這個樹狀圖中,每個葉子節點就是一個事件,而每個父節點右側顯示了該節點下的事件數目。我們可以從事件的名字瞭解這個事件的操作,例如Draw開頭的事件通常就是一個DrawCall;當單擊了某個事件時,在右側的窗口中就會顯示出該事件的細節,例如幾何圖形的細節以及使用了哪個Shader等。同時在Game視圖中我們也可以看到它的效果。如果該事件是一個Draw Call並且對應了場景中的一個Gameobject,那麼這個Gameobject也會在Hierarchy視圖中被高亮顯示出來,下圖顯示了單擊渲染某個對象的深度圖事件的結果。

如果被選中的Draw Call 是對一個渲染紋理的渲染操作,那麼這個渲染紋理就會顯示在Game視圖中。而且,此時右側面板上方的工具欄也會出現更多的選項,例如在Gane視圖中單獨顯示R、G、B 和 A通道。

Unity 5 提供的幀調試器實際上並沒有實現一個真正的幀拾取的功能,而是僅僅使用停止渲染的方法來查看渲染事件的結果。例如,如果我們想要查看第4個 Draw Call 的結果,那麼幀調試器就會在第4個Draw Call 調用完畢後停止渲染。這種方法雖然簡單,但得到的信心也有限。如果要獲取更多的信息,還是要使用外部工具。

 

Unity 在渲染平臺的差異

OpenGL 和 DirectX 在屏幕空間座標存在差異,如下圖

需要注意的是,我們不僅可以把渲染結果輸出到屏幕上,還可以輸出到不同的渲染目標中。這時,我們需要使用渲染紋理來保存這些渲染結果。

大多數情況下,這樣的差異並不會對我們造成任何影響。但當我們要使用渲染到紋理技術,把屏幕圖像渲染到一張渲染紋理中時,如果不採取任何措施的話,就會出現紋理翻轉的情況。幸運的是,Unity 在背後爲我們處理了這種翻轉問題—— 當在DirectX平臺上使用渲染到紋理技術時,Unity 會爲我們翻轉屏幕圖像紋理,以便在不同平臺上達到一致性。

在一種特殊情況下Unity不會爲我們進行這個翻轉操作,這種情況就是我們開啓了抗鋸齒(在Edit->Project Setting->Quality->Anti Aliasing 中開啓)並在此時使用了渲染到紋理技術。在這種情況下,Unity 首先渲染得到屏幕圖像,再由硬件進行抗鋸齒處理後,得到一張渲染紋理來供我們進行後續處理。此時,在DirectX 平臺下,我們得到的輸入屏幕圖像並不會被Unity 翻轉,也就是說,此時對屏幕圖像的採樣座標是需要符合DirectX 平臺規定的。如果我們的屏幕特效只需要處理一張渲染圖像,我們仍然不需要在意紋理的翻轉問題,這是因爲我們調用Graphics.Blit 函數時,Unity 已經爲我們隊屏幕圖像的採樣座標進行了處理,我們只需要按正常的採樣過程處理屏幕圖像即可。但如果我們需要同時處理多張渲染圖像(前提是開啓了抗鋸齒),例如需要同時處理屏幕圖像和法線紋理,這些圖像再豎直方向的朝向就可能是不同的(只有在DirectX這樣的平臺上纔有這樣的問題)。這種時候,我們就需要自己在頂點着色器中翻轉某些渲染紋理(例如深度紋理或由其他腳本傳遞過來的紋理)的縱座標,使之都符合DirectX 平臺的規則。例如:

 

#if UNITY_UV_STARTS_AT_TOP
	if (_MainTex_TexelSize.y < 0)
		uv.y = 1 - uv.y;
#endif	

 

 

其中UNITY_UV_STARTS_AT_TOP 用於判斷當前平臺是否是DirectX 類型的平臺,而當在這樣的平臺下卡其了抗鋸齒後,主紋理的紋素大小在豎直方向上會變成負值,以便我們對主紋理進行正確的採樣。因此,我們可以通過判斷_MainTex_TexelSize.y 是否小於 0 來校驗是否開啓了抗鋸齒。如果是,我們就需要對除主紋理外的其他紋理的採樣座標進行豎直方向上的翻轉。

 

 

在DirectX 和 OpenGL 平臺上,存在一些Shader 的語法差異,例如

 

float4 v = float4(0.0);

 

這段代碼在OpenGL 上是合法的,但是在DirectX 11 平臺上,是會報錯。應該寫成

 

float4 v = float4(0.0, 0.0, 0.0, 0.0);

 

其他語法上也有一些差異,DirectX 9 / 11 平臺也不支持在頂點着色器中使用tex2D 函數,需要使用tex2Dlod 代替。而且我們還需要添加#pragma target 3.0 ,因爲tex2Dlod 是 Shader Model 3.0 中的特性。

 

 

Shader 整潔之道

在CG/HLSL中,有3種精度的數值類型:float,half和fixed。這些精度將決定計算結果的數值範圍。下表給出了3種精度在通常情況下的數值範圍。

上面的精度範圍並不是絕地正確的,尤其是在不同平臺和GPU上,它們實際的精度可能和上面給出的範圍不一致。通常來講,大多數桌面GPU會把所有計算都按最高的浮點精度進行計算,也就是說,float、half、fixed 在這些平臺上實際是等價的。但在移動平臺上,它們的確會有不同的精度範圍,而且不同精度的浮點值的運算速度也會有所差異。因此,我們應該確保在真正的移動平臺上驗證我們的Shader。fixed精度實際上只在一些較舊的移動平臺上有用,在大多數現代GPU上,它們內部把fixed 和 half 當成同等精度來對待。

儘管有上面的不同,但一個基本建議是,儘可能使用精度較低的類型,因爲這可以優化Shader 的性能,這一點在移動平臺上尤其重要。從它們大體的值域範圍來看,我們可以使用fixed 類型來存儲顏色和單位矢量,如果要存儲更大範圍的精度可以選擇half類型,最差情況下再選擇使用float、如果我們的目標平臺是移動平臺,一定要確保在真實的手機上測試我們的Shader,這點非常重要。

如果我們毫無節制地在Shader(尤其是片元着色器)中進行了大量計算,那麼我們可能很快就會收到Unity的提示:

temporary register limit of 8 exceeded

Arithmetic instruction limit of 64 exceeded; 65 arithmetic instruction needed to compile program

出現這些錯誤信息大多是因爲我們再Shader中進行了過多的運算,使得需要的臨時寄存器數目或指令數目超過了當前可支持的數目。通常,我們可以通過制定更高級的Shader Target 來消除這些錯誤。下表給出了Unity 目前支持的Shader Target。

需要注意的是,所有類似OpenGL 的平臺(包括移動平臺)被當成是支持到 Shader Model 3.0 的。而WP8/WinRT 平臺則是隻支持到Shader Model 2.0 。

Shader Model 是由微軟提出的一套規範,通俗地理解就是它們決定了Shader 中各個特性的能力。這些特性和能力體現在Shader 能使用的運算指令數目、寄存器個數等各個方面。Shader Model 等級越高,Shader 的能力就越大。

雖然更高等級的Shader Target 可以讓我們使用更多的臨時寄存器和運算指令,但一個更好的方式是儘可能減少Shader 中的運算,或者通過預計算的方式來提供更多的數據。

如果我們再Shader 中使用了大量的流程控制語句,那麼這個Shader 的性能可能會成倍下降。一個解決方法是,我們應該儘量把計算向流水線上端移動,例如把放在片元着色器中的計算放到頂點着色器中,或者直接在CPU中進行預計算,再把結果傳遞給Shader。當然,有時我們不可避免地要使用分支語句來進行計算,那麼一些建議是:

分支判斷語句中使用的條件變量最好是常數,即在Shader運行過程中不會發生變化;

每個分支中包含的操作指令數儘可能少;

分支的嵌套層數儘可能少。

另外在Shader編程過程中,不要除以0。


 

2018年10月9日 重新修正:

1、感謝le12380的提醒,許多地方漏掉了分號。這邊進行了補充

2、發現在Unity5.3.6中,使用SV_POSITION語義會發出invalid output semantic 'SV_POSITION': Legal indices are in [0,0]的錯誤,而在低一些的版本中沒有出現此錯誤。可使用o.pos = mul(UNITY_MATRIX_VP, mul(_Object2World, v.vertex));代替SV_POSITION語義。

 

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