Shader基礎
1. Shader其實就是專門用來渲染圖形的一種技術,通過shader,我們可以自定義顯卡渲染畫面的算法,使畫面達到我們想要的效果。
2. Shader編程語言:
- 基於OpenGL的OpenGL Shading Language,簡稱GLSL。
- 基於DirectX的High Level Shading Language,簡稱HLSL。
- 還有NVIDIA公司的C for Graphic,簡稱Cg語言。
3. GLSL與HLSL分別是基於OpenGL和Direct3D的接口,兩者不能混用。而Cg語言是用於圖形的C語言,這其實說明了當時設計人員的一個初衷,就是讓基於圖形硬件的編程變得和C語言編程一樣方便,自由。正如C++和 Java的語法是基於C的,Cg語言本身也是基於C語言的。Cg語言極力保留了C語言的大部分語義,力圖讓開發人員從硬件細節中解脫出來,Cg同時擁有高級語言的好處,如代碼的易重用性,可讀性高等。
4. 在Unity中編寫的Shader最終會根據不同的平臺來編繹成不同的着色器語言,官方的建議是用Cg/HLSL來編寫,當然也可以使用GLSL,主要是因爲Cg/HLSL有更好的跨平臺性,更傾向於使用Cg/HLSL來編寫Shader程序。
5. Unity Shader嚴格來說並不是傳統上的Shader,而是Unity自身封裝後的一種便於書寫的Shader,又稱爲ShaderLab。
6. 在Unity中有3種Shader:
- Surface Shaders 表面着色器。
- Vertex/Fragment Shaders 頂點/片斷着色器。
- Fixed Function Shaders 固定管線着色器。
(1) Surface Shader其實就是Unity對Vertex/Fragment Shader的又一層包裝,以使Shader的製作方式更符合人類的思維模式,同時可以以極少的代碼來完成不同的光照模型與不同平臺下需要考慮的事情。
(2) Surface Shader也有它的侷限性,就是Vertex/Fragment Shader能實現的效果,Surface Shader不一定能實現,反過來則成立,Surface Shader能實現的Vertex/Fragment Shader則一定可以實現。
創建Shader
Unity中Shader種類
1. Standard Surface Shader
- 標準表面着色器,是一種基於物理的着色系統(使用了Physically Based Rendering(簡稱PBR)技術,即基於物理的渲染技術),以模擬現實真實的方式來模擬材質與燈光之間的關係,可以很輕易的表現出各種金屬反光效果,同時此種Shader的書寫邏輯也更符合人類的思維模式。
2. Unlit Shader
- Vertex/Fragment Shader,也就是最基本的頂點片斷着色器,不受光照影響的Shader,多用於特效、UI上的效果製作。
3. Image Effect Shader
- 也是頂點片段着色器,只不過是針對後處理而定製的模板。
4. ComputeShader
- Compute Shader是運行在圖形顯卡上的一段程序,獨立於常規渲染管線之外的,它可以直接將GPU作爲並行處理器加以利用,從而使GPU不僅具有3D渲染能力,還具有其他的運算能力。
5. Shader Variant Collection
-
Shader變體收集器,它不是製作Shader的模版,而只是對Shader變體進行打包用的容器。
注:以上的Standard Surface Shader、Unlit Shader、Image Effect Shader僅僅只是Unity爲了方便我們書寫而內置的幾個模版,完全可以建一個Unlit Shader,然後將其改成Surface Shader,同樣也可以將一個Standard Surface Shader改成頂點片斷着色器,所以這一點一定要明白,它們只是內容格式不一樣的模版本而已,我們完全可以自由修改成任意我們想要的一種着色器類型,當然我們也可以通過一些手段來定製出我們自己的模版。
材質和Shader的關係
1. 在Project視圖中創建一個材質和一個Shder,直接在Project視圖中將Shader拖動到材質上,即可對材質關聯對應的Shader。
2. Shader與材質的關係:
- 一個Shader可以與無數個材質關聯。
- 一個材質同一時刻只能關聯於一個Shader,但是可以通過代碼去動態改變材質所關聯的Shader。
- 材質可以賦與模型,但是Shader不行。
- 材質就像是Shader的實例,每個材質都可以參數不一樣呈現不同的效果,但是當Shader改變時,關聯它的所有材質都會相應的改變。
Shader框架
命名
1. 如果我們把路徑名稱放在Hidden下面的話,比如:
Shader "Hidden/TA/MyFirstShader"
則表示在材質面板中隱藏此Shader,你將無法通過材質下拉列表中找到它。這在做一些不需暴露的Shader時很有用處,可以使Shader下拉列表更精簡整潔。
2. Shader文件的名稱,也就是我們在Project面板中的資源文件的名稱,是可以與Shader內部的路徑名稱不一樣的,這點與C#是不同的。在定義內部路徑名稱時,建議統一規劃下,不要Shader過多後導致很混亂,不便於美術去使用。
Properties
1. Properties可以理解爲是材質與Shader的連接通道,在材質面板上需要設置的內容都必須通過Properties來實現並暴露。如果不需要屬性的話也可以將整個Properties以及它的{}全部刪除。
2. 語法格式:
(1) Attribute:
- 屬性標記,Unity內置的幾個屬性標記關鍵字,用於對當前這條屬性進行一些特殊的處理,此標記不是必選項,可以不添加,同時一條屬性上也可以有多條屬性標記。
(2) _Name:
-
屬性的名稱,也就是變量名,在Shader的CG代碼中就是通過這個名稱來調用此屬性內容的,在外部利用腳本調用時也是這個名稱,所以一定要用英文。
-
在名稱前一定要加上下劃線,否則會出現編繹錯誤。
(3) Display Name:
-
顯示在材質面板上的名稱,主要起到說明解釋的作用,可中文(正式項目中建議最好還是用英文)。
-
一般使用作用與通道的組合命名,如BaseRGB。
(4) Type:屬性的類型,常用的有以下幾種:
- Color顏色
- Int整數
- Float浮點數
- Vector四維數
- 2D紋理
- 3D紋理
- Cube立方體紋理
- Default Value
(5) Default Value
- 默認值,當第一次指定此Shader時,或者在材質面板上執行Reset時,屬性的值會自動恢復到默認值,不同的類型具體寫法也不太一樣。
3. Properties舉例。
注意:Shader中的浮點數值後不需要加後綴f,否則會出錯。零點幾的值可以省略零,比如0.95可以寫成.95
(1) Color顏色Type屬性。前面用HDR限制後,會多出Intensity屬性。
(2)數值和數值區間
(3) 開關和下拉列表
(4) 四維向量和2D紋理。其中2D紋理的默認值有以下幾種:
- white
- black
- gray
- bump
如果不設置默認值,即=""{},則其實與="gray"{}相同。當設置了默認值後,Shader內部會自動調用Unity內部準備的一張小圖片,white就是純白色,black就是純黑色,gray就是灰色圖,bump就是法線圖。
(5) 3D紋理和立方體紋理
(6) 常用通用屬性標記。
SubShaders
1. 實際上每個Shader中都可以包含多個SubShader,不可以沒有,必須至少有一個,因爲當前Shader的核心算法實現就是在SubShader中來實現的。
2. 在加載Shader時,Unity將遍歷所有SubShader列表,並最終選擇用戶機器支持的第一個。
3. 不同的硬件性能是不一樣,遊戲內通常把機器配置分爲高中低三種,假如我們做了一個效果很好的Shader,但是卻只能在高配機上有較好的性能表現,中低端就顯的太費性能,SubShader在這時就可以派上用場了,可以在這個Shader內做三個SubShader,分別對應於高中低不同的配置。
4. 最簡單的一個Shader:
(1) 在SubShader中,Pass即爲走通一遍整個渲染管線。具體要怎麼渲染就需要我們在Pass中添加Cg/HLSL代碼片斷來實現了,這段代碼片斷是由CGPROGRAM開始,由ENDCG結束。
(2) 接着定義執行頂點着色器與片段着色器的名字,告訴Unity分別在哪裏去執行它們。分別通過#pragma vertex和#pragma fragment去定義。
#pragma vertex VertexName
#pragma fragment FragmentName
(3) 接着定義類似下面代碼的頂點着色器:
- 頂點着色器函數的名稱,與上面已經指定了頂點着色器的名稱需要保持一致。
- 輸入參數中,僅僅定義一個或數個多維向量並不能使它擁有我們模型的頂點信息,需要爲它指定一個語義,例如POSITION就是代表着模型的頂點位置信息。此時變量vertex就表示着我們模型的頂點位置。
- 在頂色着色器中最主要的事情就是將頂點從模型座標轉換到裁剪座標,Unity已經爲我們準備好現成的命令,只需調用UnityObjectToClipPos函數即可。
- 在後面片斷着色器中需要頂點着色器中的輸出結果,所以函數中需要加上return來將轉換後的頂點返回。經過變換後返回的頂點位置,我們也需要利用語義來標記一下,以便片斷着色器可以知道哪個是從頂點着色器輸出過來的頂點位置信息,所以在函數的後面加上: SV_POSITION。
- 在頂點着色器中處理頂點時,首先需要獲取到模型的頂點數據(比如頂點位置、法線信息、頂點顏色等等),這些數據都是直接存儲在模型中的,我們在Shader中只需要通過標識語義就可以自動獲得。
(4) 經過頂點着色器的處理,已經得到了最終顯示在屏幕上的頂點矩陣,內部會自動進行插值計算,以獲得當前模型的所有片斷像素,然後每個像素都會執行一次片斷着色器,得到最終每個像素的顏色值。
- 片斷着色器的函數名與#pragma fragment定義的名字一樣,如果需要從頂點着色器傳入額外參數,可以傳入過來。
- 在Cg/HLSL中使用Properties中的變量前還需要在Cg/HLSL中再重新聲明一次,名稱要求一致。float、half、fixed,這三都是浮點數的表示,只是分別對應的精度不一樣,主要用此可以進行更進一步的優化。
- SV_TARGET是系統值,表示該函數返回的是用於下一個階段輸出的顏色值,也就是我們最終輸出到顯示器上的值。
(5) 頂點着色器與片斷着色器的執行並不是1:1的,舉個例子,一個三角面片,只有三個頂點,頂點着色器只需執行3次,而片斷着色器由最終的像素數來決定,執行幾百上千都是很正常的。所以從性能的角度來考慮,我們要儘量把計算放在頂點着色器中去執行。其次在片斷着色器中也要儘量的簡化算法,節省開支。
Properties
{
_Color("CustomColor", Color) = (1, 1, 1, 1)
}
SubShader
{
Tags { "RenderType" = "Opaque" }
LOD 100
Pass//渲染一次模型
{
CGPROGRAM//Pass中開始執行
#pragma vertex VertexName
#pragma fragment FragmentName
float4 VertexName(float4 vertex : POSITION) : SV_POSITION
{
return UnityObjectToClipPos(vertex);
}
fixed4 _Color;
float4 FragmentName() : SV_TARGET
{
return _Color;
}
ENDCG//Pass中結束執行
}
}
FallBack
1. 有的時候寫的Shader難免在一些機器上會出現不支持的問題,而這個時候我們只要添加了FallBack,並且在後面的雙引號內寫上了其它Shader的有效路徑名稱,那麼在碰到不支持的硬件時這個Shader就會自動切換成FallBack內的Shader,如果FallBack內的Shader也不支持呢,那就繼續從FallBack內的Shader中再找FallBack。
CustomEditor
1. 自定義界面,也就是說我們可以通過這個功能來自由定義材質面板的顯示結果,它可以改寫Properties中定義的顯示方式。
舉個你一定見過的例子:
Shader中變量相關問題
Cg/HLSL中的數據類型
1. 在Shader中,我們在Properties中定義的變量是爲了在材質面板中顯示並方便我們調節,如果要在Cg/HLSL中使用的話就必須要重新聲明一次(要求命名一樣)。
2. 在Cg/HLSL中的幾種常見數據類型:
(1) float/half/fixed(三個都是浮點數,只是精度不一樣而已)
- float高精度類型,32位,通常用於世界座標下的位置,紋理UV,或涉及複雜函數的標量計算,如三角函數、冪運算等。
- half中精度類型,16位,數值範圍爲[-60000,+60000],通常用於本地座標下的位置、方向向量、HDR顏色等
- fixed低精度類型,11位,數值範圍爲[-2,+2],通常用於常規的顏色與貼圖,以及低精度間的一些運算變量等。
- 在PC平臺不管你Shader中寫的是half還是fixed,統統都會被當作float來處理。half與fixed僅在一些移動設備上有效。
比較常用的一個規則是,除了位置和座標用float以外,其餘的全部用half。主要原因也是因爲大部分的現代GPU只支持32位與16位,也就是說只支持float和half,不支持fixed。
(2) integer(整型)
- 整型類型,通常用於循環與數組的索引。
(3) sampler2D(2D紋理)
(4) samplerCUBE(3D紋理)
- 默認情況下在移動平臺紋理會被自動轉換成低精度的紋理類型。
- 如果需要中精度的或者高精度的需要用以下方式來聲明:sampler2D_half(中精度2D紋理),sampler2D_float(高精度2D紋理),sampler3D_half(中精度3D紋理),sampler3D_float(高精度3D紋理),samplerCUBE_halft(中精度立方體紋理),samplerCUBE_float(高精度立方體紋理)
類型對應
1. Properties中的屬性與Cg/HLSL中的對應關係如下:
- Int/float/Range用浮點值表示,也就是float、half或者fixed,根據自己需要的精度來定義。
- Vector/Color用float4、half4或者fixed4表示。
- 2D類型用sampler2D表示。
- 3D類型sampler3D表示。
- CUBE類型用samplerCUBE表示。
2. 多Vector分量通常在顏色上用rgba,在向量上用xyzw,這樣比較直觀方便理解。
3. 在Shader中,矩陣是一個按照長方形陣列排列的浮點數集合。可以用floatMxN來表示,如果是4x4矩陣,就是float4x4(同樣支持其它精度),不過有一點要注意,在某些平臺上是不支持非方矩陣的(比如float3x2),特別是OpenGL ES 2.0平臺。
結構體傳遞多個值
1. 如果想在應用程序階段傳遞多個值,除了頂點位置還想傳遞頂點色、UV座標等到片元着色器中需要用到結構體。
2. 可通過在Shader中自定義函數調用來減輕把所有內容都寫在片段和頂點着色器裏面的問題,但是在使用之前必須要已經定義。
Pass//渲染一次模型
{
CGPROGRAM//Pass中開始執行
#pragma vertex VertexName
#pragma fragment FragmentName
fixed4 _Color;
struct SAppData
{
float4 vertext:POSITION;
float2 uv : TEXCOORD;
};
struct STranslateData
{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD;
};
STranslateData VertexName(SAppData InputMeshData)
{
STranslateData TranslateData;
TranslateData.pos = UnityObjectToClipPos(InputMeshData.vertex);
TranslateData.uv = InputMeshData.uv;
return TranslateData;
}
float4 FragmentName(STranslateData InputVertexData) : SV_TARGET
{
return fixed4(InputVertexData.uv, 0, 1);
}
ENDCG//Pass中結束執行
}
應用數據傳遞流程
應用階段傳入頂點着色器的數據
1. 當應用階段的數據傳過來時,頂點着色器怎麼知道誰是模型的頂點數據誰又是模型的法線數據呢?需要一種方式來告拆計算機我們定義的變量代表着什麼。
struct SAppData
{
float4 vertext:POSITION;
float2 uv : TEXCOORD;
};
(1) 這裏我們聲明瞭一個float4類型的變量vertext,並給予了它頂點數據的語義(在變量後加冒號並跟一個語義),也就是說vertext變量將代表着模型的頂點數據被我們使用與傳遞。
2. 常見語義:
struct appdata
{
float4 vertex : POSITION; //頂點
float4 tangent : TANGENT; //切線
float3 normal : NORMAL; //法線
float4 texcoord : TEXCOORD0; //UV1
float4 texcoord1 : TEXCOORD1; //UV2
float4 texcoord2 : TEXCOORD2; //UV3
float4 texcoord3 : TEXCOORD3; //UV4
fixed4 color : COLOR; //頂點色
};
頂點着色器到片斷着色器的數據
1. 頂點着色器在處理完應用階段傳過來的數據後,會需要輸出並傳入片斷着色器,這個時候我們同樣需要定義一個結構來承載其中的數據,同樣的,輸出給片斷着色器的值也需要語義來標識。
struct STranslateData
{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD;
};
(1) 這裏聲明瞭float4類型的變量pos,並指定爲SV_POSITION語義,表示pos就是頂點着色器輸出的屏幕裁剪空間下的頂點位置。這條語義是必須要有的,否則GPU無法進行接下來的光柵化處理。
(2) 其實在現代GPU上對這裏的語義如何定義並不關心了(除了SV_POSITION以外),主要是部分OpenGL ES2.0上面才需要特別注意而已。
2. 這裏的語義除了SV_POSITION以外,我們還有另外兩種選擇:
(1) TEXCOORD0~N系列。例如TEXCOORD0、TEXCOORD1、TEXCOORD2...等等,主要用於高精度數據。
(2) COLOR0~N系列。例如COLOR0、COLOR1、COLOR2...等等,主要用於低精度數據。
3. 雖然這兩種語義我們可以根據需要自由定義,但是,它並不是可以無限定義的,不同的GPU硬件有不同的數量限制。以下爲手機平臺的常見規則:
- OpenGL ES2.0支持最多8個
- OpenGL ES3.0支持最多16個
4. 從性能優化角度來講,數量越少性能越好。另外,每個語義是4維向量,利用好這一點,可以大大節省總數量。
5. 在片斷着色器中還有些特殊的語義的可以識別,比如VFACE,如果渲染表面朝向攝像機,則Face節點輸出正值1,如果遠離攝像機,則輸出負值-1。
完整代碼如下:
fixed4 frag (v2f i,float face:VFACE) : SV_Target
{
fixed4 col=1;
col = face > 0 ? tex2D(_FrontTex,i.uv) : tex2D(_BackTex,i.uv);
return col;
}
片斷着色器輸出相關語義
1. 通常情況下,片斷着色器最終只需返回一個顏色值即可,也是我們最常見到的編寫方式,如下:
fixed4 frag (v2f i ) : SV_TARGET
這裏的SV_TARGET就是指定輸出顏色到RenderTarget的語義。
2. 當我們利用Struct時,就可以通過下列語義來輸出多個內容:
- SV_Target0〜N
默認SV_TARGET0,也就是SV_TARGET,還有SV_TARGET1,SV_TARGET2...這個在需要輸出多個RenderTarget時很有用。
- SV_Depth
3. 一般情況下,模型的像素深度值在光柵化時會自動插值計算得出,並不需要我們做額外的處理,但這並不代表不可以修改它,通過在片斷着色器中輸出SV_DEPTH語義可以更改像素的深度值。注意此功能相對會消耗性能,在沒有特別需求的情況下儘量不要用。
Tags可選參數組
1. 在SubShader和Pass中都有Tags可選參數組,Tags的作用是告訴引擎如何去渲染我們的對象,語法格式如下:
Tags { "TagName1" = "Value1" "TagName2" = "Value2" }
SubShader Tags
SubShader中的Tags必須放置於SubShader中的Tags內。
Queue:
1. Queue渲染隊列,指定對象什麼時候渲染,每個隊列其實都是利用一個整數進行索引的。整數索引值越小,越先渲染。有以下取值:
- Background。值爲1000,此隊列的對象最先進行渲染。
- Geometry。Queue的默認值,值爲2000,通常用於不透明對象,比如場景中的物件與角色等。
- AlphaTest。值爲2450,要麼完全透明要麼完全不透明,多用於利用貼圖來實現邊緣透明的效果,也就是美術常說的透貼。
- Transparent。值爲3000,常用於半透明對象,渲染時從後往前進行渲染,建議需要混合的對象放入此隊列。
- Overlay。值爲4000,此渲染隊列用於疊加效果。最後渲染的東西應該放在這裏(例如鏡頭光暈等)。
2. 寫法示例:
Tags{ "Queue" = "Geometry" }
3. 可以通過在值後加數字的形式來進行重新指定渲染隊列
Tags{ "Queue" = "Geometry+1" }
4. 渲染隊列直接影響性能中的重複繪製,合理的隊列可極大的提升渲染效率。在Unity中,渲染隊列小於2500的對象都被認爲是不透明的物體,(如“Background”,“Geometry”,“AlphaTest”),這些物體是從前往後繪製的,而使用其他的隊列(如“Transparent”,“Overlay”)的物體則是從後往前繪製的。這意味着,我們需要儘可能地把物體的隊列設置爲不透明物體的渲染隊列,而儘量避免重複繪製。
RenderType
1. 自帶的值有以下這些:
- Opaque
- Transparent
- TransparentCutout
- Background
- Overlay
- TreeOpaque
- TreeTransparentCutout
- TreeBillboard
- Grass
- GrassBillboard
2. 這些名稱實際上只是一種內部的約定,用來區別這個Shader要渲染的對象是屬於什麼類別的,可以想像成是把各種不同的物體進行分類一樣。當然也可以改成自定義的名稱,這樣並不會影響到Shader的效果。
(1) RenderType它的作用是可以利用Camera.SetReplacementShader來更改最終的渲染效果。這是Unity的腳本API,需要在C#腳本中進行調用。其中EffectShader是我們需要渲染的Shader,Tag是用來篩選渲染對象用的。
camera.SetReplacementShader (EffectShader, Tag);
(2) 具體按以下規則執行:
- 使用SetReplacementShader("shaderA",""),這裏Tag爲空,表示全部物體Shader都替換成shaderA進行渲染
- SetReplacementShader("shaderA","RenderType"),由於這裏Tag爲"RenderType",所以先查看場景中的shader中是否有RenderType參數,如果有,再看RenderType中的值是否與shaderA中的RenderType值相等,相等則使用shaderA渲染,否則就不渲染(Game視圖什麼也沒有)。
- 關於Tag參數並不侷限於RenderType,我們同樣可以使用SetReplacementShader("shaderA","IgnoreProjector")或是其他任何參數如:SetReplacementShader("shaderA","ABC")等。
(3) 用法示例:實現替換材質開關功能。
其中taecg/Overdraw(此Shader實現的是類似於Overdraw的效果)這個Shader中的Tag如下:
而我們的物體(這頭狼),我們給它的Shader中Tag如下:
DisableBatching tag
1. 有的時候,我們會利用Shader在模型的頂點本地座標下做一些位移動畫,當此模型有批處理時會出現效果不正確的情況,這是因爲批處理會將所有幾何轉換爲世界座標空間,因此“本地座標空間”將丟失。
"DisableBatching" = "True"
2. 這時我們就可以利用此Tag在Shader中指定是否受批處理影響,有以下3個值可選:
- False(默認值),不禁用批處理
- True,始終禁用此着色器的批處理
- LODFading,僅當LOD激活時禁用批處理
ForceNoShadowCasting tag
1. 是否強制關閉投射陰影,值可爲:
- True,強制關閉投射陰影
- False(默認值),不關閉投射陰影
"ForceNoShadowCasting" = "True"
IgnoreProjector tag
1. 是否忽略Projector投影器的影響,Projector是Unity中內置的組件,可用於實現貼花等功能。
- True,使對象不受Projector影響
- False(默認值),使對象受Projector影響
CanUseSpriteAtlas tag
1. 是否可用於精靈打包圖集,意思就是如果某個圖片精靈被設置爲打包進圖集中,那麼當此精靈所指定Shader中設置爲
"CanUseSpriteAtlas"="False"
時就會使其無法工作,相應的UI上也會有警告提示。
PreviewType tag
1. 此Tag決定了材質面板的預覽窗口如何顯示模型,默認顯示的是球體,當我們把值定義爲以下兩種情況時會進行相應的變化。此功能僅僅影響的只是材質面板的預覽,對Shader本身沒有什麼影響。
- Plane平面預覽
- Skybox天空盒預覽