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);
}
最終顯示的效果如下:
可以看到這里顯示的就是圖片本身的顏色,這是因為在着色器中只是采樣了圖片的顏色,並沒有光照計算的參與。也就是在圖形引擎中,任何效果的設置只是表象,任何效果的實現都會歸結到着色器中。