1. 程式人生 > >Unity Shader入門精要學習筆記

Unity Shader入門精要學習筆記

摘錄自 馮樂樂的《Unity Shader入門精要》

笛卡爾座標系

1)二維笛卡爾座標系

在遊戲製作中,我們使用的數學絕大部分都是計算位置、距離、角度等變數。而這些計算大部分都是在笛卡爾座標系下進行的。

一個二維的笛卡爾座標系包含了兩個部分的資訊:

一個特殊的位置,即原點,它是整個座標系的中心。

兩條過原點的互相垂直的向量,即X軸和Y軸。這些座標軸也被稱為是該座標的向量。

OpenGL 和 DirectX 使用了不同的二維笛卡爾座標系。如下圖所示:

2)三維笛卡爾座標系

在三維笛卡爾座標系中,我們需要定義三個座標軸和一個原點。如下圖所示:

這三個座標軸也被稱為是該座標軸的基向量

。通常情況下,這個三個座標軸之間是相互垂直的,且長度為1,這樣的的基向量被稱為標準正交基,但這並不是必須的。例如,在一些座標系中座標軸之間相互垂直但長度不為1,這樣的基向量被稱為正交基。如非特殊說明,後續預設使用的都是標準正交基。

正交的意思是相互垂直。

三維座標系可以大致分為左手座標系和右手座標系。

三維座標系並不都是等價的。因為就出現了不同的三維座標系:左手座標系和右手座標系。如果兩個座標系具有相同的旋向性,那麼我們就可以通過旋轉的方法來讓它們的座標軸指向重合。但是如果它們具有不同的旋向性,那麼就無法達到重合的目的。下圖分別為左手座標系和右手座標系。

對於一個需要視覺化虛擬的三維世界的應用(如Unity)來說,它的設計者就要進行一個選擇。對於模型空間和世界空間,Unity使用的是左手座標系。

但對於觀察空間來說,Unity使用的是右手座標系。觀察空間,通俗來講就是以攝像機為原點的座標系。在這個座標系中,攝像機的前向是z軸的負方向,這與在模型空間和世界空間中的定義相反。也就是說,z軸座標的減少意味著場景深度的增加。

點和向量

點是n維空間中的一個位置,它沒有大小和寬度這類概念。在笛卡爾座標系中,我們可以使用2個或3個實數來表示一個點的座標。

向量的定義則複雜一些。向量存在的意義更多是為了和標量區分開來。通常的講,向量是指n維空間中一種包含了模和方向的有向線段,我們通常講到的速度就是一種典型的向量。

具體來講。

向量的模指的是這個向量的長度,一個向量的長度可以是任意的非負數。

向量的方向則描述了這個向量在空間中的指向。

下圖簡單描述了點和向量之間的 關係。

1)向量和標量的乘法/除法

只能是向量被標量除,而不能是標量被向量除。

2)

向量的加減法

需要注意的是,一個向量不能和一個標量相加或相減。向量的加減法遵守三角法則。

3)向量的模

4)單位向量

單位向量指的是那些模為1 的向量。也被稱為歸一化向量。

5)向量的點積

向量的乘法有兩種最常用的種類:點積(內積)和叉積(外積)

公式一

點積滿足交換律。

性質一:點積可結合標量乘法

性質二:點積可結合向量加法和減法,和性質一類似。

性質三:一個向量和本身進行點積的結果,是該向量的模的平方。

公式二

 

6)向量的叉積

叉積不滿足交換律,也不滿足結合律。

矩陣

1)基礎概念

矩陣是由m*n個標量組成的長方形陣列。

矩陣由行列之分。如下是一個3*4矩陣。

M(i,j) 表明了這個元素在矩陣M的第i行,第j列。

向量可以看成n*1的列矩陣或1*n的行矩陣

2)基礎運算

矩陣與標量的乘法。

矩陣與矩陣的乘法,它們的結果會是一個新的矩陣,並且這個矩陣的維度和兩個原矩陣的維度都有關係。

一個 r*n 的矩陣A和一個n*c 的矩陣B相乘,它們的結果AB將會是一個 r*c 大小的矩陣。

第一個矩陣的 列數必須和第二個矩陣的行數相同,它們相乘得到的矩陣行數是第一個矩陣的行數,而列數是第二個矩陣的列數。

如果不滿足規定,就不能相乘。

性質一:矩陣乘法並不滿足交換律。

性質二:矩陣乘法滿足結合律

3)特殊矩陣。

方塊矩陣(方陣),是指行和列數目相等的矩陣。

如果一個方陣除了對角元素之外的所有元素都為0,那麼這個矩陣就叫做對角矩陣。如下圖所示:

單位矩陣:對角矩陣中對角元素值都為1。任何矩陣和它相乘結果還是原來的矩陣。

