OpenGL入門第三課--矩陣變換與座標系統

爛筆桿發表於2019-05-21

     在 OpenGL中,物體在被渲染到螢幕之前需要經過一系列的座標變換,聽起來有點嚇人;不過呢如果有一定的線性代數的基礎利用矩陣變換,其實也就沒那麼難了。即使沒學過線性代數,只需要瞭解一些基本的矩陣運算也基本可以滿足大家學習 OpenGL的要求了。下面我們就來簡單學習一下有關矩陣的知識。

    矩陣是一種非常有用的數學工具,雖然有點難度,但是一旦你理解了它們後,它們會變得非常有用。為了深入瞭解矩陣變換,我們首先要在討論矩陣之前瞭解一下向量。

向量

       向量最基本的定義就是一個方向。或者更正式的說,向量有一個方向(Direction)和大小(Magnitude,也叫做強度或長度)。你可以把向量想像成一個藏寶圖上的指示:“向左走10步,向北走3步,然後向右走5步”;“左”就是方向,“10步”就是向量的長度。由於向量表示的是方向,起始於何處並不會改變它的值。如下圖向量v和向量w就是相等的. 

                           OpenGL入門第三課--矩陣變換與座標系統

        向量可以在任意維度上,一般用到的都是2維和3維,一個三維向量在公式中通常是這樣表示的。入下圖:

  


                                         OpenGL入門第三課--矩陣變換與座標系統

      由於向量是一個方向,所以有些時候會很難形象地將它們用位置(Position)表示出來。為了讓其更為直觀,我們通常設定這個方向的原點為(0, 0, 0),然後指向一個方向,對應一個點,使其變為位置向量(Position Vector)(你也可以把起點設定為其他的點,然後說:這個向量從這個點起始指向另一個點)。比如說位置向量(3, 5)在影象中的起點會是(0, 0),並會指向(3, 5)。

向量與標量的運算

       標量(Scalar)只是一個數字(或者說是僅有一個分量的向量)。當把一個向量加/減/乘/除一個標量,我們可以簡單的把向量的每個分量分別進行該運算。對於加法來說會像這樣:

                                              OpenGL入門第三課--矩陣變換與座標系統

      其中的+可以是+,-,·或÷,其中·是乘號。注意-和÷運算時不能顛倒(標量-/÷向量),因為顛倒的運算是沒有定義的。

向量取反

     對一個向量取反(Negate)會將其方向逆轉。一個指向東北的向量取反後就指向西南方向了。我們在一個向量的每個分量前加負號就可以實現取反了(或者說用-1數乘該向量):

                                       OpenGL入門第三課--矩陣變換與座標系統

向量相減

     向量的加法可以被定義為是分量的相加,即將一個向量中的每一個分量加上另一個向量的對應分量;就像普通數字的加減一樣,向量的減法等於加上第二個向量的相反向量:

    OpenGL入門第三課--矩陣變換與座標系統

向量的長度

     我們使用勾股定理來獲取向量的長度:

                                                          OpenGL入門第三課--矩陣變換與座標系統

      我們也可以加上Z的平方 把這個公式擴充套件到三維空間。另外有一個特殊型別的向量叫做單位向量(Unit Vector)。單位向量有一個特別的性質——它的長度是1。我們可以用任意向量的每個分量除以向量的長度得到它的單位向量。

向量相乘

     向量相乘分為點乘和叉乘:

點乘:兩個向量的點乘等於它們的數乘結果乘以兩個向量之間夾角的餘弦值。可能聽起來有點費解,我們來看一下公式。

                                   OpenGL入門第三課--矩陣變換與座標系統

      它們之間的夾角記作θ,這有什麼用,想象一下假如是兩個單位向量點乘會是什麼結果?現在點積只定義了兩個向量的夾角。是否還記得90度的餘弦值是0,0度的餘弦值是1。這樣使用點乘可以很容易測試兩個向量是否正交(Orthogonal)或平行(正交意味著兩個向量互為直角)。

    也可以通過點乘的結果計算兩個非單位向量的夾角,點乘的結果除以兩個向量的長度之積,得到的結果就是夾角的餘弦值,即cosθ。所以,我們該如何計算點乘呢?點乘是通過將對應分量逐個相乘,然後再把所得積相加來計算的。

