筆者介紹:姜雪偉,IT公司技術合夥人,IT高級講師,CSDN社區專家。特邀編輯,暢銷書做者,國家專利發明人;已出版書籍:《手把手教你架構3D遊戲引擎》電子工業出版社和《Unity3D實戰核心技術具體解釋》電子工業出版社等。數組
CSDN視頻網址:http://edu.csdn.net/lecturer/144架構
在這裏介紹立方體貼圖主要是告訴讀者,利用立方體貼圖原理。咱們可以作很是多事情:比方天空盒,環境映射中的反射和折射效果等等。固然環境映射也可以使用一張紋理貼圖實現。這個會在博文的最後給讀者介紹,如下開始介紹立方體貼圖實現原理。函數
咱們在遊戲開發中一般的作法是將2D紋理映射到物體的一個面上,本篇博文介紹的是將多個紋理組合起來映射到一個單一紋理,這就稱爲立方體貼圖。在介紹立方體貼圖前。先解釋一下紋理採樣,假設咱們有一個單位立方體。有個以原點爲起點的方向向量在它的中心。post
從立方體貼圖上使用橘黃色向量採樣一個紋理值看起來下圖:性能
注意,方向向量的大小可有可無。一旦提供了方向,OpenGL就會獲取方向向量碰觸到立方體表面上的對應的紋理像素。這樣就返回了正確的紋理採樣值。優化
方向向量觸碰到立方體表面的一點也就是立方體貼圖的紋理位置。這意味着僅僅要立方體的中心位於原點上。咱們就可以使用立方體的位置向量來對立方體貼圖進行採樣。而後咱們就可以獲取所有頂點的紋理座標。就和立方體上的頂點位置同樣。所得到的結果是一個紋理座標,經過這個紋理座標就能獲取到立方體貼圖上正確的紋理。
ui
如下開始介紹建立立方體貼圖,立方體貼圖和其它紋理同樣。因此要建立一個立方體貼圖,在進行不論什麼紋理操做以前,需要生成一個紋理。激活對應紋理單元而後綁定到合適的紋理目標上。此次要綁定到 GL_TEXTURE_CUBE_MAP
紋理類型:spa
GLuint textureID; glGenTextures(1, &textureID); glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
因爲立方體貼圖包括6個紋理,立方體的每個面一個紋理,咱們必須調用glTexImage2D
函數6次,函數的參數和前面教程講的相似。然而此次咱們必須把紋理目標(target)參數設置爲立方體貼圖特定的面。這是告訴OpenGL咱們建立的紋理是對應立方體哪一個面的。.net
所以咱們便需要爲立方體貼圖的每個面調用一次 glTexImage2D
。3d
因爲立方體貼圖有6個面,OpenGL就提供了6個不一樣的紋理目標,來應對立方體貼圖的各個面。
紋理目標(Texture target) | 方位 |
---|---|
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 | 前 |
GL_TEXTURE_CUBE_MAP_POSITIVE_X
爲起始來對它們進行遍歷,每次迭代枚舉值加
1
,這樣循環所有的紋理目標效率較高:
int width,height; unsigned char* image; for(GLuint i = 0; i < textures_faces.size(); i++) { image = SOIL_load_image(textures_faces[i], &width, &height, 0, SOIL_LOAD_RGB); glTexImage2D( GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image ); }
這兒咱們有個vector叫textures_faces
。它包括立方體貼圖所各個紋理的文件路徑,並且以上表所列的順序排列。它將爲每個當前綁定的cubemp的每個面生成一個紋理。
因爲立方體貼圖和其它紋理沒什麼不一樣,咱們也要定義它的圍繞方式和過濾方式:
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);
簡單的解釋一下參數GL_TEXTURE_WRAP_R。它的含義僅僅是簡單的設置了紋理的R座標。R座標對應於紋理的第三個維度(就像位置的z同樣)。
咱們把放置方式設置爲 GL_CLAMP_TO_EDGE
,因爲紋理座標在兩個面之間,因此可能並不能觸及哪一個面(因爲硬件限制),所以使用 GL_CLAMP_TO_EDGE
後OpenGL會返回它們的邊界的值。雖然咱們可能在兩個兩個面中間進行的採樣。
在繪製物體以前。將使用立方體貼圖,而在渲染前咱們要激活對應的紋理單元並綁定到立方體貼圖上。這和普通的2D紋理沒什麼差異。
在片斷着色器中。咱們也必須使用一個不一樣的採樣器——samplerCube,用它來從texture
函數中採樣。但是此次使用的是一個vec3
方向向量,代替vec2
。如下是一個片斷着色器使用了立方體貼圖的樣例:
in vec3 textureDir; // 用一個三維方向向量來表示立方體貼圖紋理的座標 uniform samplerCube cubemap; // 立方體貼圖紋理採樣器 void main() { color = texture(cubemap, textureDir); }立方體貼圖的技術實現了後,咱們利用該技術實現天空盒:
天空盒是一個立方體,它由六個面組成,每個面需要一個貼圖,網上有很是多這種天空盒的資源。固然美術也可以製做,這些天空盒一般有如下的樣式:
假設你把這6個面折疊到一個立方體中,你機會得到模擬了一個巨大的風景的立方體。原理清楚了。接下來使用程序建立天空盒:
因爲天空盒實際上就是一個立方體貼圖,載入天空盒和以前咱們載入立方體貼圖的沒什麼大的不一樣。爲了載入天空盒咱們將使用如下的函數。它接收一個包括6個紋理文件路徑的vector:
GLuint loadCubemap(vector<const GLchar*> faces) { GLuint textureID; glGenTextures(1, &textureID); glActiveTexture(GL_TEXTURE0); int width,height; unsigned char* image; glBindTexture(GL_TEXTURE_CUBE_MAP, textureID); for(GLuint i = 0; i < faces.size(); i++) { image = SOIL_load_image(faces[i], &width, &height, 0, SOIL_LOAD_RGB); glTexImage2D( GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image ); } 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 textureID; }在咱們調用這個函數以前,咱們將把合適的紋理路徑載入到一個vector之中,順序仍是依照立方體貼圖枚舉的特定順序:
vector<const GLchar*> faces; faces.push_back("right.jpg"); faces.push_back("left.jpg"); faces.push_back("top.jpg"); faces.push_back("bottom.jpg"); faces.push_back("back.jpg"); faces.push_back("front.jpg"); GLuint cubemapTexture = loadCubemap(faces);
立方體貼圖用於給3D立方體帖上紋理,可以用立方體的位置做爲紋理座標進行採樣。當一個立方體的中心位於原點(0,0。0)的時候。它的每個位置向量也就是以原點爲起點的方向向量。
這個方向向量就是咱們要獲得的立方體某個位置的對應紋理值。
出於這個理由,咱們僅僅需要提供位置向量。而無需紋理座標。爲了渲染天空盒,咱們需要一組新着色器,它們不會太複雜。因爲咱們僅僅有一個頂點屬性。頂點着色器很是easy:
#version 330 core layout (location = 0) in vec3 position; out vec3 TexCoords; uniform mat4 projection; uniform mat4 view; void main() { gl_Position = projection * view * vec4(position, 1.0); TexCoords = position; }注意。頂點着色器有意思的地方在於咱們把輸入的位置向量做爲輸出給片斷着色器的紋理座標。
片斷着色器就會把它們做爲輸入去採樣samplerCube:
#version 330 core in vec3 TexCoords; out vec4 color; uniform samplerCube skybox; void main() { color = texture(skybox, TexCoords); }
這樣天空盒才幹成爲所有其它物體的背景來繪製出來。
glDepthMask(GL_FALSE); skyboxShader.Use(); // ... Set view and projection matrix glBindVertexArray(skyboxVAO); glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture); glDrawArrays(GL_TRIANGLES, 0, 36); glBindVertexArray(0); glDepthMask(GL_TRUE); // ... Draw rest of the scene咱們但願天空盒以玩家爲中心。這樣無論玩家移動了多遠。天空盒都不會變近,這樣就產生一種四周的環境真的很是大的印象。當前的視圖矩陣對所有天空盒的位置進行了轉轉縮放和平移變換。因此玩家移動,立方體貼圖也會跟着移動。咱們打算移除視圖矩陣的平移部分,這樣移動就影響不到天空盒的位置向量了。在基礎光照教程裏咱們提到過咱們可以僅僅用4X4矩陣的3×3部分去除平移。咱們可以簡單地將矩陣轉爲33矩陣再轉回來。就能達到目標。
glm::mat4 view = glm::mat4(glm::mat3(camera.GetViewMatrix()));這會移除所有平移。但保留所有旋轉。所以用戶仍然可以向四面八方看。因爲有了天空盒。場景便可變得巨大了。
假設你加入些物體而後自由在當中遊蕩一下子你會發現場景的真實度有了極大提高。最後的效果看起來像這樣:
實現上述效果的核心代碼例如如下所看到的:
// Clear buffers glClearColor(0.1f, 0.1f, 0.1f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Draw skybox first glDepthMask(GL_FALSE);// Remember to turn depth writing off skyboxShader.Use(); glm::mat4 view = glm::mat4(glm::mat3(camera.GetViewMatrix())); // Remove any translation component of the view matrix glm::mat4 projection = glm::perspective(camera.Zoom, (float)screenWidth/(float)screenHeight, 0.1f, 100.0f); glUniformMatrix4fv(glGetUniformLocation(skyboxShader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view)); glUniformMatrix4fv(glGetUniformLocation(skyboxShader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection)); // skybox cube glBindVertexArray(skyboxVAO); glActiveTexture(GL_TEXTURE0); glUniform1i(glGetUniformLocation(shader.Program, "skybox"), 0); glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture); glDrawArrays(GL_TRIANGLES, 0, 36); glBindVertexArray(0); glDepthMask(GL_TRUE); // Then draw scene as normal shader.Use(); glm::mat4 model; view = camera.GetViewMatrix(); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model)); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view)); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection)); // Cubes glBindVertexArray(cubeVAO); glActiveTexture(GL_TEXTURE0); glUniform1i(glGetUniformLocation(shader.Program, "texture_diffuse1"), 0); glBindTexture(GL_TEXTURE_2D, cubeTexture); glDrawArrays(GL_TRIANGLES, 0, 36); glBindVertexArray(0); // Swap the buffers glfwSwapBuffers(window);
因此最後渲染天空盒就可以給咱們帶來輕微的性能提高。
採用這種方式,深度緩衝被所有物體的深度值全然填充,因此咱們僅僅需要渲染經過前置深度測試的那部分天空的片斷便可了,並且能顯著下降片斷着色器的調用。問題是天空盒是個立方體,極有可能會渲染失敗。因爲極有可能通只是深度測試。簡單地不用深度測試渲染它也不是解決方式,這是因爲天空盒會在以後覆蓋所有的場景中其它物體。咱們需要耍個花招讓深度緩衝相信天空盒的深度緩衝有着最大深度值1.0。如此僅僅要有個物體存在深度測試就會失敗,看似物體就在它前面了。
透視除法(perspective division)是在頂點着色器執行以後執行的。把gl_Position
的xyz座標除以w元素。咱們從深度測試教程瞭解到除法結果的z元素等於頂點的深度值。利用這個信息。咱們可以把輸出位置的z元素設置爲它的w元素,這樣就會致使z元素等於1.0了,因爲,當透視除法應用後。它的z元素轉換爲w/w = 1.0:
void main() { vec4 pos = projection * view * vec4(position, 1.0); gl_Position = pos.xyww; TexCoords = position; }
終於。標準化設備座標就總會有個與1.0相等的z值了。1.0就是深度值的最大值。僅僅有在沒有不論什麼物體可見的狀況下天空盒纔會被渲染(僅僅有經過深度測試才渲染。不然假若有不論什麼物體存在,就不會被渲染,僅僅去渲染物體)。
咱們必須改變一下深度方程,把它設置爲GL_LEQUAL
,原來默認的是GL_LESS
。深度緩衝會爲天空盒用1.0這個值填充深度緩衝。因此咱們需要保證天空盒是使用小於等於深度緩衝來經過深度測試的,而不是小於。
// Clear buffers glClearColor(0.1f, 0.1f, 0.1f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Draw scene as normal shader.Use(); glm::mat4 model; glm::mat4 view = camera.GetViewMatrix(); glm::mat4 projection = glm::perspective(camera.Zoom, (float)screenWidth/(float)screenHeight, 0.1f, 100.0f); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model)); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view)); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection)); // Cubes glBindVertexArray(cubeVAO); glActiveTexture(GL_TEXTURE0); glUniform1i(glGetUniformLocation(shader.Program, "texture_diffuse1"), 0); glBindTexture(GL_TEXTURE_2D, cubeTexture); glDrawArrays(GL_TRIANGLES, 0, 36); glBindVertexArray(0); // Draw skybox as last glDepthFunc(GL_LEQUAL); // Change depth function so depth test passes when values are equal to depth buffer's content skyboxShader.Use(); view = glm::mat4(glm::mat3(camera.GetViewMatrix())); // Remove any translation component of the view matrix glUniformMatrix4fv(glGetUniformLocation(skyboxShader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view)); glUniformMatrix4fv(glGetUniformLocation(skyboxShader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection)); // skybox cube glBindVertexArray(skyboxVAO); glActiveTexture(GL_TEXTURE0); glUniform1i(glGetUniformLocation(shader.Program, "skybox"), 0); glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture); glDrawArrays(GL_TRIANGLES, 0, 36); glBindVertexArray(0); glDepthFunc(GL_LESS); // Set depth function back to default // Swap the buffers glfwSwapBuffers(window);
使用帶有場景環境的立方體貼圖,咱們還可以讓物體有一個反射或折射屬性。像這樣使用了環境立方體貼圖的技術叫作環境貼圖技術,當中最重要的兩個是反射(reflection)和折射(refraction)。
凡是是一個物體(或物體的某部分)反射(Reflect)他周圍的環境的屬性,比方物體的顏色多少有些等於它周圍的環境,這要基於觀察者的角度。
好比一個鏡子是一個反射物體:它會基於觀察者的角度泛着它周圍的環境。
反射的基本思路不難。下圖展現了咱們怎樣計算反射向量,而後使用這個向量去從一個立方體貼圖中採樣:
咱們基於觀察方向向量I和物體的法線向量N計算出反射向量R。咱們可以使用GLSL的內建函數reflect來計算這個反射向量。
最後向量R做爲一個方向向量對立方體貼圖進行索引/採樣。返回一個環境的顏色值。
最後的效果看起來就像物體反射了天空盒。
因爲咱們在場景中已經設置了一個天空盒,建立反射就不難了。
咱們改變一下箱子使用的那個片斷着色器。給箱子一個反射屬性:
#version 330 core in vec3 Normal; in vec3 Position; out vec4 color; uniform vec3 cameraPos; uniform samplerCube skybox; void main() { vec3 I = normalize(Position - cameraPos); vec3 R = reflect(I, normalize(Normal)); color = texture(skybox, R); }咱們先來計算觀察/攝像機方向向量I,而後使用它來計算反射向量R,接着咱們用R從天空盒立方體貼圖採樣。要注意的是,咱們有了片斷的插值Normal和Position變量,因此咱們需要修正頂點着色器適應它。
#version 330 core layout (location = 0) in vec3 position; layout (location = 1) in vec3 normal; out vec3 Normal; out vec3 Position; uniform mat4 model; uniform mat4 view; uniform mat4 projection; void main() { gl_Position = projection * view * model * vec4(position, 1.0f); Normal = mat3(transpose(inverse(model))) * normal; Position = vec3(model * vec4(position, 1.0f)); }
咱們用了法線向量,因此咱們打算使用一個法線矩陣(normal matrix)變換它們。Position
輸出的向量是一個世界空間位置向量。
頂點着色器輸出的Position
用來在片斷着色器計算觀察方向向量。
因爲咱們使使用方法線。你還得更新頂點數據,更新屬性指針。還要確保設置cameraPos
的uniform。
而後在渲染箱子前咱們還得綁定立方體貼圖紋理:
glBindVertexArray(cubeVAO); glBindTexture(GL_TEXTURE_CUBE_MAP, skyboxTexture); glDrawArrays(GL_TRIANGLES, 0, 36); glBindVertexArray(0);
C++程序實現的核心代碼例如如下所看到的:
// Clear buffers glClearColor(0.1f, 0.1f, 0.1f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Draw scene as normal shader.Use(); glm::mat4 model; glm::mat4 view = camera.GetViewMatrix(); glm::mat4 projection = glm::perspective(camera.Zoom, (float)screenWidth/(float)screenHeight, 0.1f, 100.0f); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model)); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view)); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection)); glUniform3f(glGetUniformLocation(shader.Program, "cameraPos"), camera.Position.x, camera.Position.y, camera.Position.z); // Cubes glBindVertexArray(cubeVAO); glBindTexture(GL_TEXTURE_CUBE_MAP, skyboxTexture); glDrawArrays(GL_TRIANGLES, 0, 36); glBindVertexArray(0); // Draw skybox as last glDepthFunc(GL_LEQUAL); // Change depth function so depth test passes when values are equal to depth buffer's content skyboxShader.Use(); glm::mat4 view = glm::mat4(glm::mat3(camera.GetViewMatrix())); // Remove any translation component of the view matrix glUniformMatrix4fv(glGetUniformLocation(skyboxShader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view)); glUniformMatrix4fv(glGetUniformLocation(skyboxShader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection)); // skybox cube glBindVertexArray(skyboxVAO); glBindTexture(GL_TEXTURE_CUBE_MAP, skyboxTexture); glDrawArrays(GL_TRIANGLES, 0, 36); glBindVertexArray(0); glDepthFunc(GL_LESS); // Set depth function back to default // Swap the buffers glfwSwapBuffers(window);另外環境映射做用於角色身上的效果例如如下所看到的:
如下介紹反射技術,
環境映射的還有一個形式叫作折射(Refraction),它和反射差點兒相同。折射是光線經過特定材質對光線方向的改變。咱們一般看到像水同樣的表面。光線並不是直接經過的,而是讓光線彎曲了一點。它看起來像你把半僅僅手伸進水裏的效果。
折射遵照斯涅爾定律,使用環境貼圖看起來就像這樣:
咱們有個觀察向量I,一個法線向量N,此次折射向量是R。就像你所看到的那樣。觀察向量的方向有輕微彎曲。彎曲的向量R隨後用來從立方體貼圖上採樣。
折射可以經過GLSL的內建函數refract來實現。除此以外還需要一個法線向量,一個觀察方向和一個兩種材質之間的折射指數。
折射指數決定了一個材質上光線扭曲的數量,每個材質都有本身的折射指數。下表是常見的折射指數:
材質 | 折射指數 |
---|---|
空氣 | 1.00 |
水 | 1.33 |
冰 | 1.309 |
玻璃 | 1.52 |
寶石 | 2.42 |
咱們使用這些折射指數來計算光線經過兩個材質的比率。在咱們的樣例中,光線/視線從空氣進入玻璃(假設咱們假設箱子是玻璃作的)因此比率是1.001.52 = 0.658。
咱們已經綁定了立方體貼圖,提供了定點數據,設置了攝像機位置的uniform。現在僅僅需要改變片斷着色器:
void main() { float ratio = 1.00 / 1.52; vec3 I = normalize(Position - cameraPos); vec3 R = refract(I, normalize(Normal), ratio); color = texture(skybox, R); }經過改變折射指數你可以建立出全然不一樣的視覺效果。編譯執行應用,結果也不是太有趣,因爲咱們僅僅是用了一個普通箱子,這不能顯示出折射的效果。看起來像個放大鏡。
使用同一個着色器,納米服模型卻可以展現出咱們期待的效果:玻璃制物體。
以上都是利用立方體貼圖實現的技術,事實上實現環境映射並不必定必須使用立方體貼圖,也可以使用一張貼圖實現效果,詳情查看筆者已經介紹過的案例:
Cocos2d-x 3.x 圖形學渲染系列二十二 關於使用一張貼圖實現的環境映射效果,效果例如如下所看到的: