opengl 座標的理解

【目標】:學習OpenGL中的座標系統。

 

【參考】:

1、《計算機圖形學(OpenGL版) (第三版)》 Francis著 (本文主要涉及第三章~第七章)

2、《計算機圖形學(OpenGL版) (第三版)》 Donald著

 

一、前言

        座標系應該是任何圖像系統的基石。在學習Cocos2D的過程中,對着《權威指南》上草草結束的座標系介紹,實在是看的一頭霧水,找了本OpenGL書把這塊研究了一下,大致算是清楚了其中的一些基本概念。這裏總結一下,作爲記錄。

 

二、數學基礎

1、齊次座標

        (對應圖形學一書的第4.5節)

        在三維空間中的一個點,通常用p = (x, y ,z) 這樣的三維座標來表示,如果要對這樣一個點進行如下的簡單的變換(即仿射變換,下面會提到)。那麼其形式可以如下表示:

           (式2.1 - 1)

      或者用表示爲

  1. P2 = M * P1 + C  (式2.1 - 2)  
  P2 = M * P1 + C  (式2.1 - 2)

        這樣表示的變換存在一種不便:需要乘法和加法兩次計算才能完成轉換。回想多項式乘法會發現,(a+b)^n 的展開式相當之長,這樣,當變換的次數很多時,這樣的計算就會很複雜且不直觀。

        由此,引入了齊次座標的概念:對於某個三維座標點(x, y, z),增加一維 w != 0,並對原三維座標進行同樣的縮放,形成新的四維座標(wx, wy, wz, w),即是所謂齊次座標。

        引入了齊次座標後,原式2.1-2 就可以改寫爲:

  1. P2‘ = M’ * P1’  (式2.1 -3)  
   P2' = M' * P1'  (式2.1 -3)

       這樣,只需要用乘法就可以完成所有的任務了。

 

2、點、向量 的齊次座標表示

       前面的齊次座標表示實際上是“點”的齊次座標表示。且在接觸到投影矩陣之前(即在模型視圖矩陣階段),對於一個座標點 P = (wx, wy, wz, w) 的 w值都是1,也必須是1。

       而對於向量,若其原三維表示爲 v = (x, y , z), 則在齊次座標下的表示爲 v’ = (x, y, z, 0)。即對於向量來說,齊次座標表示就是在第四維上填0

       進一步的,從原來上來說,這裏的第四維座標w,實際上表示是否含有座標軸原點O的信息。(座標點可以理解爲原點O移動向量p後的結果)

 

3、仿射變換

       仿射變換的數學定義是:在幾何中,一個向量空間進行一次線性變換並接上一個平移,變換爲另一個向量空間,可以用式2.1-1表示。

       那麼在齊次座標下, 由於在前面瞭解到點的齊次座標表示都是 (x, y, z, 1),第四維必須是1。所以仿射變換可以表示爲:

          (式2.3 - 1)

 

4、基本的仿射變換

       複雜的仿射變換可以通過多次進行基本的仿射變換來完成。這些基本的原子變換包括:平移、縮放、剪切、旋轉變換。(Donald的書在這個地方講的更數學一點,更加嚴謹)

4.1  平移

      M = 

        其中,(mx, my, mz) 是平移量,也是新座標系(移動後)的原點O,在原座標系(移動前)下的座標值。

        注意,上面關於原點這個表述很有意思。如果變換看做點的移動,那麼右邊(式2.1-2中的P2)是原來的座標,左邊(P1)是新的座標。但是如果看做是座標系的移動,那麼對於同一個點P,P2是老座標,P1是新座標。

        在我們這裏,新的原點O,其老座標就是P2。

 

4.2  縮放

        M =

       其中,(Sx, Sy, Sz) 是縮放比例。很簡單不解釋了。

 

4.3  剪切

        也有翻譯爲錯位變換的,定義是某一維上(如Y)引入另一維的影響(如X),效果是方形變平行四邊形。可以看 http://baike.baidu.com/view/2424073.htm。根據兩個維度的選擇,矩陣略有不同。如果是Y上引入X的影響,則矩陣爲:

         M =

       當f = 1時,效果圖如下:

 

4.4  旋轉

        放在最後當然是最麻煩的。由於繞任意軸的旋轉很難表示,所以實際上覆雜的旋轉又被繼續分解,最基本的旋轉是繞某一個軸進行的旋轉。

