2018年10月28日 星期日

座標空間的轉換

座標轉換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的填法一般為:

[  |     |    |     |
  XYc 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的來歷有興趣的話可按此

正交matrix

[  1/(aspect*size)    0           0                             0
    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)。

透視matrix

[   1/(aspect * tan (FOV/2))    0                                0                                            0 
      0                                        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。

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)。

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)。

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上畫甚麼顏色。

沒有留言:

張貼留言