轉置矩陣:實際上是對原矩陣的一種運算,即轉置運算。轉置矩陣的計算非常簡單,只需要將原矩陣翻轉一下即可。原矩陣的第i行變成了第i列。而第j列變成了第j行。

如下所示。

性質一:矩陣轉置的轉置等於原矩陣。

性質二:矩陣串聯的轉置,等於反向串接各個矩陣的轉置。

逆矩陣

並不是所有的矩陣都有逆矩陣,第一個前提改矩陣是一個方陣。

逆矩陣的性質:該矩陣和逆矩陣相乘,得到一個單位矩陣。即

如果一個矩陣由對應的逆矩陣,我們就說這個矩陣是可逆的,或者說是非奇異的。

如何判斷一個矩陣是否可逆呢?簡單來說,如果一個矩陣的行列式不為0,那麼它就是可逆的。

性質一:逆矩陣的逆矩陣是原矩陣本身。即

性質二:單位矩陣的逆矩陣是它本身。即

性質三:轉置矩陣的逆矩陣是逆矩陣的轉置,即

性質四:矩陣串接相乘後的逆矩陣等於反向串接各個矩陣的逆矩陣,即

如果一個方陣和它的轉置矩陣的乘積是單位矩陣的話,我們就說這個矩陣是正交的。反過來也成立。

在Unity中,常規做法是把向量放在矩陣的右側,即把向量轉換為列矩陣來進行運算。

變換

變換指的是我們把一些資料,如點,方向向量甚至是顏色等,通過某種方式進行轉換的過程。

最常見的是線性變換。線性變換指的是那些可以保留向量加和標量乘的變換。如下:

類似縮放和旋轉都是一個線性變換。還有錯切,映象,正交投影等,都是線性變換。

平移變換滿足標量相乘,但是不滿足向量加法。

仿射變換是合併線性變換和平移變換的變換型別。仿射變換可以使用一個4*4的矩陣來表示,這就是齊次座標空間。

下表給出了圖形學常見變換矩陣的名稱和它們的特性。

我們知道,由於3*3矩陣不能表示平移操作,我們就把其擴充套件到了4*4的矩陣。為此,我們還需要把原來的三維向量轉換成四維向量,也就是齊次座標。

對於一個點,轉換為齊次座標就是把其w分量設為1.對於方向向量來說,需要把其w分量設為0.這樣的設定就會導致,當用一個4*4矩陣對一點進行變換時,平移、旋轉、縮放都會施加於該點。但是如果是用於變換一個方向向量,平移的效果就會被忽略。

我們已經知道,可以使用一個4*4的矩陣來表示平移、旋轉和縮放。我們把表示純平移、純旋轉、純縮放的變換矩陣叫做基礎變換矩陣、這些矩陣具有一些共同點,我們可以把一個基礎變換矩陣分解成4個組成部分:

其中左上角的矩陣M(3*3)用於表示旋轉和縮放,右上角的t(3*1)表示平移,左下角的 0(1*3) 是零矩陣,右下角的元素是標量1。

我們可以使用矩陣乘法來表示對一個點進行平移變換:

從結果來看我們可以很容易看出為什麼這個矩陣有平移效果,點的x,y,z分量分別增加了一個位置平移。

有趣的是,如果我們隊一個方向向量進行平移變換,結果如下:

可以發現,平移變換不會對方向向量產生任何影響。

平移矩陣的逆矩陣就是反向平移得到的矩陣,即

可以看出,平移矩陣並不是一個正交矩陣。

我們可以對一個模型沿空間的x軸、y軸和z軸進行縮放。同樣,我們可以使用矩陣乘法來表示一個縮放變換。

對方向向量可以使用同樣的矩陣進行縮放。

如果三個縮放係數相等,我們把這樣的縮放稱為統一縮放,否則為非統一縮放。

縮放矩陣的逆矩陣是使用原縮放係數的倒數來對點或方向向量進行縮放。即

縮放矩陣一般不是正交矩陣。

旋轉是三種常見的變換矩陣中最複雜的一種。

如果我們需要把點繞著x軸旋轉θ度,可以使用下面的矩陣:

y軸的可以使用如下矩陣:

z軸的:

旋轉矩陣的逆矩陣是旋轉相反角度得到的交換矩陣。旋轉矩陣是正交矩陣,而且多個旋轉矩陣之間的串聯同樣是正交的。

我們可以把平移、旋轉和縮放組合起來,來形成一個複雜的變換過程。例如,可以對一個模型先進行大小為(2,2,2)的縮放,再繞y軸旋轉30度,最後向z軸平移4個單位,複合變換可以通過矩陣的串聯來實現。上面的變換可以使用下面的公式進行計算。