4.4.1 基本的旋轉

        首先定義旋轉的正方向:(右手規則)用右手握住軸,大拇指指向軸的正方向,則四指所指方向爲旋轉的正方向。(也就是逆時針方向,以後基於法向量的逆時針也可以用這個法則判定)。

 

       那麼沿着某個軸旋轉 β° 的話,那麼其矩陣如下:

4.4.2 複合的旋轉

        那麼,對於任意給定的軸 v = (x, y, z, 0),旋轉β°是怎樣得到的呢?總體的思路是這樣的:

(1)先平移,使得旋轉軸通過原點(opengl不需要考慮這一步,glrotatef的旋轉軸都是從原點出發的)

(2)沿x軸和y軸旋轉,使得旋轉軸與Z軸重合

(3)沿z軸旋轉β°

(4)做第二步的反操作,依次沿y軸和x軸進行反旋轉

(5)做第一步的反操作,反向平移

        總體來說,理論依據來源於:M(β) * M(-β) = E,即旋轉矩陣可逆,且正向原子旋轉和反向原子旋轉互爲逆矩陣。

        那麼

  1. P2 = R(β) * P1  
  2. T(α) * P2 = R(β) * (T(α) * P1)  
  3. P2 = T(-α) * R(β) * T(α) * P1  
   P2 = R(β) * P1
   T(α) * P2 = R(β) * (T(α) * P1)
   P2 = T(-α) * R(β) * T(α) * P1

 

       而左乘一個矩陣,就代表着在原有變換基礎上繼續變換(後面也會講到)。更具體的推導這裏略去,可以參考Donald書上的5.11.2節。最後的結論也太長,這裏也不附了。

 

5、基本仿射變換的複合

       其實前面也提到了,如果先做一個變換a,再做一個變換b,那麼其複合的變換矩陣就是:

  1. P2 = Mb * Ma * P1  
   P2 = Mb * Ma * P1

      注意是即可,原理也很明顯,不再證明。

 

三、OpenGL座標系

 研究任何座標系(非歐的不清楚),只要把握住以下三點:1、原點;2、座標軸正方向;3、座標單位。以下均按照這個思路研究。

 

1、OpenGL座標系轉換的大致流程

一般使用攝像來做比來描述這個流程,Donald書上289頁的一張流水線圖則從數學上解釋了這個流程。兩者合併起來是這樣的:

下面具體說明各個步驟

 

2、擺放物體(模型變換):局部座標系 -> 世界座標系

        世界座標系是右手座標系,正方向無意義,單位是1,原點座標是指定的(即無意義)。局部座標系也是一樣(不過單位長度和世界座標系中的單位可能存在比例關係)。

        舉個例子,給定一個世界,我們平移(x, y, z),然後縮放(Sx, Sy, Sz)。在變換過的原點位置放了一個單位大小的立方體。那麼立方體的局部座標就是(0, 0, 0),在局部座標系中的大小是(1, 1, 1)。但是他在世界座標系中的座標是(x, y, z),大小是(Sx, Sy, Sz)。

        這裏世界座標系中各個要素的“無意義”是我的理解。意思是說,這個世界座標系是預先給定的,不變化的,在這個階段是和我們的電腦屏幕上的像素座標沒有關係的。就好像我們的地球,使用經緯度爲座標,但實際上任意指定經緯度的起止點和單位都是可以的,這就是世界座標系。而後面的物體、攝像機,都是基於這個座標系擺放的。

         而針對局部座標系,從代碼上講,我們使用glVertex3f設定的座標值實際上是局部座標系下的值。在不對模型矩陣進行任何變換的時候,這個座標系是和世界座標系重合的。

         這個階段的變換,主要包括 glTranslae (平移)、glScale (縮放)、glRotate (旋轉)【剪切不知道是什麼】。在經過這樣一系列變換之後,局部座標系上的某個點P0(x0, y0, z0, 1),會被擺放到世界座標系上的P1(x1, y1, z1, 1)點:

         P1 = M * P0 (式3-2)

         模型變換矩陣 M 可以參考從“數學基礎”一節裏面給出的結果。例如,調用 glTranslated(1, 0, 0) 將局部座標系向 x 軸正方向移動了1個單位,那麼

        M =

         那麼,局部座標系下的原點(0, 0, 0, 1),實際上就是世界座標系下的(1, 0, 0, 1),和我們平移的結果相比顯然是正確的。【這裏可以再體會一下“新座標系(移動後)的原點O,在原座標系(移動前)下的座標值”這句話,雖然世界座標系是事先給定的,但是座標的值給出的卻總是局部座標系下的值,其在真實世界的位置,是需要通過求解才能得到的】

 

3、擺放攝像機(觀察變換):世界座標系 -> 視點座標系

3.1  理論

        這個階段實際上就是調用函數 gluLookAt(eye, center, up)。通過這個操作,將攝像機擺放在了eye的位置,鏡頭的上方與up在一個平面上(這個說法不嚴謹,嚴謹的看下面的),視線指向center。

        視點座標系就是爲攝像機服務的座標系,原點在攝像機位置(即eye處),它仍然是右手座標系。三個座標軸(u, v, n) 如下確定:

        (1)視線軸 z : 從 center 點 指向 eye 點。注意:是和視線方向相反的

        (2)水平軸 x : X = up × Z (叉乘是右手規則)

        (3)上方軸 y : Y = Z × X。 y指向的方向通常和 up 很接近。如果 up本身與視線方向恰好垂直時,兩者重合。

        至於單位,肯定是單位1,但是是世界座標系下的單位1呢,還是局部座標系下的單位1呢?(兩者的區別在於前者不受glScale影響)。另外一個問題, gluLookAt 的參數。這裏面指定了三個參數 eye,center,up。前面已經有了兩個座標系:世界座標系和局部座標系,那麼這裏的座標點是以什麼座標系爲參照的呢?

        這還是要從視點變換的地位說起。在OpenGL中,並沒有專門的視點矩陣和模型矩陣,兩者是合一的,稱爲 GL_MODELVIEW 。不過兩者實質上還是屬於不同的流程,要先從局部變換到世界座標系,然後再從世界座標系變換到視點座標系。如果接着上面的公式3-2,那麼一個局部座標系下的點P0,在映射爲世界座標系下的點P1後,又會被轉換爲視點座標系下的點 P2:

         P2 = V * M * P0 (式3-3)

         這裏的V是視點變換矩陣。

         這樣的話,前面的兩個問題就可以回答了:單位是世界座標系下的單位1(這個無傷大雅),座標是世界座標系下的座標(這個很關鍵)。【至於爲什麼只使用一個矩陣,我是這樣理解的:世界座標系相當於只是一箇中間變量,最後映入眼簾的,還是基於視點座標系的世界,所以 V * M 可以合併,導致OpenGL中使用一個矩陣來描述這個過程。】

         但是這裏的視點變換矩陣V的確定方法,又與M不同,如果我們設定 gluLookAt(0, 0, 1,   0, 0, 0,   0, 1, 0); 即視點座標系只是將世界座標系平移了 (0, 0, 1),那麼是否 V 就是[1, 0, 0, 0;   0, 1, 0, 0;  0, 0, 1, 1; 0, 0, 0, 0] 呢?答案是錯的,其矩陣實際上是和平移(0, 0, -1)的變換矩陣相同。這是爲什麼呢?再回到那句話:

         “如果看做是座標系的移動,那麼對於同一個點P,左邊P2是老座標,右邊P1是新座標。”

         對做上面變換的一個場景中,考慮世界座標系中的原點。原座標值(0, 0, 0, 1)是老座標,現在座標軸平移了(0, 0, 1),想要知道的是在新座標系(視點座標系)下的新座標P1。那麼根據

        P2 = Translate * P1 (translate是平移(0, 0, 1)的變換矩陣)

        顯然,V 是 Translate 的逆矩陣。這其實在物理上也容易理解:移動攝像機,相當於反向移動物體

         這段比較麻煩,舉個例子印證一下。


3.2  例子:視點矩陣的影響

        做這樣一個操作:假設世界座標系和局部座標系重合,然後定義 gluLookAt(1, 0, 0, 0, 0, 0, 0, 1, 0); 即站在(1, 0, 0)點看着(0, 0, 0)點。這樣的視點變換相當於做了兩件事:首先沿x軸平移了1個單位,然後繞y軸旋轉了-90°,使得新的z軸和原來的x軸負方向重合。那麼其變換矩陣,根據平移和旋轉的定義,就應該是

        Translate =

        求逆矩陣得到

        V =

       可以用這樣一段代碼得到驗證:

 

  1. #include “AllHead.h”  
  2.   
  3. //通過實踐掌握視點矩陣(glulookat)  
  4.   
  5. static void init() {  
  6.     glClearColor(1, 1, 1, 0);  
  7. }  
  8.   
  9. static void display() {  
  10.     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  
  11.   
  12.     //do nothing  
  13.     glFlush();  
  14. }  
  15.   
  16. static void reshape(int w, int h) {  
  17.     glViewport(0, 0, w, h);  
  18.     //投影矩陣設爲單位矩陣  
  19.     glMatrixMode(GL_PROJECTION);  
  20.     glLoadIdentity();  
  21.   
  22.     //模型視點矩陣先設置爲單位矩陣  
  23.     glMatrixMode(GL_MODELVIEW);  
  24.     glLoadIdentity();  
  25.     printMatrix(GL_MODELVIEW_MATRIX);  
  26.     std::cout << ”set look at !” << std::endl;  
  27.     //攝像機位於(1,0,0)  
  28.     gluLookAt(1, 0, 0, 0, 0, 0, 0, 1, 0);  
  29.     printMatrix(GL_MODELVIEW_MATRIX);  
  30. }  
  31.   
  32. void LCG_cp07_pageNone_testViewPlane(int argc, char ** argv) {  
  33.     setupWindow(argc, argv, GLUT_RGB | GLUT_SINGLE, INTPAIR(600, 400));  
  34.     init();  
  35.     glutReshapeFunc(reshape);  
  36.     glutDisplayFunc(display);  
  37.     glutMainLoop();  
  38. }  
