一、著色器的分類
Unity中的著色器可以分為三大類
1.固定管線著色器(Fixed Pipeline Shader)->為了兼容老一代GPU而設(shè)計的最早的圖形學(xué)版本和最早的游戲都是基于這個著色器來編寫的。
特點是流水線作業(yè),固定指令,不可編程,比較簡單,無法自由的編寫GPU程序?qū)崿F(xiàn)不同的渲染。
2.頂點片元著色器(Fragment Shader)->比固定管線要新,功能強(qiáng)大。
特點是可編程著色器,允許開發(fā)者自由編寫GPU程序來實現(xiàn)不同的渲染功能。缺點是不支持光照。難易程度適中
3.表面著色器->Unity官方極力推薦的著色器。
最新的游戲都是基于這個著色器編寫的。
特點是可編程著色器,允許開發(fā)者自由編寫GPU程序來實現(xiàn)不同的渲染功能,而且支持光照,可以自定義光照模型,自由度最高。
固定管線是在老一代GPU能力比較有限時,對著色器的約束性比較高的一種形態(tài)。
前景:為了市場占有率,新的顯示支持部分功能特性,未來會逐漸被淘汰。
二、Shader中空間的概念
要游戲中的3D世界到屏幕上絢麗多彩的畫面需要進(jìn)行一系列的空間變換。
物體空間(本地空間)->世界空間->攝像機(jī)空間(視圖空間)->裁剪空間->標(biāo)準(zhǔn)屏幕空間->窗口空間
1.物體空間:所需要繪制的3D物體所在的原始坐標(biāo)系所代表的空間,也叫本地空間。
世界坐標(biāo)->本地坐標(biāo)方法:
Unity腳本中->transform.worldToLocalMatrix
Unity著色器中->左乘_World2Object矩陣
2.世界空間:物體最終顯示的3D場景中的擺放位置對應(yīng)的坐標(biāo)所屬的坐標(biāo)系所代表的空間。
本地坐標(biāo)->世界坐標(biāo)方法:
Unity腳本中->transform.localToWorldMatrix
Unity著色器中->左乘_Object2World矩陣
3.攝像機(jī)空間:物體經(jīng)過攝像機(jī)觀察后進(jìn)入攝像機(jī)空間,指的是以攝像機(jī)為原點的一個特定的坐標(biāo)系所代表的空間,在這個坐標(biāo)系中,攝像機(jī)位于原點,視線沿z軸負(fù)方向,y軸的方向與攝像機(jī)的UP向量方向一致。
本地坐標(biāo)->攝像機(jī)坐標(biāo)方法:
UNITY_MATRIX_MV矩陣
4.裁剪空間:
什么是視錐體?
對于正交投影來說是一個四邊平行于投影方向的四棱柱
對于透視投影來說是一個以近平面為上底,遠(yuǎn)平面為下底的棱臺(當(dāng)近平面為0時,則是一個以投影中心為頂點的四棱錐)。
什么是裁剪空間?
只有在攝像機(jī)空間中,并且位于視錐體內(nèi)的物體才能被觀察到。將攝像機(jī)空間內(nèi)視錐體內(nèi)的部分獨立出來經(jīng)過處理后形成的空間,就叫做裁剪空間。
物體坐標(biāo)->攝像機(jī)坐標(biāo)->屏幕空間的方法:
UNITY_MATRIX_MVP
5.標(biāo)準(zhǔn)設(shè)備空間:
對裁剪空間執(zhí)行透視除法后得到的空間。對于OpenGL來講,標(biāo)準(zhǔn)設(shè)備空間三個軸的坐標(biāo)范圍都是-1.0~1.0。
什么是透視除法?
將齊次坐標(biāo)[x, y, z, w]的4個分量都除以w,結(jié)果是[x/w, y/w, z/w, 1],本質(zhì)上就是對齊次坐標(biāo)進(jìn)行了規(guī)范化。
6.實際窗口空間:
代表的是設(shè)備屏幕上的一塊矩形區(qū)域,坐標(biāo)以像素為單位。主要工作是將執(zhí)行透視除法后的x,y坐標(biāo)分量轉(zhuǎn)換為實際窗口的xy像素坐標(biāo),主要的思路是將標(biāo)準(zhǔn)設(shè)備空間的xy平面對應(yīng)到視口上,將-1.0~1.0內(nèi)的x,y坐標(biāo)折算為視口上的像素坐標(biāo)。

