OpenGL學習腳印:立方體紋理和天空包圍盒(Cubemaps And Skybox)
寫在前面
之前學習了2D紋理對映,實際上還有其他型別的紋理有待我們進一步學習,本節將要學習的立方體紋理(cubemaps),是一種將多個紋理圖片複合到一個立方體表面的技術。在遊戲中應用得較多的天空包圍盒可以使用cubemap實現。本節示例程式均可以在我的github下載。
建立Cubemap
cubemap是使用6張2D紋理繫結到GL_TEXTURE_CUBE_MAP目標而建立的紋理。GL_TEXTURE_CUBE_MAP包含6個面,分別是:
繫結目標 | 紋理方向 |
---|---|
GL_TEXTURE_CUBE_MAP_POSITIVE_X | 右邊 |
GL_TEXTURE_CUBE_MAP_NEGATIVE_X | 左邊 |
GL_TEXTURE_CUBE_MAP_POSITIVE_Y | 頂部 |
GL_TEXTURE_CUBE_MAP_NEGATIVE_Y | 底部 |
GL_TEXTURE_CUBE_MAP_POSITIVE_Z | 背面 |
GL_TEXTURE_CUBE_MAP_NEGATIVE_Z | 前面 |
需要注意的是,OpenGL中相機預設朝向-z方向,因此GL_TEXTURE_CUBE_MAP_NEGATIVE_Z表示前面,而GL_TEXTURE_CUBE_MAP_POSITIVE_Z表示背面。在構建cubemaps,一般利用列舉常量遞增的特性,一次繫結到上述6個目標。例如在OpenGL中列舉常量定義為:
#define GL_TEXTURE_CUBE_MAP_POSITIVE_X 0x8515
#define GL_TEXTURE_CUBE_MAP_NEGATIVE_X 0x8516
#define GL_TEXTURE_CUBE_MAP_POSITIVE_Y 0x8517
#define GL_TEXTURE_CUBE_MAP_NEGATIVE_Y 0x8518
#define GL_TEXTURE_CUBE_MAP_POSITIVE_Z 0x8519
#define GL_TEXTURE_CUBE_MAP_NEGATIVE_Z 0x851A
可以看到上述6個列舉常量一次遞增,我們可以使用迴圈來建立這個立方體紋理,將這個函式封裝到
/*
* 載入一個cubeMap
*/
static GLuint loadCubeMapTexture(std::vector<const char*> picFilePathVec,
GLint internalFormat = GL_RGB,
GLenum picFormat = GL_RGB,
GLenum picDataType = GL_UNSIGNED_BYTE,
int loadChannels = SOIL_LOAD_RGB)
{
GLuint textId;
glGenTextures(1, &textId);
glBindTexture(GL_TEXTURE_CUBE_MAP, textId);
GLubyte *imageData = NULL;
int picWidth, picHeight;
for (std::vector<const char*>::size_type i =0; i < picFilePathVec.size(); ++i)
{
int channels = 0;
imageData = SOIL_load_image(picFilePathVec[i], &picWidth,
&picHeight, &channels, loadChannels);
if (imageData == NULL)
{
std::cerr << "Error::loadCubeMapTexture could not load texture file:"
<< picFilePathVec[i] << std::endl;
return 0;
}
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i,
0, internalFormat, picWidth, picHeight, 0, picFormat, picDataType, imageData);
SOIL_free_image_data(imageData);
}
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
return textId;
}
關於程式碼中GL_TEXTURE_WRAP_R引數,稍後會做解釋。
實際使用時載入6個2D紋理圖片,如下所示:
faces.push_back("sky_rt.jpg");
faces.push_back("sky_lf.jpg");
faces.push_back("sky_up.jpg");
faces.push_back("sky_dn.jpg");
faces.push_back("sky_bk.jpg");
faces.push_back("sky_ft.jpg");
GLuint skyBoxTextId = TextureHelper::loadCubeMapTexture(faces);
需要注意載入圖片的順序 我們使用GL_TEXTURE_CUBE_MAP_POSITIVE_X + i的方式來一次建立了6個2D紋理,載入圖片時的順序以需要對應列舉變數定義的順序。
使用cubemaps
cubemaps建立了一個立方體紋理,那麼如何對紋理進行取樣呢?
與2D紋理使用的紋理座標(s,t)不同,我們這裡需要使用三維紋理座標(s,t,r),如下圖所示(來自www.learnopengl.com Cubemaps):
圖中橙色的方向向量,當立方體中心處於原點時,即代表的是立方體表面頂點的位置,這個向量即是三維紋理座標。利用(s,t,r)決定紋理取樣時,首先根據(s,t,r)中模最大的分量決定在哪個面取樣,然後使用剩下的2個座標在對應的面上做2D紋理取樣。例如根據(s,t,r)中模最大的為s分量,並且符號為正,則決定選取+x面作為取樣的2D紋理,然後使用(t,r)座標在+x面上做2D紋理取樣。關於這個計算過程的解釋可以參考cubemaps。
在2D紋理對映一節我們提到WRAP引數會決定,當紋理座標超出[0,1]範圍時的紋理取樣方式。上述程式碼中,我們使用:
glTexParameteri(GL_TEXTURE_CUBE_MAP,GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP,GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP,GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
其中引數GL_CLAMP_TO_EDGE主要用於指定,當(s,t,r)座標沒有落在哪個面,而是落在兩個面之間時的紋理取樣,使用GL_CLAMP_TO_EDGE引數表明,當在兩個面之間取樣時使用邊緣的紋理值。
建立天空包圍盒
上面介紹了建立和使用cubemap的方法,實際遊戲應用得較多的就是利用cubemap實現天空包圍盒。天空包圍盒的主要實現思路是:
在場景中繪製一個cubemap紋理取樣的立方體,將這個立方體總是置於場景中最外圍,讓遊戲玩家感覺到好像場景非常大,觸不可及像天空一樣,即是玩家靠近一些,天空依然還是離得很遠的感覺。
例如下圖,我們繪製了一個包圍盒:
繪製包圍盒,是將1x1x1的立方體作為包圍盒,將上面建立的cubemap對映到這個包圍盒上。這個立方體的中心處於原點,因此立方體上的頂點位置,就當做前面講的用於紋理取樣的向量。
在頂點著色器中實現為:
#version 330 core
layout(location = 0) in vec3 position;
uniform mat4 projection;
uniform mat4 view;
out vec3 TextCoord;
void main()
{
gl_Position = projection * view * vec4(position, 1.0);
TextCoord = position; // 當立方體中央處於原點時 立方體上位置即等價於向量
}
在片元著色器中只需要取樣紋理即可:
#version 330 core
in vec3 TextCoord;
uniform samplerCube skybox; // 從sampler2D改為samplerCube
out vec4 color;
void main()
{
color = texture(skybox, TextCoord);
}
要將包圍盒置於場景中,最外層,基本的方式是,暫時關閉深度快取寫入,首先繪製包圍盒,這樣包圍盒總是處於場景中最外圍。同時實現的時候需要注意是如何保持玩家移動時,包圍盒看起來很遠很大的感覺,有兩種實現方式。
第一種方式,去掉視變換中移動的部分(translate部分),但保留旋轉等其他成分,這樣當你在場景內移動,轉動相機時,包圍盒仍然在以正常角度顯示,只是包圍盒沒有因為玩家的前進後退而發生移動,這樣看起來就比較正常。這種方式實現為:
// 先繪製skyBox
glDepthMask(GL_FALSE); // 禁止寫入深度緩衝區
skyBoxShader.use();
glm::mat4 projection = glm::perspective(camera.mouse_zoom,
(GLfloat)(WINDOW_WIDTH) / WINDOW_HEIGHT, 0.1f, 100.0f); // 投影矩陣
glm::mat4 view = glm::mat4(glm::mat3(camera.getViewMatrix())); // 視變換矩陣 移除translate部分
glUniformMatrix4fv(glGetUniformLocation(skyBoxShader.programId, "projection"),
1, GL_FALSE, glm::value_ptr(projection));
glUniformMatrix4fv(glGetUniformLocation(skyBoxShader.programId, "view"),1, GL_FALSE, glm::value_ptr(view));
第二種方式,是每次將包圍盒的中心設定在玩家的位置,同時以一定比例縮放包圍盒,這樣達到的效果基本相同,但是缺點是如果縮放比例不當的話,場景中物體移動時可能超出包圍盒,而引起視覺Bug。這種方式實現為:
skyBoxShader.use();
glUniformMatrix4fv(glGetUniformLocation(skyBoxShader.programId, "projection"),1, GL_FALSE, glm::value_ptr(projection));
glUniformMatrix4fv(glGetUniformLocation(skyBoxShader.programId, "view"),1, GL_FALSE, glm::value_ptr(view));
model = glm::translate(glm::mat4(), camera.position);
model = glm::scale(model, glm::vec3(20.0f, 20.0f, 20.0f));
glUniformMatrix4fv(glGetUniformLocation(skyBoxShader.programId, "model"),1, GL_FALSE, glm::value_ptr(model));
天空包圍盒的改進
上面在繪製天空包圍盒時,我們首先關閉深度快取寫入,繪製包圍盒,讓它處於場景最外圍,這樣做當然能正常工作,缺點是如果場景中物體需要顯示在包圍盒前面,最終包圍盒的某些部分會被遮擋住,按上述繪製方式我們還是繪製了這部分內容,導致了不必要的著色器呼叫,這是一種效能上的損失。
一種改進的策略是首先繪製場景中物體,然後根據利用包圍盒的深度值和當前深度值進行比較,如果通過深度測試就繪製包圍盒。我們知道預設情況下,清除深度快取時使用的值為1.0表示深度最大,因此我們也想用1.0來表示包圍盒的深度值,這樣它就始終處於場景中最外圍,當進行深度測試時,我們改變預設的測試函式,從GL_LESS變為GL_LEQUAL,如下:
glDepthFunc(GL_LEQUAL); // 深度測試條件 小於等於
那麼如何讓包圍盒的深度值總是1.0呢? 我們知道,在頂點著色器中,gl_Position表示的是當前頂點的裁剪座標系座標(對應的z分量為
void main()
{
vec4 pos = projection * view * model * vec4(position, 1.0);
gl_Position = pos.xyww; // 此處讓z=w 則對應的深度值變為depth = w / w = 1.0
TextCoord = position; // 當立方體中央處於原點時 立方體上位置即等價於向量
}
這樣通過OpenGL預設執行的透視除法和視口變換後,得到的深度值就是
glDepthFunc(GL_LEQUAL);
繪製完畢後,又恢復預設的GL_LESS。使用不同的包圍盒素材,我們得到另一個包圍盒效果如下圖所示:
最後的說明
在實現包圍盒時,需要通過移除translate部分(上文中第一種方式)或者將包圍盒設為觀察者原點,並且放大包圍盒的方式(上文中第二種方式)來使包圍盒看起來很遠很大。如果設定不當得到的錯誤效果可能如下:
在實現包圍盒時,注意調整合適的投影變換引數,這裡我們設定的引數為:
glm::mat4 projection = glm::perspective(camera.mouse_zoom,
(GLfloat)(WINDOW_WIDTH) / WINDOW_HEIGHT, 0.1f, 100.0f); // 投影矩陣
如果投影引數設定不當,得到錯誤的效果可能如下:
要想獲得更多的包圍盒,可以訪問線上資源。
另外Cubemap還可以用來實現environment mapping等技術,下一節將會學習這個主題。
參考資料
相關推薦
OpenGL學習腳印:立方體紋理和天空包圍盒(Cubemaps And Skybox)
寫在前面 之前學習了2D紋理對映,實際上還有其他型別的紋理有待我們進一步學習,本節將要學習的立方體紋理(cubemaps),是一種將多個紋理圖片複合到一個立方體表面的技術。在遊戲中應用得較多的天空包圍盒可以使用cubemap實現。本節示例程式均可
OpenGL學習腳印:模板測試(stencil testing)
寫在前面 上一節介紹了深度測試,本節繼續學習一個高階主題-模板測試(stencil testing)。模板緩衝同之前介紹的顏色緩衝、深度緩衝類似,通過它我們可以實現很多的特效,例如輪廓、鏡面效果,陰影效果等。本節示例程式均可以從我的github下載。 通過本
OpenGL學習腳印: 反走樣初步(Anti-aliasing basic)
寫在前面 目前,我們繪製的圖形中存在瑕疵的,觀察下面這個立方體: 仔細看,立方體的邊緣部分存在折線,如果我們放大了看,則可以看到這種瑕疵更明顯: 這種繪製的物體邊緣部分出現鋸齒的現象稱之為走樣(aliasing)。反走樣(Anti-
OpenGL學習腳印: 二維紋理對映(2D textures)
寫在前面 前面兩節介紹了向量和矩陣,以及座標和轉換相關的數學,再繼續討論模型變換等其他包含數學內容的部分之前,本節介紹二維紋理對映,為後面學習做一個準備。紋理對映本身也是比較大的主題,本節只限於討論二維紋理的基本使用,對於紋理對映的其他方法,後面會繼續
OpenGL學習腳印:深度測試(depth testing)
寫在前面 上一節我們使用AssImp載入了3d模型,效果已經令人激動了。但是繪製效率和場景真實感還存在不足,接下來我們還是要保持耐心,繼續學習一些高階主題,等學完後面的高階主題,我們再次來改進我們載入模型的過程。本節將會學習深度測試,文中示例程式原始碼均可
OpenGL學習腳印:建立更多的例項(instancing object)
寫在前面 前面我們學習了模型載入的相關內容,併成功載入了模型,令人十分興奮。那時候載入的是少量的模型,如果需要載入多個模型,就需要考慮到效率問題了,例如下圖所示的是載入了400多個納米戰鬥服機器人的效果圖: 渲染一個模型更多的例項,需要使用到例項
OpenGL學習腳印:幾何著色器(geometry shader)
寫在前面 一直以來我們使用了頂點著色器(vertex shader)和片元著色器(fragment shader),實際上OpenGL還提供了一個可選的幾何著色器(geometry shader)。幾何著色器位於頂點和片元著色器之間,如果沒有使用時,則
OpenGL學習腳印:Blinn-Phong光照模型
寫在前面 在前面基礎光照部分,我們學習了Phong Shading模型,Blinn-Phong模型對Phong模型的鏡面光成分進行了改進,雖然在物理上解釋沒有Phong好,但是能更好地模擬光照。本節程式碼可以在我的github下載。 Phong不
OpenGL學習筆記:GLAD和第一個視窗
環境 系統:Windows10 64位 家庭中文版 IDE:Visual Studio 2017 專業版 參考教程:https://learnopengl-cn.github.io/01 Getting started/03 Hello Window/ 步驟 1.獲取GLAD
OpenGL學習腳印: 投影矩陣和視口變換矩陣(math-projection and viewport matrix)
寫在前面 前面幾節分別介紹了模型變換,視變換,本節繼續學習OpenGL座標變換過程中的投影變換。這裡主要是從數學角度推導投影矩陣。對數學不感興趣的,可以稍微瞭解下,或者跳過本節內容。 ,這裡對他的推導思路稍微進行了整理。 通過本節可以瞭解到 透
Python學習19:函數和變量 Function and variables
Python 函數 定義一個簡單的函數,調用函數輸出不同的內容 # -*- coding: utf-8 -*- # 因為有中文註釋,為了防止腳本在運行的時候提示編碼錯誤,在腳本中需要加入上面一行代碼。 # 定義一個函數,使用格式化字符串輸出函數中參數的值 def cheese_and_crackers
javaEE學習筆記:maven下載和安裝(1)
本文只作學習筆記,僅代表個人觀點,若有雷同,純屬巧合; 工具:編輯器Eclipse,Tomcat7.0(下載地址:https://tomcat.apache.org/) JDK1.8版本 第一步:官網下載地址: http://maven.apache.org/download.cgi
OpenGL學習筆記:常用物件的建立及使用
·頂點陣列物件(Vertex Array Object,VAO) 頂點陣列物件(Vertex Array Object,VAO),用來記錄頂點的資訊,如:位置、資料格式、紋理座標等。使用VAO的好處是:在配置繪製物件的頂點屬性時,你只需要配置一次(VAO會自動記錄你的設定),想要繪製物件
OpenGL學習筆記:編譯GLFW庫
環境 系統:Windows10 64位 家庭中文版 IDE:Visual Studio 2017 專業版 工具:CMake 步驟 1.安裝CMake,CMake最新安裝包:64位 32位; 2.下載GLFW原始碼包,並解壓(記住解壓的路徑,等下要用到,如:D:\glfw-3.2.
java技術學習筆記:Maven安裝和作用
Maven是一個基於專案物件模型(POM)的概念的純java開發的開源的專案管理工具。主要用來管理java專案,進行依賴管理(jar包管理,能自動分析專案所需的依賴軟體包,併到Maven倉庫區下載)和專案構建(專案打包和部署)。此外還能分塊開發,提高開發效率。 本文將從以下三個方面寫起: 1
學習——JavaWeb02:修改埠和web專案釋出
學習——JavaWeb02:修改埠和web專案釋出 壹:修改埠 1. Tomcat伺服器的配置,全部都需要在tomcat的安裝目錄下conf目錄下完成: Tomcat的預設埠號是8080;
Python學習筆記:中文編碼和基礎語法
Python 中文編碼 Python中預設的編碼格式是 ASCII 格式,在沒修改編碼格式時無法正確列印漢字,所以在讀取中文時會報錯。 解決方法為只要在檔案開頭加入 # -- coding: UTF-8 -- 或者 #coding=utf-8 就行了(注意:#coding=utf-8
Spark學習筆記:輸入DStream和Receiver詳解
輸入DStream和Receiver詳解 輸入DStream代表了來自資料來源的輸入資料流,除了檔案資料流之外,所有的輸入DStream都會繫結一個Receiver物件,Receiver用於接收資料,然後將資料儲存在Spark的記憶體中,以供後續的操作使用。 SparkS
Python學習筆記:虛擬環境和包
Python 應用程式經常會使用一些不屬於標準庫的包和模組。應用程式有時候需要某個特定版本的庫,因為它需要一個特定的 bug 已得到修復的庫或者它是使用了一個過時版本的庫的介面編寫的。 這就意味著可能無法安裝一個 Python 來滿足每個應用程式的要求。如果應
numpy學習3:物件屬性和基本資料型別
一、ndarray物件屬性 ndim 陣列軸(維度)的個數,軸的個數被稱作秩 shape 陣列的維度, 例如一個2排3列的矩陣,它的shape屬性將是(2,3),這個元組的長度顯然是秩,即維度或者ndi