在絕大多數情況下,我們約定的變換的順序就是先縮放,再旋轉,最後平移。

座標空間

我們知道,要想定義一個座標空間,必須指明其原點位置和3個座標軸的方向。而這些數值實際上是相對於另一個座標空間的。也就是說,座標空間會形成一個層次結構——每個座標空間都是另一個座標空間的子空間,反過來說,每個空間都有一個父座標空間。對座標空間的變換實際上就是在父空間和子空間之間對點和向量進行變換。

假設。現有父座標空間P以及一個子座標空間C。我們知道在父座標空間中子座標空間的原點位置以及3個單位座標軸。我們一般會有兩種需求:一種需求是把子座標空間下表示的點或向量轉換到父座標空間下。另一個修是反過來,即把福座標空間下表示的點或向量轉換到子座標空間下。我們可以使用下面的公式來表示這兩種需求。

其中,表示的是從子座標空間變換到父座標空間的變換矩陣,而是其逆矩陣。式子如下:

變換為矩陣得到:

其中“|”符號表示是按列展開的。上面的式子實際上就是使用了我們之前所學的公式。但這個最後的表示式還不夠漂亮,因為還存在加法表示式,即平移變換,我們把上面的式子擴充套件到齊次座標空間中,得

所以的矩陣就是

一旦求出來就可以通過求逆矩陣的方式求出來,因為從座標空間C變換到座標空間P 與 從座標空間P變換到座標空間C是互逆的兩個過程。

可以看出來,變換矩陣實際上可以通過座標空間C在座標空間P的原點和座標軸的矢量表示來構建出來:把3ge座標軸一次放入矩陣的前3列,把原點向量放到最後一列,再用0和1填充最後一行即可。

我們可以利用反向思維,從這個變換矩陣反推來獲取子座標空間的元點和座標軸方向!例如,我們已知從模型空間到世界空間的一個4*4的變換矩陣,可以提取它的第一列再進行歸一化後來得到模型空間的x軸在世界空間下的單位矢量表示。同樣的方法可以提取y軸和z軸。

另一個有趣的是,對方向向量的座標空間變換。我們知道,向量是沒有位置的,因此座標空間的原點變換是可以忽略的。也就是說,我們僅僅平移座標系的原點是不會對向量造成任何影響的。

在Shader中,我們常常會看到擷取變換矩陣的前3行前3列來對法線方向、光照方向來進行空間變換,這正是原因所在。

前面說到,可以通過求的逆矩陣的方式求解出來反向變換。但有一種情況我們不需求求解逆矩陣就可以得到,這種情況就是是一個正交矩陣。如果它是一個正交矩陣的話,的逆矩陣就是等於它的轉置矩陣。這意味著我們不需要進行復雜的求逆操作就可以得到反向變換。也就是說:

而現在,我們不僅可以根據變換矩陣反推出子座標空間的座標軸方向在父座標空間中的表示,還可以反推出父座標空間的座標軸方向在子座標空間的表示,這些座標軸對應的就是的每一行!也就是說,如果我們只打座標空間變換矩陣是一個正交矩陣,那麼我們可以提取它的第一列來得到座標空間A的x軸在座標空間B下的表示,還可以提取它的第一行來得到座標空間B的x軸在座標空間A下的表示。反過來,如果我們知道座標空間B的x軸、y軸和z軸在座標空間A下的表示,就可以把它們依次放在矩陣的每一行就可以得到從A到B的變換矩陣了。

模型空間,如它的名字一樣,是和某個模型或者說是物件有關的。有時候模型空間也被稱為物件空間或區域性空間。每個模型都有自己獨立的座標空間,當它移動或旋轉的時候,模型空間也會跟著它移動和旋轉。把我們自己家當成遊戲中的模型的話,當我們在辦公室裡移動時,我們的模型空間也在跟著移動,當我們轉身時,我們本身的前後左右方向也在跟著改變。

模型空間的原點和座標軸通常是由美術人員在建模軟體裡確定好的。當匯入到Unity中後,我們可以在頂點著色器中訪問到模型的頂點資訊,其中包含了每個頂點的座標。這些座標都是相對於模型空間中的原點定義的。

世界空間是一個特殊的座標系,因為它建立了我們所關心的最大的空間。時間空間可以被用於描述絕對位置。

在Unity中,世界空間同樣使用了左手座標系。但它的x軸,y軸,z軸是固定不變的。在Unity中,我們可以通過調整Transform元件中的Position屬性來改變模型的位置,這裡的位置值是相對於這個Transform的父節點的模型座標空間中的原點定義的。如果一個Transform沒有任何父節點,那麼這個位置就是在世界座標系中的位置。

頂點變換的第一步,就是將頂點座標從模型空間變換到世界空間中。這個變換通常叫做模型變換。

