Unity Shader:相關(guān)數(shù)學基礎

本文同時發(fā)布在我的個人博客上:https://dragon_boy.gitee.io

坐標系

有左手坐標系和右手坐標系兩種,Unity使用的是右手坐標系。

點和矢量

我們使用兩個或三個以上的實數(shù)來表示一個點的坐標,如P = (P_x, P_y, P_z)。

矢量是指n維空間中一種包含了模和方向的有向線段。矢量的表示方法和點類似,如v = (x,y,z)

矢量通常由一個箭頭表示,由起點指向終點。矢量常被用于表示相對于某個點的偏移,只要矢量的模和方向保持不變,無論在哪里,都是同一個矢量。

矢量運算

矢量和標量乘除

對乘法,將矢量的每個分量和標量相乘即可:kv = (kv_x,kv_y,kv_z)

對除法,同理:\frac{v}{k} = (\frac{x}{k},\frac{y}{k},\frac{z}{k}),標量需非零。

矢量加減法

兩個矢量加減法,把對應的分量進行相加或相減即可:

a + b = (a_x+b_x, a_y+b_y,a_z+b_z)

a - b = (a_x-b_x, a_y-b_y,a_z-b_z)

幾何意義上,矢量相加即前者起點連接至后者終點,矢量相減即后者終點指向前者終點。

矢量的模

矢量的模即矢量的長度:

|v| = \sqrt{{v_x}^2+{v_y}^2+{v_z}^2}

單位矢量

單位矢量即模為1的矢量。將某個非零矢量轉(zhuǎn)化為單位矢量的過程稱為歸一化。

\hat{v} = \frac{v}{|v|}

矢量乘法

矢量之間的乘法有兩種,點積和叉積。

點積公式有兩個:
1:

a \cdot b = (a_x,a_y,a_z) \cdot (b_x,b_y,b_z) = a_xb_x+a_yb_y+a_zb_z

2(\theta為兩個矢量的夾角):

a \cdot b = |a||b|cos\theta

點積的結(jié)果在幾何意義上是獲得矢量ab上的投影與b的模的乘積,如果二者都是單位矢量,那么點積結(jié)果就是前者在后者上的投影。

叉積公式:

a \times b = (a_x,a_y,a_z) \times (b_x,b_y,b_z) = (a_yb_z - a_zb_y,a_zb_x-a_xb_z,a_xb_y-a_yb_x)

下面是另一種表示:

|a \times b| = |a||b|sin\theta

上述表明矢量叉積的結(jié)果的模是兩矢量構(gòu)成平行四邊形的面積。

在幾何意義的上,叉積的結(jié)果是一個新的矢量,分別垂直于ab,且沿a、b和叉積的方向可構(gòu)成一個右手坐標系。

矩陣

矩陣有行列之分,以3\times3矩陣為例:

\left[ \begin{matrix} m_{11}&m_{12}&m_{13}\\ m_{21}&m_{22}&m_{23}\\ m_{31}&m_{32}&m_{33} \end{matrix} \right]

矢量的矩陣表示法

矢量可以看作時n\times1的列矩陣或1\times n的行矩陣。

矩陣運算

矩陣和標量的乘法

將矩陣的每個元素和標量相乘即可。

矩陣和矩陣的乘法

兩個矩陣相乘,要求前一個矩陣的列數(shù)等于后一個矩陣的行數(shù),相乘得到的矩陣的行數(shù)是第一個矩陣的行數(shù),列數(shù)是第二個矩陣的列數(shù)。如A的維度是4\times3,B的維度是3\times6,那么AB的維度是4\times6。

矩陣的乘法即前一個矩陣的每一行(i行)與后一個矩陣的每一列(j列)相乘(可看作行構(gòu)成的矢量和列構(gòu)成的矢量點積),每次相乘的結(jié)果填寫在對應的ij列上。

注意,矩陣乘法不滿足交換律,但滿足結(jié)合律。

特殊矩陣

方陣

方陣即行列數(shù)相等的矩陣。

同時,如果除對角線的元素外全是0的方陣稱為對角矩陣。

單位矩陣

針對上述的對角矩陣,如果對角線的元素全是0的話,那么就稱為單位矩陣。

轉(zhuǎn)置矩陣

對于一個r\times c的矩陣M,它的轉(zhuǎn)置M^Tc\times r的矩陣。轉(zhuǎn)置運算即將原矩陣的行列翻轉(zhuǎn)。原矩陣的i行變?yōu)?img class="math-inline" src="https://math.jianshu.com/math?formula=i" alt="i" mathimg="1">列,j列變?yōu)?img class="math-inline" src="https://math.jianshu.com/math?formula=j" alt="j" mathimg="1">行。

注意,矩陣的轉(zhuǎn)置的轉(zhuǎn)置等于原矩陣。

矩陣的串接的轉(zhuǎn)置等于反向串接各個矩陣的轉(zhuǎn)置:

{(AB)}^T = B^TA^T

逆矩陣

只有方陣才有逆矩陣。

一個矩陣和它的逆矩陣的乘積為單位矩陣:

MM^{-1} = M^{-1}M = I

零矩陣沒有逆矩陣。如果一個矩陣有逆矩陣,那么這個矩陣是可逆的,或非奇異的,相反則是不可逆的或奇異的。

如果一個矩陣的行列式為不為0,那么該矩陣就是可逆的(具體不解釋)。

注意,逆矩陣的逆矩陣為原矩陣。

單位矩陣的逆矩陣是它本身。

轉(zhuǎn)置矩陣的逆矩陣是逆矩陣的轉(zhuǎn)置:

{(M^T)}^{-1} = (M^{-1})^T

矩陣串接相乘后的逆矩陣等于反向串接各個矩陣的逆矩陣:

(AB)^{-1} = B^{-1}A^{-1}

注意,如果某個矩陣或矢量進行了一個矩陣變化,那么使用逆矩陣可以還原這個變換:

M^{-1}(Mv) = (M^{-1}M)v = Iv = v

正交矩陣

如果一個方陣和它的轉(zhuǎn)置矩陣的乘積是單位矩陣,那么這個方陣就是正交矩陣:

MM^T = M^TM = I

將正交矩陣與逆矩陣的概念結(jié)合起來,就可以得到,如果一個矩陣正交,那么它的轉(zhuǎn)置矩陣和逆矩陣相等:
M^T = M^{-1}

上述式子在實際計算中非常有用,因為逆矩陣的計算量很大(涉及伴隨矩陣),所以如果能判斷某個變換矩陣是正交的話,可以很簡單地用轉(zhuǎn)置矩陣代替它的逆矩陣進行計算。

那么如何判斷一個矩陣是否正交,我們把一個矩陣的列看作是一個矢量,稱為基矢量,那么根據(jù)正交矩陣定義:

M^TM = \left[ \begin{matrix} {-} & c_1& {-}\\ {-} & c_2 &{-}\\ {-} & c_3 &{-} \end{matrix} \right] \left[ \begin{matrix} {|} & {|} &{|}\\ c_1 & c_2 & c_3\\ {|} & {|} &{|} \end{matrix} \right] = \left[ \begin{matrix} c_1 \cdot c_1 & c_1 \cdot c_2 & c_1 \cdot c_3\\ c_2 \cdot c_1 & c_2 \cdot c_2 & c_2 \cdot c_3\\ c_3 \cdot c_1 & c_3 \cdot c_2 & c_3 \cdot c_3 \end{matrix} \right] = \left[ \begin{matrix} 1&0&0\\ 0&1&0\\ 0&0&1 \end{matrix} \right]

根據(jù)上述等式,得:

c_1,c_2,c_3都是單位矢量,且兩兩垂直。那么滿足這樣條件的矩陣的就是正交矩陣。

矢量的矩陣表示

在Unity中,常將矢量當做是列矩陣來使用,即放到矩陣的右邊來進行乘法。這種情況下,矩陣乘法通常是右乘,即:

CBAv = C(B(Av)))

變換

概念

變換是將一些數(shù)據(jù)通過某種方式進行轉(zhuǎn)換的過程。

一個非常常見的變換是線性變換,指的是可以保留矢量加和標量乘的變換。數(shù)學公式為:

f(x)+f(y)=f(x+y)

kf(x)=f(kx)

縮放是一種線性變換,旋轉(zhuǎn)也是一種線性變換,我們可以使用一個3\times3的矩陣就可以對一個三維向量進行線性變換。除此之外還有錯切,鏡像,正交投影。

但平移變換不是線性變換,我們不能使用一個3\times3的矩陣對一個三維向量進行變換。

