座標轉換matrix的填法
設有一個parent space P和一個child space C,要把一個C下的vector Vc轉到P下,可以這樣表達:
Vp = (Mc→p) Vc
相反地,把一個vector Vp從P轉到C,就是: Vc = (Mp→c) Vp
而 Mc→p的inverse就是Mp→c,反之亦然。
至於中間的這個Transformation matrix要填甚麼呢?我們需要知道P和C之間的關係。
拿Mc→p做例子,我們需要知道C的座標是在P的位置。
為甚麼不是求P在C的位置呢?試想想,原本我們是以C為中心,但如果要轉到P下面,以P為中心的話,那C肯定就飛到另外一個位置了。
Mc→p的填法一般為:
[ | | | |
Xc Yc Zc Oc
| | | |
0 0 0 1 ]
其中Xc Yc和Zc 是代表了C在P下的三個axis vector,而Oc是C在P下的origin。
按著上一篇所提到的affine matrix的結構來分析的話,就會知道左邊的Xc Yc Zc是做linear transform,而Oc是做translation。所以如果你只是需要做linear transform的話(即只是針對方向vector做轉換,因為方向vector不需要translate),就可以只用Mc→p中左上角3x3的部分。
快速求Inverse
還記得orthogonal matrix的特性嗎?
(1) M^T = M^-1
(2) 每個column vector和其他column vector是互相垂直的
因此,而如果我們知道這個座標的x y z axis是互相垂直的話,我們在計算這個transformation matrix的inverse的時候,直接把它transpose就可以了。
換言之,Mp→c就變成:
[ - Xc -
- Yc -
- Zc - ]
繪製過程涉及的座標空間
Model Space/Local Space/Object Space(模型空間/局部空間/物件空間)
以這個遊戲物件本身的其中一個中心點為origin。這在建立遊戲模型的時候應該就定好了。
World Space(世界空間)
以Unity scene view中的中心點為origin。
Camera Space/Eye Space/View Space(攝影機空間/眼睛空間/觀察空間)
以Unity中的camera的位置為origin。要注意的是Unity為了配合OpenGL的傳統,這個空間使用的是右手法則(right-hand rule),即是說攝影機前方的方向和z axis的正數方向是相反的。這是唯一使用右手法則的空間,之後的空間會重新轉回左手法則。
Clip Space(修改空間,又譯裁剪空間)
同樣以camera的位置做origin,但所有可視範圍以外的物件都會被剔除,只有一部分在可視範圍內的物件就會修改成只保留可視的部分,這個範圍叫view frustum(視椎體),是一個六面體。這裡也會看攝影機使用了正交投影(orthographic projection)還是透視投影(perspective projection)來決定view frustum的大小。
從camera space轉到clip space的matrix叫做clip matrix或者projection matrix。雖然名字叫做projection matrix,但嚴格來說只是在「準備」做projection,真正做projection的是在從clip space轉到screen space的部分。
下面列出的matrix假設camera space是用右手法則,轉換後就會變回左手法則(根據Unity的做法)。對於這些matrix的來歷有興趣的話可按此。
0 1/size 0 0
0 0 -2/(Far-Near) -(Far+Near)/(Far-Near)
0 0 0 1]
這個matrix對一個vector的x y z都做了不同程度的縮放,也對z做了translation。
而做完正交matrix轉換後的view frustum會變成一個立方體(vertex的x y z w是-1至1)。
0 1/tan(FOV/2) 0 0
0 0 -(Far+Near)/(Far-Near) -2(Near*Far)/(Far-Near)
0 0 -1 0]
從camera space轉到clip space的matrix叫做clip matrix或者projection matrix。雖然名字叫做projection matrix,但嚴格來說只是在「準備」做projection,真正做projection的是在從clip space轉到screen space的部分。
下面列出的matrix假設camera space是用右手法則,轉換後就會變回左手法則(根據Unity的做法)。對於這些matrix的來歷有興趣的話可按此。
正交matrix
[ 1/(aspect*size) 0 0 00 1/size 0 0
0 0 -2/(Far-Near) -(Far+Near)/(Far-Near)
0 0 0 1]
這個matrix對一個vector的x y z都做了不同程度的縮放,也對z做了translation。
而做完正交matrix轉換後的view frustum會變成一個立方體(vertex的x y z w是-1至1)。
透視matrix
[ 1/(aspect * tan (FOV/2)) 0 0 00 1/tan(FOV/2) 0 0
0 0 -(Far+Near)/(Far-Near) -2(Near*Far)/(Far-Near)
0 0 -1 0]
這個matrix和正交matrix同樣對一個vector的x y z都做了不同程度的縮放,也對z做了translation。但不同的是,轉換完後的vector的w此時不再是1,而是-z。這時候,如果一個vexter的x y z均小於或等於w並大於或等於-w(-w<= x y z <= w),就代表這個vertex在view frustum裡面。
做完透視matrix轉換後的view frustum,其near panel的vertex的x y z w都會變成near或-near,而far panel的vertex的x y z w都會變成far或-far。
做完透視matrix轉換後的view frustum,其near panel的vertex的x y z w都會變成near或-near,而far panel的vertex的x y z w都會變成far或-far。
Screen Space(螢幕空間)
這裡已經表示了我們要在螢幕上畫些甚麼,因此這裡是一個2D Space。
從clip space轉換成2d space的步驟:
要做homogeneous division(齊次除法),也叫perspective division(透視除法),其實就只是把vector上的x y z除以w而已。對透視投影來說,除完之後就會發現view frustum從原本錐體變成了立方體(vertex的x y z w是-1至1,如果是DirectX則是0至1),而對正交投影就沒有影響(因為w是1)。要注意的是,除法是比較耗用CPU的,所以我們想盡量確保先做完clipping,把所有不需要的物件移除後,才對剩下必要的物件做除法。
做完除法後的座標在OpenGL叫做NDC(Normalized Device Coordinates)。
最後要用這些vertex的x和y來轉換成螢幕上的x和y(下方公式包含了homogeneous division):
screen x = clipx * pixelWidth / 2clipw + pixelWidth / 2
screen y = clipy * pixdelHeight / 2clipw + pixelHeight / 2
在這裡Unity依然遵照著左手法則(或者說是OpenGL傳統),因此螢幕左下是(0,0),右上是(pxielWidth, pixelHeight)。
如果M本身是orthogonal的,那麼(M^-1)^T會等於回M,因此可以直接用M來轉換normal。
之前有提過rotation matrix無論是單一還是複合都是orthogonal的,所以只帶有rotation的matrix可以直接用來轉換normal。
如果M還包含了scaling的話,設scaling constant為k,那麼matrix就是 (1/k)M。
其他情況就可能真的需要慢慢計算inverse transpose matrix了。
從clip space轉換成2d space的步驟:
要做homogeneous division(齊次除法),也叫perspective division(透視除法),其實就只是把vector上的x y z除以w而已。對透視投影來說,除完之後就會發現view frustum從原本錐體變成了立方體(vertex的x y z w是-1至1,如果是DirectX則是0至1),而對正交投影就沒有影響(因為w是1)。要注意的是,除法是比較耗用CPU的,所以我們想盡量確保先做完clipping,把所有不需要的物件移除後,才對剩下必要的物件做除法。
做完除法後的座標在OpenGL叫做NDC(Normalized Device Coordinates)。
最後要用這些vertex的x和y來轉換成螢幕上的x和y(下方公式包含了homogeneous division):
screen x = clipx * pixelWidth / 2clipw + pixelWidth / 2
screen y = clipy * pixdelHeight / 2clipw + pixelHeight / 2
在這裡Unity依然遵照著左手法則(或者說是OpenGL傳統),因此螢幕左下是(0,0),右上是(pxielWidth, pixelHeight)。
Normal (法線) 的空間轉換
要留意的是Vertex可能會帶有Normal vector(光源用),這些normal vector在空間轉換的時候不能用和vertex一樣的matrix,否則會「走樣」(尤其因為縮放比例不一,和三角平面的角度可能變得不太準確了)。設轉換vertex的matrix為M,那麼轉換normal的matrix是M的inverse transpose matrix(即 (M^-1) ^T)。如果M本身是orthogonal的,那麼(M^-1)^T會等於回M,因此可以直接用M來轉換normal。
之前有提過rotation matrix無論是單一還是複合都是orthogonal的,所以只帶有rotation的matrix可以直接用來轉換normal。
如果M還包含了scaling的話,設scaling constant為k,那麼matrix就是 (1/k)M。
其他情況就可能真的需要慢慢計算inverse transpose matrix了。
這些空間和Shader的關係
其實上述的各類空間描述了繪製管線中的Geometry stage中的流程,而當中有一部分的流程我們是可以自己寫程式控制的(這些就是Shader),有些則由GPU完全操控。那麼讓我們重提一下Vertex Shader和Fragment Shader負責甚麼吧。
Vertex Shader
重溫:Vertex Shader的主要作用是把vertex從model space轉到clip space。
還記得mul(UNITY_MATRIX_MVP, v.vertex)嗎?UNITY_MATRIX_MVP中的MVP代表了:
Model:Model Space → World Space
View:World Space → View Space
Projection:View Space → Clip Space
UNITY_MATRIX_MVP代表了上述三個transformation matrix結合出來的matrix,Unity會自動幫你計算這個matrix是甚麼。因此這句程式碼其實就是把vertex從model space轉到clip space。
Unity還提供了其他多種matrix組合的變數給你選擇,只需要搞清楚你到底是想從哪個space轉到哪個space,再選用適當的matrix便可。例如你只想計算從Model Space轉到View Space,那就只用UNITY_MATRIX_MV即可。
問:為甚麼沒有UNITY_MATRIX_I_MV?
雖然不清楚確實原因,但其實我們只要把UNITY_MATRIX_IT_MV做transpose之後就能得出UNITY_MATRIX_I_MV了。又或者連transpose也不用而是直接改變mul的次序(即mul(v.vertex, UNITY_MATRIX_IT_MV)),這樣的結果和mul(transpose(UNITY_MATRIX_IT_MV), v.vertex)是一模一樣的,因為Unity會根據argument中vector和matrix的先後次序而決定要用row vector還是column vector(但不會幫matrix做transpose)。
問:為甚麼沒有UNITY_MATRIX_I_MV?
雖然不清楚確實原因,但其實我們只要把UNITY_MATRIX_IT_MV做transpose之後就能得出UNITY_MATRIX_I_MV了。又或者連transpose也不用而是直接改變mul的次序(即mul(v.vertex, UNITY_MATRIX_IT_MV)),這樣的結果和mul(transpose(UNITY_MATRIX_IT_MV), v.vertex)是一模一樣的,因為Unity會根據argument中vector和matrix的先後次序而決定要用row vector還是column vector(但不會幫matrix做transpose)。
Fragment Shader
重溫:Fragment Shader的主要作用是決定該物件在每個pixel上的顏色。
Fragment shader其實位於比Screen space更後的位置。雖然Screen space中決定了每個vertex在螢幕中的位置,但我們還沒把它們轉成pixel去針對每個pixel做一些處理。因此中間還需要經過Triangle Setup(三角設定,負責計算三角邊的像素坐標)和Triangle traversal(三角檢查,負責檢查每個pixel是否被一個由vertex組成的triangle mesh覆蓋)後,知道了哪些pixel(準確點叫fragment或potential pixel,因為我們還沒知道那個pixel實際上會否被畫上去)是在這個物件下的,然後就每個pixel去執行一次fragment shader,來決定到底在這個pixel上畫甚麼顏色。
Fragment shader其實位於比Screen space更後的位置。雖然Screen space中決定了每個vertex在螢幕中的位置,但我們還沒把它們轉成pixel去針對每個pixel做一些處理。因此中間還需要經過Triangle Setup(三角設定,負責計算三角邊的像素坐標)和Triangle traversal(三角檢查,負責檢查每個pixel是否被一個由vertex組成的triangle mesh覆蓋)後,知道了哪些pixel(準確點叫fragment或potential pixel,因為我們還沒知道那個pixel實際上會否被畫上去)是在這個物件下的,然後就每個pixel去執行一次fragment shader,來決定到底在這個pixel上畫甚麼顏色。