#include "AllHead.h"

//通過實踐掌握視點矩陣(glulookat)

static void init() {
    glClearColor(1, 1, 1, 0);
}

static void display() {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    //do nothing
    glFlush();
}

static void reshape(int w, int h) {
    glViewport(0, 0, w, h);
    //投影矩陣設爲單位矩陣
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();

    //模型視點矩陣先設置爲單位矩陣
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    printMatrix(GL_MODELVIEW_MATRIX);
    std::cout << "set look at !" << std::endl;
    //攝像機位於(1,0,0)
    gluLookAt(1, 0, 0, 0, 0, 0, 0, 1, 0);
    printMatrix(GL_MODELVIEW_MATRIX);
}

void LCG_cp07_pageNone_testViewPlane(int argc, char ** argv) {
    setupWindow(argc, argv, GLUT_RGB | GLUT_SINGLE, INTPAIR(600, 400));
    init();
    glutReshapeFunc(reshape);
    glutDisplayFunc(display);
    glutMainLoop();
}

        可以看到打印出來的結果和我們的預期是一樣的。


4、觀察者的所見:視點座標系 -> 投影座標系

4.1  基礎

       【這一步是和下面的裁剪緊密結合的,這裏認爲是分開的兩步,且只討論和投影相關的部分】

        投影就是將攝像機在三維的視點座標系中的所見,映射到一張二維的投影平面(照片)上的過程。這樣看來,似乎投影座標系應該是二維的啦?不過爲了方便深度測試等操作,實際上投影座標系仍然是三維的。不過其z軸座標值的意義已經不是那麼重要了。

        既然是座標系變換,那麼根據前面的經驗,這裏顯然又會有一個變換矩陣,稱之爲 投影矩陣Mp。在OpenGL中是 GL_PROJECTION。則有:

       P3 = Mp * P2 (式 3-4)

       這裏的P2是視點座標系下的座標值。【這裏扯一句題外話,如果給定原座標系要素和變換矩陣,是否可以唯一確定新座標系的要素呢?答案是肯定的,因爲原點和單位向量都可以通過變換求解出來】

      那麼,這個Mp 要如何確定呢?這就要說到幾種不同的投影方式了。


      具體來說,有以上三大類投影。都比較形象就不解釋了。下面分別來看三種投影的變換矩陣。