由此出現(xiàn)仿射變換,即合并線性變換和平移變換的變換類型。仿射變換使用一個4\times4的矩陣來表示,這樣我們需要將矢量擴展到四維空間下,即齊次坐標空間。

齊次坐標

對于一個三維向量,我們擴展一個維度,分量稱為w。對于坐標,w常設為1,而對于方向,常設為0。因為我們只想對位置進行平移變換,方向不行。

基礎變換矩陣

我們使用一個4\times4的矩陣來表示平移、旋轉(zhuǎn)和縮放。一個基礎的變換矩陣可以分解為4個部分:

\left[ \begin{matrix} M_{3\times3}& t_{3\times1}\\ 0_{1\times3}& 1 \end{matrix} \right]

M_{3\times3}表示旋轉(zhuǎn)縮放的矩陣,t_{3\times1}表示平移。

平移矩陣

下面是一個平移矩陣(針對點)的例子:
\left[ \begin{matrix} 1&0&0&t_x\\ 0&1&0&t_y\\ 0&0&1&t_z\\ 0&0&0&1 \end{matrix} \right] \left[ \begin{matrix} x\\y\\z\\1 \end{matrix} \right] = \left[ \begin{matrix} x+t_x\\ y+t_y\\ z+t_z\\ 1 \end{matrix} \right]

如果是方向矢量的話,由于w分量為0,平移變換不會有影響:

\left[ \begin{matrix} 1&0&0&t_x\\ 0&1&0&t_y\\ 0&0&1&t_z\\ 0&0&0&1 \end{matrix} \right] \left[ \begin{matrix} x\\y\\z\\0 \end{matrix} \right] = \left[ \begin{matrix} x\\ y\\ z\\ 0 \end{matrix} \right]

平移變換的逆矩陣就是反向平移得到的矩陣:
\left[ \begin{matrix} 1&0&0&-t_x\\ 0&1&0&-t_y\\ 0&0&1&-t_z\\ 0&0&0&1 \end{matrix} \right]

平移矩陣并不是正交矩陣。

縮放矩陣

下面是一個縮放矩陣(針對點)的例子:

\left[ \begin{matrix} k_x&0&0&0\\ 0&k_y&0&0\\ 0&0&k_z&0\\ 0&0&0&1 \end{matrix} \right] \left[ \begin{matrix} x\\y\\z\\1 \end{matrix} \right] = \left[ \begin{matrix} k_xx\\ k_yy\\ k_zz\\ 1 \end{matrix} \right]

對方向矢量縮放:

\left[ \begin{matrix} k_x&0&0&0\\ 0&k_y&0&0\\ 0&0&k_z&0\\ 0&0&0&1 \end{matrix} \right] \left[ \begin{matrix} x\\y\\z\\0 \end{matrix} \right] = \left[ \begin{matrix} k_xx\\ k_yy\\ k_zz\\ 0 \end{matrix} \right]

如果k_x=k_y=k_z,那么這樣的縮放為統(tǒng)一縮放,否則為非統(tǒng)一縮放。由于非統(tǒng)一縮放會改變角度和比例信息,所以針對方向向量的縮放不能使用上述的方式。

縮放矩陣的逆矩陣如下:
\left[ \begin{matrix} {\frac{1}{k_x}}&0&0&0\\ 0&{\frac{1}{k_y}}&0&0\\ 0&0&{\frac{1}{k_z}}&0\\ 0&0&0&1 \end{matrix} \right]

縮放矩陣不是正交矩陣。

上面的矩陣只適合沿坐標軸方向縮放,如果要在任意方向縮放,就要使用一個復合變換,先縮放軸,再沿坐標軸縮放。

旋轉(zhuǎn)矩陣

繞x軸旋轉(zhuǎn):

R_x(\theta) = \left[ \begin{matrix} 1&0&0&0\\ 0&cos\theta&-sin\theta&0\\ 0&sin\theta&cos\theta&0\\ 0&0&0&1 \end{matrix} \right]

繞y軸旋轉(zhuǎn):

R_y(\theta) = \left[ \begin{matrix} cos\theta&0&sin\theta&0\\ 0&1&0&0\\ -sin\theta&0&cos\theta&0\\ 0&0&0&1 \end{matrix} \right]

繞z軸旋轉(zhuǎn):

