OpenGL ES on iOS --- 座標系統與矩陣轉換

dearmiku發表於2017-12-14

簡述

本文記錄我記錄我學習 座標體系和矩陣轉換的過程,加深學習便於後續查詢,可能有些描述不夠準確,或者內容不夠充實,還請多多指正,共同學習

矩陣變換

我們將物體座標進行一系列變換,達到自己期望的位置,需要使用到矩陣.先說一下矩陣的公式.這裡我是本著瞭解的心態去學習的,因為已經有趁手的數學工具了,把重要的學完~ 我會再來研究這裡的.

矩陣相乘

這是一個簡單的矩陣相乘例子,

例子

這是矩陣乘法過程~

矩陣乘法過程

注意:

1, 矩陣相乘不遵守交換律 即 A * B ≠ B * A 2, 只有當左側矩陣列數 等於 右側矩陣行數 兩矩陣才能相乘.

在我們對座標進行縮放,位移,旋轉 等變換時,我們多用4x4矩陣來進行~

縮放

我們把縮放變數表示為(S1,S2,S3)我們可以為任意向量(x,y,z)定義一個縮放矩陣:

縮放矩陣

位移

如果我們把位移向量表示為(Tx,Ty,Tz),我們就能把位移矩陣定義為:

位移矩陣

旋轉

繞X軸旋轉

x軸旋轉

繞y軸旋轉

y軸旋轉

繞z軸旋轉

z軸旋轉

將旋轉分為繞3個軸進行旋轉,以達到自己希望的位置,見下面這個公式,(Rx,Ry,Rz)代表任意旋轉軸:

任意旋轉

這種處理方式,簡單容易理解,但是 會出現一個問題萬向節死鎖.