我們可以對妞妞的鼻子進行模型變換。如下圖

根據Transform 元件上的資訊,我們知道在世界空間中,妞妞進行了(2,2,2)的縮放,又進行了(0,150,0)的旋轉,以及(5,0,25)的平移。注意這裡的變換順序是不能互換的,即先進行縮放,再進行旋轉,最後是平移。據此我們可以構建出模型變換的變換矩陣:

現在我們可以用它來對妞妞的鼻子進行模型變換了:

也就是說,在世界空間下,妞妞鼻子的位置是(9,4,18.072).注意,這個的浮點數都是近似值。實際數值和Unity採用的浮點值精度有關。

觀察空間也被稱為攝像機空間。觀察空間可以認為是模型空間的一個特例。在所有的模型中有一個非常特殊的模型,就是攝像機。這個模型空間就是觀察空間。

攝像機決定了我們渲染遊戲所使用的視角。在觀察空間中,攝像機位於原點,同樣,其座標軸的選擇可以是任意的,但本文以Unity為主,而Unity中觀察空間的座標軸選擇是:+x軸指向右方,+y軸指向上方,而+z軸指向的是攝像機的後方。Unity在模型空間和世界空間中選用的都是左手座標系,而在觀察空間中使用的是右手座標系。

這種左右手座標系之間的改變很少會對我們再Unity中的程式設計產生影響,因為Unity為我們做了很多渲染的底層工作,包括很多座標空間的轉換。但是,如果我們需要呼叫類似Camera.cameraToWorldMatrix、Camera.worldToCameraMatrix等介面自行計算某模型在觀察空間中的位置,就要小心這樣的差異。

觀察空間和螢幕空間是不同的,觀察空間是三維的,而螢幕空間是二維的。

頂點變換的第二步,就是將頂點座標從世界空間變換到觀察空間中,這個變換通常叫做觀察變換。

現在我們需要把妞妞的鼻子從世界空間變換到觀察空間中。為此,我們需要知道世界座標系下攝像機的變換資訊。這同樣可以通過攝像機面板的Transform 元件得到。如下圖。

為了得到定在在觀察空間中的位置,有兩種方法。一種是計算觀察空間的3個座標軸在世界空間下的表示,構建出從觀察空間變換到世界空間的變換矩陣,再對該矩陣求逆來得到從世界空間變換到觀察空間的變換矩陣。我們還可以使用另一種方法,即想象平移整個觀察空間,讓攝像機原點位於世界座標的原點,座標軸與世界空間中的座標軸重合即可。這兩種方法的變換矩陣都是一樣的。

這裡我們使用第二種方法:由Transform 元件可以知道,攝像機在世界空間中的變換是先按(30,0,0)進行旋轉,然後按(0,10,-10)進行了平移。那麼,為了把攝像機重新移回到初始狀態,我們需要進行逆向變換,即先按(0,-10,10)平移,以便攝像機回到原點,再按(-30,0,0)進行旋轉,以便讓座標軸重合。因此,變換矩陣就是:

但是,由於觀察空間使用的是右手座標系,因此需要對z分量進行取反操作。我們可以通過乘以另一個特殊的矩陣來得到最終的觀察變換矩陣:

現在我們可以用它來對妞妞的鼻子進行頂點變換了:

這樣我們就得到了觀察空間中妞妞鼻子的位置——(9,8.84,-27.31)。

頂點接下來要從觀察空間轉換到裁剪空間(也稱為齊次裁剪空間)中,這個用於變換的矩陣叫做裁剪矩陣,也被稱為投影矩陣。

裁剪空間的目標是能夠方便地對渲染圖元進行裁剪:完全位於這塊空間內部的圖元將會被保留,完全位於這個空間外部的圖元將會被剔除,而與這塊空間邊界相交的圖元就會被裁剪。這塊空間由視錐體來決定。

視錐體指的是空間中的一塊區域,這塊區域決定了攝像機可以看到的空間。視錐體由留個平面包圍而成,這些平面也被稱為裁剪平面。視錐體有兩種型別,這涉及兩種頭像型別:一種是正交投影,一種是透視投影。

下圖顯示了從同一個位置、同一角度渲染同一個場景的兩種攝像機的渲染結果。(左圖為透視投影,右圖為正交投影)

從圖中可以發現,在透視投影中,地板的平行線並不會保持平行,離攝像機越近網格越大,離攝像機越遠網格越小。而在正交投影中,所有的網格大小都一樣,而且平行線會一直保持平行。可以注意到,透視投影模擬了人眼看世界的方式,而正交投影則完全保留了物體的距離和角度。因此在追求真實感的3D遊戲中我們往往會使用透視投影,而在一些2D遊戲或渲染小地圖等其他HUD元素時,我們會使用正交投影。

