原本打算直接寫教程 04 的,可是想到3D 變換涉及的數學知識較多,每每是不少初學者的攔路虎(好比我本身)。再加上OpenGL ES 2.0 再也不提供OpenGL ES 1.0中 3D 變換相關的一些重量級函數,如 glMatrixMode(GL_PROJECTION); glMatrixMode(GL_MODELVIEW); glLoadMatrixf; glMultMatrix 等,這些函數在 OpenGL ES 2.0 中均須要咱們本身去實現。 若是不對線性代數與幾何知識做一些簡單介紹,恐怕很多人難以理解文中的一些步驟爲何要那麼作。所以今天這一篇文章將放棄原定計劃,先來介紹一些 3D 數學以及 3D 變換相關的知識。BTW,原定計劃的代碼示例已經寫好了,有興趣的同窗能夠先行瀏覽,代碼放在這裏,運行效果以下:php
咱們都學過幾何學,應該都知道歐幾里得(公元前3世紀希臘數學家)這位幾何學鼻祖,正是這位大牛建立了歐幾里得幾何學,他提出了基於 X,Y,Z 三軸的三維空間概念。到了17世紀,又出了位大牛笛卡爾,咱們一般所說的笛卡爾座標就是他的創造,笛卡爾座標很是完美地將歐幾里得幾何學理論與代數學聯繫到一塊。正是由於有了笛卡爾座標,咱們纔可以用簡單的矩陣(Matrix)來表示三維變換。但用矩陣來表示三維變換操做有一個沒法解決的問題-萬向節鎖 。什麼是萬向節鎖呢?簡單地說就是兩個軸旋轉到同一個方向上去了,這兩個軸平行了,所以就比原來少了一維(詳情可參考這裏)。過了一百多年,漢密爾頓(Sir William Rowan Hamilton)建立了四元數(quaternion)解決了由於旋轉而致使萬向節鎖的問題,而後四元數還有其餘用處,但在3D數學裏主要是用來處理旋轉問題。html
好吧,或許你看得一頭霧水,沒關係,你只要知道:用矩陣來表示3D變換,但矩陣在表示旋轉時可能會致使萬向節鎖的問題,而使用四元數能夠避免萬向節鎖就能夠了。git
在前面提到可以使用 Matrix 來表示三維變換操做,那麼變換又是如何經過 Matrix 實現的呢?下面就來說這個。在這裏我推薦一本3D數學入門書籍:《3D數學基礎:圖形與遊戲開發》github
一般咱們使用 4 維向量 (x, y, z, w) 表示在3D空間中的一個點,最後一維 w 表示齊次座標。齊次座標的含義是兩條平行線在投影平面的無窮遠處相交於一點,但在 Matrix 中沒有表示無窮大,因此增長了齊次座標這一維。你能夠想象下,火車軌道的兩條邊在無限遠處看起來就相交於一點,齊次座標詳細的介紹能夠參考這篇文章。編程
矩陣運算規則:數組
1) 若矩陣 A 和 B 不是互逆矩陣,則不知足乘法交換律,即 A × B 不等於 B × A;
2) M × N 階的矩陣只能和 N × O 階的矩陣相乘,即 N 的階數相等,結果爲 M × O 階的矩陣;
3) 矩陣 A × B 的運算過程是 A 的每一行依次乘以 B 的每一列做爲結果矩陣中的一行;
4) 矩陣 A 的逆矩陣 B 知足 A × B = B × A = 單位矩陣。
5) 單位矩陣是對角線上的值爲1,其他均爲 0 的矩陣。單位矩陣不影響座標變換(你能夠將下面的3D變換矩陣換成單位矩陣來思考下)。編程語言
3D空間的物體投影到2D平面上時,就須要使用到齊次座標,所以咱們須要使用 4 × 4 的 Matrix 來表示變換。在編程語言中,這樣的 Matrix 可用大小爲 16 的一維數組或4 × 4 的二維數組來表示。因爲矩陣乘法不知足乘法交換律,用數組表示 Matrix 又分爲兩種形式:行主序和列主序,它們在本質上是等價的,只不過是一個是右乘(行主序,矩陣放右邊)和一個是左乘(列主序,矩陣放左邊)。OpenGL 使用列主序矩陣,即列矩陣,所以咱們老是倒過來算的(左乘矩陣,變換效果是按從右向左的順序進行): 投影矩陣 × 視圖矩陣 × 模型矩陣 × 3D位置。ide
4× 4列矩陣的數組表示:數字表示數組下標對應的行列位置:函數
那麼工具
平移矩陣可表示爲:
平移矩陣 × 列矩陣(a, b, c, 1) = 列矩陣(a + x, b + y, c + z, 1)。
縮放矩陣可表示爲:
縮放矩陣 × 列矩陣(a, b, c, 1) = 列矩陣(a × sx, b × sy, c × sz, 1)。
繞 X 軸旋轉的旋轉矩陣可表示爲:
繞 X 軸旋轉的旋轉矩陣 × 列矩陣(a, b, c, 1) = 列矩陣(a, b × cos(θ) - c × sin(θ), b × -sin(θ) + c × cos(θ), 1)。
繞 Y 軸旋轉的旋轉矩陣可表示爲:
繞 Y 軸旋轉的旋轉矩陣 × 列矩陣(a, b, c, 1) = 列矩陣(a × cos(θ) - c × sin(θ), b , a × -sin(θ) + c × cos(θ), 1)。
繞 Z 軸旋轉的旋轉矩陣可表示爲:
繞 Z 軸旋轉的旋轉矩陣 × 列矩陣(a, b, c, 1) = 列矩陣(a × cos(θ) - b × sin(θ), a × -sin(θ) + b × cos(θ), c, 1)。
OpenGL 使用右手規則進行旋轉,所以逆時針方向的選擇是正角度的,而順時針方向的旋轉是負角度的。還記得中學學物理時候的右手規則麼?忘記了的話,看下圖:
注意:
前面說到矩陣乘法不知足乘法交換律,所以你對一個3D座標先進行旋轉,而後進行平移(平移矩陣 × 旋轉矩陣 × 3D座標);與先進行平移,而後進行旋轉(旋轉矩陣 × 平移矩陣 × 3D座標)獲得的效果是大爲迥異的。以下圖所示:
在第一種狀況下,咱們一般稱旋轉是在 local space 中進行,由於它是繞着物體本身的中心點進行的,而在後一種狀況下的旋轉一般稱爲是在 world space 中進行的。咱們知道點是能夠在座標空間之間相互轉換的,這是一個很重要的概念。OpenGL 中物體最初是在本地座標空間中,而後轉換到世界座標空間,再到 camera 視圖空間,再到投影空間,這一系列轉換都是靠 matrix 計算來實現。
上面的這個過程在 OpenGL 及 OpenGL ES 1.0 中,對應的代碼相似於:
glViewport (0, 0, (GLsizei) w, (GLsizei) h); a) glMatrixMode (GL_PROJECTION); b) glLoadIdentity (); glFrustum (-1.0, 1.0, -1.0, 1.0, 1.5, 20.0); c) glMatrixMode (GL_MODELVIEW); d) glClear (GL_COLOR_BUFFER_BIT); glColor3f (1.0, 1.0, 1.0); glLoadIdentity (); /* clear the matrix */ /* viewing transformation */ gluLookAt (0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); e) glScalef (1.0, 2.0, 1.0); /* modeling transformation */ f) glutWireCube (1.0); g) glFlush ();
說明:
a) 是用於viewport(視口)變換,viewport 變換髮生在投影到2D 投影平面以後,該變換是將投影以後歸一化的點映射到屏幕上一塊區域內的座標。視口變換的目的是指定投影以後圖像在屏幕上顯示的區域。以下示意圖所示:
視口變換 glViewport(x, y, width, height); x,y 是投影平面描繪在屏幕或窗口上的起始位置(注意屏幕座標以左上方爲原點),width和height是以像素爲單位,指投影平面在屏幕上描繪的區域大小。若是投影平面的寬高比,與width/height比不相同(如上面的右圖),那麼描繪的場景就會扭曲。
從裁剪到屏幕的整個過程以下圖所示,w 就是前面提到的齊次座標那一維,從 Clip Space 到 Normalized Device Space 就是投影規範化的過程,從 Normalized Device Space 到 Window Space 就是 viewport 變換過程。
該轉換內部計算公式爲:
(xw, yw)是屏幕座標,(x, y, width, height)是傳入的參數,(xnd, ynd)是投影以後經歸一化以後的點(上圖中 Normalized Device Space 空間的點)。所以 viewport 變換就是將投影以後歸一化的點轉換爲真正可用於在屏幕上進行渲染的屏幕座標;
b) 是說明下面的 matrix 是用於投影變換的,在本例中,是經過語句 c) glFrustum 來設置透視投影變換的。投影變換有兩種:正交投影和透視投影,後面會有詳細介紹;
d) 是說明下面的 matrix 是用於模型視圖變換,注意,OpenGL 和 OpenGL ES 都將模型變換與視圖變換結合在一塊兒,而不是分開爲兩個,這是由於模型變換等價於視圖變換的逆變換。視圖變換是將物體轉換到觀察者(通常稱之爲 camera)的視線空間中。你能夠想象一下,照相時,你能夠:A)照相機不懂,旋轉本身的頭找個側面像,也能夠B)本身不動,照相機旋轉必定的角度來達到一樣的效果。下面的兩幅圖分別描述了情形A)和情形B):
情形A):旋轉物體,相機不動
情形B):旋轉相機,物體不動
在 OpenGL 中,咱們在設置場景(scene)的時候一般是採起情形B)的作法,所以在語句 e) 處,咱們設置相機的位置和朝向,來設定視圖變換,以後的語句 f) glScale 是設定在模型變換的,最後語句 g) 在本地空間描繪物體。
注意
寫 OpenGL 代碼時從前到後的順序依次是:設定 viewport(視口變換),設定投影變換,設定視圖變換,設定模型變換,在本地座標空間描繪物體。而在前面爲了便於理解作介紹時,說的順序是OpenGL 中物體最初是在本地座標空間中,而後轉換到世界座標空間,再到 camera 視圖空間,再到投影空間。因爲模型變換包括了本地空間變換到世界座標空間,因此咱們理解3D 變換是一個順序,而真正寫代碼時則是以相反的順序進行的,若是從左乘矩陣這點上去理解就很容易明白爲何會是反序的。
投影變換的目的是肯定 3D 空間的物體如何投影到 2D 平面上,從而造成2D圖像,這
透視投影能夠經過兩種方式來表述,OpenGL 及 OpenGL ES 1.0 提供其中一種: glFrustum,而 glut 輔助庫提供了另一種:gluPerspective。它們本質上是相同的,只不過是不一樣的表述而已:
視錐體/視景體:
glFrustum(left, right, bottom, top, zNear, zFar);
left,right, bootom,top 定義了 near 裁剪面大小,而 zNear 和 zFar 定義了從 Camera/Viewer 到遠近兩個裁剪面的距離(注意這兩個距離都是正值)。由這六個參數能夠定義出六個裁剪面構成的錐體,這個錐體一般被稱之爲視錐體或視景體。只有在這個錐體內的物體纔是能夠見的,不在這個錐體內的物體就至關於再也不視線範圍內,於是會被裁減掉,OpenGL 不會這些物體進行渲染。
因爲 OpenGL ES 2.0 不提供此函數,所以咱們須要本身實現該函數。其計算公式以下:
假設:l = left, r = right, b = bottom, t = top, n = zNear, f = zFar,有
透視圖:
glOrtho(left, right, bottom, top, zNear, zFar);
left,right, bootom,top 定義了 near 裁剪面大小,而 zNear 和 zFar 定義了從 Camera/Viewer 到遠近兩個裁剪面的距離(注意這兩個距離都是正值)。
假設:xmax = right, xmin = left, ymax = top, ymin = bottom, zmax = far, zmin = near,正交投影的計算可分爲兩步:首先平移到視錐體的中心,而後縮放。
平移矩陣:(圖中的2min 應爲 zmin)
縮放矩陣:
正交投影矩陣 R = S × T:
視圖變換的目的是爲了讓咱們能觀察到某個角度的場景(從觀察者的角度來講)或者說是爲了將物體從世界座標轉換到相機視線所在視圖空間中來(從3D物體角度來講)。這能夠經過設定觀察者的位置和朝向來實現的或對物體進行3D變換來實現,一般前面一種方式來實現(即設定觀察者的位置與朝向)。以下圖所示,xyz座標軸表示的是世界座標,藍白色區域爲視圖空間,視圖變換就是要將長方體從世界空間中轉換到視圖空間的座標體系中去,而後再投影規範化,而後再經 viewport 轉換映射到屏幕上渲染出來。
在 OpenGL 中,咱們能夠經過工具庫提供的 gluLookAt 這個函數來實現此功能。該函數的原型爲:
gluLookAt(eyex, eyey, eyez, centerx, centery, centerz, upx, upy, upz);
eye 表示 camera/viewer 的位置, center 表示相機或眼睛的焦點(它與 eye 共同來決定 eye 的朝向),而 up 表示 eye 的正上方向,注意 up 只表示方向,與大小無關。經過調用此函數,就可以設定觀察的場景,在這個場景中的物體就會被 OpenGL 處理。在 OpenGL 中,eye 的默認位置是在原點,指向 Z 軸的負方向(屏幕往裏),up 方向爲 Y 軸的正方向。在接下來的教程 04 中,使用的就是這個默認設置。
OpenGL ES 2.0 也沒有提供該函數,glulookat 的內部實現其實就是先旋轉到與觀察者視線相同的方向,而後再平移到觀察者所在的位置。其實現僞碼以下:
|
上面代碼中的 cross 是叉積,normalize 是規範化,Matrix4 是列主序,translate 是平移。
3D 變換是對初學者來講是比較困難的,我儘可能寫得明白點,但效果如何就不得而知了。寫這一篇花了我很多時間,但對四元數和萬向節鎖也只是說起而已,未詳細介紹,之後再單獨介紹吧。Nate Robin 寫了一個3D 變換的可視化教程工具,對於理解投影,視圖,模型變換很是有幫助,強烈建議下載運行該程序,並調整相關參數看看效果。下面傳張截圖以誘惑你去下載:點此進入下載頁面(Windows 和 Mac 版本都有)
1,《OpenGL 編程指南》
2,《3D數學基礎:圖形與遊戲開發》
3,http://cse.csusb.edu/tong/courses/cs420/notes/viewing2.php