1. 程式人生 > >OpenGL學習腳印:立方體紋理和天空包圍盒(Cubemaps And Skybox)

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個列舉常量一次遞增,我們可以使用迴圈來建立這個立方體紋理,將這個函式封裝到

texture.h中如下:

   /*
    * 載入一個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分量為Zclip),而一個頂點最終的深度值是通過透視除法得到NDC座標(對應的z分量為Zndc),以及最後的視口變換後得到視窗座標的Zwin值決定的。關於這個深度值的計算,如果感覺陌生,可以回過頭去檢視深度測試一節。這裡使用的技巧是,手動將gl_Position的z值設定為w,即在頂點主色器中輸出:

   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預設執行的透視除法和視口變換後,得到的深度值就是Zwin=1.0,達到我們的目的。這種方式首先繪製場景中物體,最後渲染包圍盒。需要注意的是,繪製包圍盒時將深度測試函式變為:

   glDepthFunc(GL_LEQUAL);

繪製完畢後,又恢復預設的GL_LESS。使用不同的包圍盒素材,我們得到另一個包圍盒效果如下圖所示:

包圍盒2

最後的說明

在實現包圍盒時,需要通過移除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學習筆記輸入DStreamReceiver詳解

輸入DStream和Receiver詳解 輸入DStream代表了來自資料來源的輸入資料流,除了檔案資料流之外,所有的輸入DStream都會繫結一個Receiver物件,Receiver用於接收資料,然後將資料儲存在Spark的記憶體中,以供後續的操作使用。 SparkS

Python學習筆記虛擬環境

Python 應用程式經常會使用一些不屬於標準庫的包和模組。應用程式有時候需要某個特定版本的庫,因為它需要一個特定的 bug 已得到修復的庫或者它是使用了一個過時版本的庫的介面編寫的。 這就意味著可能無法安裝一個 Python 來滿足每個應用程式的要求。如果應

numpy學習3物件屬性基本資料型別

一、ndarray物件屬性 ndim 陣列軸(維度)的個數,軸的個數被稱作秩 shape 陣列的維度, 例如一個2排3列的矩陣,它的shape屬性將是(2,3),這個元組的長度顯然是秩,即維度或者ndi