3.Unity Shader 基礎
目錄
一對好兄弟:材質和Unity Shader
物體想要達到需要的效果一般流程為:
- 建立一個材質;
- 建立一個 Unity Shader,並把它賦給上一步建立的材質;
- 把材質賦給想要渲染的物件;
- 把材質面板中調整Unity Shader 的屬性,以得到滿意的效果。
Unity Shader和材質。首先建立需要的Unity Shader和材質,然後把Unity Shader賦給材質,並在材質面板上調整屬性(如使用的紋理、漫反射係數等)。最後,將材質賦給相應的模型來檢視最終的渲染效果 。
Unity中shader
為了與前面通用的Shader語義進行區分,我們把Unity中的shader檔案統稱為 Unity Shader。
建立新的Unity Shader,我們可以在 Unity 選單欄選擇 Assets -> Creat -> Shader 來選擇,或者直接 Project 檢視右擊 -> Creat->Shader來建立。
在Unity2017.4.2(書中為5.2)一共提供了4種 Unity Shader 模板來選擇 —— Standard Surface Shader , Unlit Shader , Image Effect Shader , Compute Shader.
其中 Standard Surface Shader 會產生一個包含了標準光照模型的表面著色器模板,Unlit Shader則會產生一個不包含光照(但包含霧效)的基本的頂點/片元著色器,Image Effect Shader則為我們實現各種屏幕後處理效果提供一個模板。最後,Compute Shader 會產生一個特殊的 Shader 檔案,這類Shader旨在利用GPU的並行性來進行一些與常規渲染流水線無關的計算。
關於Compute Shader具體可以在 Unity 手冊的 Compute Shader 一文(https://docs.unity3d.com/Manual/class-ComputeShader.html)中找到更多介紹。
由於在本人看的書中重點在於如何在Unity 中編寫頂點/片元著色器,因此以後的學習中,通常會使用Unlit Shader來生成一個基本的頂點/片元著色器模板。
一個unity shader Standard Surface Shader面板如下
在 Default Maps 可以指定該 Unity Shader 使用的預設紋理。
在接下去的面板中,Unity會顯示出和該Unity Shader相關的資訊,例如它是否是一個 表面著色器(Surface Shader)、是否是一個固定函式著色器(Fixed Function Shader)等,還有一些資訊是和我們在 Unity Shader 中的標籤設定有關,例如是否投射陰影、使用的渲染佇列、LOD值等。
對於表面著色器,可以單擊 show generated code 開啟一個新的檔案,在該檔案裡將顯示 Unity 在背後為該表面著色器生成的頂點/片元著色器。
同樣如果Unity Shader是一個固定函式著色器, 在Fixed Function 的後面也會出現一個 Show generated code 按鈕。
Compile and show code 下拉列表可以讓開發者檢查該Unity Shader針對不同影象程式設計介面(例如OpenGL、D3D9等),如果直接點選也可以檢視底層命令。
ShaerLab
Unity提供了一種專門為 Unity Shader 服務的語言——ShaerLab。
Unity Shader 為控制渲染過程提供了一層抽象。如果沒有使用Unity Shader(左圖),開發者需要和很多檔案和設定打交道,才能讓畫面呈現出想要的效果;而在Unity Shader的幫助下(右圖),開發者只需要使用ShaderLab來編寫Unity Shader檔案就可以完成所有的工作。
在Unity中,所有的 Unity Shader 都是使用 ShaderLab 來編寫的。 ShaderLab是Unity提供的編寫 Unity Shader 的一種說明性語言。它使用了一些巢狀在花括號內部的語義來描述一個 Unity Shader 檔案的結構。
一個 Unity Shader 的基礎結構如下:
Shader "ShaderName" {
Properties {
//屬性
}
SubShader {
//顯示卡A使用的子著色器
}
SubShader {
//顯示卡B使用的子著色器
}
Fallback "VertecLit"
}
Unity Shader 的結構
1.建立
給建立的 Unity Shader 起一個名字的時候,如下,起了一個”MyShader“
我們可以在材質裡面找到的 Shader裡面找到它,
開啟這個 MyShader 的程式,我們可以看見第一行就有它的位置資訊。
Shader "Custom/MyShader" {
這裡用"/"可以控制 Unity Shader 在材質面板出現的位置。
2.Properties
Properties語義塊中包含了一系列屬性,這些屬性將會出現在材質面板中。
Properties語義塊的定義通常如下:
Properties {
Name ("display name", PropertyType) = DefaultValue
Name ("display name", PropertyType) = DefaultValue
//更多屬性
}
這些屬性的名字(Name)通常由下劃線開始,是顯示在 Unity Shader 的面板。
顯示的名字(display name)則是出現在材質面板上的名字,我們需要為每個屬性定義它的型別(PropertyType)
常見的屬性如下(除此之外我們還需要為每個屬性指定一個預設值,當我們第一次把該Unity Shader賦給某個材質時,材質就顯示這些預設值):
屬性型別 | 預設值的定義語法 | 例子 |
Int | number | _Int("Int",Int)=2 |
Float | number | _Float("Float",Float)=1.5 |
Range(min,max) | number | _Range("Range",Range(0.0,5.0))=3.0 |
Color | (number,number,number,number) | _Color("Color",Color)=(1,1,1,1) |
Vector | (number,number,number,number) | _Vector("Vector",Vector)=(2,3,6,1) |
2D | "defaulttexture" {} | _2D("2D",2D)="" {} |
Cube | "defaulttexture" {} | _Cube("Cube",Cube)="white" {} |
3D | "defaulttexture" {} | _3D("3D",3D)="black" {} |
對於 Int、Float、Range 這些數字型別的屬性,其預設值就是一個單獨的數字;
對於Color和Vector這類屬性,預設值是用圓括號包圍的一個四維向量;
對於2D、Cube、3D這3種紋理型別,預設值的定義稍微複雜,它們的預設值是通過一個字串後跟一個花括號來指定,其中,字串要麼是空的,要麼是內建的紋理名稱,如"white"、"black"、"gray"或者"bump"。花括號的用處原本是用於指定一些紋理屬性的,但是Unity已刪除了本身自帶的,如果我們需要類似的功能,就需要自己在頂點著色器中編寫計算相應的紋理座標程式碼。
PS:需要說明的是,Properties語義塊的作用僅僅是為了讓這些屬效能夠出現在材質面板中,即使不宣告,也能在程式裡定義修改。
3.SubShader
每一個 Unity Shader 檔案可以包含多個 SubShader 語義塊,但最少要有一個。
當Unity 需要載入這個 Unity Shader 時,Unity 會掃描所有的 SubShader 語義塊,然後選擇第一個能夠在該目標平臺上執行的SubShader。如果都不支援的話, Unity就會使用 Fallback 語義指定的 Unity Shader。
Unity 提供這種語義的原因在於,不同的顯示卡具有不同的能力。
SubShader 語義塊中包含的定義通常如下:
SubShader {
//可選的
[Tags]
//可選的
[RenderSetup]
Pass {
}
//Other Passes
}
SubShader中定義了一系列 Pass 以及可選的狀態([RenderSetup])和 標籤([Tags])設定。
每個Pass定義了一次完整的渲染流程,但如果Pass的數目過多的動,往往會造成渲染效能下降。因此,我們應儘量使用最小數目的Pass. 狀態和標籤同樣可以在Pass宣告。不同的是,在SubShader中的一些標籤設定是特定的。也就是說,這些標籤設定和Pass中使用的標籤是不一樣的。而對於狀態設定來說,其使用的語法是相同的。但是,如果我們在SubShader 進行了這些設定,那麼將會用於所以的Pass.
-
狀態設定([RenderSetup])
SubShader 提供了一系列渲染狀態的設定命令,這些指令可以設定顯示卡的各種狀態,例如是否開啟混合/深度測試等。
下表給出了 SubShader 中常見的渲染狀態設定選項:
狀態名稱 | 設定指令 | 解釋 |
Cull | Cull Back|Front|Off | 設定剔除模式:剔除背面/正面/關閉剔除 |
ZTest | ZTest Less Greater|LEqual|GEqual|Equal|NotEqual|Always | 設定深度測試時使用的函式 |
ZWrite | ZWrite On|Off | 開啟/關閉深度寫入 |
Blend | Blend SrcFactor DstFactor | 開啟並設定混合模式 |
當SubShader塊中設定了上述渲染狀態時,將會應用到所有的Pass。
如果不想這樣,我們可以在 Pass語義塊中單獨進行上面的設定。
-
SubShader的標籤([Tags])
SubShader的標籤是一個鍵值對(Key / Value Pair),它的鍵和值都是字串型別。這些鍵值對是SubShader和渲染引擎之間的溝通橋樑。它們用來告訴Unity的渲染引擎:我們希望怎麼樣以及何時渲染這個物件。
結構如下:
Tags {"TagName1" = "Value1" "TagName2" = "Value2"}
SubShader的標籤塊支援的標籤型別如下:
標籤型別 | 說明 | 例子 |
Queue | 控制渲染順序,指定該物體屬於哪一個渲染佇列,通過這種方式可以保證所有的透明物體可以在所有不透明物體後面被渲染,我們也可以自定義使用的渲染佇列來控制物體的渲染順序 | Tags {"Queue" = "Transparent"} |
RenderType | 對著色器進行分類,例如這是一個不透明的著色器,或是一個透明的著色器等.這可以被用於著色器替換(Shader Replacement)功能 | Tags {"RenderType" = "Opaque"} |
DisableBatching | 一些SubShader在使用Unity的批處理功能時會出現問題,例如使用了模型空間下的座標進行頂點動畫.這時可以通過該標籤來直接指明是否對該SubShader使用批處理 | Tags {"DisableBatching" = "True"} |
ForceNoShadowCasting | 控制使用該SubShader的物體是否會投射陰影 | Tags {"ForceNoShadowCasting" = "True"} |
IgnoreProjector | 如果該標籤值為"True",那麼使用該SubShader的物體將不會受Projector的影響.通常用於半透明物體 | Tags {"IgnoreProjector" = "True"} |
CanUseSpriteAtlas | 當該SubShader是用於精靈(sprites)時,將該標籤設為"False" | Tags {"CanUseSpriteAtlas" = "False"} |
PreviewType | 指明材質面板將如何預覽該材質.預設情況下,材質將顯示為一個球形,我們可以通過把該標籤的值設為"Plane""SkyBox"來改變預覽型別 | Tags {"PreviewType" = "Plane"} |
需要注意,上述標籤僅可以在 SubShader的標籤塊中宣告,而不可以在Pass塊中宣告。Pass塊雖然也可以定義標籤,但這些標籤不同於SubShader的標籤型別。
-
Pass語義塊
Pass語義塊的含義如下:
Pass {
[Name]
[Tags]
[RenderSetup]
// Other code
}
我們可以定義該Pass的名稱,例如:
Name "MyPassName"
通過這個名稱,我們可以使用 ShaderLab 的 UsePass 命令來直接使用其他 Unity Shader 中的 Pass。如:
UsePass "MyShader/MYPASSNAME"
這樣可提高程式碼的複用性。需要注意,由於Unity 內部會把所有Pass的名稱換成大寫字母的表示,因此,在使用 UsePass命令時必須使用大寫形式。
Pass中同樣可以設定渲染狀態,SubShader 的狀態設定同樣適用於 Pass。
Pass同樣可以設定標籤,但它的標籤不同於SubShader 的標籤。這些標籤也是用於告訴渲染引擎我們希望怎麼樣來渲染該物體。標籤型別如下:
標籤型別 | 說明 | 例子 |
LightMode | 定義該Pass在Unity的渲染流水線中的角色 | Tags {"LightMode" = "ForwardBase"} |
RequireOptions | 用於指定當滿足某些條件時才渲染該Pass,它的值是一個由空格分隔的字串.目前,Unity支援的選項有:SoftVegetation.在後面的版本中,可能會增加更多的選項 | Tags {"RequireOptions" = "SoftVegetation"} |
除了上面普通的Pass定義之外, Unity Shader 還支援一些特殊的 Pass,以便進行程式碼複用或實現更復雜的效果。
- UsePass:如我們之前提到的一樣,可以使用該命令來複用其他Unity Shader中的Pass。
- GrabPass:該Pass負責抓取螢幕並將結果儲存在一張紋理中,以用於後續的Pass處理。
4.Fallback(留一條後路)
緊跟在各個SubShader語義塊後面的,可以是一個Fallback指令。它用於告訴Unity,”如果上面所有的SubShader在這塊顯示卡上都不能執行,那麼就使用這個最低階的Shder吧!“
語義如下:
Fallback "name"
//或者
Fallback off
例子如下:
Fallback "VertexLit"
Fallback 還會影響陰影的投射。在渲染陰影紋理時,Unity會在每個 Unity Shader 中尋找一個陰影投射的Pass。通常情況下,我們不需要專門實現一個Pass,這是因為Fallback使用內建Shader中包含了這樣一個通用的Pass。因此,每個Unity Shader正確設定Fallback是非常重要的。
PS:Unity Shader還有其他語義,如CustomEditor語義來擴充套件編輯介面;Category語義來對Unity Shader 中的命令進行分組。
Unity Shader的編寫形式
我們可以使用以下3種形式來編寫Unity Shader(表面著色器、頂點/片元著色器、固定函式著色器)。而且不管使用哪種形式,真正意義上的Shader程式碼都需要在 ShaderLab語義塊中,如下:
Shader "MyShader" {
Properties {
//所需要的各種屬性
}
SubShder {
//真正意義上的 Shader 程式碼會出現在這裡
//表面著色器 (Surface Shader)或者
//頂點/片元著色器(Vertex/Fragment Shader)或者
//固定函式著色器(Fixed Function Shader)
}
SubShder {
//和上一個SubShder類似
}
}
1.表面著色器(寵兒)
表面著色器是Unity自己創造的一種著色器程式碼型別。它需要的程式碼量很少,在Unity背後做了很多的工作,但渲染的代價比較大。它本質上和下面講的頂點/片元著色器是一樣的,也就是說,當給Unity提供一個表面著色器的時候,它在背後仍舊把它轉換為對應的頂點/片元著色器。我們可以理解成,表面著色器是Unity對頂點/片元著色器的更高一層的抽象。它存在的價值在於,Unity為我們處理了很多的光照細節,使得我們不需要在操心這些”煩人的事情“。
表面著色器的例子如下:
Shader "Custom/Simple Surface Shader" {
SubShader {
Tags { "RenderType" = "Opaque" }
CGPROGRAM
#pragma surface surf Lambert
struct Input {
float4 color : COLOR;
};
void surf (Input IN, inout SurfaceOutput o) {
o.Albedo = 1;
}
ENDCG
}
Fallback "Diffuse"
}
從上面可以看出來,表面著色器被定義在 SubShader 語義塊(而非Pass語義塊)中的 CGPROGRAM 和 ENDCG之間。
原因是,表面著色器不需要開發者關心使用多少個 Pass、每個Pass如何渲染等問題,Unity會在背後為我們做好這些事情。我們要做的只是告訴它:”使用這些紋理去填充顏色,使用這個紋理去填充顏色,使用這個法線紋理去填充法線,使用Lambert光照模型,其他的都不要煩我!“。
CGPROGRAM 和 ENDCG之間的程式碼是用 Cg/HLSL編寫的,也就是說,我們需要把Cg/HLSL語言巢狀在 ShaderLab語言中。
值得注意的是,這裡的Cg/HLSL是Unity經過封裝後提供的,它的語法和標準的Cg/HLSL語法幾乎一樣,但還是有細微不同,例如有些原生函式和用法Unity並沒有支援。
2.頂點/片元著色器(最聰明)
在Unity中我們可以使用Cg/HLSL語言來編寫 頂點/片元著色器,它們更加複雜,但靈活度也更高。
一個簡單的頂點/片元著色器示例程式碼如下:
Shader "Custom/Simple VertexFragment Shader" {
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
float vert(float4 v:POSITION): SV_POSITION {
return mul (UNITY_MATRIX_MVP,v);
}
fixed4 frag() : SV_Target {
return fixed4(1.0,0.0,0.0,1.0);
}
ENDCG
}
}
}
和表面著色器類似,頂點/片元著色器的程式碼也需要定義在 CGPROGRAM 和 ENDCG之間,但不同的是,頂點/片元著色器是寫在Pass語義塊內,而非SubShader內的。
原因是,我們需要自己定義每個Pass需要使用的Shader程式碼。雖然我們需要編寫更多的程式碼,但帶來的好處是靈活度很高。更重要的是,我們可以控制渲染的實現細節,同樣這裡的CGPROGRAM 和 ENDCG之間的程式碼也是使用Cg/HLSL編寫的。
3.固定函式著色器(被拋棄的角落)
上面的兩種 Unity Shader 形式都使用了可程式設計管線,而對於一些較舊的裝置(其GPU僅支援DirectX7.0、OpenGL1.5或OpenGL ES 1.1),例如 IPHONE3,它們不支援可程式設計管線著色器,因此,這時候我們就需要使用固定函式著色器來完成渲染。這些著色器往往只可以完成非常簡單的效果。
示例程式碼如下:
Shader "Tutorial/Basic" {
Properties {
_Color ("Main Color", Color) = (1,0.5,0.5,1)
}
SubShader {
Pass {
Material {
Diffuse [_Color]
}
Lighting On
}
}
}
可以看出,固定函式著色器的程式碼被定義在Pass語義塊中,這些程式碼相當於Pass中的一些渲染設定,正如我們之前講的一樣。對於固定函式著色器來說,我們需要完全使用 ShaderLab 的語法 (即使用 ShaderLab 的渲染設定命令)來編寫,而非使用Cg/HLSL。
PS:由於現在絕大數GPU都支援可程式設計的渲染管線,這種固定管線的程式設計方式已經逐漸被拋棄。
4.選擇哪種Unity Shader 形式
作者建議:
- 除非有明確的需要要求必須要使用 固定函式著色器,例如在舊裝置中,否則請使用 可程式設計管線的著色器,即表面著色器或者頂點/片元著色器。
- 如果想跟各種光源打交道,則可能更喜歡使用表面著色器,但需要小心在移動平臺的效能表現。
- 如果你需要使用的光源數目非常少,例如只有一個平行光,那麼使用頂點/片元著色器是一個更好的選擇。
- 最重要的是,如果你有很多自定義的渲染效果,那麼請選擇頂點/片元著色器。
5.該書使用的 Unity Shader 形式
著重講述使用頂點/片元著色器進行Unity Shader的編寫。當然對於表面著色器來說,會在之後進行剖析。