R_y(\theta) = \left[ \begin{matrix} cos\theta&-sin\theta&0&0\\ -sin\theta&cos\theta&0&0\\ 0&0&1&0\\ 0&0&0&1 \end{matrix} \right]

旋轉(zhuǎn)矩陣是正交矩陣。

復合變換

大多數(shù)情況下,將平移、旋轉(zhuǎn)、縮放結(jié)合起來的順序往往是:先縮放,后旋轉(zhuǎn),再平移,也就是:

v_{new} = TRSv_{old}

注意,針對旋轉(zhuǎn)的順序,Unity的順序是zxy,即旋轉(zhuǎn)變換矩陣是:

M_zM_xM_y

旋轉(zhuǎn)時的坐標系有兩種可以選擇:

  • 繞坐標系E的z軸旋轉(zhuǎn),繞y軸旋轉(zhuǎn),再繞x軸旋轉(zhuǎn)。即進行一次旋轉(zhuǎn)時不一起旋轉(zhuǎn)當前坐標系。
  • 繞E的z軸旋轉(zhuǎn),E繞z軸旋轉(zhuǎn)相同角度的E',繞E‘的y軸旋轉(zhuǎn),E’繞y軸旋轉(zhuǎn)相同角度的E'',最后繞E''的x軸旋轉(zhuǎn)。即再旋轉(zhuǎn)時,把坐標系一起轉(zhuǎn)動。

在上述兩種方式中,旋轉(zhuǎn)的結(jié)果不一樣,Unity是第一種。

坐標空間

模型空間

模型空間即模型的局部坐標系,在Unity中是左手坐標系。

世界空間

世界空間被用來描述物體在場景中的位置,在Unity中,世界空間是左手坐標系。

將頂點從模型空間變換到世界空間的變換稱為模型變換(Model)。這一變換通常由TRS組成。

觀察空間

也被稱為攝像機空間。

攝像機決定了我們渲染游戲所使用的視角。在觀察空間中,攝像機位于原點,在Unity中,+x指向攝像機的右方,+y指向攝像機的上方,+z指向攝像機的后方,即攝像機指向-z軸。

將頂點從世界空間轉(zhuǎn)換到觀察空間的變換為觀察變換(View)。

為得到頂點在觀察空間中的位置,有兩種方法:一是計算觀察空間的三個坐標軸在世界空間下的表示,然后構(gòu)建出從觀察空間變換到世界空間的變換矩陣,再對矩陣求逆來得到從世界空間變換到世界空間的矩陣。二是想象平移整個觀察空間,讓攝像機的原點位于世界坐標的原點,坐標軸與世界空間的坐標軸重合即可。

這里使用第二種方法,即想辦法讓攝像機變換到世界空間的原點即可(注意,由于觀察空間使用右手坐標系,因此需要對z分量取反)。

裁剪空間

將頂點從觀察空間轉(zhuǎn)換到裁剪空間的變換矩陣稱為裁剪矩陣,也被稱為投影矩陣(Projection)。裁剪空間由視錐體決定。

視錐體是空間中的一塊區(qū)域,視錐體內(nèi)的區(qū)域是攝像機可以看到的空間。視錐體由六個平面構(gòu)成,這些平面稱為裁剪平面。視錐體有兩種類型,透視投影和正交投影。

在視錐體的裁剪平面中有兩塊很特殊,分別稱為近裁剪平面和遠裁剪平面,它們決定了攝像機可以看到的深度范圍。


投影矩陣有兩個目的:

  • 為投影做準備
  • 對x、y、z分量進行縮放
透視投影

我們利用上圖的參數(shù)可以計算出近裁剪平面和遠裁剪平面的高度:

nearClipPlaneHeight = 2\cdot Near\cdot tan\frac{FOV}{2}

farClipPlaneHeight = 2\cdot Far\cdot tan\frac{FOV}{2}

裁剪平面的寬度我們可以通過橫縱比得到。假設攝像機的橫縱比為Aspect:

Aspect = \frac{nearClipPlaneWidth}{nearClipPlaneHeight}

Aspect = \frac{farClipPlaneWidth}{farClipPlaneHeight}

根據(jù)上述信息,投影矩陣為:

M_{projection} = \left[ \begin{matrix} \frac{cot\frac{FOV}{2}}{Aspect}&0&0&0\\ 0&cot\frac{FOV}{2}&0&0\\ 0&0&-\frac{Far+Near}{Far-Near}&-\frac{2\cdot Near\cdot Far}{Far-Near}\\ 0&0&-1&0 \end{matrix} \right]

