零基礎學習OpenGL(八)--立方體貼圖、天空盒、環境對映
立方體貼圖
將多個紋理組合起來對映到一張紋理上的一種紋理型別:立方體貼圖(Cube Map)。
立方體貼圖:一個包含了6個2D紋理的紋理,每個2D紋理都組成了立方體的一個面:一個有紋理的立方體。之所以使用6個紋理合並在一張紋理而不使用6個單獨的紋理,是因為可以通過一個方向向量來進行索引或採樣。方向向量的原點在立方體的中心。方向向量的大小並不重要,只要提供了方向,OpenGL就會獲取方向向量(最終)所擊中的紋素,並返回對應的取樣紋理值。
建立立方體貼圖:
unsigned int textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
立方體貼圖包含有6個紋理,每個面一個,將紋理目標(target
int width, height, nrChannels;
unsigned char *data;
for(unsigned int i = 0; i < textures_faces.size(); i++)
{
data = stbi_load(textures_faces[i].c_str(), &width, &height, &nrChannels, 0);
glTexImage2D( GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data );
}
textures_faces的vector,它包含了立方體貼圖所需的所有紋理路徑,並以表中的順序排列。這將為當前繫結的立方體貼圖中的每個面生成一個紋理。
也需要設定它的環繞和過濾方式:
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_CLAMP_TO_EDGE,這是因為正好處於兩個面之間的紋理座標可能不能擊中一個面(由於一些硬體限制),所以通過使用GL_CLAMP_TO_EDGE,OpenGL將在我們對兩個面之間取樣的時候,永遠返回它們的邊界值。
使用立方體貼圖的片段著色器:
in vec3 textureDir; // 代表3D紋理座標的方向向量
uniform samplerCube cubemap; // 立方體貼圖的紋理取樣器
void main()
{
FragColor = texture(cubemap, textureDir);
}
天空盒
天空盒是一個包含了整個場景的(大)立方體,它包含周圍環境的6個影象,讓玩家以為他處在一個比實際大得多的環境當中。將這六個面折成一個立方體,你就會得到一個完全貼圖的立方體,模擬一個巨大的場景。一些資源可能會提供了這樣格式的天空盒,你必須手動提取六個面的影象,但在大部分情況下它們都是6張單獨的紋理影象。
載入天空盒:
vector<std::string> faces;
{ "right.jpg", "left.jpg", "top.jpg", "bottom.jpg", "front.jpg", "back.jpg" };
unsigned int cubemapTexture = loadCubemap(faces);
unsigned int loadCubemap(vector<std::string> faces)
{
unsigned int textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
int width, height, nrChannels;
for (unsigned int i = 0; i < faces.size(); i++)
{
unsigned char *data = stbi_load(faces[i].c_str(), &width, &height, &nrChannels, 0);
if (data)
{
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data );
stbi_image_free(data);
}
else
{
std::cout << "Cubemap texture failed to load at path: " << faces[i] << std::endl;
stbi_image_free(data);
}
}
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_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);
return textureID;
}
顯示天空盒:
頂點著色器:
#version 330 core
layout (location = 0) in vec3 aPos;
out vec3 TexCoords;
uniform mat4 projection;
uniform mat4 view;
void main()
{
TexCoords = aPos;
gl_Position = projection * view * vec4(aPos, 1.0);
}
將輸入的位置向量作為輸出給片段著色器的紋理座標。片段著色器會將它作為輸入來取樣Cube。
#version 330 core
out vec4 FragColor;
in vec3 TexCoords;
uniform samplerCube skybox;
void main()
{
FragColor = texture(skybox, TexCoords);
}
繪製天空盒時,我們需要將它變為場景中的第一個渲染的物體,並且禁用深度寫入。這樣子天空盒就會永遠被繪製在其它物體的背後了。
glDepthMask(GL_FALSE);
skyboxShader.use();
// ... 設定觀察和投影矩陣
glBindVertexArray(skyboxVAO);
glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture);
glDrawArrays(GL_TRIANGLES, 0, 36);
glDepthMask(GL_TRUE);
// ... 繪製剩下的場景
希望移除觀察矩陣中的位移部分,讓移動不會影響天空盒的位置向量。通過取4x4矩陣左上角的3x3矩陣來移除變換矩陣的位移部分。我們可以將觀察矩陣轉換為3x3矩陣(移除位移),再將其轉換回4x4矩陣,來達到類似的效果。
glm::mat4 view = glm::mat4(glm::mat3(camera.GetViewMatrix()));
優化:
我們現在是首先渲染天空盒,之後再渲染場景中的其它物體。如果我們先渲染天空盒,我們就會對螢幕上的每一個畫素執行一遍片段著色器,即便只有一小部分的天空盒最終是可見的。可以使用提前深度測試(Early Depth Testing)輕鬆丟棄掉的片段能夠節省我們很多寶貴的頻寬。
天空盒只是一個1x1x1的立方體,它很可能會不通過大部分的深度測試,導致渲染失敗。我們需要欺騙深度緩衝,讓它認為天空盒有著最大的深度值1.0,只要它前面有一個物體,深度測試就會失敗。z分量等於頂點的深度值。
TexCoords = aPos;
vec4 pos = projection * view * vec4(aPos, 1.0);
gl_Position = pos.xyww;
這樣天空盒就只會在沒有物體的地方渲染了。
環境對映
通過使用環境的立方體貼圖,我們可以給物體反射和折射的屬性。
根據觀察方向向量I和物體的法向量N,來計算反射向量R。我們可以使用GLSL內建的reflect函式來計算這個反射向量。最終的R向量將會作為索引/取樣立方體貼圖的方向向量,返回環境的顏色值。最終的結果是物體看起來反射了天空盒。
物體的頂點著色器:
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
out vec3 Normal;
out vec3 Position;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
Normal = mat3(transpose(inverse(model))) * aNormal;
Position = vec3(model * vec4(aPos, 1.0));
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
改變物體的片段著色器就可以讓物體有反射性:
#version 330 core
out vec4 FragColor;
in vec3 Normal;
in vec3 Position;
uniform vec3 cameraPos;
uniform samplerCube skybox;
void main()
{
vec3 I = normalize(Position - cameraPos);
vec3 R = reflect(I, normalize(Normal));
FragColor = vec4(texture(skybox, R).rgb, 1.0);
}
還可以使用反射貼圖,它也是可以取樣的紋理影象,它決定這片段的反射性。
使用折射可以創建出類玻璃的效果,reflect換為refract就好。
動態環境貼圖:
通過使用幀緩衝,我們能夠為物體的6個不同角度創建出場景的紋理,並在每個渲染迭代中將它們儲存到一個立方體貼圖中。之後我們就可以使用這個(動態生成的)立方體貼圖來創建出更真實的,包含其它物體的,反射和折射表面了。這就叫做動態環境對映(Dynamic Environment Mapping),因為我們動態建立了物體周圍的立方體貼圖,並將其用作環境貼圖。