1. 概述
在上一篇文章《Unity3D學習筆記2——繪製一個帶紋理的面》中介紹瞭如何繪製一個帶紋理材質的面,並且通過調整光照,使得材質生效(變亮)。不過,上篇文章隱藏了一個很重要的細節——Unity Shader。Shader(著色器)是渲染管線中可被使用者程式設計的階段,依靠著色器可以控制渲染管線的細節。現代影像渲染技術,都把Shader封裝成與Material(材質)相關的元件。所以這篇文章,我們就初步學習下在Unity中使用Shader。
2. 詳論
2.1. 建立材質
在上一章中,材質、以及材質相關的資源是在Unity3D編輯器中建立,在C#指令碼中直接引用的。這裡為了學習使用Shader,我們使用自定義的Shader,可以在C#指令碼中建立材質。修改上一章程式碼的材質部分:
Shader shader = Shader.Find("Custom/MainShader");
Material material = new Material(shader);
Texture2D texture = Resources.Load<Texture2D>("ImageDemo");
material.mainTexture = texture;
MeshRenderer meshRenderer = newGameObject.AddComponent<MeshRenderer>();
meshRenderer.material = material;
可以看到,要建立一個Material,首先得建立一個Shader。我們在Project檢視中右鍵選單->Create->Standard Surface Shader,建立一個標準表面著色器MainShader:
雙擊開啟這個Shader,可以看到這個Shader的具體內容。標準著色器很複雜,我們清空裡面的內容,填入我們這個更簡單的著色器示例:
Shader "Custom/MainShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags{"Queue" = "Geometry"}
Cull Back
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
sampler2D _MainTex;
//頂點著色器輸入
struct a2v
{
float4 position : POSITION;
float3 normal: NORMAL;
float2 texcoord : TEXCOORD0;
};
//頂點著色器輸出
struct v2f
{
float4 position: SV_POSITION;
float2 texcoord: TEXCOORD0;
};
v2f vert(a2v v)
{
v2f o;
o.position = mul(UNITY_MATRIX_MVP, v.position);
o.texcoord = v.texcoord;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
return tex2D(_MainTex, i.texcoord);
}
ENDCG
}
}
FallBack "Diffuse"
}
2.2. 著色器
Unity使用的著色器語言叫做ShaderLab,它是圖形渲染中Shader(例如GLSL,HLSL以及CG)的更高階更抽象一級的封裝。ShaderLab是個非常簡單的說明性描述語言,通過巢狀在花括號中的語義來描述Unity Shader檔案。
2.2.1. 名稱
通過Shader語義指定Unity Shader的名稱:
Shader "Custom/MainShader"
{
}
這個名稱非常重要,在Unity編輯器中需要通過這個名字來引用Shader。
2.2.2. 屬性
Shader語義塊的第一個語義塊是Properties語義塊,它連線著材質和Unity3d編輯器,設定了這個屬性就能夠通過材質皮膚調整材質,調整材質的本質就是調整Shader。Properties的定義通常描述如下:
Properties {
Name ("display name",PropertyType) = DefaultValue
}
Name指的是在Shader中使用的名稱,display name指的是顯示在材質皮膚的名稱。PropertyType則有點容易混淆,它指的是顯示在材質皮膚中的屬性型別,借用一下《Unity Shader入門精要》的圖表:
2.2.3. SubShader
每個Unity Shader都至少包含一個SubShader語義塊,Unity會優先選擇第一個能夠在當前平臺下執行的SubShader作為最終渲染效果的Shader。
這個語義塊下面又會包含三個語義塊:
2.2.3.1. 標籤(Tags)
SubShader的標籤用於用於標識何時以何種方式被渲染到渲染引擎,它由一系列鍵值對組成。Queue是最常用的標籤,用於標識渲染物體在渲染佇列中的位置:
我們這裡,把這個渲染物體放到Geometry佇列中,這個位置通常放置不透明物體的渲染:
Tags{"Queue" = "Geometry"}
2.2.3.2. 渲染狀態(RenderSetup)
渲染狀態用於設定圖形硬體的各種狀態,例如是否應開啟 Alpha 混合或是否應使用深度測試等。在像OpenGL這樣的圖形介面中,通常是以函式的形式進行呼叫的,Unity3d將其放在Shader裡面,也有一定的道理。
這裡的渲染狀態設定成將背面裁剪掉:
Cull Back
2.2.3.3. 通道(Pass)
在Pass語義塊中,才是像OpenGL/DirectX中使用的Shader。OpenGL使用的著色器語言叫做GLSL,DirectX使用的著色器語言叫做HLSL,Unity3D則推薦使用Cg語言,這是一種類C語言,與HLSL非常相似。Cg語言程式碼段在Pass語義塊中被包裹在CGPROGRAM和ENDCG之間:
CGPROGRAM
//...
ENDCG
2.2.4. 回退(FallBack)
FallBack定義了一種退化策略,由於不同機器支援的效能特性不同,如果之前的子著色器都不生效,那麼就使用這個著色器,通常這個著色器是內建的:
FallBack "Diffuse"
2.3. 渲染管線
圖形渲染引擎的渲染管線其實是個內涵非常豐富的概念,再次借用《Unity Shader入門精要》的插圖,渲染管線的描述大致如下:
當然只看這個圖是不夠的,但是我們可以直接從程式碼層面去了解它。鑲嵌在CGPROGRAM和ENDCG之間的CG程式碼,體現的正是渲染管線的思維。
首先,通過編譯指令,分別指定頂點著色器程式和片元著色器程式:
#pragma vertex vert
#pragma fragment frag
vert就是頂點著色器的函式,在這個著色器程式中指定了計算了頂點座標和紋理座標:
v2f vert(a2v v)
{
v2f o;
o.position = mul(UNITY_MATRIX_MVP, v.position);
o.texcoord = v.texcoord;
return o;
}
傳入引數是一個結構體,POSITION,NORMAL,TEXCOORD0是Unity Shader中固定的語義,分別代表這位置、法向量以及紋理座標,他們也被稱為頂點屬性。還記得在上一篇文章《Unity3D學習筆記2——繪製一個帶紋理的面》中建立Mesh時給Mesh建立的成員變數vertices、uv和normals吧?給他們傳入的資料正是在這裡用到了。
//頂點著色器輸入
struct a2v
{
float4 position : POSITION;
float3 normal: NORMAL;
float2 texcoord : TEXCOORD0;
};
傳出引數則是另外一個結構體:
//頂點著色器輸出
struct v2f
{
float4 position: SV_POSITION;
float2 texcoord: TEXCOORD0;
};
SV_POSITION表示的是裁剪空間座標,也就是在頂點著色器中計算的頂點值。這個計算內容的內涵也挺豐富的,簡單來說,建立Mesh時的頂點座標,經過一個模型變換(Model)、檢視變換(View)、投影變換(Projection),最終變成了裁剪空間座標系中的座標,體現在著色器中,就是內建的MVP矩陣UNITY_MATRIX_MVP。
剩下的就是片元著色器函式的部分了。在這個著色器中,_MainTex也就是我們先前建立的,並且傳遞到材質中的紋理,通過將頂點著色器中傳遞過來的紋理座標進行取樣,得到具體的片元顏色:
sampler2D _MainTex;
fixed4 frag(v2f i) : SV_Target
{
return tex2D(_MainTex, i.texcoord);
}
最終顯示的效果如下:
可以看到這裡顯示的就是圖片本身的顏色,這是因為在著色器中只是取樣了圖片的顏色,並沒有光照計算的參與。也就是在圖形引擎中,任何效果的設定只是表象,任何效果的實現都會歸結到著色器中。