叉乘:叉乘只在3D空間中有定義,它需要兩個不平行向量作為輸入,生成一個正交於兩個輸入向量的第三個向量.如果輸入的兩個向量也是正交的,那麼叉乘之後將會產生3個互相正交的向量。下面的圖片展示了3D空間中叉乘的樣子:

                                OpenGL入門第三課--矩陣變換與座標系統

      不同於其他運算,如果你沒有鑽研過線性代數,可能會覺得叉乘很反直覺,所以只記住公式就沒問題(記不住也沒問題)。下面你會看到兩個正交向量A和B叉積:

                OpenGL入門第三課--矩陣變換與座標系統

矩陣

       矩陣就是一個矩形的數學表示式陣列,矩陣中每一項叫做矩陣的元素(Element)。下面是一個2×3矩陣的例子:

                                             OpenGL入門第三課--矩陣變換與座標系統

     矩陣可以通過(i, j)進行索引,i是行,j是列,這就是上面的矩陣叫做2×3矩陣的原因。

矩陣的加減法

     矩陣與標量之間的加減定義如下:

OpenGL入門第三課--矩陣變換與座標系統

矩陣與標量的減法也相似:

OpenGL入門第三課--矩陣變換與座標系統

    矩陣與矩陣之間的加減就是兩個矩陣對應元素的加減運算,所以總體的規則和與標量運算是差不多的,只不過在相同索引下的元素才能進行運算。這也就是說加法和減法只對同維度的矩陣才是有定義的。一個3×2矩陣和一個2×3矩陣(或一個3×3矩陣與4×4矩陣)是不能進行加減的。我們看看兩個2×2矩陣是怎樣相加的:

OpenGL入門第三課--矩陣變換與座標系統

同樣的法則也適用於減法:

OpenGL入門第三課--矩陣變換與座標系統

矩陣的數乘

     和矩陣與標量的加減一樣,矩陣與標量之間的乘法也是矩陣的每一個元素分別乘以該標量。下面的例子展示了乘法的過程:

 OpenGL入門第三課--矩陣變換與座標系統

矩陣相乘

      矩陣之間的乘法不見得有多複雜,但的確很難讓人適應。矩陣乘法基本上意味著遵照規定好的法則進行相乘。當然,相乘還有一些限制:

  • 只有當左側矩陣的列數與右側矩陣的行數相等,兩個矩陣才能相乘;
  • 矩陣相乘不遵守交換律(Commutative),也就是說A⋅B≠B⋅A;

我們先看一個兩個2×2矩陣相乘的例子:

OpenGL入門第三課--矩陣變換與座標系統

    可以看到,矩陣相乘非常繁瑣而容易出錯,不夠不要緊,在我們OpenGL中是不會讓大家自己去計算這些的,都有相應API來完成,這裡只是希望大家瞭解一下矩陣相乘的原理。

矩陣與向量相乘

    現在我們已經相當瞭解向量了。在OpenGL中可以用向量來表示位置,表示顏色,甚至是紋理座標。與矩陣類比一下,它其實就是一個N×1矩陣,N表示向量分量的個數(也叫N維(N-dimensional)向量)。仔細想一下,其實向量和矩陣一樣都是一個數字序列,但它只有1列。那麼,這個新的定義對我們有什麼幫助呢?如果我們有一個M×N矩陣,我們可以用這個矩陣乘以我們的N×1向量,因為這個矩陣的列數等於向量的行數,所以它們就能相乘。

但是為什麼我們會關心矩陣能否乘以一個向量?好吧,正巧,很多有趣的2D/3D變換都可以放在一個矩陣中,用這個矩陣乘以我們的向量將變換(Transform)這個向量。如果你仍然有些困惑,我們來看一些例子,你很快就能明白了。