舉個栗子~ 加入在三維空間中有一個平行於X中的向量,然後將它繞Y軸旋轉至它平行於Z軸,這時繞z軸的任何旋轉都不會改變 向量的方向了. 在正常情況下,關於3個軸的旋轉過程應該是可以任意組合的,最終旋轉結果都是一致的,但是當出現了萬向節死鎖後, 就會導致各個軸旋轉順序 組合不同,而最終旋轉結果不同~ 大家可以找根筆試一試~也可以看看這裡:尤拉角與萬向節死鎖(圖文版

那麼如何解決呢~ 使用四元數 旋轉矩陣與四元數 因為複變函式早已還給老師~ 後續再研究補充~~

齊次座標

在上面關於描述3D座標的向量 是四維向量,多出一個分量w,w分量的用處是來創造3D視覺效果的,根據w分量的大小對物體進行拉伸,最後將w=1的截面進行展示,從而產生物體遠近效果. 這篇文章我覺得介紹的比較詳細 寫給大家看的“透視除法” —— 齊次座標和投影

組合

我將不同變換的矩陣組合起來~ 將一個放大2倍的矩陣 和位移(1,2,3)的矩陣組合起來~,得到新的變換矩陣

矩陣組合

將得到的變換矩陣 進行驗證

矩陣驗證

因為矩陣相乘是不遵守交換律的,所以在矩陣組合時,順序就十分重要, 建議 先縮放 --> 再旋轉 --> 再位移. 並且矩陣的順序是從右到左的 所以應該是 位移矩陣 * 旋轉矩陣 * 縮放矩陣 = 所需矩陣

補充

為什麼我們要用矩陣來進行著一系列的變換呢? 據我瞭解 是因為這樣做,將 旋轉,位移,縮放加以統一.簡化計算流程,提高計算機的計算效率.

最後總結:像這樣利用矩陣進行位移,縮放,旋轉 這一系列的變換叫做:仿射變換

座標體系

OpenGL 頂點著色器 希望接受的的頂點 都是標準化裝置座標(Normalized Device Coordinate, NDC)的座標,也就是(x,y,z)都是在 -1~1之間變換.在此之外的頂點丟棄,並且按照傳入的頂點進行繪製.

將3D的物體座標轉換到理想的繪製效果需要進行一些列的轉換過程. 區域性空間(Local Space)/物體空間(Object Space) ---> 世界空間(World Space) ---> 觀察空間(View Space)/視覺空間(Eye Space) --->裁剪空間(Clip Space) ---> 螢幕空間(Screen Space)

OpenGL是不提供數學工具的~ 我們可以使用 GLM(OpenGL Mathematics) GLM官網 我用的是0.9.9版本~

示意圖:

座標空間變換

區域性空間

區域性空間: 就表示物體在自己本身座標系裡的座標,比較像view的bounds屬性.可以理解為建模時模型的座標.

世界空間

世界空間: 表示物體需要展示世界裡的座標,比較像view的frame屬性,好比我們的模型是個房子,將它放到小鎮(世界)中, 這時它的座標就是在世界空間的座標.

將區域性空間座標轉換為世界空間座標需要進行一系列轉換,就像在象棋棋盤上放棋子,我們需要將棋子旋轉,位移...操作才能將棋子放到正確的位置上.

觀察空間

在最終展示時,我們展示的是使用者觀察的介面, 我們需要將世界空間的座標 轉換為 以使用者座標觀察視野產生的結果.

裁剪空間

在OpenGL 中所期望的座標是 標準化裝置座標, 所以我們 需要將自己的座標集進行轉換,將需要顯示的座標 落在 -1.0~1.0之間.

如果只是圖元(Primitive),例如三角形,的一部分超出了裁剪體積(Clipping Volume),則OpenGL會重新構建這個三角形為一個或多個三角形讓其能夠適合這個裁剪範圍,就像三角形的角被切了一刀變成四邊形那樣~

投影

在裁剪時,我們是將3D空間的物體 轉換為 2D空間的平面影象, 這樣的過程叫做投影, 像投影擷取的顯示的3D空間 平截頭體,它就像一個容器,在這裡面的所有座標都不會被裁剪掉~

正射投影

在裁剪空間階段,獲得平截頭體的方式, 就是通過 正射投影. 使用正射投影矩陣創天平截頭體 需要指定 近平面寬高, 遠平面寬高.

正射投影

通過正射投影矩陣,將3D空間的座標 對映到2D平面中,但是這樣產生的問題是 並沒有遠近縮放的效果,這時就需要 透視投影.

透視投影

透視投影,就是利用齊次座標w 來生成遠近效果的, 離觀察者越遠的頂點 w分量越大.在顯示時,頂點座標的每個分量都會除以w分量,進而使 遠端的物體小,近端的物體大.

透視矩陣 會根據平截頭體的頂點的遠近,對頂點w分量進行修改

組合

若上面每一個步驟都產生一個變換矩陣的話,那麼最終的頂點座標應該是這個樣子的 目標頂點 = 投影矩陣 * 觀察矩陣 * 模型矩陣 *原始頂點

標準化裝置座標

    //此方法建立的視窗就是對應的OpenGL 的標準化裝置視窗
    glViewport(0, 0, self.frame.size.width, self.frame.size.height);
複製程式碼

模型矩陣

模型矩陣 包含了 位移,縮放,旋轉操作, 它將應用到物體的所有定點上. 該矩陣的目的就是將原來位於 世界空間(0,0,0)點的物體 移動到它應該出現的位置.

觀察矩陣

觀察矩陣就像是 3D世界裡的攝像機,最終顯示的畫面 就是攝影機的位置 和 方向 觀察的畫面~ 觀察矩陣的建立就需要 GLM 提供的LooK AT 函式:

tmat4x4<T, P> lookAt(tvec3<T, P> const & eye, tvec3<T, P> const & center, tvec3<T, P> const & up)

該函式需要輸入 3個 vec3變數,返回觀察矩陣:

引數1: 相機在世界座標系的位置 引數2: 相機鏡頭指向的位置 引數3: 世界的上向量,上向量的方向 在顯示時 是指向螢幕上方的向量

通過對這三個變數控制,就可以實現我們希望的效果,我在練習時 實現的是類似 遊戲CS裡 攝像頭移動的方式

投影矩陣

glm::mat4 proj = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);

此函式其實是建立了一個 定義了 可視控制元件的 平頭截體,此空間以外的東西都將被拋棄~

投影矩陣

引數:

第一個引數定義了fov值,它表示了視野(Field of View),就相當於攝影機的攝影角度~~ 第二個引數 為寬高比, 由視口的寬/高所得 第三和第四個引數 設定了平截頭體的近和遠平面。我們通常設定近距離為0.1f,而遠距離設為100.0f。所有在近平面和遠平面內且處於平截頭體內的頂點都會被渲染.

最後經過透視矩陣的處理,就產生了 物體 遠小近大的效果了~

程式碼

這裡就撿與本文相關的說~

頂點著色器

#version 300 es

layout(location = 0) in vec3 position;
layout(location = 1) in vec3 color;
layout(location = 2) in vec2 texCoord;  //紋理座標

//uniform mat4 transform;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