在視錐體的6塊裁剪平面中,有兩塊裁剪平面比較特殊,它們分別稱為近裁剪平面和遠裁剪平面。它們決定了攝像機可以看到的深度範圍。正交投影和透視投影的視錐體如下圖所示。

由上圖可以看出,透視投影的視錐體是一個金字塔形,側面的4個裁剪平面將會在攝像機處相交。它更符合視錐體這個詞語。正交投影的視錐體是一個長方體。前面講到,我們希望根據視錐體圍城的區域對圖元進行裁剪,但是,如果直接使用視錐體定義的空間來進行裁剪,那麼不同的視錐體就需要不同的處理過程,而且對於透視投影的視錐體來說,想要判斷一個頂點是否處於一個金字塔內部是比較麻煩的,因此,我們相擁一種更加通用、方便、整潔的方式來進行裁剪的工作,這種方式就是通過一個投影矩陣把頂點轉換到一個裁剪空間中。

投影矩陣有兩個目的。

首先是為投影做準備。這是個迷惑點,雖然投影矩陣的名稱包含了投影二字,但是它並沒有記性真正的投影工作,而是在為投影做準備。真正的投影發生在後面的齊次除法過程中。而經過投影矩陣的變換後,頂點的w分量將會具有特殊的意義。

齊次是對x、y、z分量進行縮放。我們上面講到直接使用視錐體的6個裁剪平面來進行裁剪會比較麻煩。而經過投影矩陣的縮放後,我們可以直接使用w分量作為一個範圍值,如果x、y、z都在這個範圍內,就說明該頂點位於裁剪空間內。

在裁剪空間之前,雖然我們使用了齊次座標來表示點和向量,但它們的第四個分量都是固定的:點的w分量為1,方向向量的w分量是0。經過投影矩陣的變換後,我們就會賦予齊次座標的第4個座標更豐富的含義。下面,我們看下兩種投影型別使用的投影矩陣具體是什麼。

透視投影

視錐體的意義在於定義了場景中的一塊三維空間。所有位於這塊空間內的物體將會被渲染,否則就會被剔除或裁剪。我們已經知道,這塊區域由6個裁剪平面定義,那麼這6個裁剪平面又是怎麼決定的呢?在Unity中,它們由Camera元件中的引數和Game檢視的橫縱比共同決定。如下圖

上圖可以看出,我們可以通過Camera元件的Field Of View(簡稱FOV)屬性來改變視錐體豎直方向的張開角度,而Clipping Planes 中的 Near 和 Far 引數可以控制視錐體的近裁剪平面和遠裁剪平面距離攝像機的遠近。這樣,我們可以求出視錐體近裁剪平面和遠裁剪平面的高度,也就是:

現在我們還缺乏橫向的資訊。這可以通過攝像機的橫縱比得到。在Unity中,一個攝像機的橫縱比由Game檢視的橫縱比和Viewport Rect中的W和H屬性共同決定(實際上,Unity允許我們再指令碼中通過Camera.aspect進行更改)。假設,當前攝像機的橫縱比為Aspect,我們定義:

現在,我們可以根據已知的Near、Far、FOV和Aspect的值來確定透視投影的投影矩陣。如下:

需要注意的是,這裡的投影矩陣是建立在Unity對座標系的假定上面的,也就是說,我們針對的是觀察空間為右手座標系,使用列矩陣在矩陣右側進行相乘,且變換後z分量範圍將在[-w,w]之間的情況。而在類似DirectX這樣的圖形介面中,它們希望變換後z分量範圍將在[0,w]之間,因此就需要對上面的透視矩陣進行一些更改。

而一個頂點和上述投影矩陣相乘後,可以由觀察空間變換到裁剪空間,結果如下:

從結果可以看出,這個投影矩陣本質就是對x、y、z進行了不同程度的縮放(當然,z分量還做了一個平移),縮放的目的是為了方便裁剪,我們可以注意到,此時頂點的w分量不再是1,而是原先z分量的取反結果。現在,我們就可以按如下不等式來判斷一個變換後的頂點是否位於視錐體內。如果一個頂點在視錐體內,那麼它變換後的座標必須滿足:

任何不滿足上述條件的圖元都需要被剔除或者裁剪。下圖顯示了經過上述投影矩陣後,視錐體的變化。

從上圖可以看到,裁剪矩陣會改變空間的旋向性:空間從右手座標系變換到了左手座標系。這意味著,離攝像機越遠,z值將越大。

正交投影

首先,我們看下正交投影中的6個裁剪平面是如何定義的。和透視投影類似,在Unity中,它們也是由Camera元件中的引數和Game檢視的橫縱比共同決定,如下圖:

正交投影的視錐體是一個長方體,因此計算上比透視投影來說更為簡單。由上圖可以看出,我們可以通過Camera元件的Size屬性來改變視錐體豎直方向上的高度的一半,而Clipping Planes 中的Near 和 Far 引數可以控制視錐體的近裁剪平面和遠裁剪平面距離攝像機的遠近。這樣,我們可以求出視錐體近裁剪平面和遠裁剪平面的高度,也就是:

現在,我們還缺乏橫向的資訊,同樣,我們可以通過攝像機的橫縱比得到,假設,當前攝像機的橫縱比為Aspect,那麼:

現在,我們可以根據已知的Near、Far、Size和Aspect 的值來確定正交投影的裁剪矩陣。如下:

上面公式的推導部分可以參見本章的擴充套件閱讀部分。同樣,這裡的投影矩陣是建立在Unity對座標系的假定上面的。

一個頂點和上述投影矩陣相乘後的結果如下:

注意到,和透視投影不同的是,使用正交投影的投影矩陣對頂點進行變換後,其w分量仍然為1,本質是因為投影矩陣的最後一行不同,透視投影的投影矩陣的最後一行是[0,0,-1,0],而正交投影的投影矩陣的最後一行是[0,0,0,1]。這樣的選擇是由原因的,是為了為其次除法做準備。

判斷一個變換後的頂點是否位於視錐體內使用的不等式和透視投影中的一樣,這種通用性也是為什麼要使用投影矩陣的原因之一。下圖顯示了經過上述投影矩陣後,正交投影的視錐體的變化。

同樣,裁剪矩陣改變了空間的旋向性。可以注意到,經過正交投影變換後的頂點實際已經位於一個立方體內了。

現在,我們要計算妞妞鼻子在裁剪空間的位置。

我們使用了透視攝像機,攝像機引數和Game檢視的橫縱比如下圖所示:

據此,我們可以知道透視投影的引數:FOV為60°,Near 為 5,Far 40,Aspect 為4/3 =1.333。

那麼,對應的投影矩陣就是:

然後,我們用這個投影矩陣來把妞妞的鼻子從觀察空間轉換到裁剪空間中。如下:

我們就求出了妞妞鼻子在裁剪空間中的位置(11.691,15.311,23.692,27.31)。接來下,Unity會判斷妞妞的鼻子是否需要裁剪,通過比較得到,妞妞的鼻子滿足下面的不等式:

由此,我們判斷出,妞妞的鼻子位於視錐體內,不需要被裁剪。

螢幕空間

經過投影矩陣的變換後,我們可以進行裁剪操作。當完成了所有的裁剪工作後,就需要進行真正的投影了,也就是說,我們需要把視錐體投影到螢幕空間中。經過這一步變換,我們會得到真正的畫素位置,而不是虛擬的三維座標。

螢幕空間是一個二維空間,因此,我們必須把頂點從裁剪空間投影到螢幕空間中,來生成對應的2D座標。這個過程可以理解成兩個步驟。

首先,我們需要進行標準齊次除法,也被稱為透視除法。雖然這個步驟聽起來很陌生,但是它實際上非常簡單,就是用齊次座標的w分量去除以x、y、z分量。在OpenGL中,我們把這一步得到的座標叫做歸一化的裝置座標。經過這一步,我們可以把座標從齊次裁剪座標空間轉到NDC中。經過透視投影變換後的裁剪空間,經過齊次除法後會變換到一個立方體內。按照OpenGL的傳統,這個立方體的x、y、z分量的範圍都是[-1,1],但是在DirectX這樣的API中,z分量的範圍會是[0,1]。而Unity選擇了OpenGL 這樣的齊次裁剪空間。如下圖所示:

而對於正交投影來說,它的裁剪空間實際已經是一個立方體了,而且由於經過正交投影矩陣變換後的頂點的w分量是1,因此齊次除法並不會對頂點的x、y、z座標產生影響,如下圖所示:

經過齊次除法後,透視投影和正交投影的視錐體都變換到一個相同的立方體內。現在,我們可以根據變換後的x和y座標來對映輸出視窗的對應畫素座標。

在Unity中,螢幕空間左下角的畫素座標是(0,0),右上角的畫素座標是(pixelWidth,pixelHeight)。由於當前x和y座標都是[-1,1],因此這個對映的過程就是一個縮放的過程。

齊次除法和螢幕對映的過程可以使用下面的公式來總結:

上面的式子對x和y分量都進行了處理,而z分量被用於深度緩衝。一個傳統的方式是把的值直接存進深度緩衝中,但這並不是必須的。通常驅動生產商會根據硬體來選擇最好的儲存格式。此時clipw也並不會被拋棄,雖然它已經完成了它的主要工作——在齊次除法中作為分母來得到NDC,但它仍然會在後續的一些工作中起到重要的工作,例如進行透視校正插值。