4.2  正投影

        最簡單的就是這種投影。在OpenGL中使用 glOrtho(left,  right, bottom, top,  near, far); 來進行設置。這裏的幾個參數主要是關係到下面的剪裁和規範化的,對於投影本身只有一點影響:以 x 軸座標爲例,若 left <= right,則 Xp = X,但是若  left > right, 那麼就有 Xp = -X。

       這裏另外需要注意的是 (near, far) 這對變量。由於Opengl默認的座標系中,Z軸是從屏幕指向用戶的,所以實際上座標越大,距離用戶的距離就越近,所以實際上的近平面是 -near,遠平面點是 -far 【參考 http://baike.baidu.com/view/1280554.htm】。所以默認的正投影矩陣實際上是 

  1. glOrtho(-1, 1,   -1, 1,   1, -1);   
 glOrtho(-1, 1,   -1, 1,   1, -1); 


       (注意這裏和Donald書上317頁的結論有出入,但實際測試gluOrtho2D的結論也是和我這裏相同的)

       正投影的投影矩陣比較簡單,基本就是單位矩陣,當左右什麼的發生逆向的時候,對應分量就變爲-1。當然 GL_PROJECTION 裏面還包含一些後面的因素。

       這裏貼一個例子:

  1. #include “AllHead.h”  
  2.   
  3. //通過實驗掌握投影變換  
  4.   
  5. typedef std::pair<GLfloat, GLfloat>  GloatPair;  
  6.   
  7. static void init() {  
  8.     glClearColor(1, 1, 1, 0);  
  9.     //打開深度測試,這樣才能看到遮蓋的效果  
  10.     glClearDepth(1.0f);  
  11.     glEnable(GL_DEPTH_TEST);  
  12. }  
  13.   
  14. static void reshape(int w, int h) {  
  15.     glViewport(0, 0, w, h);  
  16. }  
  17.   
  18. static void drawRectangle(const GloatPair & leftTop, const GloatPair & rightButtom, GLfloat z) {  
  19.     glBegin(GL_QUADS);  
  20.     glVertex3f(leftTop.first, leftTop.second, z);  
  21.     glVertex3f(rightButtom.first, leftTop.second, z);  
  22.     glVertex3f(rightButtom.first, rightButtom.second, z);  
  23.     glVertex3f(leftTop.first, rightButtom.second, z);  
  24.     glEnd();  
  25. }  
  26.   
  27. static void display() {  
  28.     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  
  29.   
  30.     //設置與否對本實例沒有影響,不過可以開啓看看  
  31.     //glMatrixMode(GL_MODELVIEW);  
  32.     //glLoadIdentity();  
  33.     //gluLookAt(0,0,0.5, 0,0,0, 0,1,0);  
  34.     //std::cout << “print model view” << std::endl;  
  35.     //printMatrix(GL_MODELVIEW_MATRIX);  
  36.   
  37.     glMatrixMode(GL_PROJECTION);  
  38.     glLoadIdentity();  
  39.     glOrtho(-2, 2, -2, 2, 5, -5);  
  40.   
  41.     std::cout << ”print projection ” << std::endl;  
  42.     printMatrix(GL_PROJECTION_MATRIX);  
  43.   
  44.     //畫兩個方塊  
  45.     glColor3f(1, 0, 0); //先是個紅的  
  46.     drawRectangle(GloatPair(-0.5, 0.5), GloatPair(0.7, -0.7), 0.2);  
  47.     glColor3f(0, 1, 0); //再在背面畫個綠色的  
  48.     drawRectangle(GloatPair(-0.7, 0.7), GloatPair(0.5, -0.5), -0.2);  
  49.   
  50.     glFlush();  
  51.   
  52. }  
  53.   
  54. void LCG_cp07_pageNone_testProjectionMatrix(int argc, char ** argv) {  
  55.     setupWindow(argc, argv, GLUT_SINGLE | GLUT_RGB, INTPAIR(400, 400));  
  56.     init();  
  57.     glutReshapeFunc(reshape);  
  58.     glutDisplayFunc(display);  
  59.     glutMainLoop();  
  60. }  
#include "AllHead.h"

//通過實驗掌握投影變換

typedef std::pair<GLfloat, GLfloat>  GloatPair;

static void init() {
    glClearColor(1, 1, 1, 0);
    //打開深度測試,這樣才能看到遮蓋的效果
    glClearDepth(1.0f);
    glEnable(GL_DEPTH_TEST);
}

static void reshape(int w, int h) {
    glViewport(0, 0, w, h);
}

static void drawRectangle(const GloatPair & leftTop, const GloatPair & rightButtom, GLfloat z) {
    glBegin(GL_QUADS);
    glVertex3f(leftTop.first, leftTop.second, z);
    glVertex3f(rightButtom.first, leftTop.second, z);
    glVertex3f(rightButtom.first, rightButtom.second, z);
    glVertex3f(leftTop.first, rightButtom.second, z);
    glEnd();
}

static void display() {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    //設置與否對本實例沒有影響,不過可以開啓看看
    //glMatrixMode(GL_MODELVIEW);
    //glLoadIdentity();
    //gluLookAt(0,0,0.5, 0,0,0, 0,1,0);
    //std::cout << "print model view" << std::endl;
    //printMatrix(GL_MODELVIEW_MATRIX);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(-2, 2, -2, 2, 5, -5);

    std::cout << "print projection " << std::endl;
    printMatrix(GL_PROJECTION_MATRIX);

    //畫兩個方塊
    glColor3f(1, 0, 0); //先是個紅的
    drawRectangle(GloatPair(-0.5, 0.5), GloatPair(0.7, -0.7), 0.2);
    glColor3f(0, 1, 0); //再在背面畫個綠色的
    drawRectangle(GloatPair(-0.7, 0.7), GloatPair(0.5, -0.5), -0.2);

    glFlush();

}

void LCG_cp07_pageNone_testProjectionMatrix(int argc, char ** argv) {
    setupWindow(argc, argv, GLUT_SINGLE | GLUT_RGB, INTPAIR(400, 400));
    init();
    glutReshapeFunc(reshape);
    glutDisplayFunc(display);
    glutMainLoop();
}


可以通過修改這裏的 glOrtho(-2, 2, -2, 2, 5, -5); 來進行實驗。

 

4.3  斜投影

       在OpenGL中貌似沒有直接的斜投影方法。那麼我們用直接指定矩陣的方法也可以達到目的。不用被4.1開頭的非平行平面的斜投影嚇到,實際上只需要考慮平行平面的投影效果就可以求取投影矩陣了。

       例如上面例子中畫的兩個方塊,如果希望其重合,且紅的遮蓋綠的。那麼由於兩個方塊實際上是相等大小的。所以只需要考慮正方形中心的變換:(0.1,  -0.1,  0.2)【紅色的中心】 ->  (-0.1,  0.1, -0.2)【綠色的中心】。那麼投影線都是平行於這條線的。

         假設我們的投影平面是z = 0 平面,容易得到 投影的關係是 Xp = X - Z/2  ; Yp = Y + Z/2,和正投影一樣,我們保留z的值來做深度測試,代碼如下(僅提供與上面的程序不同的部分):

 

  1. static const GLfloat PROJECTION_MATRIX [16] =  {  
  2.        1,   0,   0,  0,  
  3.        0,   1,   0,  0,  
  4.        0,   0,  -1,  0,  
  5.     -0.5, 0.5,   0,  1  
  6. };  
  7.   
  8. static void display() {  
  9.     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  
  10.   
  11.     glMatrixMode(GL_PROJECTION);  
  12.     glLoadIdentity();  
  13.     glMultMatrixf(PROJECTION_MATRIX);  
  14.   
  15.     std::cout << ”print projection ” << std::endl;  
  16.     printMatrix(GL_PROJECTION_MATRIX);  
  17.   
  18.     //畫兩個方塊  
  19.     glColor3f(1, 0, 0); //先是個紅的  
  20.     drawRectangle(GloatPair(-0.5, 0.5), GloatPair(0.7, -0.7), 0.2);  
  21.     glColor3f(0, 1, 0); //再在背面畫個綠色的  
  22.     drawRectangle(GloatPair(-0.7, 0.7), GloatPair(0.5, -0.5), -0.2);  
  23.   
  24.     glFlush();  
  25.   
  26. }  
static const GLfloat PROJECTION_MATRIX [16] =  {
       1,   0,   0,  0,
       0,   1,   0,  0,
       0,   0,  -1,  0,
    -0.5, 0.5,   0,  1
};

static void display() {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glMultMatrixf(PROJECTION_MATRIX);

    std::cout << "print projection " << std::endl;
    printMatrix(GL_PROJECTION_MATRIX);

    //畫兩個方塊
    glColor3f(1, 0, 0); //先是個紅的
    drawRectangle(GloatPair(-0.5, 0.5), GloatPair(0.7, -0.7), 0.2);
    glColor3f(0, 1, 0); //再在背面畫個綠色的
    drawRectangle(GloatPair(-0.7, 0.7), GloatPair(0.5, -0.5), -0.2);

    glFlush();

}

     使用glMultMatrixf來直接指定矩陣。注意這裏的 PROJECTION_MATRIX 實際上是所需要的矩陣的轉置。更加複雜的斜投影在這裏就不繼續研究了。