使用上述投影矩陣后:

P_{clip} = M_{projection}P_{view} = \left[ \begin{matrix} x\frac{cot\frac{FOV}{2}}{Aspect}\\ ycot\frac{FOV}{2}\\ -z\frac{Far+Near}{Far-Near}-\frac{2\cdot Near\cdot Far}{Far-Near}\\ -z \end{matrix} \right]

可以看到,投影矩陣本質(zhì)上是對x、y、z坐標進行一些縮放,同時z分量也進行了平移,縮放的目的是為了方便裁剪。此時w分量不再是1,而是-z,通過這個w,我們可以判斷一個頂點是否在視錐體內(nèi),在的話必須滿足下面條件:

-w<=x,y,z<=w

透視投影后,空間從右手坐標系變?yōu)樽笫肿鴺讼怠?/p>

正交投影

nearClipPlaneHeight = 2\cdot Size

farClipPlaneHeight = nearClipPlaneHeight

假設橫縱比為Aspect,那么:

nearClipPlaneWidth = Aspect\cdot nearClipPlaneHeight

farClipPlaneWidth = nearClipPlaneWidth

正交投影矩陣:

M_{ortho} = \left[ \begin{matrix} \frac{1}{Aspect\cdot Size}&0&0&0\\ 0&\frac{1}{Size}&0&0\\ 0&0&-\frac{2}{Far-Near}&-\frac{Far + Near}{Far-Near}\\ 0&0&-1&0 \end{matrix} \right]

使用正交投影矩陣后:

P_{clip} = M_{ortho}P_{view}= \left[ \begin{matrix} \frac{x}{Aspect\cdot Size}\\ \frac{y}{Size}\\ -\frac{2z}{Far-Near}-\frac{Far + Near}{Far-Near}\\ 1 \end{matrix} \right]

使用正交投影后,w分量仍是1。

屏幕空間

這一步的變換,我們會得到真正的像素位置。

將頂點從裁剪空間投影到屏幕空間有兩個步驟:首先進行透視除法,用x、y、z分量除以w分量。這一步得到的坐標被稱為NDC,經(jīng)過透視除法的裁剪空間變換到一個立方體內(nèi):


對于正交投影沒什么區(qū)別。

在Unity中,屏幕空間左下角的像素坐標是(0,0)。接下來就是將NDC縮放值屏幕坐標系。

透視除法和屏幕映射的過程總結(jié)如下:

screen_x = \frac{clip_x\cdot pixelWidth}{2\cdot clip_w} + \frac{pixelWidth}{2}
screen_y = \frac{clip_y\cdot pixelHeight}{2\cdot clip_w} + \frac{pixelHeight}{2}

法線變換

法線是一個方向矢量,垂直于表面,與其垂直的一個矢量被稱為切線。

切線由兩個頂點之間的差值計算得來,因此可以使用變換頂點的變換矩陣來變換切線。假設不考慮平移變換,變換后的切線為:

T_B = M_{A->B}T_A

不過,如何直接這么變換的話,法線就有可能不垂直于表面了:


下面我們可以來推導一下正確的法線變換矩陣。

首先,切線與法線垂直,即T_A\cdot N_A = 0,假設變換法線的矩陣為G,變換后法線仍與切線垂直,那么:

T_B\cdot N_B = (M_{A->B}T_A)\cdot(GN_A)=0

經(jīng)推導得:

(M_{A->B}T_A)\cdot(GN_A) = (M^T_{A->B}T_A)(GN_A) = {T^T_A}{M^T_{A->B}}GN_A = {T_A}^T({M^T_{A->B}}G)N_A = 0

由于T_A\cdot N_A = 0,如果{M^T_{A->B}}G=I,則上式成立,即G=(M^T_{A->B})^{-1} = (M^{-1}_{A->B})^T。

如果變換矩陣M_{A->B}是正交矩陣,那么G = M_{A->B},當然,這限于只包含旋轉(zhuǎn)變換。如果只包含旋轉(zhuǎn)和統(tǒng)一縮放,那么G = \frac{1}{k}M_{A->B}。

Unity Shader的內(nèi)置相關(guān)數(shù)學變量

內(nèi)置變量:Built-in shader variables
內(nèi)置方法:Built-in shader helper functions

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容