OpenGL 程式設計/3D/矩陣
構建變換矩陣背後的概念很容易理解,但矩陣究竟代表什麼?本節介紹了一種機制,用於解釋包含多個平移、旋轉和縮放的複合變換矩陣,並將其解釋為參考系之間的轉換。它提供了一個模型,用於將基本變換組織成巢狀參考系的層次結構。
我們通常認為物體是“向前和向後”,“向上和向下”或“向左和向右”移動;我們通常不會考慮它們相對於任意參考系的運動。它們“轉動”(偏航)、“抬頭”(俯仰)和“翻滾”(滾轉)、“向前移動”、“橫向移動”和“向上跳躍”。那麼我們如何組合變換來建立行為上更像現實世界中物體的物體呢?
讓我們將物體的參考系定義為其位置和方向。這可以透過兩個正交向量來唯一定義,它們指定了物體的“前進”和“向上”方向(暗示著第三個,“向右” == 前進叉乘向上),以及一個指定了物體位置的頂點。
當子參考系的引數由父參考系的座標中的向量/頂點指定時,我們可以透過建立由四個列向量(向右、向上、向前、位置)組成的矩陣,來建立一個將向量從子參考系變換到父參考系的矩陣,其中向右 == 前進叉乘向上。
讓我們更具體一些。
我們首先看看單位矩陣
根據我們的定義,我們將第一列 ({1,0,0,0}) 解釋為“向右”方向,第二列 ({0,1,0,0}) 解釋為“向上”,第三列 ({0,0,1,0}) 解釋為“向前”(或者“向後”,如果你願意),第四列 ({0,0,0,1}) 解釋為“位置”頂點。請注意,位置頂點的 w 座標為 1,而方向向量(實際上是兩個頂點之間的差)的 w 座標為 0。
這個例子看起來微不足道,但它為我們提供了基礎。我們可以稱其為“宇宙”參考系。向右為 +x,向上為 +y,向前(或向後,如果你願意)為 +z,原點位於 {0,0,0,1}。
現在,我們可以看看宇宙中子物件的示例。我們可以透過矩陣來指定該物件的參考系
其中向右、向上和向前是參考系的歸一化方向向量,位置是參考系的位置頂點,在父(或“宇宙”)物件的參考系中指定。當所有方向的“w”座標為 0,而位置頂點的“w”座標為 1 時,這最容易理解,就像“四維技巧”約定一樣。
為了演示:我們在 framechild 的座標中有一個頂點 v = {2, 0, 0, 1}。將 framechild 乘以 v,我們得到
請注意,當 rightw = 0 且 positionw = 1 時,2*rightw + positionw = 1。我們可以看到,framechild 中的頂點 v 等價於 2*frameright + frameposition,這正是 v 在父參考系中的描述。還需要注意的是,在方向不帶位置 (w=0) 的情況下,會應用旋轉,但不會累積位置 - 方向會被重新定向,而不會錯誤地分配位置。
每個 "frame" 矩陣都是從指定座標所在的參考系到父物件的參考系的變換,最終我們得到
frameparent*framechild*framenested_child*vertex_in_nested_child_coords == frameparent*framechild*vertex_in_child_coords == frameparent*vertex_in_parent_coords == vertex_in_universe_coords
這個矩陣 (frameparent*framechild*framenested_child) 在與宇宙的參考系矩陣後乘後,將把座標從 nested_child 的座標系轉換為宇宙的座標系。這些參考系可以無限巢狀。
參考系層次結構示例
universe -> galaxy -> solar system -> earth -> position on earth
每個參考系的運動可以被認為是相互 "獨立" 的。當銀河系運動時,太陽系會隨之運動,地球也會隨之運動,而你在地球上的位置也會隨之運動。
縮放操作
[edit | edit source]雖然作者認為應該儘量避免縮放操作,因為它們會破壞頂點法線,但有時還是需要它們。幸運的是,這很容易實現。讓我們考慮縮放變換
| αx 0 0 0 |
scale_xform = | 0 αy 0 0 |
| 0 0 αz 0 |
| 0 0 0 αw |
其中 αw == 1,除非你瘋了。
通常,我們希望將頂點作為執行的第一個操作進行縮放;當我們將 "x" 縮放 3 倍時,我們通常不希望縮放其方向和位置(即變換矩陣的列),因為它們是在 *父* 參考系中指定的。因此,縮放是 "參考系" 變換模型中附加到矩陣的最後一個操作。
當我們將 framechild 乘以 scale_xform 時,我們得到
| αx*rightx αy*upx αz*forwardx αw*positionx |
final_xform = | αx*righty αy*upy αz*forwardy αw*positiony |
| αx*rightz αy*upz αz*forwardz αw*positionz |
| αx*rightw αy*upw αz*forwardw αw*positionw |
縮放因子只是作為方向向量的幅度出現。
"Camera" 變換
[edit | edit source]FIXME: 我只證明了這一點,但沒有演示它是否有效。
從宇宙的參考系到相機的參考系的變換與上面描述的變換存在細微但至關重要的差異。與之前的討論不同,"相機"(*父* 參考系)通常是在 "宇宙"(*子* 參考系)座標中指定的。使用相機在宇宙中自由移動的傳統邏輯需要額外的考慮。
為了在宇宙和相機參考系之間轉換,我們需要建立一個 "固定" 座標集,它將 "宇宙" 表示為 "相機" 的子系;我們需要在相機座標系中得到宇宙的參考系。
FIXME: 急需插圖。正在思考這個問題
- 如果相機與宇宙的方向相同:不進行變換。
- 如果相機向左傾斜,則宇宙需要向右傾斜。
- 如果相機向上傾斜,則宇宙需要向下傾斜。
- 如果相機與宇宙的方向相反,則宇宙需要朝另一個方向。
- 如果相機相對於宇宙是顛倒的,則宇宙需要翻轉。
這些是反射的特徵;整個 "相機" 參考系矩陣是相機 '宇宙' 座標關於宇宙軸的反射的列向量組合。定義
c = camera reference frame in universe coordinates u = universe reference frame (identity) refl(a, b) = reflection of a over b = 2*dot(a,b)*b - a:
我們可以將相機的參考系矩陣構建為
framecamera = { refl(cright, uright), refl(cup, uup), refl(cforward, uforward), refl(cposition, uposition) }
有趣的是,這包括位置頂點,它被 "反射" 到原點。
這適用於所有 "父" 參考系儲存在 "子" 參考系座標中的參考系。作者無法想到其他任何此類情況。
投影變換
[edit | edit source]投影變換應該只用於將相機擬合到視窗。glOrtho*()、glFrustum() 和 gluPerspective() 通常用於實現此目標。
"相機" 框架 - 無論如上所述指定,還是透過 gluLookAt() - 通常將是模型檢視矩陣堆疊上的 "底部" 框架。該矩陣將宇宙座標轉換為相機座標。投影矩陣在乘法順序中位於模型檢視矩陣之前,因此投影矩陣可以被認為是從相機座標到螢幕座標的變換。
使用 "相機" 框架與使用 gluLookAt() 沒有區別(FIXME:檢查一致性),但這兩個操作的引數化略有不同。
總結
[edit | edit source]透過將變換矩陣視為由四個列向量 { c0, c1, c2, c3 } 組成的參考系,我們可以將矩陣分解成更易用的屬性
- "右" 軸:c0.direction()
- "上" 軸:c1.direction()
- "前" 軸:c2.direction()
- "位置":c3
- "x" 縮放:c0.magnitude()
- "y" 縮放:c1.magnitude()
- "z" 縮放:c2.magnitude()
- "w" 縮放:c3.magnitude()
我們可以使用這些屬性更直觀地實現運動
- 向前移動距離 β:xform += { 0, 0, 0, β*c2 }
- 向上移動距離 γ:xform += {0, 0, 0, γ*c1 }
- 向左轉 30 度:xform *= rotation_matrix(axis = c1, angle = π/6.0)
等等。
我們可以透過簡單地將參考系矩陣(如上所述構建)乘以當前矩陣,從 "當前" 參考系(即在 OpenGL 上下文中由模型檢視矩陣表示的參考系)轉換為 "子" 參考系。完整的繪圖操作可以透過以下方式執行
glMatrixMode(GL_MODELVIEW);
glLoadMatrixd(camera_refframe_matrix);
glPushMatrix();
glMultMatrixd(object_refframe_matrix);
draw_object();
glPushMatrix();
glMultMatrixd(subobject_refframe_matrix);
draw_sub_object();
glPopMatrix();
glPopMatrix();
glFlush();