寫在前面
OpenGL中的座標處理過程包括模型變換、視變換、投影變換、視口變換等內容,這個主題的內容有些多,因此分節學習,主題將分爲5節內容來學習。上一節模型變換,本節學習模型變換的下一階段——視變換。到目前位置,主要在2D下編寫程序,學習了視變換後,我們可以看到3D應用的效果了。本節示例程序均可在我的github下載。
通過本節可以瞭解到
- 視變換的概念
- 索引繪製立方體
- LookAt矩陣的推導(對數學不感興趣,可以跳過)
- 相機位置隨時間改變的應用程序
座標處理的全局過程(瞭解,另文詳述)
OpenGL中的座標處理包括模型變換、視變換、投影變換、視口變換等內容,具體過程如下圖1所示:
每一個過程處理都有其原因,這些內容計劃將會在不同節裏分別介紹,最後再整體把握一遍。
今天我們學習第二個階段——視變換。
並不存在真正的相機
OpenGL成像採用的是虛擬相機模型。在場景中你通過模型變換,將物體放在場景中不同位置後,最終哪些部分需要成像,顯示在屏幕上,主要由視變換和後面要介紹的投影變換、視口變換等決定。
其中視變換階段,通過假想的相機來處理矩陣計算能夠方便處理。對於OpenGL來說並不存在真正的相機,所謂的相機座標空間(camera space 或者eye space)只是爲了方便處理,而引入的座標空間。
在現實生活中,我們通過移動相機來拍照,而在OpenGL中我們通過以相反方式調整物體,讓物體以適當方式呈現出來。例如,初始時,相機鏡頭指向-z軸,要觀察-z軸上的一個立方體的右側面,那麼有兩種方式:
相機繞着+y軸,旋轉+90度,此時相機鏡頭朝向立方體的右側面,實現目的。注意這時立方體並沒有轉動。
相機不動,讓立方體繞着+y軸,旋轉-90度,此時也能實現同樣的目的。注意這時相機沒有轉動。完成這一旋轉的矩陣記作
Ry(−π2)
在OpenGL中,採用方式2來完成物體成像的調整。例如下面的圖表示了假想的相機:
進一步說明
進一步說明這裏相對的概念,對這個概念不感興趣的可以跳過。默認時相機位於(0,0,0),指向-z軸,相當於調用了:
glm::lookAt(glm::vec(0.0f,0.0f,0.0f),
glm::vec3(0.0f, 0.0f, -1.0f),
glm::vec3(0.0f, 1.0f, 0.0f)),
得到是單位矩陣,這是相機的默認情況。
上述第一種方式,相機繞着+y軸旋轉90度,相機指向-x軸,則等價於調用變爲:
glm::mat4 view =glm::lookAt(glm::vec(0.0f,0.0f,0.0f),
glm::vec3(-1.0f, 0.0f, 0.0f),
glm::vec3(0.0f, 1.0f, 0.0f)),
得到的視變換矩陣爲:
上述第二種方式,通過立方體繞着+y軸旋轉-90度,則得到的矩陣M,相當於:
glm::mat4 model = glm::rotate(glm::mat4(1.0), glm::radians(-90.0f), glm::vec3(0.0, 1.0, 0.0));
這裏得到的矩陣M和上面的矩陣view是相同的,可以自行驗證下。
也就是說,通過旋轉相機+y軸90度,和旋轉立方體+y軸-90度,最終計算得到的矩陣相同。調整相機來得到觀察效果,可以通過相應的方式來調整物體達到相同的效果。在OpenGL中並不存在真正的相機,這只是一個虛構的概念。
視變換矩陣的推導(瞭解,對數學不感興趣可跳過)
相機座標系由相機位置eye和UVN基向量(或者說由forward, side ,up)構成,如下圖所示:
各個參數的含義如下:
- 相機位置 也稱爲觀察參考點 (View Reference Point) 在世界座標系下指定相機的位置eye。
- 相機鏡頭方向,由相機位置和相機指向的目標(target)位置計算出,
forwrad=(target−eye) 。 - 相機頂部正朝向: View Up Vector 確定在相機哪個方向是向上的,一般取(0, 1, 0)。這個參數稍後詳細解釋。
上面的圖簡化爲:
在使用過程中,我們是要指定的參數即爲相機位置(eye),相機指向的目標位置(target)和viewUp vector三個參數。
Step1 : 首選計算相機鏡頭方向
進行標準化
Step2: 根據view-up vector和forward確定相機的side向量:
Step3 : 根據forward和side計算up向量:
這樣eye位置,以及forward、side、up三個基向量構成一個新的座標系,注意這個座標系是一個左手座標系,因此在實際使用中,需要對forward進行一個翻轉,利用-forward、side、up和eye來構成一個右手座標系。
我們的目標是計算世界座標系中的物體在相機座標系下的座標,也就是從相機的角度來解釋物體的座標。從一個座標系的座標變換到另一個座標系,這就是不同座標系間座標轉換的過程。
計算方法1——直接計算變換矩陣
從座標和變換一節,瞭解到,要實現不同座標系之間的座標轉換,需要求取一個變換矩陣。而這個矩陣就是一個座標系A中的原點和基在另一個座標系B下的表示。
我們將相機座標系的原點和基,使用世界座標系表示爲(s代表side基向量,u代表up基向量,f代表forward基向量):
現在要求取的是座標從世界座標系變換到相機座標系,則計算點p在相機座標系下表示爲:
即求得視變換矩陣爲
上面計算逆矩陣的過程中使用到了分塊矩陣求逆矩陣的定理:
設方陣A、D可逆,那麼分塊矩陣
(A0BD) 可逆,且其逆矩陣爲T−1=(A−10−A−1BD−1D−1)
這種方式對應的計算代碼如下:
// 手動構造LookAt矩陣 方式1
glm::mat4 computeLookAtMatrix1(glm::vec3 eye, glm::vec3 target, glm::vec3 viewUp)
{
glm::vec3 f = glm::normalize(target - eye); // forward vector
glm::vec3 s = glm::normalize(glm::cross(f, viewUp)); // side vector
glm::vec3 u = glm::normalize(glm::cross(s, f)); // up vector
glm::mat4 lookAtMat(
glm::vec4(s.x, u.x, -f.x, 0.0), // 第一列
glm::vec4(s.y, u.y, -f.y, 0.0), // 第二列
glm::vec4(s.z, u.z, -f.z, 0.0), // 第三列
glm::vec4(-glm::dot(s, eye),
-glm::dot(u, eye), glm::dot(f, eye), 1.0) // 第四列
);
return lookAtMat;
}
這種方式求取過程中涉及到了分塊矩陣的逆矩陣計算,如果不習慣,可以看下面的方式2,這是比較常用的方式。
計算方法2——利用旋轉和平移矩陣求逆矩陣
求取座標轉換矩陣的過程,也可以從另外一個角度出發,即將世界座標系旋轉和平移至於相機座標系重合,這樣這個旋轉
其中R就是上面求得的side、up、forward基向量構成的矩陣,如下:
那麼所求的矩陣view計算過程如下:
在計算過程中,使用到了旋轉矩陣的性質,即旋轉矩陣是正交矩陣,它的逆矩陣等於矩陣的轉置。
因此所求的:
同樣計算得到視變換矩陣爲:
這種方式對應的計算代碼如下:
// 手動構造LookAt矩陣 方式2
glm::mat4 computeLookAtMatrix2(glm::vec3 eye, glm::vec3 target, glm::vec3 viewUp)
{
glm::vec3 f = glm::normalize(target - eye); // forward vector
glm::vec3 s = glm::normalize(glm::cross(f, viewUp)); // side vector
glm::vec3 u = glm::normalize(glm::cross(s, f)); // up vector
glm::mat4 rotate(
glm::vec4(s.x, u.x, -f.x, 0.0), // 第一列
glm::vec4(s.y, u.y, -f.y, 0.0), // 第二列
glm::vec4(s.z, u.z, -f.z, 0.0), // 第三列
glm::vec4(0.0, 0.0, 0.0, 1.0) // 第四列
);
glm::mat4 translate;
translate = glm::translate(translate, -eye);
return rotate * translate;
}
OpenGL中視變換的實現
在OpenGL中,我們可以通過函數glm::lookAt來實現相機指定,這個函數計算的就是上面求出的視變換矩陣。以前glu版本實現爲gluLookAt,這兩個函數完成的功能是一樣的,參數定義如下:
API lookAt ( GLdouble eyeX, GLdouble eyeY, GLdouble eyeZ, GLdouble centerX, GLdouble centerY, GLdouble centerZ, GLdouble upX, GLdouble upY, GLdouble upZ)
其中eye指定相機位置,center指定相機指向目標位置,up指定viewUp向量。
利用GLM數學庫一般實現爲:
glm::mat4 view = glm::lookAt(eyePos,
glm::vec3(0.0f, 0.0f, 0.0f),
glm::vec3(0.0f, 1.0f, 0.0f));
下面利用這個函數進行一些實驗,以幫助理解。在設置相機參數之前,我們學習下繪製立方體,爲實驗增加素材。
繪製立方體
前面索引繪製矩形一節使用了索引了矩形,如果利用索引繪製立方體,表面上看確實可以節省頂點數據,但是存在的問題是,不能爲不同面上的共同頂點指定不同的紋理座標,這在某些情況下會出現問題的。例如下面使用索引繪製的立方體:
由於在正面和側面的頂點制定了相同的紋理座標,插值後紋理一致,並沒有出現可愛的貓咪圖案。爲此,我們需要爲共用頂點指定不同的頂點屬性,那麼解決辦法之一是,繼續使用頂點數組繪製方式,定義立方體的數據如下:
// 指定頂點屬性數據 頂點位置 顏色 紋理
GLfloat vertices[] = {
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // A
0.5f, -0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // B
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, // C
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, // C
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, // D
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // A
-0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, // E
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 0.0, 1.0f, // H
0.5f, 0.5f, -0.5f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, // G
0.5f, 0.5f, -0.5f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, // G
0.5f, -0.5f, -0.5f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, // F
-0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, // E
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, // D
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0, 1.0f, // H
-0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, // E
-0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, // E
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // A
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, // D
0.5f, -0.5f, -0.5f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, // F
0.5f, 0.5f, -0.5f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, // G
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, // C
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, // C
0.5f, -0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, // B
0.5f, -0.5f, -0.5f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, // F
0.5f, 0.5f, -0.5f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, // G
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 0.0, 1.0f, // H
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, // D
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, // D
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, // C
0.5f, 0.5f, -0.5f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, // G
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // A
-0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, // E
0.5f, -0.5f, -0.5f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, // F
0.5f, -0.5f, -0.5f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, // F
0.5f, -0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // B
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // A
};
着色器使用上一節繪製矩形的着色器程序。繪製立方體後,通過設定相機位置隨着時間發生改變來觀察這個立方體。指定相機位置爲在xoz平面圓周運動的點軌跡,代碼爲:
GLfloat radius = 3.0f;
GLfloat xPos = radius * cos(glfwGetTime());
GLfloat zPos = radius * sin(glfwGetTime());
glm::vec3 eyePos(xPos, 0.0f, zPos);
glm::mat4 view = glm::lookAt(eyePos,
glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
同時在代碼中指定投影方式爲透視投影,代碼爲:
// 投影矩陣
glm::mat4 projection = glm::perspective(glm::radians(45.0f),
(GLfloat)(WINDOW_WIDTH) / WINDOW_HEIGHT, 1.0f, 100.0f);
投影方式和投影矩陣的計算將在後面小結介紹,這裏只需要知道使用方法即可。
實現繪製立方體,效果如下圖所示:
從上面的圖中我們看到了奇怪的現象,立方體後面的部分繪製在了前面的部分上,這種現象是由於深度測試(Depth Test)未開啓影響的。深度測試根據物體在場景中到觀察者的距離,根據設定的glDepthFunc函數判定是否通過深度測試,默認爲GL_LESS,即深度小者通過測試繪製在最終的屏幕上。關於深度測試這個主題,後面會繼續學習,這裏不再展開。
OpenGL中開啓深度測試方法:
glEnable(GL_DEPTH_TEST);
同時在主循環中,清除深度緩衝區和顏色緩衝區:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
開啓深度測試後,旋轉相機來觀察立方體,效果如下:
viewUp向量
上面提到了在指定相機時需要指定相機的viewUP向量,這個向量指定了相機中哪個方向是向上的。對於相機而言,指定了相機位置eye和相機指向位置target後確定了相機的指向,位置不變,指向不變時,還是可以通過改變這個viewUp而影響成像的。這個類似於你眼睛的位置不變,看着的方向不變,但是你可以扭動脖子來確定哪個方向是向上,這個viewUp好比頭頂給定的方向。相機位置固定在(0,0,3.0),指向原點,依次取viewUp爲
圖中viewUp爲(0,1,0)時貓的尾巴朝上,爲(1,0,0)時相當於把脖子右旋轉90度,看到貓的尾巴是在左邊的;爲(0,-1,0)相當於倒立過來看,貓的尾巴是向下的。如果想了解更多關於viewUp的解釋,可以參考What exactly is the UP vector in OpenGL’s LookAt function.
更多立方體
上一節介紹了模型變換,我們可以利用模型變換,在場景中繪製多個立方體,同時相機的位置可以採用圓的參數方程或者球面參數方程設定。繪製多個立方體的方法:
// 指定立方體位移
glm::vec3 cubePostitions[] = {
glm::vec3(0.0f, 0.0f, 1.2f),
glm::vec3(0.0f, 0.0f, 0.0f),
glm::vec3(1.2f, 1.2f, 0.0f),
glm::vec3(-1.2f, 1.2f, 0.0f),
glm::vec3(-1.2f, -1.5f, 0.0f),
glm::vec3(1.2f, -1.5f, 0.0f),
glm::vec3(0.0f, 0.0f, -1.2f),
};
// 在主循環中繪製立方體
for (int i = 0; i < sizeof(cubePostitions) / sizeof(cubePostitions[0]); ++i)
{
model = glm::mat4();
model = glm::translate(model, cubePostitions[i]);
glUniformMatrix4fv(glGetUniformLocation(shader.programId, "model"),
1, GL_FALSE, glm::value_ptr(model));
glDrawArrays(GL_TRIANGLES, 0, 36);
}
對相機位置隨着時間進行改變,可以採用圓的參數方程或者球面參數方程設定。這裏只是作爲一個示例來設定,可以根據你的具體需求設定對應角度值。示例代碼如下:
// xoz平面內圓形座標
glm::vec3 getEyePosCircle()
{
GLfloat radius = 6.0f;
GLfloat xPos = radius * cos(glfwGetTime());
GLfloat zPos = radius * sin(glfwGetTime());
return glm::vec3(xPos, 0.0f, zPos);
}
// 球形座標 這裏計算theta phi角度僅做示例演示
// 可以根據需要設定
glm::vec3 getEyePosSphere()
{
GLfloat radius = 6.0f;
GLfloat theta = glfwGetTime(), phi = glfwGetTime() / 2.0f;
GLfloat xPos = radius * sin(theta) * cos(phi);
GLfloat yPos = radius * sin(theta) * sin(phi);
GLfloat zPos = radius * cos(theta);
return glm::vec3(xPos, yPos, zPos);
}
例如利用球面座標方程設定的相機位置,效果如下圖所示:
最後的說明
經過視變換後,世界座標系中座標轉換到了相機座標系下。需要注意的相機在OpenGL中是個假想的概念,本質是通過矩陣來完成計算的。本節設定相機位置爲圓周或者球面運動軌跡,並不能讓用戶來交互地觀察場景中物體,下一節將設計一個第一人稱FPS相機,讓用戶通過鍵盤和鼠標控制相機,更好地觀察場景。