1、本書的源代碼
https://github.com/candycat1992/Unity_Shaders_Book
2、第1章
Shader是面向GPU的工作方式
3、第2章 渲染流水線
Shader:著色器
渲染流水線:目標是渲染一張二維紋理,輸入是一個虛擬攝像機、一些光源、一些Shader以及紋理等。
渲染的3個階段:
1)應用階段:
由應用主導,由CPU負責實現。
3個任務:
1. 準備好場景數據:攝像機位置、模型、光源。
2.粗粒度剔除工作:把不可見的物體剔除出去。
3.設置好每個模型的渲染狀態,材質(漫反射顏色、高光反射顏色)、使用的紋理、使用的Shader燈。
輸出:渲染所需的幾何信息,即渲染圖元(點、線、三角面等)。
2)幾何階段:
GPU上運行。
任務:把頂點坐標變換到屏幕空間中,再交給光柵器進行處理。
輸出:屏幕空間的二維頂點坐標、每個頂點對應的深度值、著色等相關信息。
3)光柵化階段:
GPU上運行。
使用上個階段傳遞的數據來產生屏幕上的像素,并渲染出最終的圖像。
需要對上一個階段得到的逐頂點數據(例如紋理坐標、頂點顏色等)進行插值,然后再進行逐像素處理。
渲染狀態:定義了場景中的網格是怎樣被渲染的。比如使用什么頂點著色器/片元著色器、光源屬性、材質等。如果沒有更改渲染狀態,那么所有的網格都將使用同一種渲染狀態。
Draw Call:CPU向GPU發送命令開始進行一個渲染過程。當給定了一個Draw Call時,GPU就會根據渲染狀態(例如材質、紋理、著色器等)和所有輸入的頂點數據來進行計算,最終輸出成屏幕上顯示的那些漂亮的像素。
頂點著色器:可編程,實現頂點的空間變換、頂點著色。
曲面細分著色器、幾何著色器:可選的著色器。
裁剪:把不在攝像機視野內的頂點裁剪掉,并剔除某些三角圖元的面片。不可編程,可配置。
屏幕映射:不可編程。把每個圖元的坐標轉換到屏幕坐標系中。
片元著色器:可編程的,用于實現逐片元的著色操作。
逐片元操作:執行很多重要的操作,例如修改顏色、深度緩沖、進行混合等。不可編程,可配置。
頂點著色器的主要任務:坐標變換和逐頂點光照。坐標變換:對頂點的坐標(即位置)進行某種變換,比如通過改變頂點位置來模擬水面、布料等。
一個頂點著色器必須完成的一個工作:把頂點坐標從模擬空間轉換到齊次裁剪空間。
光柵化的2個重要目標:計算每個圖元覆蓋了哪些像素,以及為這些像素計算它們的顏色。
三角形設置:幾何階段輸出三角網格的頂點,即每條邊的兩個端點。如果要得到整個三角網格對像素的覆蓋情況,就必須計算每條邊上的像素坐標。為了能夠計算邊界像素的坐標信息,我們就需要得到三角形邊界的表示方式。
三角形遍歷:檢查每個像素是否被一個三角網格所覆蓋。如果被覆蓋的話,就會生成一個片元。
三角形遍歷階段會根據上一個階段的計算結果來判斷一個三角網格覆蓋了哪些像素,并使用三角網格3個頂點的頂點信息對整個覆蓋區域的像素進行插值,這一步的輸出是得到一個片元序列。
一個片元并不是真正意義上的像素,而是包含了很多狀態的集合,這些狀態用于計算每個像素的最終顏色,這些狀態包括了(但不限于)它的屏幕坐標、深度信息,以及其他從幾何階段輸出的頂點信息,例如法線、紋理坐標等。
片元著色器:輸入是上一個階段對頂點信息插值得到的結果,而它的輸出是一個或者多個顏色值。僅可以影響單個片元。
這里采用的渲染技術是紋理采樣。
逐片元操作:輸出合并階段。任務:
1.決定每個片元的可見性。這涉及了很多測試工作,例如深度測試、模板測試等。
2.如果一個片元通過了所有的測試,就需要把這個片元的顏色值和已經存儲在顏色緩沖區中的顏色進行混合。
模板測試:GPU會首先讀取模板緩沖區中該片元位置的模板值,然后將該值和讀取到的參考值進行比較。
深度測試:GPU會把 該片元的深度值和已經存在于深度緩沖區中的深度值進行比較。例如,只想顯示出離攝像機最近的物體,而那些被其他物體遮擋的就不需要出現在屏幕上。
為什么要合并?渲染過程是一個物體接著一個物體畫到屏幕上的。而每個像素的顏色信息被存儲在一個名為顏色緩沖的地方。當我們執行這次渲染時,顏色緩沖中往往已經有了上次渲染之后的顏色結果。合并就是處理兩個顏色的邏輯。
對于不透明物體,可以關閉混合,這樣片元著色器計算得到的顏色值就會直接進行覆蓋了。對于半透明物體,就需要使用混合操作來讓這個物體看起來是透明的。
OpenGL和DirectX是圖像應用編程接口,這些接口架起了上層應用程序和底層GPU的溝通橋梁。
顯卡的組成:圖像處理單元GPU、顯卡內存稱為顯存。
CPU和GPU的并行工作:使用命令緩沖區,它包含了一個命令隊列,由CPU向其中添加命令,而由GPU從中讀取命令,添加和讀取的過程是互相獨立的。命令緩沖區的命令有很多種類,而Draw Call是其中一種。
Draw Call多了會影響幀率。CPU發送命令有很多準備工作,如果Draw Call的數量太多,CPU就會把大量時間花費在提交Draw Call上,造成CPU的過載。
減少Draw Call的方法:批處理。
CPU在RAM把多個網格合并成一個更大的網格,再發送給GPU,然后在一個Draw Call中渲染它們。但是,使用批處理合并的網格將會使用同一種渲染狀態。所以,批處理適合處理靜態的物體,比如大地、石頭等。
減少Draw Call的開銷:
1)避免使用大量很小的網格。當不可避免地需要使用很小地網格結構時,考慮是否可以合并它們。
2)避免使用過多地材質。盡量在不同地網格之間共用同一個材質。
總結:頂點著色器進行頂點變換以及傳遞數據,片元著色器進行逐像素地渲染。
4、Shader基礎
Shader:渲染流水線中的某些特定階段 ,如頂點著色器階段、片元著色器階段。
材質和Unity Shader搭配流程:
1)創建一個材質
2)創建一個Unity Shader,并把它賦給上一步中創建的材質
3)把材質賦給要渲染的對象
4)在材質面板中調整Unity Shader的屬性,以得到滿意的效果
Unity Shader的4種模板:
1)Standard Surface Shader:包含了標準光照模型的表面著色器模板
2)Unlit Shader:不包含光照的基本的頂點/片元著色器
3)Image Effect Shader:為我們實現各種屏幕后處理效果提供了一個基本模板
4)Compute Shader:利用GPU的并行性來進行一些與常規渲染流水線無關的計算
【ShaderLab】
Unity Shader是Unity為開發者提供的高層級的渲染抽象層,這樣可以更加輕松地控制渲染。
如果沒有使用Unity Shader,需要和很多文件和設置打交道;而在Unity Shader幫助下,開發者只需要使用ShaderLab來編寫Unity Shader文件就可以完成所有的工作。
一個Unity Shader的基礎結構:
Unity會根據使用的平臺來把這些結構編譯成真正的代碼和Shader文件,而開發者只需要和Unity Shader打交道即可。
【材質和Unity Shader的橋梁:Properties】
這些屬性將出現在材質面板中,開發者能夠方便地調整各種材質屬性。
為了在Shader中可以訪問到這些屬性,需要在CG代碼片中定義和這些屬性類型相匹配的變量。
【SubShader】
一個Unity Shader文件可以包含多個SubShader語義塊,最少一個。
當Unity加載這個Unity Shader時,會掃描所有的SubShader語義塊,然后選擇第一個能夠在目標平臺上運行的SubShader。如果都不支持,就會使用Fallback語義指定的Unity Shader。
語義塊:
RenderSetup:狀態
Tags:標簽。
每個Pass定義了一次完整的渲染流程,如果Pass數目過多,往往會造成渲染性能的下降。
常見的渲染狀態[RenderSetup]設置:
當在SubShader塊中設置了上述渲染狀態時,將會應用到所有的Pass。也可以在Pass語義塊中單獨進行上面的設置。
【SubShader標簽】
是一個鍵值對,都是字符串類型。它們是SubShader和渲染引擎之間的溝通橋梁。它們用來告訴Unity引擎:SubShader我希望怎樣以及何時渲染這個對象。
標簽結構:
SubShader支持的標簽類型:
上述標簽僅可以在SubShader中聲明,而不可以在Pass塊中聲明。Pass塊中的標簽不同于SubShader的標簽類型。
【Pass語義塊】
1)Name說明
Name為該Pass的名稱,比如:Name "MyPassName",通過這個名稱,可以使用ShaderLab的UsePass命令來直接使用其他Unity Shader中的Pass,比如:
UsePass "MyShader/MYPASSNAME"
這樣可以提高代碼的復用性。由于Unity內部會把所有Pass的名稱轉換成大寫字母的表示,因此在使用UsePass命令時必須使用大寫形式的名字。
2)RenderSetup設置渲染狀態
SubShader的狀態設置同樣適用于Pass。
3)Tags標簽
與SubShader的標簽不同。
4)特殊的Pass
- UsePass:使用該命令來復用其他Unity Shader中的Pass
- GrabPass:抓取屏幕并將結果存儲在一張紋理中,以用于后續的Pass處理。
【Fallback】
作用:告訴Unity,如果上面所有的SubShader在這塊顯卡上都不運行,那么就使用這個最低級的Shader吧。
可以通過一個字符串來告訴Unity這個最低級的Unity Shader是誰,也可以任性地關閉Fallback功能。
例子:
Fallback影響陰影投射:在渲染陰影紋理時,Unity會在每個Unity Shader中尋找一個陰影投射地Pass。通常情況下,我們不需要專門實現一個Pass,因為Fallback使用的內置Shader中包含了這樣一個通用的Pass。
【Unity Shader形式】
【表面著色器Surface Shader】
表面著色器是Unity對頂點/片元著色器的更高一層的抽象。它存在的價值在于,Unity為我們處理了很多光照細節,使得我們不需要再操心這些事情。
例子:
表面著色器被定義在SubShader語義塊(而非Pass語義塊)中的CGPROGRAM和ENDCG之間。
表面著色器不需要開發者關心使用多少個Pass、每個Pass如何渲染等問題,Unity會在背后為我們做好這些事情。
好比告訴Unity:使用這些紋理去填充顏色,使用這個法線紋理去填充法線,使用Lambert光照模型,其他的不要來煩我。
【頂點/片元著色器Vertex/Fragment Shader】
例子:
頂點/片元著色器的代碼也定義在CGPROGRAM和ENDCG之間,但是是寫在Pass語義塊內,而非SubShader內的。因為我們需要自己定義每個Pass需要使用的Shader代碼。
5、數學基礎
【笛卡爾坐標系】
xyz軸互相垂直,這些的基矢量被稱為正交基 。
正交:相互垂直的意思。
【矢量加法的三角形定則】
【矢量的模】
【矢量的點積公式一】
幾何意義就是投影。
【矢量的點積公式二】
【矢量的叉積】
會得到一個同時垂直于這兩個矢量的新矢量。
【矩陣】
一個矩陣可以把一個矢量從一個坐標空間轉換到另一個坐標空間。
矩陣乘法的性值:
1)不滿足交換律:
2)滿足結合律:
矩陣串接的轉置:
【逆矩陣】
不是所有的矩陣都有逆矩陣,第一個前提就是,該矩陣必須是一個方陣。
如果一個矩陣有對應的逆矩陣,我們就說這個矩陣是可逆的,或者說是非奇異的。
一個矩陣的行列式不為0,那么它就是可逆的。
逆矩陣的性值:
1)逆矩陣的逆矩陣是原矩陣。
2)單位矩陣的逆矩陣是它本身
3)轉置矩陣的逆矩陣是逆矩陣的轉置。
4)矩陣串接相乘后的逆矩陣等于反向串接各個矩陣的逆矩陣。
一個矩陣可以表示一個變換,而逆矩陣允許我們還原這個變換。
【正交矩陣】
一個方陣M和它的轉置矩陣的乘積是單位矩陣的話,這個矩陣就是正交的。
如果一個矩陣是正交的,那么它的轉置矩陣和逆矩陣是一樣的。
Unity中,常規做法是把矢量放在矩陣的右側,即把矢量轉換成列矩陣來進行運算。
使用列向量的結果是,我們的閱讀順序是從右到左,即先對v使用A進行變換,再使用B進行變換,最后使用C進行變換。
【線性變換】
保留矢量加和標量乘的變換。
主要幾何變換:旋轉、縮放、錯切、鏡像、正交投影。
平移變換f(x)= x + (1,2,3)不滿足f(x)+f(x)=f(x+x),因此不能用一個3*3矩陣來表示一個平移變換。
【仿射變換】
合并線性變換和平移變換的變換類型,可以用一個4*4的矩陣來表示,此時可以表示平移、旋轉和縮放。
需要把矢量擴展到思維空間下,這就是齊次坐標空間。
【基礎變換矩陣】
M3*3用于表示旋轉和縮放,t3*1用于表示平移,01*3是零矩陣,右下角的元素就是標量1。
1)平移變換
2)縮放變換
3)旋轉變換
以下特指繞著x\y\z軸進行變換。
4)復合變換
把平移、旋轉和縮放組合起來形成一個復雜的變換過程。
變換公式是:
約定的變換順序:先縮放、再旋轉、最后平移。
【坐標空間變換】
要想定義一個坐標空間,必須指明其原點位置和3個坐標軸的方向。
坐標空間會形成一個層次結構:每個坐標空間都是另一個坐標空間的子空間,反過來說,每個空間都有一個父坐標空間。對坐標空間的變換實際上就是在父空間和子空間之間對點和矢量進行變換。
變換矩陣可以通過坐標空間C在坐標空間P中的原點和坐標軸的矢量表示來構建出來:把3個坐標軸一次放入矩陣的前3列,把原點矢量放到最后一列,再用0和1填充最后一行即可。
因為矢量是沒有位置的,所以坐標空間的原點變換是可以忽略的,即我們僅僅平移坐標系的原點是不會對矢量造成任何影響的,所以變換矩陣可以優化為:
通過求解Mc->p的逆矩陣的方式求解出反向變換Mp->c。
如果Mc->p是一個正交矩陣,那么Mc->p的逆矩陣就等于它的轉置矩陣。
如果我們知道坐標空間變換矩陣MA->B是一個正交矩陣,那么我們可以提取它的第一列來得到坐標空間A的x軸在坐標空間B下的表示,還可以提取它的第一行來得到坐標空間B的x軸在坐標空間A下的表示。
反過來,如果我們知道坐標空間B的x軸、y軸和z軸(必須是單位矢量,否則構建出來的就不是正交矩陣了)在坐標空間A下的表示,就可以把它們依次放在矩陣的每一行就可以得到從A到B的變換矩陣了。
【模型空間】
也被稱為局部空間。
每個模型都有自己獨立的坐標空間,當它移動或旋轉的時候,模型空間也會跟著它移動和旋轉。把我們當成游戲中的模型的話,當我們在辦公室里移動時,我們的模型空間也在跟著移動,當我們轉身時,我們本身的前后左右方向也在跟著改變。
【世界坐標】
它時一個特殊的坐標系,因為它建立了我們所關心的最大的空間。
如果一個Transform沒有任何父節點,那么這個位置就是在世界坐標系中的位置。
頂點變換的第一步,就是將頂點坐標從模型空間變換到世界空間中,這個變換叫做模型變換。
在世界空間中,模型進行了(2,2,2)的縮放,又進行了(0,150,0)的旋轉以及(5,0,25)的平移。這里的變換順序是不能互換的,即先進行縮放,再進行旋轉,最后是平移。
第2個矩陣是y軸的旋轉矩陣。
【觀察空間view space】
也被稱為攝像機空間。
觀察空間和屏幕空間是不同的。觀察空間是一個三維空間,而屏幕空間是一個二維空間。從觀察空間到屏幕空間的轉換需要經過投影操作。
頂點變換的第二步,就是將頂點坐標從世界空間變換到觀察空間中,這個變換叫做觀察變換。
攝像機在世界坐標中所作的變換。
求變換矩陣,一種方式是構建從觀察空間到世界空間的變換矩陣 然后再求逆。另一種方法是讓攝像機原點位于世界坐標的原點,坐標軸與世界空間中的坐標軸重合即可。
攝像機在世界空間中的變換是按(30,0,0)進行旋轉,然后按(0,10,-10)進行平移。那么為了把攝像機重新移回到初始狀態,我們需要進行逆向變換,即先按(0,-10,10)平移,再按(-30,0,0)進行旋轉,以便讓坐標軸重合。
由于觀察看空間使用的右手坐標系,世界空間是左手坐標系,因此需要對z分量進行取反操作。
【裁剪空間】
頂點接下來要從觀察空間轉換到裁剪空間(也被稱為齊次裁剪空間)。用于變換的矩陣叫做裁剪矩陣,也被稱為投影矩陣。
裁剪空間的目標是能夠方便地對渲染圖元進行裁剪:完全位于這塊空間內部的圖元將會被保留,完全位于這塊空間外部的圖元將會被剔除,而與這塊空間邊界相交的圖元就會被裁剪。那么這塊空間如何決定的呢?是由視錐體決定的。
視錐體是空間中的一塊區域,這塊區域決定了攝像機可以看到的空間。視錐體由6個平面包圍而成,這些平面被稱為裁剪平面。視錐體有兩種類型,這涉及兩種投影類型:正交投影和透視投影。
在透視投影中網格是近大遠小,地板上的平行線不會保持平行。
在正交投影中,所有網格大小都一樣,而且平行線一直保持平行。
透視投影模擬了人眼看到世界的方式,而正交投影則完全保留了物體的距離和角度。
視錐體的6個面:
通過Camera組件的Field of View(FOV)屬性來改變視錐體豎直方向的張開角度,而Clipping Planes中的Near和Far參數可以控制視錐體的近裁剪平面和遠裁剪平面距離攝像機的遠近,這樣可以求出視錐體近裁剪平面和遠裁剪平面的高度。橫向的信息可以通過攝像機的橫縱比得到。
現在可以根據已知的Near、Far、FOV和Aspect的值來確定透視投影的投影矩陣:
【屏幕空間】
經過投影矩陣的變換后,可以進行裁剪操作了。當完成了所有的裁剪工作后,就需要進行真正的投影了,需要把視錐體投影到屏幕空間。經過這一步變換,就會得到真正的像素位置,而不是虛擬的三維坐標。
屏幕空間是二維空間,因此必須把頂點從裁剪空間投影到屏幕空間中,來生成對應的2D坐標。
在Unity中,從裁剪空間到屏幕空間的轉換是由Unity幫我們完成的,頂點著色器只需要把頂點轉換到裁剪空間即可。
【頂點的空間變換過程】
頂點著色器的最基本的任務就是把頂點坐標從模擬空間轉換到裁剪空間中。
在片元著色器中,可以得到該片元在屏幕空間的像素位置。
Unity中各個坐標空間的旋向性:
【法線】
模型的一個頂點往往會攜帶額外的信息,而頂點法線就是其中一種信息。
當我們變換一個模型的時候,不僅需要變換它的頂點,還需要變換頂點法線,以便在后續處理中計算光照等。
一般來說,點和絕大部分方向矢量都可以使用同一個4*4或3*3的變矩陣MA->B把其從坐標空間A變換到坐標空間B中。但在變換法線的時候,如果使用同一個變換矩陣,可能就無法確保維持法線的垂直性。
【切線】
切線往往也是模型頂點攜帶的一種信息,它通常與紋理空間對齊,而且與法線方向垂直。
由于切線是由兩個頂點之間的差值計算得到的,因此可以直接使用用于變換頂點的變換矩陣來變換切線。
其中TA/TB分別表示在坐標空間A和B下的切線方向。
如果直接使用MA->B來變換切線,得到的新法線方向可能就不會與表面垂直了。
如果變換只包含旋轉變換,那么這個變換矩陣就是正交矩陣。
【齊次坐標】
點坐標:就是3D世界里的門牌號(X,Y,Z)
空間轉換:把方向從一個參考系換到另一個
齊次坐標:給點坐標加一個W=1(給方向加w=0),讓一個4*4變換矩陣能同時搞定移動、旋轉、縮放。平時使用的Vector3,Unity在背后默默都用Vector4和4*4矩陣算好了所有變換。
【內置轉換矩陣】
UNITY_MATRIX_MVP:當前的模型-觀察-投影矩陣,用于將頂點/方向矢量從模型空間變換到裁剪空間
UNITY_MATRIX_MV:當前的模型-觀察矩陣,用于將頂點/方向矢量從模型空間變換到觀察空間
UNITY_MATRIX_V:當前的觀察矩陣,用于將頂點/方向矢量從世界空間變換到觀察空間
UNITY_MATRIX_P:當前的投影矩陣,用于將頂點/方向矢量從觀察空間變換到裁剪空間
UNITY_MATRIX_VP:當前的觀察-投影矩陣,用于將頂點/方向矢量從世界空間變換到裁剪空間
UNITY_MATRIX_T_MV:UNITY_MATRIX_MV 的轉置矩陣
UNITY_MATRIX_IT_MV:UNITY_MATRIX_MV的逆轉置矩陣,用于將法線從模擬空間變換到觀察空間,也可用于得到UNITY_MATRIX_MV的逆矩陣
_Object2World:當前的模型矩陣,用于將頂點/方向矢量從模型空間變換到世界空間
_World2Object:_Object2World的逆矩陣,用于將頂點/方向矢量從世界空間變換到模型空間
6、Shader基礎編碼
(1)創建Shader使用案例
Assets -> Create -> Shader -> Standard Surface Shader
創建完放到Assets -> Shaders目錄下,命名為SimpleShader2。
創建材質:Assets -> Create -> Material,
創建完放到Assets -> Material目錄下,命名為SimpleShaderMat。
在Material上應用Shader:
在SimpleShaderMat的Inspector的Shader中選擇Custom -> SimpleShader2(剛創建的Shader)。
點擊Open重寫Shader的代碼
代碼如下:
Shader "Custom/SimpleShader2"
{SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag float4 vert(float4 v: POSITION) : SV_POSITION{return mul(UNITY_MATRIX_MVP, v);}float4 frag() : SV_Target{return fixed4(1.0, 1.0, 1.0, 1.0);}ENDCG}}
}
得到的效果如下:
代碼解釋:
1)Properties語義并不是必需的,所以可以選擇不聲明任何材質屬性。
2)CG代碼片段在CGPROGRAM和ENDCG之間
3)以下格式指明了頂點著色器和片段著色器的函數
#pragma vertex name
#pragma fragment name
name是我們指定的函數名,名字不一定是vert和frag。
4)vert(float4 v:POSITION):SV_POSITION是頂點著色器代碼,它是逐頂點執行的。
vert函數的輸入v包含了這個頂點的位置,這是通過POSITION語義指定的。它的返回值是一個float4類型的變量,它是該頂點在裁剪空間中的位置,POSITION和SV_POSITION都是CG/HLSL中的語義,它們是不可省略的,這些語義將告訴系統用戶需要哪些輸入值,以及用戶的輸出是什么。例如:POSITION將告訴Unity,把模型的頂點坐標填充到輸入參數中,SV_POSITION將告訴Unity,頂點著色器的輸出是裁剪空間中的頂點坐標。
在上面的vert函數中,就是把頂點坐標從模型空間轉換到裁剪空間中。
在frag函數中沒有任何輸入。它的輸出是一個fixed4類型的變量,并且使用了SV_Target語義進行限定。SV_Target也是HLSL中的一個系統語義,它等同于告訴渲染器,把用戶的輸出顏色存儲到一個渲染目標(render target)中,這里將輸出到默認的幀緩存中。
(2)更復雜案例
得到模型上每個頂點的紋理坐標和法線方向。
使用紋理坐標來訪問紋理,而法線可用于計算光照。
代碼:
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'Shader "Custom/SimpleShader2"
{SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag // 使用一個結構體來定義頂點著色器的輸入struct a2v{// POSITION語義告訴Unity,用模型空間的頂點坐標填充vertex變量float4 vertex: POSITION;// NORMAL語義告訴Unity,用模型空間的法線方向填充normal變量float3 normal: NORMAL;// TEXCOORD0語義告訴Unity,用模型的第一套紋理填充texcoord變量float4 texcoord: TEXCOORD0;};float4 vert(a2v v) : SV_POSITION{return UnityObjectToClipPos(v.vertex);}float4 frag() : SV_Target{return fixed4(1.0, 1.0, 1.0, 1.0);}ENDCG}}
}
在上面的代碼中,聲明了一個新的結構體a2v,包含了頂點著色器需要的模型數據。
對于頂點著色器的輸出,Unity支持的語義有:
POSITION,TANGENT,NORMAL,TEXCOORD0,TEXCOORD1,TEXCOORD2,TEXCOORD3,COLOR等。
定義自定義結構體的格式:
struct STRUCTName{Type Name : Semantic;Type Name : Semantic;......
};
Semantic語義部分是不可以被省略的。
填充到POSITION,TANGENT,NORMAL這些語義中的數據是從哪里來的呢?在Unity中,它們是由使用該材質的Mesh Render組件提供的。在每幀調用Draw Call的時候,Mesh Render組件會把它負責渲染的模型數據發送給Unity Shader。一個模型通常包含了一組三角面片,每個三角面片由3個頂點構成,而每個頂點又包含了一些數據,例如頂點位置、法線、切線、紋理坐標、頂點顏色等。通過上面的方法,我們就可以在頂點著色器中訪問頂點的這些模型數據。
【頂點著色器和片元著色器之間的通信】
Shader "Custom/SimpleShader2"
{SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag // 使用一個結構體來定義頂點著色器的輸入struct a2v{// POSITION語義告訴Unity,用模型空間的頂點坐標填充vertex變量float4 vertex: POSITION;// NORMAL語義告訴Unity,用模型空間的法線方向填充normal變量float3 normal: NORMAL;// TEXCOORD0語義告訴Unity,用模型的第一套紋理填充texcoord變量float4 texcoord: TEXCOORD0;};// 使用一個結構體來定義頂點著色器的輸出struct v2f{// SV_POSITION語義告訴Unity,pos里包含了頂點在裁剪空間中的位置信息float4 pos: SV_POSITION;// Color語義可以用于存儲顏色信息fixed3 color: COLOR0;};v2f vert(a2v v) {// 聲明輸出結構v2f o;o.pos = UnityObjectToClipPos(v.vertex);// v.normal包含了頂點的法線方向,其分量范圍在[-1.0, 1.0]// 下面的代碼把分量范圍映射到了[0.0, 1.0]// 存儲到o.color中傳遞給片元著色器o.color = v.normal * 0.5 + fixed3(0.5, 0.5, 0.5);return o;}fixed4 frag(v2f i) : SV_Target{return fixed4(i.color, 1.0);}ENDCG}}
}
v2f用于在頂點著色器和片元著色器之間傳遞信息。
得到的效果:
頂點著色器的輸出結構中,必須包含一個變量,它的語義是SV_POSITION。否則,渲染器將無法得到裁剪空間中的頂點坐標,也就無法把頂點渲染到屏幕上。
片元著色器中的輸入實際上是把頂點著色器的輸出進行插值后得到的結果。
【使用屬性】
屬性是材質和Shader溝通的參數。
材質提供給我們一個可以方便地調節Unity Shader中參數地方式,通過這些參數,我們可以隨時調整材質的效果。而這些參數就需要寫在Properties語義塊中。
新的代碼:
Shader "Custom/SimpleShader2"
{Properties{// 聲明一個Color類型的屬性_Color("Color Tint", Color) = (1.0, 1.0, 1.0, 1.0)}SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag // 在CG代碼中,我們需要定義一個與屬性名稱和類型都匹配的變量fixed4 _Color;// 使用一個結構體來定義頂點著色器的輸入struct a2v{// POSITION語義告訴Unity,用模型空間的頂點坐標填充vertex變量float4 vertex: POSITION;// NORMAL語義告訴Unity,用模型空間的法線方向填充normal變量float3 normal: NORMAL;// TEXCOORD0語義告訴Unity,用模型的第一套紋理填充texcoord變量float4 texcoord: TEXCOORD0;};// 使用一個結構體來定義頂點著色器的輸出struct v2f{// SV_POSITION語義告訴Unity,pos里包含了頂點在裁剪空間中的位置信息float4 pos: SV_POSITION;// Color語義可以用于存儲顏色信息fixed3 color: COLOR0;};v2f vert(a2v v) {// 聲明輸出結構v2f o;o.pos = UnityObjectToClipPos(v.vertex);// v.normal包含了頂點的法線方向,其分量范圍在[-1.0, 1.0]// 下面的代碼把分量范圍映射到了[0.0, 1.0]// 存儲到o.color中傳遞給片元著色器o.color = v.normal * 0.5 + fixed3(0.5, 0.5, 0.5);return o;}fixed4 frag(v2f i) : SV_Target{fixed3 c = i.color;// 使用_Color屬性來控制輸出顏色c *= _Color.rgb;return fixed4(c, 1.0);}ENDCG}}
}
(3)內置文件與變量
文件后綴 .cginc
在編寫Shader時,可以使用#include指令把這些文件包含進來,這樣就可以使用Unity為我們提供的一些非常有用的變量和幫助函數。
#CGPROGRAM
// ...
#include "UnityCG.cginc"
// ...
ENDCG
windows上的文件位于:D:\programs\unity_editor\2022.3.42f1c1\Editor\Data\CGIncludes
- UnityCG.cginc:包含了最常使用的幫助函數、宏和結構體等
- UnityShaderVariables.cginc:在編譯Unity Shader時,會被自動包含進來。包含了許多內置的全局變量,如UNITY_MATRIX_MVP等
- Lighting.cginc:包含了各種內置的光照模型,如果編寫的是Surface Shader的話,會自動包含進來
- HLSLSupport.cginc:在編譯Unity Shader時,會被自動包含進來。聲明了很多跨平臺編譯的宏和定義。
(4)CG/HLSL語義
頂點著色器、片元著色器的輸入輸出變量后的一個冒號和一個全部大寫的名稱,比如SV_POSITION、POSITION、COLOR0等。
SV開頭:system-value semantics,SV代表的含義就是系統數值。這些語義在渲染流水線中有特殊的含義。比如:使用SV_POSITION語義去修飾頂點著色器的輸出變量pos,那么就表示pos包含了可用于光柵化的變換后的頂點坐標(即齊次裁剪空間中的坐標)。用這些語義描述的變量是不可以隨便賦值的,因為流水線需要使用它們來完成特定的目的,例如渲染引擎會把用SV_POSITION修飾的變量經過光柵化后顯示在屏幕上。
(5)Unity支持的常用語義
1)從應用階段傳遞模型數據給頂點著色器時Unity支持的常用語義
- POSITION:模型空間中的頂點位置,通常時float4類型
- NORMAL:頂點法線,通常是float3類型
- TANGENT:頂點切線,通常是float4類型
- TEXCOORDn:如TEXCOORD0、TEXCOORD1,該頂點的紋理坐標,TEXCOORD0表示第一組紋理坐標,依次類推,通常是float2或float4類型
- COLOR:頂點顏色,通常是fixed4或float4類型
2)從頂點著色器傳遞數據給片元著色器時Unity使用的常用語義
- SV_POSITION:裁剪空間中的頂點坐標,結構體中必須包含一個用該語義修飾的變量
- COLOR0:通常用于輸出第一組頂點顏色,但不是必需的
- COLOR1:通常用于輸出第二組頂點顏色,但不是必需的
- TEXCOORD0~TEXCOORD7:通常用于輸出紋理坐標,但不是必需的
除了SV_POSITION是有特別含義外,其他語義對變量的含義沒有明確要求,也就是說,我們可以存儲任意值到這些語義描述變量中。
通常,如果我們需要把一些自定義的數據從頂點著色器傳遞給片元著色器,一般選用TEXCOORD0等。
3)片元著色器輸出時Unity支持的常用語義
SV_Target:輸出值將會存儲到渲染目標(render target)中。
(6)Debug
【假彩色圖像方法】
使用假彩色圖像:把需要調試的變量映射到[0,1]之間,把它們作為顏色輸出到屏幕上,然后通過屏幕上顯示的像素顏色來判斷這個值是否正確。
如果要調試的數據是一個一維數據,那么可以選擇一個單獨的顏色分量(如R分量)進行輸出,而把其他顏色分量置為0。如果是多維數據,可以選擇對它的每一個分量單獨調試,或者選擇多個顏色分量進行輸出。
參考代碼:
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'Shader "Custom/FalseShader"
{SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct v2f{float4 pos: SV_POSITION;fixed4 color: COLOR0;};v2f vert(appdata_full v){v2f o;o.pos = UnityObjectToClipPos(v.vertex);// 可視化法線方向o.color = fixed4(v.normal * 0.5 + fixed3(0.5, 0.5, 0.5), 1.0);// 可視化切線方向o.color = fixed4(v.tangent * 0.5 + fixed3(0.5, 0.5, 0.5), 1.0);// 可視化副切線方向fixed3 binormal = cross(v.normal, v.tangent.xyz) * v.tangent.w;o.color = fixed4(binormal * 0.5 + fixed3(0.5, 0.5, 0.5), 1.0);// 可視化第一組紋理坐標o.color = fixed4(v.texcoord.xy, 0.0, 1.0);// 可視化第二組紋理坐標o.color = fixed4(v.texcoord1.xy, 0.0, 1.0);// 可視化第一組紋理坐標的小數部分o.color = frac(v.texcoord);if(any(saturate(v.texcoord) - v.texcoord)){o.color.b = 0.5;}o.color.a = 1.0; // 可視化第二組紋理坐標的小數部分o.color = frac(v.texcoord1);if(any(saturate(v.texcoord1) - v.texcoord1)){o.color.b = 0.5;}o.color.a = 1.0;return o;}fixed4 frag(v2f i): SV_Target{return i.color;}ENDCG}}
}
【幀調試器】
位于Windows ->?Analysis ->?Frame Debugger。
(7)渲染紋理的坐標差異
(8)Shader整潔之道
1)CG/HLSL中3種精度的數值類型
float:最高精度的浮點值
half:中等精度的浮點值
fixed:最低精度的浮點值
桌面GPU會把所有計算都按最高的浮點精度進行計算,也就是說,float、half、fixed在這些平臺上實際是等價的。
但在移動平臺的GPU上,它們的確會有不同的精度范圍,而且不同精度的浮點值的運算速度也會有所差異。
盡可能使用精度較低的類型,因為這可以優化Shader的性能,這一點在移動平臺上尤其重要。
2)Shader Model
由微軟提出的一套規范,它們決定了Shader種各個特性的能力。這些特性和能力體現在Shader能使用的運算指令數目、寄存器個數等各個方面。
Shader Model等級越高,Shader的能力就越大。
雖然更高等級的Shader Target可以讓我們使用更多的臨時寄存器和運算指令,但一個更好的方法是盡可能減少Shader中的運算,或者通過預計算的方式來提供更多的數據。