單位矩陣

      在OpenGL中,由於某些原因我們通常使用4×4的變換矩陣,而其中最重要的原因就是大部分的向量都是4分量的。我們能想到的最簡單的變換矩陣就是單位矩陣(Identity Matrix)。單位矩陣是一個除了對左角線是1以外其他都是0的N×N矩陣。在下式中可以看到,這種變換矩陣使一個向量完全不變:

   OpenGL入門第三課--矩陣變換與座標系統

     你可能會奇怪一個沒變換的變換矩陣有什麼用?單位矩陣通常是生成其他變換矩陣的起點,如果我們深挖線性代數,這還是一個對證明定理、解線性方程非常有用的矩陣。

縮放

    下面會構造一個變換矩陣來為我們提供縮放功能。我們從單位矩陣瞭解到,每個對角線元素會分別與向量的對應元素相乘。如果我們把1變為3會怎樣?這樣子的話,我們就把向量的每個元素乘以3了,這事實上就把向量縮放3倍。如果我們把縮放變數表示為(S1,S2,S3)我們可以為任意向量(x,y,z)定義一個縮放矩陣:

          OpenGL入門第三課--矩陣變換與座標系統

     注意,第四個縮放向量仍然是1,因為在3D空間中縮放w分量是無意義的。w分量另有其他用途。

位移

    位移(Translation)是在原始向量的基礎上加上另一個向量從而獲得一個在不同位置的新向量的過程,從而在位移向量基礎上移動了原始向量。我們已經討論了向量加法,所以這應該不會太陌生。和縮放矩陣一樣,在4×4矩陣上有幾個特別的位置用來執行特定的操作,對於位移來說它們是第四列最上面的3個值。如果我們把位移向量表示為(Tx,Ty,Tz),我們就能把位移矩陣定義為:

        OpenGL入門第三課--矩陣變換與座標系統

    有了位移矩陣我們就可以在3個方向(x、y、z)上移動物體,它是我們的變換工具箱中非常有用的一個變換矩陣。

旋轉

    在3D空間中旋轉需要定義一個角和一個旋轉軸(Rotation Axis)。物體會沿著給定的旋轉軸旋轉特定角度。當2D向量在3D空間中旋轉時,我們把旋轉軸設為z軸。

     使用三角學,給定一個角度,可以把一個向量變換為一個經過旋轉的新向量。這通常是使用一系列正弦和餘弦函式(一般簡稱sin和cos)各種巧妙的組合得到的。

       旋轉矩陣在3D空間中每個單位軸都有不同定義,旋轉角度用θ表示:

沿x軸旋轉:

OpenGL入門第三課--矩陣變換與座標系統

沿y軸旋轉:

OpenGL入門第三課--矩陣變換與座標系統

沿z軸旋轉:

OpenGL入門第三課--矩陣變換與座標系統

    利用旋轉矩陣我們可以把任意位置向量沿一個單位旋轉軸進行旋轉。也可以將多個矩陣複合,比如先沿著x軸旋轉再沿著y軸旋轉。

    是不是感覺旋轉看起來好複雜,這些旋轉矩陣都是怎麼計算出來的,其實這些都不需要太過在意,只有理解旋轉也是通過一個特定的矩陣相乘完成的就可以了。

矩陣的組合

    使用矩陣進行變換的真正力量在於,根據矩陣之間的乘法,我們可以把多個變換組合到一個矩陣中。讓我們看看我們是否能生成一個變換矩陣,讓它組合多個變換。假設我們有一個頂點(x, y, z),我們希望將其縮放2倍,然後位移(1, 2, 3)個單位。我們需要一個位移和縮放矩陣來完成這些變換。結果的變換矩陣看起來像這樣:

OpenGL入門第三課--矩陣變換與座標系統

    注意,當矩陣相乘時我們先寫位移再寫縮放變換的。矩陣乘法是不遵守交換律的,這意味著它們的順序很重要。當矩陣相乘時,在最右邊的矩陣是第一個與向量相乘的,所以你應該從右向左讀這個乘法。建議您在組合矩陣時,先進行縮放操作,然後是旋轉,最後才是位移,否則它們會(消極地)互相影響。比如,如果你先位移再縮放,位移的向量也會同樣被縮放(譯註:比如向某方向移動2米,2米也許會被縮放成1米)!

用最終的變換矩陣左乘我們的向量會得到以下結果:

           OpenGL入門第三課--矩陣變換與座標系統

不錯!向量先縮放2倍,然後位移了(1, 2, 3)個單位。

OpenGL中的座標系

       OpenGL中頂點著色後,我們的可見頂點都為標準化裝置座標(Normailzed Device Coordinate,NDC)。也就是每個頂點的x,y,z都應該在-1到1直接,否則對我們都是不可見的。

      一個頂點在被轉化為片段之前需要依次經歷一下幾個重要的座標系:

  1. 區域性空間(Local Space 或者稱為 物體空間 Object Space)
  2. 世界空間(World Space)
  3. 觀察空間 (View Space 或者稱為 視覺空間 Eye Space)
  4. 裁剪空間(Clip Space)
  5. 螢幕空間 (Screen Space)

   從一個座標系變到另外一個座標系需要利用變換矩陣,最重要的幾個分別是模型(Model)、觀察(View)、投影(Projection)三個矩陣.物體頂點的起始座標再區域性空間(Local Space),這裡稱它為區域性座標(Local Coordinate),它在之後會變成世界座標(world Coordinate),觀測座標(View Coordinate),裁剪座標(Clip Coordinate),並最後以螢幕座標(Screen Corrdinate)的形式結束.

       想要把3D圖形最終渲染到2D裝置螢幕上,除了使用模型變換和視變換將物體座標轉換到照相機座標系外,還需要進行投影變換將座標變為裁剪座標系,然後經過透視除法變換到規範化裝置座標系(NDC),最後進行視口變換渲染到2D螢幕上,如下圖:

OpenGL入門第三課--矩陣變換與座標系統

     在上面的圖中,OpenGL只定義了裁剪座標系、規範化裝置座標系和螢幕座標系,而區域性座標系(物體座標系)、世界座標系和照相機座標系都是為了方便使用者設計而自定義的座標系。也就是說,模型變換、視變換、投影變換,這些變換可以根據開發者的需求自行定義,這些內容在頂點著色器中完成。另外的兩個透視除法和視口變換,這兩個步驟是OpenGL自動執行的,在頂點著色器處理後的階段完成。

     上面的每一個變換都建立了一個變換矩陣,模型矩陣、觀察矩陣和投影矩陣。講這些矩陣組合起來,一個頂點座標將會根據以下過程被變換到裁剪座標:

OpenGL入門第三課--矩陣變換與座標系統

     注意矩陣運算的順序是相反的(記住我們需要從右往左閱讀矩陣的乘法)。最後的頂點應該被賦值到頂點著色器中的gl_Position,OpenGL將會自動進行透視除法和裁剪。

    說了那麼多理論,好像都不知道怎麼用,下面我們來寫一個簡單的案例,看看如何利用矩陣變換講3d影象到渲染到2d螢幕上。

案例

    我們首先來看一下案例效果再來說說如何實現:

                                                            OpenGL入門第三課--矩陣變換與座標系統

    這這個案例中我們先通過先通過平移矩陣與旋轉矩陣叉乘得到模型檢視矩陣,然後通過投影矩陣叉乘模型檢視矩陣得到模型檢視投影矩陣也就是我們常說的mvp,然後通過平面著色器畫出圖形。具體程式碼如下:

main函式一些初始化操作和回撥函式的註冊:

int main(int argc, char* argv[]){   
 gltSetWorkingDirectory(argv[0]);   
 glutInit(&argc, argv);   
 glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_SINGLE);   
 glutInitWindowSize(800, 600);   
 glutCreateWindow("矩陣變換Demo");   
 glutReshapeFunc(ChangeSize);   
 glutDisplayFunc(RenderScene);   
 GLenum error = glewInit();    
 if(GLEW_OK != error) {        
   fprintf(stderr,"GLEW Error: %s\n",glewGetErrorString(error));      
   return 1;  
  }   
 setupRC();   
 glutMainLoop();   
 return 0;
}複製程式碼

這裡有三個方法比較重要 第一個 setupRC,主要完成一些繪圖前的準備工作:

void setupRC () {
    glClearColor(0.8, 0.8, 0.8, 1.0f); //設定清屏顏色  
    shaderManager.InitializeStockShaders();//初始化固定管線
    glEnable(GL_DEPTH_TEST);//開啟深度測試   
    gltMakeSphere(torusBatch, 0.4, 10, 20);//形成一個球    
    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);//設定多邊形填充模式
}複製程式碼

    changeSize這個函式在初次視窗顯示 或者其他任何時候視窗改變的時候將會被呼叫。主要完成視口和投影矩陣的設定,具體如下:

void ChangeSize(int w, int h){
    if(h == 0) h = 1;
    glViewport(0, 0, w, h);   
     viewFrustum.SetPerspective(35, float(w)/float(h), 1, 1000); 
 }複製程式碼

RenderScene函式就是具體完成繪製的函式,具體程式碼如下:

void RenderScene(void){    
    //清除螢幕、深度快取區
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    //1.建立基於時間變化的動畫
    static CStopWatch rotTimer;
    //當前時間 * 60s
    float yRot = rotTimer.GetElapsedSeconds() * 60.0f;
    //2.矩陣變數
    /*
     mTranslate: 平移
     mRotate: 旋轉
     mModelview: 模型檢視
     mModelViewProjection: 模型檢視投影MVP
     */
    M3DMatrix44f mTranslate, mRotate, mModelview, mModelViewProjection;
    //建立一個4*4矩陣變數,將花托沿著Z軸負方向移動2.5個單位長度
    m3dTranslationMatrix44(mTranslate, 0.0f, 0.0f, -2.5f);
    //建立一個4*4矩陣變數,將花托在Y軸上渲染yRot度,yRot根據經過時間設定動畫幀率
     m3dRotationMatrix44(mRotate, m3dDegToRad(yRot), 0.0f, 1.0f, 0.0f);
    //為mModerView 通過矩陣旋轉矩陣、移動矩陣相乘,將結果新增到mModerView上
    m3dMatrixMultiply44(mModelview, mTranslate, mRotate);
    // 將投影矩陣乘以模型檢視矩陣,將變化結果通過矩陣乘法應用到mModelViewProjection矩陣上
    //注意順序: 投影 * 模型 != 模型 * 投影
     m3dMatrixMultiply44(mModelViewProjection, viewFrustum.GetProjectionMatrix(),mModelview);
    //繪圖顏色
    GLfloat vBlack[] = { 0.0f, 0.0f, 0.0f, 1.0f };
    //通過平面著色器提交矩陣,和顏色。
    shaderManager.UseStockShader(GLT_SHADER_FLAT, mModelViewProjection, vBlack);
    //開始繪圖
    torusBatch.Draw();
    // 交換緩衝區,並立即重新整理
    glutSwapBuffers();
    glutPostRedisplay();
} 複製程式碼

   這裡我們先建立了平移和旋轉矩陣,通過叉乘 :平移矩陣 X 旋轉矩陣 = 模型檢視矩陣。從右往左度實際是旋轉後平移,為什麼要先旋轉後平移在上文矩陣的組合中已經說過了。

然後通過投影矩陣叉乘模型檢視矩陣得到模型檢視投影矩陣(mvp),這樣就通過固定管線的平面著色器需要的引數就有了,然後呼叫相關OpenGL API 就能順利完成繪製。










相關文章