三、頂點片元著色器
頂點片元著色器是可編程著色器,相對于固定管線著色器可以給開發(fā)人員更大的發(fā)揮空間。但是缺點是不能直接和光照進(jìn)行交互。
頂點片元著色器的程序使用CG或HLSL來進(jìn)行編寫,嵌入在著色器的渲染通道塊中。
CG的代碼被編寫在CGPROGRAM和ENDCG之間。
1.編譯指令:
使用#pragma指令
pragma vertex <name> 將名稱為name的函數(shù)編譯為頂點著色器
pragma fragment <name> 將名稱為name的函數(shù)編譯為片元著色器
pragma fragmentoption <option> 添加option到已編譯的OpenGL片元程序
pragma target <name> 要編譯成哪個著色器目標(biāo)
A.頂點變換,決定頂點最終的位置,頂點函數(shù)
將(網(wǎng)格,模型)原始頂點數(shù)據(jù)在頂點函數(shù)中經(jīng)過特別的處理變?yōu)橐呀?jīng)經(jīng)過3D渲染管線變
換的可以投影到2D屏幕上的位置確定的頂點。
B.像素著色,決定每一個像素點最終的顏色,片元函數(shù)
將頂點函數(shù)變換后的頂點進(jìn)行著色,確定其最終顏色,對所有經(jīng)過頂點變換后的頂點進(jìn)
行光柵化(上色)
渲染管線的核心流程:
(1)把3D的頂點變成2D->頂點變換,算出頂點的最終位置->vertex頂點函數(shù)功能
(2)給2D屏幕上的頂點著色->頂點光柵化,算出頂點的最終顏色->fragment片元函數(shù)功能
片元著色器處理流水線,渲染管線
原始網(wǎng)格頂點數(shù)據(jù)->頂點函數(shù)->最終位置頂點數(shù)據(jù)->片元函數(shù)->頂點的顏色值
特別注意的:
每一個Shader代碼文件都必須包含一個頂點程序或一個片元程序或兩個都有。
必須使用#pragma vertex或#pragma fragment或兩個都使用。
2.頂點數(shù)據(jù)結(jié)構(gòu)體:
頂點著色器中頂點數(shù)據(jù)必須以一個結(jié)構(gòu)體的形式提交給CG/HLSL頂點程序,下面介紹一些常用的頂點數(shù)據(jù)結(jié)構(gòu)體:
(1)appdata_base:由頂點位置,法線以及一個紋理坐標(biāo)組成
vertex->頂點坐標(biāo)
normal->法線
texcoord->紋理坐標(biāo)
(2)appdata_tan:由頂點位置,切線,法線以及一個紋理坐標(biāo)組成
vertex->頂點坐標(biāo)
tangent->切線
normal->法線
texcoord->紋理坐標(biāo)
(3)appdata_full:由頂點位置,切線,法線,兩個紋理坐標(biāo)以及顏色組成
vertex->頂點坐標(biāo)
tangent->切線
normal->法線
texcoord->紋理坐標(biāo)1
texcoord1->紋理坐標(biāo)2
color->顏色
3.內(nèi)置變化矩陣
(1)頂點著色器的流程:傳入頂點數(shù)據(jù)->頂點著色器->頂點運算,變換矩陣變換后的位置->返回
(2)片元著色器的流程:頂點著色器返回的最終值->片元著色器->計算顏色->返回最終顏色
Unity著色器內(nèi)置的常用變換矩陣有如下幾個:
UNITY_MATRIX_MVP -> 基本變換矩陣x攝像機(jī)矩陣x投影矩陣
UNITY_MATRIX_MV -> 基本變換矩陣x攝像機(jī)矩陣
UNITY_MATRIX_V -> 攝像機(jī)矩陣
UNITY_MATRIX_P -> 投影矩陣
UNITY_MATRIX_VP -> 攝像機(jī)矩陣x投影矩陣
_Object2World -> 本地坐標(biāo)系轉(zhuǎn)換為世界坐標(biāo)系
_World2Object -> 世界坐標(biāo)系轉(zhuǎn)換為本地坐標(biāo)系
4、什么是GrabPass?
GrabPass是一種特殊的pass類型。當(dāng)物體將要被繪制時,它抓取屏幕內(nèi)容并繪制到一張texture里。
GrabPass的特點?
運算開銷較大,不如AlphaBlend等指令。能用AlphaBlend實現(xiàn)的就不用GrabPass。
GrabPass的使用方式
(1)GrabPass{} 抓取當(dāng)前屏幕內(nèi)容。
(2)GrabPass{"TextureName"}將抓取屏幕內(nèi)容并保存至一張texture里。每幀只為第一次使用這張紋理的物體做一次,這種更高效
UnityObjectToClipPos的作用?
其作用等同于UNITY_MATRIX_MVP,但是如果直接使用UNITY_MATRIX_MVP,會引入一個額外的矩陣乘法運算,所以推薦使用UnityObjectToClipPos / UnityObjectToViewPos函數(shù),它們會把這一次額外的矩陣乘法優(yōu)化為向量-矩陣乘法。
tex2D函數(shù)的作用?
tex2D是紋理采樣函數(shù),可以通過紋理圖像素信息與紋理坐標(biāo)計算該位置的頂點的紋理像素點顏色。
tex2D(texture,uv)第1個參數(shù)為紋理圖,第2個參數(shù)為紋理坐標(biāo),返回值為指定紋理坐標(biāo)位置的顏色值
四、****表面著色器:
頂點片元著色器最大的缺點是不能直接和光照交互。為了讓開發(fā)人員更方便快捷地處理光照,Unity提供了表面著色器。表面著色器代碼也是使用CG或HLSL語言編寫的。
著色器的三種形態(tài)中,表面著色器比固定管線著色器更加靈活,又比頂點片元著色器更加方便地處理光照,所以在游戲開發(fā)中最常用的是表面著色器。下面介紹表面著色器的基礎(chǔ)知識。
1.編譯指令
表面著色器與其他任何著色器一樣放置于CGPROGRAM….ENDCG塊中。區(qū)別是必須將其放置于子著色器塊中,而不能放在通道中,表面著色器自身會編譯為多個通道。它使用#pragma surface指令來表明它是個表面著色器。
pragma surface指令格式如下:
pragma surface<surfaceFunction><lightModel>[optionalparams]
這其中:
surfaceFunction為表面著色器函數(shù)名稱,通過該指令告訴編譯器cg代碼中surfaceFunction函數(shù)為表面著色器函數(shù)。
lightModel為光照模型。通過該指令告訴編譯器這個表面著色器使用哪個光照模型。Unity內(nèi)置的光照模型為Lambert(漫反射)和BlinnPhong(高光),也可以自定義光照模型。
Optionalparams為可選參數(shù)??捎玫目蛇x參數(shù)如下表所示:

