GL中的座標系是標準裝置座標,即他的每個座標軸的取值範圍都是[-1.0,1.0]。通常,我們輸入到頂點著色器中的頂點座標都會被轉換為標準化裝置座標,然後進行光柵化,轉變成螢幕座標。然而事實上,從頂點座標到螢幕座標是一個較為複雜的過程。總體來講為了某些計算更加方便,會經過5個座標系統的變換:
- 區域性空間(Local Space,或者稱為物體空間(Object Space))
- 世界空間(World Space)
- 觀察空間(View Space,或者稱為視覺空間(Eye Space))
- 裁剪空間(Clip Space)
- 螢幕空間(Screen Space)
下面就是座標系統的具體意義。
1. 概述
經過了這麼久的介紹,我們都一直在繪製螢幕中的一個矩形。而事實上,之前的一切過程,我們還只是停留在建造模型的過程。這時候我們的畫布裡只有這一個模型,我們把當前的座標系統稱作是區域性空間
。
當然,我們的螢幕中大多數情況下不可能只有一個模型,而是千千萬萬個模型。我們應該透過一系列矩陣變換將模型變換到一個更大的畫布中。當然,我們的螢幕是不可能變大的,所以我們是透過一系列的矩陣縮放我們的模型然後放到原始畫布中來模擬把模型放入大畫布中這個過程。這裡,我們把變換到大畫布後的座標系統稱為是世界空間
。而從區域性空間變換到世界空間轉換所需要的這個矩陣,我們成為模型矩陣
。
我們知道,OpenGL是一個3維的世界,然而我們螢幕是一個2維的畫面。就好像我們生活在3維空間中但是我們所觀察的世界實際是以我們的眼睛作為起始點獲取的3維空間在我們的視網膜上投影在將資訊傳給我們的大腦。那麼OpenGL模擬了這個過程,首先我們需要一個眼睛,再其次我們需要將世界投影到我們的螢幕上。
在OpenGL中我們把這個眼睛稱作是攝像機。而攝像機針對的座標系統稱為觀察空間
。我們從世界空間轉換到觀察空間所經過的矩陣為觀察矩陣
。經過觀察矩陣轉換後,實際上我們看到的就是3維世界在我們攝像機所面對的方向上的一個投影了。
生活中我們有一個常識,物體*大遠小。物體距離我們越遠,他看起來將會越小,我們把這種現象稱為透視現象。然而在觀察空間我們看到的投影缺不具備這種特點,我們把這種按照物體原比例顯示的投影稱為正投影。然而這樣的投影卻與我們*常所觀察到的世界不一樣,為了讓事物看起來更加真實,我們要給物體加上透視效果。經過透視的投影,就是透視投影。GL中,我們為了使物體具有透視效果,我們要將物體經過一個透視投影矩陣
進行轉換,轉換至的空間我們稱為裁剪空間
。之所以稱為裁剪空間,是因為除了投食之外,我們還要把超出視野的地方裁減掉。而GL中就是把超出螢幕空間的物體裁減掉。所以稱之為裁剪空間。
最後,我們要把裁剪空間中的物體轉換到我們的螢幕上進行輸出。螢幕輸出的空間我們叫做螢幕空間
。這個過程呢,就不用我們費心了,因為到了裁剪空間之後我們已經完全完成了模型到透視投影的轉換。接下來只需要將這部分物體展示在螢幕上就好,所以這部分工作由GL替我們完成。這個過程,我們叫視口變換
。視口變換將位於-1.0到1.0範圍的座標變換到由glViewport
函式所定義的座標範圍內。最後變換出來的座標將會送到光柵器,將其轉化為片段。
到這裡我們已經大概清楚了這些空間的作用,那麼在對應空間中的座標就分別稱為區域性座標(Local Coordinate)
、世界座標(World Coordinate)
、觀察座標(View Coordinate)
、裁剪座標(Clip Coordinate)
和螢幕座標(Screen Coordinate)
。
2. 組合
我們知道,從我們的區域性空間到螢幕空間需要我們先把區域性座標轉換至裁剪座標,再交由GL轉換為螢幕座標。所以我們應該經過的過程就是:
順序一定不要搞錯,記住矩陣乘法是從右向左的。
3. 進入3D
這裡我就不放全部程式碼了,先放一段模型構建的程式碼
1 void configVAO(unsigned int * VAO,unsigned int * VBO,unsigned int * EBO) { 2 ///頂點資料 3 float vertices[] = { 4 0.5,0.5,0.5,0.0,0.0,0.0, 5 0.5,-0.5,0.5,1.0,0.0,0.0, 6 -0.5,-0.5,0.5,0.0,1.0,0.0, 7 -0.5,0.5,0.5,0.0,0.0,1.0, 8 0.5,0.5,-0.5,1,1,1, 9 0.5,-0.5,-0.5,0,1,1, 10 -0.5,-0.5,-0.5,1,0,1, 11 -0.5,0.5,-0.5,1,1,0 12 }; 13 14 ///索引資料 15 unsigned int indices[] = { 16 0,1,2, 17 0,2,3, 18 1,4,5, 19 0,1,4, 20 5,6,7, 21 4,5,7, 22 2,3,6, 23 3,6,7, 24 0,3,4, 25 3,4,7, 26 1,5,6, 27 1,2,6, 28 }; 29 30 ///建立頂點陣列物件 31 glGenVertexArrays(1, VAO); 32 33 ///建立頂點緩衝物件 34 glGenBuffers(1, VBO); 35 ///建立索引緩衝物件 36 glGenBuffers(1, EBO); 37 38 ///繫結定點陣列物件至上下文 39 glBindVertexArray(*VAO); 40 41 ///繫結定點緩衝物件至上下文 42 glBindBuffer(GL_ARRAY_BUFFER, *VBO); 43 ///把頂點陣列複製到頂點緩衝物件中 44 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); 45 ///設定頂點屬性並啟用屬性 46 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0); 47 glEnableVertexAttribArray(0); 48 glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,6 * sizeof(float), (void*)(3 * sizeof(float))); 49 glEnableVertexAttribArray(1); 50 ///繫結索引緩衝物件至上下文 51 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, *EBO); 52 ///把索引資料複製到索引緩衝物件中 53 glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); 54 55 ///解除頂點陣列物件的繫結 56 glBindVertexArray(0); 57 ///解除頂點緩衝物件的繫結 58 glBindBuffer(GL_ARRAY_BUFFER, 0); 59 ///解除索引緩衝物件的繫結 60 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0); 61 }
上面我們建立了一個正方體,八個頂點分別有八個顏色。目前,他還在區域性空間內。
接下來我們來將他們轉換到裁剪空間內:
1 glm::vec3 postions[] = { 2 glm::vec3(0.0,0.0,0.0), 3 glm::vec3( 2.0f, 5.0f, -15.0f), 4 glm::vec3(-1.5f, -2.2f, -2.5f), 5 glm::vec3(-3.8f, -2.0f, -12.3f), 6 glm::vec3( 2.4f, -0.4f, -3.5f), 7 glm::vec3(-1.7f, 3.0f, -7.5f), 8 glm::vec3( 1.3f, -2.0f, -2.5f), 9 glm::vec3( 1.5f, 2.0f, -2.5f), 10 glm::vec3( 1.5f, 0.2f, -1.5f), 11 glm::vec3(-1.3f, 1.0f, -1.5f) 12 }; 13 14 glm::mat4 view = glm::mat4(1.0f); 15 view = glm::translate(view, glm::vec3(0.f, 0.f, -3.f)); 16 glm::mat4 projection = glm::mat4(1.0f); 17 projection = glm::perspective(glm::radians(45.0f), (float)(SCR_WIDTH * 1.0 / SCR_HEIGHT), 0.1f, 100.0f); 18 ourShader.setMtx4fv("view", view); 19 ourShader.setMtx4fv("projection", projection); 20 21 while (!glfwWindowShouldClose(window)) 22 { 23 processInput(window); 24 25 ///設定清屏顏色 26 glClearColor(0.2f, 0.3f, 0.3f, 1.0f); 27 ///清屏 28 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 29 30 ///繫結定點陣列物件 31 glBindVertexArray(VAO); 32 33 for (int i = 0; i < 10; ++i) { 34 glm::mat4 model = glm::mat4(1.0f); 35 model = glm::translate(model, postions[i]); 36 float angle = 20.0f * i; 37 model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f)); 38 ourShader.setMtx4fv("model", model); 39 40 ///以索引繪製頂點資料 41 // glDrawArrays(GL_TRIANGLES, 0, 3); 42 glDrawElements(GL_TRIANGLES,36,GL_UNSIGNED_INT,0); 43 } 44 45 46 ///交換顏色緩衝 47 glfwSwapBuffers(window); 48 ///拉取使用者事件 49 glfwPollEvents(); 50 }
我們看到,我們為每個物體定單獨定義了一個模型矩陣,這樣,我們每個模型在世界空間中的狀態都不同,然後在定義了唯一一個觀察矩陣和透視投影矩陣,這樣就模擬出我看眼睛看到物體的一個過程。
這裡我們只對glm為我們提供的幾個新出現的函式做一下簡單講解:
這是用來指定透視投影矩陣的函式。
-
第一個引數radians指的是Fov,它表示的是視野(Field of View),並且設定了觀察空間的大小。如果想要一個真實的觀察效果,它的值通常設定為45.0f。他就是圖中兩個藍色實線的空間夾角。
-
第二個引數scale設定了寬高比,由視口的寬除以高所得。
-
第三和第四個引數設定了*截頭體的*和遠*面。我們通常設定*距離為0.1f,而遠距離設為100.0f。所有在**面和遠*面內且處於*截頭體內的頂點都會被渲染。圖中粉色截面即為**面,藍色截面即為遠*面。
接下來雖然程式碼中沒有,我們還是提一下正投影矩陣的建立方法:
當你把透視矩陣的 near 值設定太大時(如10.0f),OpenGL會將靠*攝像機的座標(在0.0f和10.0f之間)都裁剪掉,這會導致一個你在遊戲中很熟悉的視覺效果:在太過靠*一個物體的時候你的視線會直接穿過去。