[OpenGL ES 03]3D變換:模型,檢視,投影與Viewport

idaretobe發表於2014-12-23
http://blog.csdn.net/kesalin/article/details/7168967

系列文章
[OpenGL ES 02]OpenGL ES渲染管線與著色器


前言

本來打算直接寫教程 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,原定計劃的程式碼示例已經寫好了,有興趣的同學可以先行瀏覽,程式碼放在這裡,執行效果如下:


 

一,3D數學歷史

我們都學過幾何學,應該都知道歐幾里得(公元前3世紀希臘數學家)這位幾何學鼻祖,正是這位大牛建立了歐幾里得幾何學,他提出了基於 X,Y,Z 三軸的三維空間概念。到了17世紀,又出了位大牛笛卡爾,我們通常所說的笛卡爾座標就是他的創造,笛卡爾座標非常完美地將歐幾里得幾何學理論與代數學聯絡到一塊。正是因為有了笛卡爾座標,我們才能夠用簡單的矩陣(Matrix)來表示三維變換。但用矩陣來表示三維變換操作有一個無法解決的問題-萬向節鎖 。什麼是萬向節鎖呢?簡單地說就是兩個軸旋轉到同一個方向上去了,這兩個軸平行了,因此就比原來少了一維(詳情可參考這裡)。過了一百多年,漢密爾頓(Sir William Rowan Hamilton)建立了四元數(quaternion)解決了因為旋轉而導致萬向節鎖的問題,四元數還有其他用處,但在3D數學裡主要是用來處理旋轉問題。

好吧,或許你看得一頭霧水,不要緊,你只要知道:用矩陣來表示3D變換,但矩陣在表示旋轉時可能會導致萬向節鎖的問題,而使用四元數可以避免萬向節鎖就可以了。

 

二,矩陣變換

在前面提到可使用 Matrix 來表示三維變換操作,那麼變換又是如何通過 Matrix 實現的呢?下面就來講這個。在這裡我推薦一本3D數學入門書籍:《3D數學基礎:圖形與遊戲開發

通常我們使用 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位置。

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 中的實現

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 變換的整體概念,下面來詳細說說投影變換與檢視變換。

 

四,投影變換

投影變換的目的是確定 3D 空間的物體如何投影到 2D 平面上,從而形成2D影象,這些 2D 影象再經視口變換就被渲染到螢幕上。前面提到投影變換有兩種:正交投影和透視投影。透視投影用的比較廣泛,它與真實世界更相近:近處的物體看起來要比遠處的物體大;而正交投影沒有這個效果,正交投影通常用於CAD或建築設計。下面是正交投影與透視投影效果示意圖:

正交投影 透視投影

 

       

 

 

 

 

透視投影可以通過兩種方式來表述,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,有


透檢視


gluPerspective(fovy, aspect, zNear, zFar);

fovy 定義了 camera 在 y 方向上的視線角度(介於 0 ~ 180 之間),aspect 定義了近裁剪面的寬高比 aspect = w/h,而 zNear 和 zFar 定義了從 Camera/Viewer 到遠近兩個裁剪面的距離(注意這兩個距離都是正值)。這四個引數同樣也定義了一個視錐體。

在 OpenGL ES 2.0 中,我們也需要自己實現該函式。我們可以通過三角公式 tan(fovy/2)  = (h / 2)/zNear 計算出 h ,然後再根據 w =  h * aspect 計算出 w,這樣就可以得到 left, right, top, bottom, zNear, zFar 六個引數,代入在介紹視錐體時提到的公式即可。 

正交投影在 OpenGL 及 OpenGL ES 1.0 中是由 glOrtho 來提供的,我們可以把正交投影看成是透視投影的特殊形式:即近裁剪面與遠裁剪面除了Z 位置外完全相同,因此物體始終保持一致的大小,即便是在遠處看上去也不會變小。


glOrtho(leftrightbottomtopzNearzFar);

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 的內部實現其實就是先旋轉到與觀察者視線相同的方向,然後再平移到觀察者所在的位置。其實現偽碼如下:

  1. Matrix4 GetLookAtMatrix(Vector3 eye, Vector3 at, Vector3 up){  
  2.     Vector3 forward, side;  
  3.     forward = at - eye;  
  4.     normalize(forward);  
  5.     side = cross(forward, up);  
  6.     normalize(side);  
  7.     up = cross(side, forward);  
  8.   
  9.     Matrix4 res = Matrix4(  
  10.                           side.x, up.x, -forward.x, 0,  
  11.                           side.y, up.y, -forward.y, 0,  
  12.                           side.z, up.z, -forward.z, 0,  
  13.                           0, 0, 0, 1);  
  14.     translate(res, Vector3(0 - eye));  
  15.     return res;  
  16. }  

上面程式碼中的 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

4,http://www.mesa3d.org/

5,http://db-in.com/blog/2011/04/cameras-on-opengl-es-2-x/

相關文章