4.4  透視投影

        經過了上面的洗禮,透視投影也沒什麼難以理解的概念了。透視投影的核心就是近大遠小。顯然一個物體如果就放在視點位置,那麼就是無窮大了(比如你的眼皮)。所以視點的觀察半徑一般是取非零值的。

      一般設定透視投影的方法有兩種:

     1、gluPerspective(theta, aspect, near, far)

     2、glFrustum(left, right, bottom, top, near, far)

     兩種實際上差不多,這裏只介紹後面一種。


       這張圖是在網上下的。注意這張圖畫的非常有技巧。left,right,bottom,top都是實實在在的座標值,但是near和far是個距離,這就意味着這兩個值不應該爲負,實際上也確實如此,如果設置爲負,則此次設置是不起作用的。

       從上面的視點座標系的分析可以理解,這裏的攝像機擺放在視點座標系的原點(他也就是書上所說的投影參考點),視線指向 z 軸負方向。剪裁的近平面和原平面的深度都爲正,所以實際上這些平面的Z座標都爲負(在視點座標系下)。即有:Znear = - near, Zfar = -far。

        而投影的平面,這條語句實際上是選擇的近平面,這樣可以減少很多麻煩的計算。

        如果只是理解這種投影的概念,那麼到這裏基本也就夠了,不過如果想要確定下面透視投影矩陣,就需要真正用到齊次座標,而且要牽涉到規範化的部分。所以放在下一節一起講。

        這裏貼一小段測試代碼以供備份,非常簡單,display的時候調用即可,log是我自己封裝的函數。

  1. static void testPerspectiveProjection() {  
  2.     log(”before”);  
  3.     printMatrix(GL_PROJECTION_MATRIX);  
  4.   
  5.     glMatrixMode(GL_PROJECTION);  
  6.     glLoadIdentity();  
  7.     //兩種調用,效果相同,注意near和far必須爲正,但實際指的是z軸的負座標  
  8.     //glFrustum(-1, 1, -1, 1, 1, 2);  
  9.     gluPerspective(90, 1, 1, 2);  
  10.   
  11.     log(”after”);  
  12.     printMatrix(GL_PROJECTION_MATRIX);  
  13.   
  14.   
  15.     //在-1.5處畫個方塊  
  16.     glColor3f(1, 0, 0);  
  17.     drawRectangle(-1.4, 1.4, -1.4, 1.4, -1.5);  
  18. }  
static void testPerspectiveProjection() {
    log("before");
    printMatrix(GL_PROJECTION_MATRIX);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    //兩種調用,效果相同,注意near和far必須爲正,但實際指的是z軸的負座標
    //glFrustum(-1, 1, -1, 1, 1, 2);
    gluPerspective(90, 1, 1, 2);

    log("after");
    printMatrix(GL_PROJECTION_MATRIX);


    //在-1.5處畫個方塊
    glColor3f(1, 0, 0);
    drawRectangle(-1.4, 1.4, -1.4, 1.4, -1.5);
}

5、裁剪照片:規範化

5.1  爲什麼要規範化

        世界雖大,但是能放在一張照片裏面顯示的東西是有限的。所以要對所見到的世界進行裁剪,取出想要的部分。

        但是在算法上實現裁剪是很複雜的。爲了方便這一流程,首先將需要的部分規範化,縮放到一個單位大小的格子裏面,然後再用一個單元大小的相框去丈量,將相框以外的全部捨棄,這就是規範化和裁剪的一個主要出發點和思路。

        對應的物理世界中的場景,就是將照相機(老式的似乎更加貼近)的所見縮放到一張標準大小的底片上,超過底片大小的部分全都不會顯示出來。

        那麼在OpenGL中,照相機的所見實際上已經被轉換成了投影平面上的一張二維圖像(各個點上也帶有z軸信息),現在要處理的就是這個東西。


5.2  規範化的目標

        既然要規範化,那麼就得先有一個規範。前面在投影部分也已經看到,每種投影,都有一個剪裁空間,稱之爲觀察體,對正投影來說是一個立方體,對斜投影來說是一個平行六面體,對透視投影來說是一個棱臺。如果一個觀察體是一個x、y、z座標範圍都是 [-1, 1] 的立方體,則稱之爲規範化立方體,這個就是所謂的規範。那麼,將原來的觀察體,映射到規範化立方體的過程,就是規範化。

        一個格外需要注意的地方是,由於後面的屏幕座標系通常是左手座標系,所以這裏的規範化觀察體也使用左手座標系,意味着 x 軸和 y 軸沒有改變,但是 z 軸的正方向轉了個。這帶來的結果是,在這樣的座標系下,z 的座標值越小,距離觀察者(也就是你)越近。實際上,在opengl中,進行規範化之後,近裁剪平面的z軸座標是 -1,遠裁剪平面的z軸座標是1。


