[OpenGL ES 03]3D變換:模型惭等,視圖珍手,投影與Viewport
羅朝輝 (http://www.cnblogs.com/kesalin/)
本文遵循“署名-非商業(yè)用途-保持一致”創(chuàng)作公用協(xié)議
前言
本來打算直接寫教程 04 的,但是想到3D 變換涉及的數(shù)學知識較多辞做,往往是很多初學者的攔路虎(比如我自己)琳要。再加上OpenGL ES 2.0 不再提供OpenGL ES 1.0中 3D 變換相關(guān)的一些重量級函數(shù),如 glMatrixMode(GL_PROJECTION); glMatrixMode(GL_MODELVIEW); glLoadMatrixf; glMultMatrix 等秤茅,這些函數(shù)在 OpenGL ES 2.0 中均需要我們自己去實現(xiàn)稚补。 如果不對線性代數(shù)與幾何知識作一些簡單介紹,恐怕不少人難以理解文中的一些步驟為什么要那么做嫂伞。因此今天這一篇文章將放棄原定計劃孔厉,先來介紹一些 3D 數(shù)學以及 3D 變換相關(guān)的知識。BTW帖努,原定計劃的代碼示例已經(jīng)寫好了,有興趣的同學可以先行瀏覽粪般,代碼放在這里拼余,運行效果如下:
一,3D數(shù)學歷史
我們都學過幾何學亩歹,應該都知道歐幾里得(公元前3世紀希臘數(shù)學家)這位幾何學鼻祖匙监,正是這位大牛創(chuàng)建了歐幾里得幾何學,他提出了基于 X小作,Y亭姥,Z 三軸的三維空間概念。到了17世紀顾稀,又出了位大牛笛卡爾达罗,我們通常所說的笛卡爾坐標就是他的創(chuàng)造,笛卡爾坐標非常完美地將歐幾里得幾何學理論與代數(shù)學聯(lián)系到一塊静秆。正是因為有了笛卡爾坐標粮揉,我們才能夠用簡單的矩陣(Matrix)來表示三維變換。但用矩陣來表示三維變換操作有一個無法解決的問題-萬向節(jié)鎖 抚笔。什么是萬向節(jié)鎖呢扶认?簡單地說就是兩個軸旋轉(zhuǎn)到同一個方向上去了,這兩個軸平行了殊橙,因此就比原來少了一維(詳情可參考這里)辐宾。過了一百多年狱从,漢密爾頓(Sir William Rowan Hamilton)創(chuàng)建了四元數(shù)(quaternion)解決了因為旋轉(zhuǎn)而導致萬向節(jié)鎖的問題,然后四元數(shù)還有其他用處叠纹,但在3D數(shù)學里主要是用來處理旋轉(zhuǎn)問題季研。
好吧,或許你看得一頭霧水吊洼,不要緊训貌,你只要知道:用矩陣來表示3D變換,但矩陣在表示旋轉(zhuǎn)時可能會導致萬向節(jié)鎖的問題冒窍,而使用四元數(shù)可以避免萬向節(jié)鎖就可以了递沪。
二,矩陣變換
在前面提到可使用 Matrix 來表示三維變換操作综液,那么變換又是如何通過 Matrix 實現(xiàn)的呢款慨?下面就來講這個。在這里我推薦一本3D數(shù)學入門書籍:《3D數(shù)學基礎(chǔ):圖形與游戲開發(fā)》
通常我們使用 4 維向量 (x, y, z, w) 表示在3D空間中的一個點谬莹,最后一維 w 表示齊次坐標檩奠。齊次坐標的含義是兩條平行線在投影平面的無窮遠處相交于一點,但在 Matrix 中沒有表示無窮大附帽,所以增加了齊次坐標這一維埠戳。你可以想象下,火車軌道的兩條邊在無限遠處看起來就相交于一點蕉扮,齊次坐標詳細的介紹可以參考這篇文章整胃。
矩陣運算規(guī)則:
- 若矩陣 A 和 B 不是互逆矩陣,則不滿足乘法交換律喳钟,即 A × B 不等于 B × A屁使;
- M × N 階的矩陣只能和 N × O 階的矩陣相乘,即 N 的階數(shù)相等奔则,結(jié)果為 M × O 階的矩陣蛮寂;
- 矩陣 A × B 的運算過程是 A 的每一行依次乘以 B 的每一列作為結(jié)果矩陣中的一行;
- 矩陣 A 的逆矩陣 B 滿足 A × B = B × A = 單位矩陣易茬。
- 單位矩陣是對角線上的值為1酬蹋,其余均為 0 的矩陣。單位矩陣不影響坐標變換(你可以將下面的3D變換矩陣換成單位矩陣來思考下)疾呻。
3D空間的物體投影到2D平面上時除嘹,就需要使用到齊次坐標,因此我們需要使用 4 × 4 的 Matrix 來表示變換岸蜗。在編程語言中尉咕,這樣的 Matrix 可用大小為 16 的一維數(shù)組或4 × 4 的二維數(shù)組來表示。由于矩陣乘法不滿足乘法交換律璃岳,用數(shù)組表示 Matrix 又分為兩種形式:行主序和列主序年缎,它們在本質(zhì)上是等價的悔捶,只不過是一個是右乘(行主序,矩陣放右邊)和一個是左乘(列主序单芜,矩陣放左邊)蜕该。OpenGL 使用列主序矩陣,即列矩陣洲鸠,因此我們總是倒過來算的(左乘矩陣堂淡,變換效果是按從右向左的順序進行): 投影矩陣 × 視圖矩陣 × 模型矩陣 × 3D位置。
4× 4列矩陣的數(shù)組表示:數(shù)字表示數(shù)組下標對應的行列位置:
那么
平移矩陣可表示為:
平移矩陣 × 列矩陣(a, b, c, 1) = 列矩陣(a + x, b + y, c + z, 1)扒腕。
縮放矩陣可表示為:
縮放矩陣 × 列矩陣(a, b, c, 1) = 列矩陣(a × sx, b × sy, c × sz, 1)绢淀。
繞 X 軸旋轉(zhuǎn)的旋轉(zhuǎn)矩陣可表示為:
繞 X 軸旋轉(zhuǎn)的旋轉(zhuǎn)矩陣 × 列矩陣(a, b, c, 1) = 列矩陣(a, b × cos(θ) - c × sin(θ), b × -sin(θ) + c × cos(θ), 1)。
繞 Y 軸旋轉(zhuǎn)的旋轉(zhuǎn)矩陣可表示為:
繞 Y 軸旋轉(zhuǎn)的旋轉(zhuǎn)矩陣 × 列矩陣(a, b, c, 1) = 列矩陣(a × cos(θ) - c × sin(θ), b , a × -sin(θ) + c × cos(θ), 1)瘾腰。
繞 Z 軸旋轉(zhuǎn)的旋轉(zhuǎn)矩陣可表示為:
繞 Z 軸旋轉(zhuǎn)的旋轉(zhuǎn)矩陣 × 列矩陣(a, b, c, 1) = 列矩陣(a × cos(θ) - b × sin(θ), a × -sin(θ) + b × cos(θ), c, 1)皆的。
三,OpenGL 中的實現(xiàn)
OpenGL 使用右手規(guī)則進行旋轉(zhuǎn)蹋盆,因此逆時針方向的選擇是正角度的费薄,而順時針方向的旋轉(zhuǎn)是負角度的。還記得中學學物理時候的右手規(guī)則么栖雾?忘記了的話楞抡,看下圖:
注意:
前面說到矩陣乘法不滿足乘法交換律,因此你對一個3D坐標先進行旋轉(zhuǎn)析藕,然后進行平移(平移矩陣 × 旋轉(zhuǎn)矩陣 × 3D坐標)拌倍;與先進行平移,然后進行旋轉(zhuǎn)(旋轉(zhuǎn)矩陣 × 平移矩陣 × 3D坐標)得到的效果是大為迥異的噪径。如下圖所示:
在第一種情況下,我們通常稱旋轉(zhuǎn)是在 local space 中進行数初,因為它是繞著物體自己的中心點進行的找爱,而在后一種情況下的旋轉(zhuǎn)通常稱為是在 world space 中進行的。我們知道點是可以在坐標空間之間相互轉(zhuǎn)換的泡孩,這是一個很重要的概念车摄。OpenGL 中物體最初是在本地坐標空間中,然后轉(zhuǎn)換到世界坐標空間仑鸥,再到 camera 視圖空間吮播,再到投影空間,這一系列轉(zhuǎn)換都是靠 matrix 計算來實現(xiàn)眼俊。
上面的這個過程在 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 變換發(fā)生在投影到2D 投影平面之后疮胖,該變換是將投影之后歸一化的點映射到屏幕上一塊區(qū)域內(nèi)的坐標环戈。視口變換的目的是指定投影之后圖像在屏幕上顯示的區(qū)域闷板。如下示意圖所示:
視口變換 glViewport(x, y, width, height); x,y 是投影平面描繪在屏幕或窗口上的起始位置(注意屏幕坐標以左上方為原點),width和height是以像素為單位院塞,指投影平面在屏幕上描繪的區(qū)域大小遮晚。如果投影平面的寬高比,與width/height比不相同(如上面的右圖)拦止,那么描繪的場景就會扭曲县遣。
從裁剪到屏幕的整個過程如下圖所示,w 就是前面提到的齊次坐標那一維汹族,從 Clip Space 到 Normalized Device Space 就是投影規(guī)范化的過程萧求,從 Normalized Device Space 到 Window Space 就是 viewport 變換過程。
該轉(zhuǎn)換內(nèi)部計算公式為:
(xw, yw)是屏幕坐標饭聚,(x, y, width, height)是傳入的參數(shù),(xnd, ynd)是投影之后經(jīng)歸一化之后的點(上圖中 Normalized Device Space 空間的點)搁拙。因此 viewport 變換就是將投影之后歸一化的點轉(zhuǎn)換為真正可用于在屏幕上進行渲染的屏幕坐標秒梳;
b) 是說明下面的 matrix 是用于投影變換的,在本例中箕速,是通過語句 c) glFrustum 來設(shè)置透視投影變換的酪碘。投影變換有兩種:正交投影和透視投影,后面會有詳細介紹盐茎;
d) 是說明下面的 matrix 是用于模型視圖變換兴垦,注意,OpenGL 和 OpenGL ES 都將模型變換與視圖變換結(jié)合在一起字柠,而不是分開為兩個探越,這是因為模型變換等價于視圖變換的逆變換。視圖變換是將物體轉(zhuǎn)換到觀察者(一般稱之為 camera)的視線空間中窑业。你可以想象一下钦幔,照相時,你可以:A)照相機不懂常柄,旋轉(zhuǎn)自己的頭找個側(cè)面像鲤氢,也可以B)自己不動,照相機旋轉(zhuǎn)一定的角度來達到同樣的效果西潘。下面的兩幅圖分別描述了情形A)和情形B):
情形A):旋轉(zhuǎn)物體卷玉,相機不動
情形B):旋轉(zhuǎn)相機喷市,物體不動
在 OpenGL 中,我們在設(shè)置場景(scene)的時候通常是采取情形B)的做法东抹,因此在語句 e) 處蚂子,我們設(shè)置相機的位置和朝向沃测,來設(shè)定視圖變換,之后的語句 f) glScale 是設(shè)定在模型變換的食茎,最后語句 g) 在本地空間描繪物體蒂破。
注意
寫 OpenGL 代碼時從前到后的順序依次是:設(shè)定 viewport(視口變換),設(shè)定投影變換别渔,設(shè)定視圖變換附迷,設(shè)定模型變換,在本地坐標空間描繪物體哎媚。而在前面為了便于理解做介紹時喇伯,說的順序是OpenGL 中物體最初是在本地坐標空間中,然后轉(zhuǎn)換到世界坐標空間拨与,再到 camera 視圖空間稻据,再到投影空間。由于模型變換包括了本地空間變換到世界坐標空間买喧,所以我們理解3D 變換是一個順序捻悯,而真正寫代碼時則是以相反的順序進行的,如果從左乘矩陣這點上去理解就很容易明白為什么會是反序的淤毛。
有了上面 3D 變換的整體概念今缚,下面來詳細說說投影變換與視圖變換。
四低淡,投影變換
投影變換的目的是確定 3D 空間的物體如何投影到 2D 平面上姓言,從而形成2D圖像,這些 2D 圖像再經(jīng)視口變換就被渲染到屏幕上蔗蹋。前面提到投影變換有兩種:正交投影和透視投影何荚。透視投影用的比較廣泛,它與真實世界更相近:近處的物體看起來要比遠處的物體大猪杭;而正交投影沒有這個效果兽泣,正交投影通常用于CAD或建筑設(shè)計。下面是正交投影與透視投影效果示意圖:
| 正交投影 | 透視投影 |
|
透視投影可以通過兩種方式來表述胁孙,OpenGL 及 OpenGL ES 1.0 提供其中一種: glFrustum,而 glut 輔助庫提供了另外一種:gluPerspective称鳞。它們本質(zhì)上是相同的涮较,只不過是不同的表述而已:
視錐體/視景體:
glFrustum(left, right, bottom, top, zNear, zFar);
left,right冈止, bootom狂票,top 定義了 near 裁剪面大小,而 zNear 和 zFar 定義了從 Camera/Viewer 到遠近兩個裁剪面的距離(注意這兩個距離都是正值)熙暴。由這六個參數(shù)可以定義出六個裁剪面構(gòu)成的錐體闺属,這個錐體通常被稱之為視錐體或視景體慌盯。只有在這個錐體內(nèi)的物體才是可以見的,不在這個錐體內(nèi)的物體就相當于不再視線范圍內(nèi)掂器,因而會被裁減掉亚皂,OpenGL 不會這些物體進行渲染。
由于 OpenGL ES 2.0 不提供此函數(shù)国瓮,因此我們需要自己實現(xiàn)該函數(shù)灭必。其計算公式如下:
假設(shè):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 到遠近兩個裁剪面的距離(注意這兩個距離都是正值)。這四個參數(shù)同樣也定義了一個視錐體孵睬。
在 OpenGL ES 2.0 中播歼,我們也需要自己實現(xiàn)該函數(shù)。我們可以通過三角公式 tan(fovy/2) = (h / 2)/zNear 計算出 h 掰读,然后再根據(jù) w = h * aspect 計算出 w秘狞,這樣就可以得到 left, right, top, bottom, zNear, zFar 六個參數(shù),代入在介紹視錐體時提到的公式即可磷支。
正交投影在 OpenGL 及 OpenGL ES 1.0 中是由 glOrtho 來提供的谒撼,我們可以把正交投影看成是透視投影的特殊形式:即近裁剪面與遠裁剪面除了Z 位置外完全相同,因此物體始終保持一致的大小雾狈,即便是在遠處看上去也不會變小廓潜。
glOrtho(left, right, bottom, top, zNear, zFar);
left,right善榛, bootom辩蛋,top 定義了 near 裁剪面大小,而 zNear 和 zFar 定義了從 Camera/Viewer 到遠近兩個裁剪面的距離(注意這兩個距離都是正值)移盆。
假設(shè):xmax = right, xmin = left, ymax = top, ymin = bottom, zmax = far, zmin = near悼院,正交投影的計算可分為兩步:首先平移到視錐體的中心,然后縮放咒循。
平移矩陣:(圖中的2min 應為 zmin)
縮放矩陣:
正交投影矩陣 R = S × T:
五据途,視圖變換
視圖變換的目的是為了讓我們能觀察到某個角度的場景(從觀察者的角度來說)或者說是為了將物體從世界坐標轉(zhuǎn)換到相機視線所在視圖空間中來(從3D物體角度來說)。這可以通過設(shè)定觀察者的位置和朝向來實現(xiàn)的或?qū)ξ矬w進行3D變換來實現(xiàn)叙甸,通常前面一種方式來實現(xiàn)(即設(shè)定觀察者的位置與朝向)颖医。如下圖所示,xyz坐標軸表示的是世界坐標裆蒸,藍白色區(qū)域為視圖空間熔萧,視圖變換就是要將長方體從世界空間中轉(zhuǎn)換到視圖空間的坐標體系中去,然后再投影規(guī)范化,然后再經(jīng) viewport 轉(zhuǎn)換映射到屏幕上渲染出來佛致。
在 OpenGL 中贮缕,我們可以通過工具庫提供的 gluLookAt 這個函數(shù)來實現(xiàn)此功能。該函數(shù)的原型為:
gluLookAt(eyex, eyey, eyez, centerx, centery, centerz, upx, upy, upz);
eye 表示 camera/viewer 的位置俺榆, center 表示相機或眼睛的焦點(它與 eye 共同來決定 eye 的朝向)感昼,而 up 表示 eye 的正上方向,注意 up 只表示方向肋演,與大小無關(guān)抑诸。通過調(diào)用此函數(shù),就能夠設(shè)定觀察的場景爹殊,在這個場景中的物體就會被 OpenGL 處理蜕乡。在 OpenGL 中,eye 的默認位置是在原點梗夸,指向 Z 軸的負方向(屏幕往里)层玲,up 方向為 Y 軸的正方向。在接下來的教程 04 中反症,使用的就是這個默認設(shè)置辛块。
OpenGL ES 2.0 也沒有提供該函數(shù),glulookat 的內(nèi)部實現(xiàn)其實就是先旋轉(zhuǎn)到與觀察者視線相同的方向铅碍,然后再平移到觀察者所在的位置润绵。其實現(xiàn)偽碼如下:
Matrix4 GetLookAtMatrix(Vector3 eye, Vector3 at, Vector3 up){
Vector3 forward, side;
forward = at - eye;
normalize(forward);
side = cross(forward, up);
normalize(side);
up = cross(side, forward);
Matrix4 res = Matrix4(
side.x, up.x, -forward.x, 0,
side.y, up.y, -forward.y, 0,
side.z, up.z, -forward.z, 0,
0, 0, 0, 1);
translate(res, Vector3(0 - eye));
return res;
}
上面代碼中的 cross 是叉積,normalize 是規(guī)范化胞谈,Matrix4 是列主序尘盼,translate 是平移。
六烦绳,后記
3D 變換是對初學者來說是比較困難的卿捎,我盡量寫得明白點,但效果如何就不得而知了径密。寫這一篇花了我不少時間午阵,但對四元數(shù)和萬向節(jié)鎖也只是提及而已,未詳細介紹享扔,以后再單獨介紹吧底桂。Nate Robin 寫了一個3D 變換的可視化教程工具,對于理解投影惧眠,視圖戚啥,模型變換非常有幫助,強烈建議下載運行該程序锉试,并調(diào)整相關(guān)參數(shù)看看效果。下面?zhèn)鲝埥貓D以誘惑你去下載:點此進入下載頁面(Windows 和 Mac 版本都有)
七,引用
1呆盖,《OpenGL 編程指南》
2拖云,《3D數(shù)學基礎(chǔ):圖形與游戲開發(fā)》
3,http://cse.csusb.edu/tong/courses/cs420/notes/viewing2.php
5宙项,http://db-in.com/blog/2011/04/cameras-on-opengl-es-2-x/
分類: 3D技術(shù),Cocoa開發(fā)