在Unity中,從裁剪空間到螢幕空間的轉換是由Unity幫我們完成的。我們的頂點著色器只需要把頂點轉換到裁剪空間即可。

現在我們可以確定妞妞的鼻子在螢幕上的畫素位置了,假設螢幕畫素寬度為400,高度為300。十一選不我們需要進行齊次除法,把裁剪空間的座標投影到NDC中。然後,再對映到螢幕空間中。這個過程如下:

由此,我們就知道了妞妞鼻子在螢幕上的位置——(285.617,234.096).

法線變換

法線,也被稱為法向量。在遊戲中,模型的一個頂點往往會攜帶額外的資訊,而頂點法線就是其中的一種資訊。當我們變換一個模型的時候,不僅需要變換它的頂點,還需要變換頂點法線,以便在後續處理中計算光照。

一般來說,點和絕大部分方向向量都可以使用同一個4*4或3*3的變換矩陣把其從座標空間A變換到座標空間B中。但在變換髮現的 時候,如果使用同一個變換矩陣,可能就無法確保維持法線的垂直線。

切線,也被稱為切向量與法線類似,切線往往也是模型頂點攜帶的一種資訊,它通常與紋理空間對其,而且與法線方向垂直。如下圖:

由於切線是兩個頂點之間的差值計算得到的,因此我們可以直接使用用於變換頂點的變換矩陣來變換切線。假設,我們使用3*3的變換矩陣來變換頂點,可以由下面的式子直接得到變換後的切線。

其中T(a)和T(b)分別表示在座標看空間A下和座標空間B下的切線方向。但如果直接使用來變換法線,得到的新的法線方向可能就不會與表面垂直了。下圖給出了一個例子:

我們知道同一個頂點的切線T(a)和法線N(a)必須滿足垂直條件,T(a)·N(a) = 0.給定變換矩陣,我們已經知道了。現在我們想要找到一個矩陣G來變換法線N(a),使得變換後的法線仍然與切線垂直。即

對上式進行一些推導可以得到:

由於,因此如果,那麼上式即可成立。也就是說,如果,即使用原變換矩陣的逆轉置矩陣來變換法線就可以得到正確的結果。

Unity Shader 的內建變數

使用Unity 寫 Shader 的一個好處在於,它提供了很多內建的引數,這使得我們不再需要自己手動計算一些值。本節將給出Unity內建的用於空間變換和攝像機以及螢幕引數的內建變數。這些內建變數可以在UnityShaderVariables.chnic檔案中找到定義和說明。

下表給出了Unity5.2 版本提供的所有內建變換矩陣,下面所有的矩陣都是float4×4型別的。

其中有一個矩陣比較特殊,即UNITY_MATRIX_T_MV矩陣。

下表給出了Unity5.2版本提供的攝像機和螢幕引數資訊

對於線性變換來說(例如旋轉和縮放),僅適用3×3的矩陣就足夠表示所有的變換了。但如果存在平移變換,我們就需要使用4×4的矩陣,因此,在對頂點的變換中個,我們通常使用4×4的變換矩陣。當然,在變換前我們需要把點座標轉換成齊次座標的表示會,即把頂點的w分量設為1。而在對方向向量的變換中,我們通常使用3×3的矩陣就足夠了,這是因為平移變換對方向向量是沒有影響的。

我們通常在Unity Shader中使用CG作為著色器程式語言。在CG中變數型別有很多種。

在CG中,矩陣型別是由float3×3、float4×4等關鍵詞進行宣告和定義的。而對於float3、float4等型別的變數,我們即可以把它當成一個向量,也可以把它當成是一個1×n的行矩陣或者一個n×1的列矩陣。這取決於運算的 種類和它們在運算中的位置。例如,當我們進行點積操作時,兩個運算元就被當成向量型別,如下:

float4 a = float4(1.0,2.0,3.0,4.0);
float4 b = float4(1.0,2.0,3.0,4.0);
//對兩個向量進行點積操作
float result = dot(a, b);

但在進行矩陣相乘時,引數的位置將決定是按列矩陣還是行矩陣進行乘法。在CG中,矩陣乘法是通過mul函式實現的。例如:

float4 v = float4(1.0, 2.0, 3.0, 4.0);
float4×4 M = float4×4(1.0, 0.0, 0.0, 0.0,
			0.0, 1.0, 0.0, 0.0,
			0.0, 0.0, 1.0, 0.0,
			0.0, 0.0, 0.0, 1.0);
//把v當成列矩陣和矩陣M進行右乘
float4 column_mul_result = mul(M, v);
//把v當成行矩陣和矩陣M進行左乘
float4 row_mul_result = mul(v, M);