5.3  正投影的規範化

        前面雖然是在透視投影中提到的規範化,不過還是先從簡單的說起。代碼中通過 glOrtho(left, right, bottom, top, near, far); 來指定了一個觀察體,這個觀察體本來就是一個立方體,x 的範圍是 [left, right],y 的範圍是 [bottom, top],z 的範圍是 [-far, -near],現在要將其放到一個左手座標系下的規範化立方體中,只需要進行平移和縮放。雖然座標軸體系發生了變換,不過實際上只是 z 軸座標取了個反,所以變動也很容易得到。綜合前面投影的變換,最後的矩陣 GL_PROJECTION 結果是:


         時刻牢記:這裏的near和far,在原視點座標系中的代表的 z 軸座標是 -near 和 -far。

         再回想4.2中的例子,紅色方塊的z軸座標是0.2,綠色的是-0.2,在原視點座標系中,紅色的距離視點更近(這樣的值可能還不太容易理解,如果換成紅色-0.6、綠色-1就更明顯了)。如果對GL_PROJECTION 調用 glLoadIdentity(),對應上面矩陣可以知道 near = 1, far = -1。換算成原視點座標系下 z 軸座標就是 近平面z = -1,遠平面 z = 1。正好是逆着視線的,所以應該看到綠色遮蓋紅色。

        另一方面,直接將紅綠點的座標進行轉換,紅色最終的z軸座標是 0.2,綠色是-0.2,從5.2上最後一句可以知道,座標值越小,距離觀察者越近,所以綠色更近,也應該看到綠色遮蓋紅色。這樣的判斷和程序運行的結果是完全相同的。


5.4 透視投影的規範化

       斜投影這裏不打算研究了,以後用到的時候再說吧。直接來看透視投影。這裏考慮使用 glFrustum(left, right, bottom, top, near, far) 設置的情況。

       

        以上圖爲例,(x, y, z) 投影到觀察平面的 (x’, y’, z’)。顯然 z’ = Znear,這張圖給出了 y-z 平面的剖面,容易看出  y’ = y ÷ z × z‘ = y ÷ z × Znear。同理可以得到  x’ = x ÷ z × Znear。即

        

        但是如果以這樣的結論去構造矩陣,就會碰到麻煩:變換矩陣要求對所有的點都適用,但是針對不同的點(x, y, z),其縮放比例卻和 z 的值有關,這樣的話,普通意義上來說,就無法構造變換矩陣了。

        這個時候,就體現出了齊次座標的好處:這裏實際上只需要保存一個額外的變量z,那麼爲什麼不用用第四個座標去記錄Z值呢?事實上,這是完全可行的。回憶2.1中的說明,齊次座標是四維座標(wx, wy, wz, w),只不過我們前面一直使用的w = 1罷了。

        接下去的問題是,現在 w = z了,那麼 z 座標必須進行變換,否則在齊次化的時候,就會始終爲1。和正投影一樣,z軸的值本身並不具有意義,但是其範圍和大小是有意義的。變換的最終目標是 Znear -> -1, Zfar -> 1,越近的點的z值越小。要做到這一點,只需要利用一下變換前的 w = 1,具體的方法可以在下面看到:


       這裏的a和b是兩個待定參數,利用Znear -> -1, Zfar -> 1,可以求取a和b的值:a = (Zfar +Znear) / (Zfar - Znear),b = 2 * Zfar * Znear / (Zfar - Znear)。即

       

       下一步就是要將(x’, y’, z’, w’)規範化。顯然通過縮放和平移就可以做到,現在已知的條件有以下這些: 

       (1)對x, left -> -1,right ->1

       (2)對y, bottom -> -1, top -> 1,上面兩條實際上是和正投影類似的。

       (3)對 z,已經在 [-1, 1] 範圍了 不變即可。

      這樣,很容易得到這一步的變換矩陣


      這個時候基本上就可以算是結束了,不過注意到  glFrustum 裏面設置的 near 值必須爲正,而 Znear 實際就是負,所以按照現在的變換矩陣求出來的 w 一般是負的。好在由於是齊次的,所以四個維度上都乘個 -1  就可以解決問題。

      考慮到 near = -Znear, far = - Zfar,替換進變換矩陣,所以最後的變換矩陣是這樣的:


       可以隨手寫個例子驗證一下,使用4.4中的例程即可。


6、展示照片:視口

        OK,萬事俱備,終於到了最後一步,將圖片展示出來。我們現在手上有的是一張單位大小的照片。現在只需要將它沖洗到指定的大小,並擺放在指定的位置。使用的方法就是 glViewport(x,y,  width, height),調用這個方法之後,會將照片的左下角擺放在(x,y)點,並將其縮放,使得原來單位大小的圖片,放大到寬爲 width,高爲 height。

        到這裏爲止,基於座標系的變換就結束了。


        最後再把變化的過程貼一遍


轉載出處: http://blog.csdn.net/ronintao/article/details/9157221

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章