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

2018年10月20日 星期六

Unity上常用的基本矩陣計算 (Matrix)

常見的Matrix種類

Identity Matrix單位矩陣,簡稱I):左上到右下的對角線是1,其他都是0,MI = IM = M

Transposed Matrix轉置矩陣,用上標T表示):把Matrix的Row變成Column,Column變成Row地反轉,(AB)^T = (B^T)(A^T)

Inverse Matrix反矩陣,用上標-1表示):M * M^-1 = M^-1 * M = I

Orthogonal Matrix正交矩陣):M * M^T = M^T * M = I ,即 M^T = M^-1

判斷一個matrix是否orthogonal,一般教科書都是直接用其定義來判斷,即計算Matrix乘以自己的transposed matrix是否等於I。但是在寫遊戲程式的時候,計算matrix相乘其實會用很多時間,因此我們想用一個更快的方法來做判斷:

把orthogonal matrix拆成每個每個column來檢查的話,會發現它們都是unit vector(因為這些vector和自己的dot product是1),而和matrix中除自己外的其他column是垂直的(因為dot product是0)。因此我們在用vector們建構一個matrix的時候,已經可以透過vector本身的特質來判斷建構出來的matrix是否orthogonal。

(參考:How to identify an orthogonal(orthonormal matrix)?

這一特點在做座標轉換的時候非常有用,因為我們一般都是使用正交座標(即xyz axis互相垂直),而由這三個axis的vector形成的matrix就是orthogonal的。如果我們知道一個matrix是orthogonal的話,那麼當我們想要計算它的inverse時,就可以直接用它的transpose,可以減少很多繁複的計算。

另外,多於一個orthogonal matrix互相相乘的結果也是orthogonal。

(參考:Why is the matrix product of 2 orthogonal matrices also an orthogonal matrix?

Vector和Matrix相乘的先後次序

兩個Matrix如果相乘的先後次序不一樣,結果也會不同(即AB =/= BA)。如果我們需要對一個Vector進行transformation,那麼究竟是要用row Vector乘以matrix,還是用matrix乘以column vector呢?在Unity中一般是matrix乘column vector,不過如果涉及多個matrix相乘時我們可以做一點optimization讓相乘更快:

例如我們要把一個vector v做三次transformation:
CBAv

如果從左至右開始計算(即((CB)A)v),那麼每次乘完後的matrix都會很大(不止一個column),但如果從右邊開始計算(即(C(B(Av))),那麼每次乘完的matrix都只有一個column。

你也可以把ABC做transpose,這樣就能把v放在左邊了:
v(A^T)(B^T)(C^T)

到底是橫列直行還是橫行直列?

經常會看見「列矩陣」和「橫矩陣」這兩個詞,但「列矩陣」到底是指大小1 x n(打橫n個數字)還是 n x 1(打直n個數字)的matrix?

我發現不同的教材和討論之間有分歧,似乎在不同地區或者社群中對於列是指橫還是直都有不同的定義。我甚至見過在同一份教材中列矩陣有時是指橫、有時是指直的情況。在英文中就不會有這個問題,Row從來都是橫的,Column永遠是直的,Row Matrix就是橫著三個數字(1x3)。

大概只能根據上下文中的例子來推斷到底該文章是使用橫列直行抑或橫行直列了。

Matrix Transformation (矩陣轉換)的種類

在遊戲中我們經常要對vector進行轉換(transform),例如移動或者縮放,又或者是轉到另外一個世界,最容易做轉換的方式就是用一個matrix來乘以這個vector(也就是單靠這個matrix已經能表達出我們想做的轉換),這種方式能做到大部分我們想做的效果。

Linear Transformation(線性轉換)

線性轉換的定義是符合下面兩個特性:
Vector加:  f(x)+f(y)=f(x+y)
Scalar乘:  kf(x) = f(kx)

線性轉換只需要和vector同一dimension的matrix就足以表達(2D遊戲是2x2,3D遊戲是3x3)。

以下列出常見的線性轉換。下列的matrix都是以origin為中心進行轉換,如果你是需要以遊戲物件的中心進行轉換的話,就需要先把vector本身轉成物件的local position再進行轉換,然後再轉回world position。

Scaling (縮放)

matrix的左上到右下的對角線上的數字為縮放比例k
[ kx 0 0
  0 ky 0
  0 0 kz]

Rotation(旋轉)

以x axis為中心逆時針旋轉:
[1    0     0
 0  cosθ  -sinθ
 0  sinθ  cosθ]

以y axis為中心逆時針旋轉:
[cosθ   0  sinθ
   0      1     0
 -sinθ  0  cosθ]

以z axis為中心逆時針旋轉:(一般來說2D遊戲中的旋轉都是用這個)
[cosθ    -sinθ    0
 sinθ      cosθ    0
   0           0       1]

要點:

  • Rotation matrix都是orthogonal的。
  • 這些matrix的inverse就是向順時針方向旋轉同樣的角度。
  • 如果需要進行多於一個axis的旋轉,要留意Unity定義的預設旋轉順序是MzMxMy

Shear(錯切,例如把一個長方形扭成平行四邊形)


Mirroring/Reflection(鏡像)


Orthographic projection(正交投影)


Translation Transformation(平移轉換)

簡單來說translate就是把一個物件從一個位置移動到另外一個位置。

以公式表達的話一般是這樣:
f(x) = x + c
因為有constant的存在,所以並不符合線性轉換的定義。

想用一個matrix來表示translation的話,需要比原本的vector多一個dimension(2D遊戲需要3x3,3D遊戲需要4x4)。

參考:Why do 2D transformations need 3x3 matrices?

Affine Transformation(仿射轉換)

Affine其實就是包括了線性和平移的轉換,即是一個matrix就能同時表示兩種轉換(當然也可以只表示一種)。
既然包含了平移轉換,那就需要比vector多一個dimension的matrix,然後在相乘之前,把vector也擴充到相同的dimension(我們稱為homogeneous coordinate,齊次座標)。我們把多出來的那個dimension的數叫做w,即整個vector是(x,y,z,w)。

首先要看看該vector是哪一種vector:
位置(點):把w設為1(代表以該點為中心進行轉換)
方向:把w設為0(代表以origin為中心進行轉換)

而matrix的結構如下:
[ M 3x3   t 3x1
   0 1x3    1      ]
M代表線性轉換,t則代表平移(試試乘一下就能發現t單純是加在位置vector上,而對方向vector就不會有任何影響)
如果不要做線性轉換,把M設成Identity Matrix即可
如果不要做平移轉換,把t設成0即可(又或者只做3x3的matrix multiplication)

在此結構下,可以用一個matrix便能表達數次複合的轉換。一般來說按「縮放→旋轉→平移」的次序進行轉換最不容易出錯。如果不按這個順序的話可能會讓後面的轉換影響到前面的轉換(例如先平移再縮放的話,會導致原本平移的距離也一起被縮放,容易讓人混淆並質疑是不是真的要這樣做)。(參考:What is the correct order to multiply scale, rotation and translation matrices for a proper world matrix?

例如想對一個vector v先縮放再旋轉再平移,那麼要計算轉換後的vector v'的公式就是:
v' = (Mtranslation)(Mrotation)(Mscaling)v

因為Unity是使用column vector,要放在最右邊,所以公式也要從右開始寫。
而把 (Mtranslation)(Mrotation)(Mscaling)這三個matrix相乘便能得出一個結合了三次轉換的matrix。




2018年10月13日 星期六

Unity上常用的基本向量計算 (Vector)


甚麼是Vector(向量)?簡單來說就是幾個代表了各自維度的數字的組合,例如2D世界的Vector就是(x,y)兩個數字,3D世界的Vector就是(x,y,z)。不過比起這些數字上組合,數學上對待Vector比較重視其magnitude(模)和direction(方向)這兩個特點的組合。

在一個遊戲世界的coordinates中,一個遊戲物件通常會有位置方位,而這兩個要素都可以分別用Vector表達。在Unity中寫程式時,假如是2D遊戲的話,就用Vector2這個datatype來表達,3D遊戲則是用Vector3。雖然位置看起來似乎並不是一個向量(因為它不是方向),但你可以想像它是由原點(0,0,0)指著遊戲物件所在的位置,而Unity為了方便計算所以統一把位置和方向都用Vector來表達,然後提供了許多方便你做Vector計算的函數庫。不過要留意之後在做Matrix Transformation的時候,對於位置Vector和方向Vector的處理方式會不太一樣。

Magnitude (模)

Vector的magnitude其實就是它的長度。要計算兩個物件之間的直線距離的話,就要相減它們的位置(先後次序無所謂),得出一個Vector(代表從其中一個物件指向另外一個物件),再計算這個Vector的magnitude。

Vector V的magnitude的計算方法: |V| = sqrt(Vx^2 + Vy^2 + Vz^2)  (如果是2D就不用加Vz^2,之後的公式也是)

其實這個公式就是來自勾股定理(Pythagorean theorem),也能看得出Magnitude不會有負數。

Unit Vector/Normalized Vector (單位向量)

Unit vector(又叫Normalized vector)是指magnitude只有1的vector,可以說這個unit vector只有direction而沒有magnitude。只要把一個Vector內的每個值除以它的magnitude,就能得到Unit vector了,這個過程叫做Normalization(歸一化)。Unit vector是其中一種表達物件方向的方法(尤其在3D世界內難以用角度來表達方向)。

要判斷一個vector是否unit vector,最快的方法是看其和自己的dot product是否1(下面會詳細講解)。

Dot Product (點積)

Dot product的計算方法: A.B = AxBx + AyBy + AzBz
可以看得出兩個Vector的Dot product只是一個數而非Vector。

一個常用的角度公式:
A.B = |A| |B| cos (A和B的夾角)

Dot product的常見用途:

計算投影

一個Vector A和一個Unit Vector B的Dot product代表了A在B這個方向上的投影的長度。

另外,一個Unit vector和自己的dot product是1(可以想像兩個一模一樣的vector出來的投影肯定也和自己一樣),因此要判斷一個vector是否unit vector最快的方法是計算dot product是否1(而非使用浪費時間的sqrt來計算vector的magnitude)。

比較方向

如果兩個Vector的Dot product大於0,代表這兩個Vector間的角度小於90度(我稱為指著大致相同方向)。
如果等於0,代表兩個Vector互相垂直。
如果小於0,代表兩個Vector的角度大於90度(我稱之為相反方向)。

判斷視線範圍

在很多遊戲中,一個怪物會有牠的視線範圍,當你步入了這個範圍中牠就會攻擊你。一個常用的視線範圍判定方法是:以怪物面向的方向為一個Vector(這個Vector的長度代表了怪物能觀察的距離),然後設定觀察角度(即怪物只能看到Vector左邊角度除以2和右邊角度除以2內的物件)。

那麼要如何判定玩家是否在怪物的視線範圍內?條件為以下兩項同時符合:

  • 觀察距離:玩家和怪物間的距離 <= 怪物面向Vector的magnitude
  • 觀察角度:怪物指著玩家的Vector 與 怪物面向Vector間的夾角 <= 觀察角度/2
如何計算上述提到的夾角?可以利用上面提到過的角度公式:

設「怪物指著玩家的Vector」為A,「怪物面向Vector」為B
(留意A的magnitude其實就是玩家和怪物間的距離)
A.B = |A| |B| cos (A和B間的夾角)
夾角 = arccos (  (A.B) / (|A| |B|)   )

另外要注意,怪物指著玩家的Vector是 玩家的位置 - 怪物的位置 (而非相反)
(如果你覺得很容易搞混相減的次序,試想想從怪物指著玩家,即是怪物是原點,這樣就知道是要玩家減怪物了)

Cross Product (叉積)

兩個Vector的Cross product是一個新的Vector,而非像dot product般只是一個數。

Cross product的計算方法:A X B = (AyBz - AzBy, AzBx - AxBz, AxBy - AyBx)
(尤其注意y點是先z後x,而非先x後z)

另外,A X B = -(B X A)
(從公式就能看得出,因為減法次序的改變,所以A X B和B X A的關係是正負相反)

一個常用的角度公式:
|A X B| = |A| |B| sin (A和B的夾角)

Cross product的用途:

3D遊戲:計算垂直Vector

在3D遊戲的情況下,A X B得出來的Vector會同時和A和B垂直(若A和B平行的話會得出0,0,0)。
能和A和B同時垂直的方向有兩邊,那麼怎麼知道這個Vector是指著哪邊?
Unity內的Scene View用的是Left-hand rule,在Left-hand rule下A是拇指、B是食指的話,A X B就是中指指著的方向。

若你想要的是相反方向的垂直Vector,使用B X A計算或者直接用-(A X B)即可。

假如我們需要知道3D世界內一個三角形的平面對著哪個方向,可以用三角形的三個邊的其中兩個作為Vector,便能計算出與這個平面垂直的Vector。

2D遊戲:計算面積

在2D世界的情況下,A X B得出來的Vector的magnitude是A和B形成的平行四邊形的面積,除以二的話就是三角形的面積。

2018年10月8日 星期一

在CentOS 7上設立SFTP server

想必大家都有聽過FTP (File Transfer Protocol),能讓你像檔案總管一樣瀏覽伺服器上面放置的檔案。但FTP本身是個純文字的協議,並沒有任何加密功能,所以傳送的內容很容易會被人輕易看到。而FTPS則是FTP的延伸,幫FTP加了一層TLS/SSL加密,不過本身需要開多個port分別負責驗證、傳送檔案等不同的功能。

SFTP則是另一套完全不同的傳輸協議。它的作用雖然和FTP一樣能讓你瀏覽伺服器上的檔案,但技術上的協議內容運作模式完全不一樣(使用了更加節省bandwidth的binary protocol),因此一個程式支援FTP或FTPS不代表它支援SFTP。由於SFTP是經SSH(兩部電腦之間加密通訊的協議,一般用於遠端控制Linux主機)運行的,所以我比較喜歡以SSH File Transfer Protocol作為SFTP的全名,更能反映SFTP的特性。

如果想要設立SFTP伺服器的話,建議在Linux上設立,因為Windows對於SSH的支援比較差,一般需要安裝額外軟件,連帶一堆麻煩的設定,而Linux則對SSH和SFTP均有原生的支援,一般來說不必安裝額外軟件,只要小心設定便可。

這次我們以CentOs 7作例子安裝SFTP伺服器。一般來說CentOs 7安裝的時候會自動帶有SSH功能,如果不肯定可以用以下Command檢查一下:

rpm -qa|grep ssh

(-qa代表query all,列出所有已安裝的package,grep ssh則是列出帶有ssh這三個字的package)

看到有libssh2、opeenssh(包括server和clients)基本上就沒問題了。

接下來我們會建立一個只能存取SFTP的user:(記得把myusername改成自己想要的賬戶名稱。)

groupadd sftpusers
useradd -g sftpusers -d /upload -s /sbin/nologin myusername
passwd myusername

(-g sftpusers代表把user加到這個sftpusers群組內)
(-d /upload代表設立user的main directory為/upload)
(-s /sbin/nologin代表把user加進nologin這個shell,讓他無法登入這部電腦,確保只能存取SFTP)

然後我們可以建立這個user專用的資料夾,並設定權限:(同樣記得把myusername改成自己想要的賬戶名稱)
mkdir -p /data/mysftpuser/upload
chown -R root:sftpusers /data/myusername
chown -R mysftpuser:sftpusers /data/myusername/upload


最後,在/etc/ssh/sshd_config這個設定檔下新增一些設定:
Match Group sftpusers
ChrootDirectory /data/%u
ForceCommand internal-sftp

(Match Group sftpusers代表下方的設定套用到sftpusers群組身上)
(ChrootDirectory代表更改user的root directory,讓他只能使用我們指定的目錄)
(ForceCommand internal-sftp代表啟用SFTP subsystem)

最後重啟SSH:

service sshd restart

安裝完成,你可以直接試試打sftp來試試連接自己電腦的SFTP server,或者用FileZilla之類的SFTP client軟件來連接。

參考資料:
https://www.howtoforge.com/tutorial/how-to-setup-an-sftp-server-on-centos/