Games101:MVP Transformation
Games101:MVP Transformation
我們的最終目的是將三維的世界顯示在二維的螢幕上,在現實生活中我們可以透過相機拍照來達到這個效果,而在電腦內我們就要試著把這個過程變成一個轉換寫出來
首先我們要決定場景、物品要擺在哪裡,這個我們稱為 model transformation。 這會將物體從自己的座標系轉換到世界座標系(世界空間)中
接下來要找一個好的位置,橋一個好的角度來放相機,這個我們稱為 view transformation。 這會將物體從世界空間轉換到相機空間(camera space),也有人稱之為視圖空間(view space),是以相機為原點的座標系
這兩步完成後我們就可以準備拍照了,所以要將三維的東西投影到二維,這個動作我們稱為 projection transformation。 這會將物體從相機空間轉換到剪裁空間(clip space)中,其會將相機視角外的物體裁剪掉,並保留深度的效果
下面這張是圖來自 Learn OpenGL:
這三個轉換我們常合起來簡稱為 MVP 轉換。 而前兩個變換通常會一起做,也因此會被合稱為 model-view transformation,接下來我們就來詳細看一下這三個轉換
Model-View Transformation
先來看 model-view transformation,我們的目的是要生出一個以相機為原點的座標系。 首先先講 model transoformation,由於 model transformation 一般來說比較隨意,所以老師也沒多提,這邊補充一下
model transformation 取決於你想把物體放到世界座標系的哪裡,同時套用你要做的旋轉跟伸縮即可,因此你可以用上一章學到的三種矩陣來組合而成,直接給大家看 code 可能比較好理解:
glm::mat4 GameObject::calculateTransformMatrix_() const
{
glm::mat4 t = glm::mat4(1.0f);
// Apply transformations in the order: Scale -> Rotate -> Translate
t = glm::translate(t, position);
t = glm::rotate(t, glm::radians(rotationDeg.x), glm::vec3(1, 0, 0));
t = glm::rotate(t, glm::radians(rotationDeg.y), glm::vec3(0, 1, 0));
t = glm::rotate(t, glm::radians(rotationDeg.z), glm::vec3(0, 0, 1));
t = glm::scale(t, scale);
return t;
}
這是我自己引擎裡面更新 model matrix 的函式,基本上就是把 Identity matrix 依序乘上平移、旋轉、伸縮矩陣就可以了。 這裡面的參數串接到了 ImGUI 的介面上,所以在引擎裡面是可以用拉桿之類的元件調整模型位置的
那接下來我們就繼續來看 view transformation,首先思考一下,該如何在電腦裡面擺放一個相機,這需要三個資訊:
- Position
相機的位置 - Look-at / gaze direction
這個相機面向哪裡,往哪邊看 - Up direction
相機的上下,這很重要,例如閃光燈要裝在相機上方,拿的時候閃光燈要在上面,不能拿反,有時候我們會也會將相機旋轉 45 度之類的。 這個我們稱為 Up direction,用一個向量表示向上方向
如此一來我們就可以把相機給定義下來了,那要這些東西幹嘛呢?
這邊要先提一個問題,在現實生活中當大家坐在車上,如果不往窗戶外面看,是感覺不到自己在移動的,這在物理上稱為相對運動,大家肯定都學過或聽過
那在我們相機的觀察上面也是同一個道理,假如大家是在攝影棚裡拍照,那相同的人與相機,只要相對位置相同,不管實際上在哪個攝影棚拍,拍出來的效果都是一樣的
這個現象可以幫助我們得出一個結果,要達到相機看起來在移動的效果,我們是可以透過改成移動周圍的物體來達到相同目的的,只要保證他們之間沒有相對運動就行,這樣我們就可以讓相機固定在同一位置上了
在習慣上,我們會將相機移動到

這裡用的是右手坐標系,X 外積 Y 為 Z 方向
那就開始操作看看,繼續沿用前方的符號,假設相機原本是在點
步驟很簡單:
- 將相機平移到原點
- 將
旋轉到 方向 - 將
旋轉到 方向
這樣做完後自然而然 X 方向也就對上了,這就是我們的基本思路