out vec3 outColor;
out vec2 outTexCoord;

void main()
{
    gl_Position = projection*view*model*vec4(position,1.0);
    outColor = color;
    outTexCoord = texCoord;
}
複製程式碼

開啟深度測試

OpenGL儲存它的所有深度資訊於一個Z緩衝(Z-buffer)中,也被稱為深度緩衝(Depth Buffer)。GLFW會自動為你生成這樣一個緩衝(就像它也有一個顏色緩衝來儲存輸出影象的顏色)。深度值儲存在每個片段裡面(作為片段的z值),當片段想要輸出它的顏色時,OpenGL會將它的深度值和z緩衝進行比較,如果當前的片段在其它片段之後,它將會被丟棄,否則將會覆蓋。這個過程稱為深度測試(Depth Testing),它是由OpenGL自動完成的。

深度測試還有其他關於 自定以的函式,以後再說~~

    int wi,he;
    //檢索有關繫結緩衝區的物件的資訊 ,這裡獲得了layer的寬高
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &wi);
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &he);

    glGenRenderbuffers(1, &depthBuf);
    glBindRenderbuffer(GL_RENDERBUFFER, depthBuf);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, wi, he);
    
    //還要記得開啟深度測試
      glEnable(GL_DEPTH_TEST);
複製程式碼

模型矩陣

  glm::vec3 cubePositions[] = {
        glm::vec3( 0.0f,  0.0f,  0.0f),
        glm::vec3( 2.0f,  5.0f, -15.0f),
        glm::vec3(-1.5f, -2.2f, -2.5f),
        glm::vec3(-3.8f, -2.0f, -12.3f),
        glm::vec3( 2.4f, -0.4f, -3.5f),
        glm::vec3(-1.7f,  3.0f, -7.5f),
        glm::vec3( 1.3f, -2.0f, -2.5f),
        glm::vec3( 1.5f,  2.0f, -2.5f),
        glm::vec3( 1.5f,  0.2f, -1.5f),
        glm::vec3(-1.3f,  1.0f, -1.5f)
    };
    for(unsigned int i = 0; i < 10; i++)
    {
        //模型矩陣
        glm::mat4 model;
        model = glm::translate(model, cubePositions[i]);
        float angle = 20.0f * i;
        model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
        glUniformMatrix4fv(glGetUniformLocation(program, "model"), 1, GL_FALSE, glm::value_ptr(model));

        glDrawArrays(GL_TRIANGLES, 0, 36);
    }
複製程式碼

這裡的模型矩陣 將物體 進行位移,旋轉後 放在世界座標系中合適的位置.

觀察矩陣

定義了 相機的初始位置 與 初始方向(這裡講方向保持為單位向量)
glm::vec3 cameraLo = glm::vec3(10.0f,0.0f,0.0f);
glm::vec3 cameraDir = glm::vec3(1.0f,0.0f,0.0f);

.........其他程式碼.........


glm::mat4 view;
view = glm::lookAt(cameraLo,cameraLo-cameraDir, glm::vec3(0.0, 1.0, 0.0));

//將鏡頭觀察點 保持為 鏡頭方向位置  向上方向 設定為y軸.

glUniformMatrix4fv(glGetUniformLocation(program, "view"), 1, GL_FALSE, glm::value_ptr(view));
複製程式碼

在Display計時器中持續呼叫該方法

-(void)move{
    if (_isAdvance) {
        // 使攝像機 向朝向方向移動~~
        cameraLo -=(cameraDir*(speed/24));
    }
    if (_isback) {
        cameraLo +=(cameraDir*(speed/24));
    }
    
    //改變攝像機朝向
    if (_isLeft) {
        cameraDir = glm::normalize(glm::vec3(cameraDir[0],cameraDir[1],cameraDir[2]-rotateSpeed));
    }
    if (_isRight) {
        cameraDir = glm::normalize(glm::vec3(cameraDir[0],cameraDir[1],cameraDir[2]+rotateSpeed));
    }
    if (_isUp) {
        cameraDir = glm::normalize(glm::vec3(cameraDir[0],cameraDir[1]-rotateSpeed,cameraDir[2]));
    }
    if (_isDown) {
        cameraDir = glm::normalize(glm::vec3(cameraDir[0],cameraDir[1]+rotateSpeed,cameraDir[2]));
    }
    [self render];
}
複製程式碼

demo地址

最終實現效果: [圖片上傳失敗...(image-2d9806-1512195914588)]

相關文章