因此,引數的位置會直接影響結果值。通常在變換頂點時,我們都是使用右乘的方式來按列矩陣進行乘法。這是因為,Unity提供的內建矩陣(如UNITY_MATRIX_MVP等)都是按列儲存的。但有時,我們也會使用左乘的方式,這是因為可以省去對矩陣的轉置的操作。

需要注意的一點是,CG對矩陣型別中元素的初始化和訪問順序。在CG中,對float4×4等型別的變數是按行優先進行填充的。假設我們使用數字(1,2,3,4,5,6,7,8,9)去填充一個3×3的矩陣,如果是按照行優先的方式,得到的矩陣是:

如果是按列優先的話,得到的矩陣是:


CG使用的是行優先的方法,即使一行一行地填充矩陣的

類似地,當我們再CG中訪問一個矩陣中的元素時,也是按行來索引的。例如:

//按行優先的方式初始化矩陣M
float3×3 M = float3×3(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0);
//得到M的第一行,即(1.0, 2.0, 3.0)
float3 row = M[0];
//得到M的第2行第一列的元素,即4.0
float ele = M[1][0]

之所以Unity Shader中的矩陣型別滿足上述規則,是因為使用的是CG語言,換句話說,上面的特性都是CG的規定。

在頂點/片元著色器中,有兩種方式來獲得片元的螢幕座標。

一種是在片元著色器的輸入中宣告VPOS或WPOS語義。VPOS是HLSL中對螢幕座標的語義,而WPOS是CG中對螢幕座標的語義,兩者在Unity Shader都是等價的。我們可以在HLSL/CG中通過語義的方式來定義頂點/片元著色器的預設輸入,而不需要自己定義輸入輸出的資料結構。使用這種寫法,可以在片元著色器中這樣寫:

fixed4 frag(float4 sp : VPOS) : SV_TARGET
{
	//用螢幕座標除以螢幕解析度_ScreenParams.xy,得到視口空間中的座標
	return fixed4(sp.xy/_ScreenParams.xy, 0.0, 1.0);
}

得到的效果如下圖所示:

VPOS/WPOS語義定義的輸入是一個float4型別的變數。我們已經知道它的xy值代表了在螢幕空間中的畫素座標。如果螢幕的解析度為400×300,那麼x的範圍就是[0.5,400.5],y的範圍就是[0.5,300.5]。注意,這裡的畫素座標並不是整數值,這是因為openg 和DirectX 10 以後的版本認為畫素中心對應的是浮點值中的0.5。在Unity中,VPOS/WPOS的z分量範圍是[0,1],在攝像機的近裁剪平面處,z值為0,在遠裁剪平面處,z值為1.對於w分量,我們需要考慮攝像機的投影型別。如果是透視投影, 那麼w分量的範圍是
,Near和Far對應了Camera元件中設定的近裁剪平面和遠裁剪平面矩陣攝像機的遠近;如果使用的是正交投影,那麼w分量的值恆為1.這些值是通過對經過投影矩陣變換後的w分量取倒數後得到的。在程式碼的最後,我們把螢幕空間除以螢幕解析度來得到的視口空間中的座標。視口座標很簡單,就是把螢幕座標歸一化,這樣螢幕左下角就是(0,0),右上角就是(1,1)。如果已知螢幕座標的話,我們只需要把xy值除以螢幕解析度即可。

另一種方式是通過Unity提供的ComputeScreenPos函式。這個函式在UnityCGcginc裡被定義。通常的用法需要兩個步驟,首先在頂點著色器中將ComputeScreenPos的結果儲存在輸出結構體中,然後在片元著色器中進行一個齊次除法運算後得到視口空間下的座標。例如:

struct vertOut
{
	float4 pos : SV_POSITION;
	float4 scrPos : TEXCOORD0;
}

vertOut vert(appdata_base v)
{
	vertOut o;
	o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
	//第一步:把ComputeScreenPos的結果儲存到scrPos中
	o.scrPos = ComputeScreenPos(o.pos)
	return 0;
}

fixed4 frag(vertOut i) : SV_Target
{
	//第二步,用scrPos.xy除以scrPos.w得到視口空間中的座標
	float2 wcoord = (i.scrPos.xy / i.scrPos.w);
	return fixed4(wcoord, 0.0, 1.0);
}

上面程式碼的實現效果和上面的程式碼一樣。這種方法實際上是手動實現了螢幕對映的過程,而且它得到的座標直接就是視口空間中的座標。我們已經知道了如何將裁剪座標空間中的點對映到螢幕座標中。據此,我們可以得到視口空間中的座標,公式如下:

上面公式的思想就是,首先對裁剪空間下的座標進行齊次除法,得到閥內在[-1,1]的NDC,然後再將其對映到範圍在[0,1]的視口空間下的座標。