我們可以將 model-view transformation 的矩陣記為
對於平移,很簡單的就是將相機原本的位置
重點在旋轉,這裡其實不好寫,你要將任意軸給旋轉到一個規範化的軸上,這個計算很複雜。 但是反過來寫卻很好寫:先將
所以我們先將
隨便拿個方向看一下效果,例如
的確可以轉到
如此一來我們就非常輕鬆地將這個矩陣寫出來了,但我們想要的是把
這時就要再用到一個很重要的性質了,旋轉矩陣是正交矩陣! 因此
換句話說我們只要再將這個矩陣轉置就可以得到我們想要的矩陣了:
接著只要把它與 model matrix 組合起來就可以了。 至此,我們就成功完成 model-view transformation 了
Projection Transformation
將相機擺好後我們就要將相機面向的東西全部投影到感光元件上,換句話說我們要將面前 3D 的東西全部投影到一個 2D 平面上,這個 3D -> 2D 的轉換就是 Projection Transformation 在做的事情
我們有兩種不同的投影方式:
- Orthographic projection (正交投影 / 平行投影)
- Perspective projection (透视投影)
虎書裡面給了一個比較不直觀的例子:

左邊和右邊分別使用了兩種不同的投影方式,你可以看到一個現象,立方體不同的面上有不同組的平行線,一個面由兩組不同的平行線組成
左邊這個立方體的平行線看上去還是平行的,但右邊的立方體就不是了,如果你延長任意一組平行線,你會發現最終它們會相交於某一個點上
如果你學習過素描,或是畫畫,就會知道說右邊的這種投影方式更接近人眼的成像,它會有一個性質:看到的平行線不再平行,最終都會相交到某一個點去,也因此其可以反映近大遠小的特性
那現在就來看看在數學上要怎麼說這件事情,所謂的透視投影,我們可以認為是把相機放在某一個位置,並近似的將相機認為是一個點,再從這個點連出一個空間中的錐
這是一個四稜錐,在這個四稜錐裡面,我們把近平面到遠平面的區域內的所有東西都顯示出來,並畫在近平面上
而對於正交投影,其實就是假設相機離的無限遠。 以我們上面透視投影的例子來說,將相機拿得越來越遠,近平面與遠平面看起來就會越來越接近,當我們把相機拿到無限遠時,就會發現近平面與遠平面變的一樣大了。 因此在投影出來的結果就會看到,無論物體有多遠,投影到近平面上是不會有近大遠小的效果的
Orthographic projection(正交投影)
那我們就從正交投影開始講,其非常好理解,只要不管遠近,統一將物體擠到某個平面上去就可以了,那這要怎麼做呢?

假設相機已被放在了原點,往
上圖中可以看到有一個三維空間中的字母 E,還有一個小方塊,它們兩個在不同的方向上,如果我們直接把 Z 丟掉,只剩下 X 跟 Y,那得到的結果就是上圖中右下角的結果了。 但這時會有個問題,也就是無法區分物體的前後,因此實際的步驟並不會僅僅是將 Z 丟掉,而是再更複雜一點
此外,我們還需要做個約定俗成的操作:把所有物體都移到

我們只需要定義立方體的左右在 X 軸上是多少,上下在 Y 軸上是多少,遠近在 Z 軸上各占多少的範圍,就可以將這個立方體給描述出來了
我們的目的是將最左方的立方體映射為最右方的標準立方體,英文叫 canonical cube。 可以看到中間多了一個步驟,要先將立方體的中心移到原點,然後把 XYZ 軸都拉伸到
Tips
在 X 軸上我們定義左比右小,Y 軸上定義下比上小,但對於 Z 軸的遠近會有點不一樣,仔細看圖中的 Z 是向外的(出螢幕方向),我們是看向
要注意你用的是哪種座標系,這裡我們用的是右手系所以會有這個問題,如果是左手系,在這點上會比較方便,但左手系的 X 外積 Y 不再等於 Z,因此我們還是偏好右手系
我們現在就來把這個變換寫成數學的形式,用矩陣來做:
右邊的矩陣將立方體平移到中心,左邊的矩陣則將立方體伸縮為長度為 2 的立方體
Tips
- 立方體的中心為各組頂點相加除以二
- 平移矩陣內填的是負數是因為要將立方體移到原點
- 伸縮為長度為 2 的立方體是因為目標立方體覆蓋各軸的
至
Perspective projection(透視投影)
理解的正交投影後,透視投影並不難,透視投影是用得最廣泛的一種投影,如前面所說的它滿足近大遠小的性質,帶來平行線不再平行的視覺效果
在說透視投影之前,我們要再回憶一下齊次座標,它有一個定義,向量
回到透視投影,它是由一個點開始往外延伸出來的四稜錐所形成的,這個形狀和長方體的差別在於遠平面相對大一點:

所以我們要做的事情基本上有兩步:
- 把左邊區域中的線拉成右邊的樣子
- 進行正交投影
至於第一步要怎麼拉呢? 我們把遠平面及中間的區域往裡面擠,就可以把四角錐擠成長方體的形狀了,在這個過程中有三點要注意
- 近平面上的點座標不變
- 遠平面上的點 Z 值不變
- 每個平面中心點的 x, y 座標不變
現在就開始擠它,我們從側面來看這個四角錐的話它長這樣(省略下面部分):

我們想知道對於任何一個點
再來透過這個側面的視圖,我們可以發現近平面形成的三角形,與遠平面形成的三角形是相似三角形。 因此我們可得
按照這個關係,對於任意 Z 值,我們都可以算出對應平面的比值,因此任意一個點的
我們可以將擠壓這個變換以矩陣
矩陣內可見
因此
現在回頭來處理
- 近平面上的點座標不變
- 遠平面上的點 Z 值不變
- 每個平面中心點的 x, y 座標不變
這剛好告訴我們近平面和遠平面上的點 Z 值不會變,因此我們可以將這兩個平面上的點代入剛剛的關係:
將近平面上的頂點
這個
也就是
此時我們再將遠平面的中心點
Tips
這邊為了避免符號的問題將遠平面的 Z 值由符號
將這個聯立方程式解開後我們可得:
至此,我們便能將這個矩陣填完了:
利用這個矩陣,我們就能將透視投影的四角錐擠成一個長方體了,這之後再繼續做正交投影即可
因此,若將透視投影的變換寫為矩陣
乘出來的結果為:
如果是 OpenGL,由於其 NDC 使用左手系,因此長的會不一樣:
可以到這邊看更多:https://www.songho.ca/opengl/gl_projectionmatrix.html
FOV of Frustum
這邊補充一下我們如何定義一個四角錐,前面提到立方體可以用 X、Y、Z 軸的覆蓋,也就是六個數字去表示一個立方體
而要定義一個四角錐其實也很簡單,我們從相機出發,看向某一個區域,如果假設我們看到的就是近平面,那麼我們可以給近平面定義一個寬度和高度,就好像人在看螢幕一樣

我們可以給螢幕定義一個寬高比,這被稱為 aspect ration,值為寬度除以高度。 另外還有一個概念,如果有玩過相機的可能會知道,叫做視角,英文叫 field of view,表示可以看到的角度範圍
以上圖為例,我們可以在螢幕的中間畫出兩條紅線來,各連到上下兩邊的中心點,而這兩條紅線所夾的角度就被稱為視角
有了這兩個概念我們就可以來定義四角錐了:

左上角的右邊那條線為近平面,與相機的距離為
Viewport Transformation
到目前為止,MVP 就已經做完了,無論用的是正交投影還是透視投影,此時所有的物體就都已經被擺到了 -1 ~ 1 的立方體裡,下一步我們需要將它畫到螢幕上
因此我們需要先把螢幕的概念定義好,在圖學中螢幕被抽象的認為是一個二維陣列,陣列的每個元素是一個像素,假設你的螢幕是 1920x1080
的,那就表示有這麼多的像素形成了一個二維陣列
換句話說螢幕可以被表示成一個坐標系,這個坐標系我們通常稱其為螢幕空間(screen space),虎書內有自己的一套坐標定義方式,而這邊(GAMES101) 我們有另一套定義方式:

螢幕的左下角為原點,向右是 0 ~ width-1
,高度為 0 ~ height-1
雖然我們是用整數的坐標來描述它,但你可以很明顯地看到像素的中心,以藍色的像素為例,是
到這邊就可以來表示螢幕空間了,我們現在有一個 -1 ~ 1 的立方體,要把它轉換到 0 ~ width
乘以 0 ~ height
上。 我們知道 -1 ~ 1 的立方體寬度和高度都為 2,因此首先將其除以 2,再乘以對應的 width 和 height,這就是所謂的 Viewport Transformation:
記得這會先做拉伸再做平移
左上角的
Tips
你可能會想到一個問題,