2.輸入輸出參數(shù)結(jié)構(gòu)體
表面著色器函數(shù)可以有兩個參數(shù),其中一個參數(shù)為Input結(jié)構(gòu)體,用于為表面著色器函數(shù)輸入所需的紋理坐標(biāo)和其他數(shù)據(jù)。另一個參數(shù)為SurfaceOutput結(jié)構(gòu)體,需在表面著色器函數(shù)中寫入相應(yīng)的值,用于輸出數(shù)據(jù)。
//如果屬性中有紋理,則需要寫這個結(jié)構(gòu)體
struct Input {
// uv+變量名
float2 uv_MainTex;
};
//需要對Properties中的屬性重寫定義
//重新設(shè)置類型
//2D == sampler2D
//Range == half
//color == fixed4
sampler2D _MainTex;
half _Glossiness;
half _Metallic;
fixed4 _Color;
fixed4 _AlphaColor;
Input結(jié)構(gòu)體中的紋理坐標(biāo)必須在紋理名稱前面加上”uv”或”uv2”,帶”uv”的紋理坐標(biāo)為物體所帶的第一個紋理坐標(biāo),如果物體帶有第二個紋理坐標(biāo),則帶”uv2”的紋理坐標(biāo)為物體所帶的第二個紋理坐標(biāo)。其他可用的數(shù)據(jù)如下表所示:

Input結(jié)構(gòu)體不但可以包含上面所列的數(shù)據(jù),也可以包含自定義的數(shù)據(jù),用于從頂點函數(shù)傳遞數(shù)據(jù)給表面著色器。表面著色器的輸出結(jié)構(gòu)體SurfaceOutput是內(nèi)置定義好的,只需在表面著色器函數(shù)中為需要的變量賦值就可以了。標(biāo)準(zhǔn)的表面著色器輸出結(jié)構(gòu)體如下:


也可以自定義表面著色器的輸出結(jié)構(gòu)體,但自定義的結(jié)構(gòu)體必須包括SurfaceOutput結(jié)構(gòu)體的所有變量,然后可以添加自己需要的變量用于從自定義光照模型函數(shù)傳遞數(shù)據(jù)給表面著色器函數(shù)。
下面用一段自定義表面著色器材質(zhì)的基礎(chǔ)Shader代碼來解釋一下表面著色器的結(jié)構(gòu):
Shader "Custom/BaseForm2" {
Properties { //定義屬性塊
_Color ("Color", Color) = (1,1,1,1) //定義主顏色數(shù)值
_MainTex ("Albedo (RGB)", 2D) = "white" {} //定義紋理數(shù)值
_Glossiness ("Smoothness", Range(0,1)) = 0.5 //定義高光系數(shù)數(shù)值
_Metallic ("Metallic", Range(0,1)) = 0.0 //定義金屬材質(zhì)系數(shù)數(shù)值
}
SubShader {
Tags { "RenderType"="Opaque" } //標(biāo)簽
LOD 200 //LOD數(shù)值
CGPROGRAM
#pragma surface surf Standard fullforwardshadows //表面著色器編譯指令
#pragma target 3.0 //著色器編譯目標(biāo)
sampler2D _MainTex; //2D紋理屬性
struct Input { //定義輸入?yún)?shù)結(jié)構(gòu)體
float2 uv_MainTex; //紋理UV坐標(biāo)
};
half _Glossiness; //定義高光系數(shù)屬性
half _Metallic; //定義金屬材質(zhì)系數(shù)屬性
fixed4 _Color; //定義主顏色屬性
void surf (Input IN, inout SurfaceOutputStandard o) { //表面著色器函數(shù)
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; //根據(jù)UV坐標(biāo)從紋理提取顏色
o.Albedo = c.rgb; //設(shè)置顏色
o.Metallic = _Metallic; //設(shè)置金屬材質(zhì)系數(shù)
o.Smoothness = _Glossiness; //設(shè)置高光系數(shù)
o.Alpha = c.a; //設(shè)置透明度
}
ENDCG
}
FallBack "Diffuse" //降級著色器
}
這其中"RenderType"="Opaque"為子著色器標(biāo)簽的一組值,詳細(xì)的解釋如下圖所示:

3.自定義光照模型
編寫表面著色器就是描述一個表面的屬性(如反射率,顏色,法線等),并由光照模型完成光照交互的計算。系統(tǒng)內(nèi)置了Lambert(漫反射)和BlinnPhong(高光)兩個光照模型。有時也需要開發(fā)自定義光照模型。
自定義的光照模型是由名稱為”Lighting”開頭的函數(shù)實現(xiàn)的。自定義光照模型函數(shù)的聲明有以下幾種形式,用于不同的需求。
(1).half4 Lighting<Name>(SurfaceOutput s, half3 lightDir, half atten)
其在正向渲染路徑中用于非與視線方向相關(guān)的光照模型(例如,漫反射)。
(2).half4 Lighting<Name>(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
其在正向渲染路徑中用于與視線方向相關(guān)的光照模型。
(3).half4 Lighting<Name> _PrePass(SurfaceOutput s, half4 light)
其用于延時光照路徑中的光照模型。
這其中,SurfaceOutput結(jié)構(gòu)體用于和表面著色器函數(shù)傳輸數(shù)據(jù)。這個結(jié)構(gòu)體也可以自己定義,但必須與表面著色器函數(shù)的輸出結(jié)構(gòu)體相同?!眑ightDir”參數(shù)為點到光源的單位向量,”viewDir”參數(shù)為點到攝像機(jī)的單位向量,”atten”參數(shù)為光源的衰減系數(shù)。
光照模型函數(shù)的返回值為經(jīng)過光照計算的顏色值。下面通過一個帶自定義光照模型的表面著色器來詳細(xì)介紹自定義光照模型:
Shader "Custom/BaseForm3" {
Properties {
_Color ("Color", Color) = (1,1,1,1) //主顏色數(shù)值
_MainTex ("Albedo (RGB)", 2D) = "white" {} //2D紋理數(shù)值
_Shininess ("Shininess ", Range(0,10)) = 10 //鏡面反射系數(shù)
}
SubShader {
CGPROGRAM
#pragma surface surf Phong //表面著色器編譯指令
sampler2D _MainTex; //2D紋理屬性
fixed4 _Color; //主顏色屬性
float _Shininess; //鏡面反射系數(shù)屬性
struct Input {
float2 uv_MainTex; //uv紋理坐標(biāo)
};
//光照模型函數(shù)
float4 LightingPhong(SurfaceOutput s, float3 lightDir,half3 viewDir, half atten){
float4 c;
float diffuseF = max(0,dot(s.Normal,lightDir)); //計算漫反射強(qiáng)度
float specF;
float3 H = normalize(lightDir+viewDir); //計算視線與光線的半向量
float specBase = max(0,dot(s.Normal,H)); //計算法線與半向量的點積
specF = pow(specBase,_Shininess); //計算鏡面反射強(qiáng)度
c.rgb = s.Albedo * _LightColor0 * diffuseF *atten + _LightColor0*specF;
//結(jié)合漫反射光與鏡面反射光計算最終光照顏色
c.a = s.Alpha;
return c; //返回最終光照顏色
}
void surf (Input IN, inout SurfaceOutput o) { //表面著色器函數(shù)
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;//根據(jù)UV坐標(biāo)從紋理提取顏色
o.Albedo = c.rgb; //設(shè)置顏色
o.Alpha = c.a; //設(shè)置透明度
}
ENDCG
}
FallBack "Diffuse" //降級著色器
}
4.頂點變換函數(shù)
頂點變化函數(shù)可以修改頂點著色器中的輸入頂點數(shù)據(jù)以及為表面著色器函數(shù)傳遞頂點數(shù)據(jù)。這可用于程序性動畫,沿法線的擠壓等效果。使用表面著色器編譯指令vertex:<Name>,其中”Name”為頂點函數(shù)的名稱。頂點函數(shù)的聲明有以下幾種形式,用于不同的需求。
void <Name> (inout appdata_full v)。
其用于只修改頂點著色器中的輸入頂點數(shù)據(jù)。
half4 <Name>(inout appdata_full v, out Input o)。
其用于修改頂點著色器中的輸入頂點數(shù)據(jù)以及為表面著色器函數(shù)傳遞數(shù)據(jù)。
其中inout類型的結(jié)構(gòu)體使用了頂點數(shù)據(jù)結(jié)構(gòu)體,用于給頂點函數(shù)輸入頂點數(shù)據(jù)。out類型的結(jié)構(gòu)體為表面著色器中使用的輸入結(jié)構(gòu)體,用于頂點變換函數(shù)為表面著色器函數(shù)傳遞數(shù)據(jù)。下面我們通過一個Surface Shader案例來實現(xiàn)頂點變換函數(shù)實現(xiàn)吹氣膨脹效果從而來詳細(xì)的了解一下頂點函數(shù)。
Shader "Custom/BaseForm4" {
Properties {
_MainTex ("Texture", 2D) = "white" {} //2D紋理數(shù)值
_Amount ("Extrusion Amount", Range(0,0.1)) = 0.05 //膨脹系數(shù)數(shù)值
}
SubShader {
CGPROGRAM
#pragma surface surf Lambert vertex:vert //表面著色器編譯指令
struct Input { //Input結(jié)構(gòu)體
float2 uv_MainTex; //uv紋理坐標(biāo)
};
float _Amount; //定義膨脹系數(shù)屬性
sampler2D _MainTex; //定義2D紋理
void vert (inout appdata_base v) { //頂點變換函數(shù)
v.vertex.xyz += v.normal * _Amount; //通過法線擠壓實現(xiàn)充氣的效果
}
void surf (Input IN, inout SurfaceOutput o) { //表面著色器函數(shù)
o.Albedo=tex2D (_MainTex, IN.uv_MainTex).rgb; //從紋理提取顏色為漫反射顏色賦值
}
ENDCG
}
Fallback "Diffuse" //降級著色器
}
5.最終顏色修改函數(shù)
最終顏色修改函數(shù)用于修改表面著色器的最終顏色。這可用于繪制物體表面的最終調(diào)色。使用表面著色器編譯指令finalcolor:<Name>,其中”Name”為最終顏色修改函數(shù)的名稱。最終顏色修改函數(shù)的聲明形式如下。
void <Name> (Input IN, SurfaceOutput o, inout fixed4 color)
其中,Input結(jié)構(gòu)體用于頂點變換函數(shù)為最終顏色修改函數(shù)傳遞數(shù)據(jù),SurfaceOutput結(jié)構(gòu)體用于為最終顏色修改函數(shù)傳輸數(shù)據(jù),inout類型的”color”參數(shù)為最終顏色修改函數(shù)輸出最終顏色。下面通過最終顏色修改函數(shù)實現(xiàn)調(diào)色的表面著色器來詳細(xì)介紹最終顏色修改函數(shù)。
Shader "Custom/BaseForm5" {
Properties {
_MainTex ("Texture", 2D) = "white" {} //2D紋理數(shù)值
_ColorTint ("Tint", Color) = (1.0, 0.6, 0.6, 1.0) //調(diào)色數(shù)值
}
SubShader {
Tags { "RenderType" = "Opaque" }
CGPROGRAM
#pragma surface surf Lambert finalcolor:mycolor //表面著色器編譯指令
struct Input { //Input結(jié)構(gòu)體
float2 uv_MainTex; //uv紋理坐標(biāo)
};
fixed4 _ColorTint; //調(diào)色數(shù)值屬性
sampler2D _MainTex; // 2D紋理屬性
void mycolor(Input IN, SurfaceOutput o, inout fixed4 color){ //最終顏色修改函數(shù)
color *= _ColorTint; //通過調(diào)色數(shù)值修改最終顏色
}
void surf (Input IN, inout SurfaceOutput o) { //表面著色器函數(shù)
o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb; //從紋理提取顏色為漫反射顏色賦值
}
ENDCG
}
Fallback "Diffuse" //降級著色器
}