立方體貼圖(Cubemaps)
-
立方體貼圖(cubemap)是一種紋理闸拿,包含來自立方體每個平面的6個獨立的2D紋理:一個紋理立方體必尼。立方體貼圖的一種有用特性就是它可以使用方向矢量進行索引/采樣愕撰。下面展示一個1X1X1單位立方體览露,棕色為方向矢量的圖示:(圖片取自書中)
- 方向矢量的大小并不重要,OpenGL通過方向矢量與紋理面的撞擊點檢索和采樣相應(yīng)紋理值丽惶。
1. 創(chuàng)建立方體貼圖
- 立方體貼圖與其他紋理類型相似炫七,我們在操作前創(chuàng)建并綁定到相應(yīng)的紋理單元。
unsigned int textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
- 因為立方體貼圖包含6個紋理蚊夫,這意味著我們需要為每個面的紋理調(diào)用
glTexImage2D
函數(shù)诉字。由于有6個面,OpenGL為我們提供了6個特別的紋理目標(biāo)。如下表所示:
紋理目標(biāo) | 方向 |
---|---|
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 | 前 |
- 上面表格的紋理目標(biāo)值與其他OpenGL枚舉值相似壤圃,值為
int
類型陵霉,并且線性遞增。因此我們可以從GL_TEXTURE_CUBE_MAP_POSITIVE_X
開始循環(huán)所有紋理目標(biāo)伍绳。
int width, height, nrChannels;
unsigned char* data;
for(unsigned int i = 0; i < texture_faces.size(); i++)
{
data = stb_load(texture_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);
}
- 為立方體貼圖指定裁剪和過濾方法踊挠。
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);
- 對于立方體貼圖,在著色器中我們需要使用另外一種取樣器類型
samplerCube
并使用函數(shù)texture
來進行采樣冲杀。而且我們應(yīng)該使用vec3
類型的方向矢量而不是vec2
效床。
in vec3 textureDir; // 方向矢量:3D紋理的坐標(biāo)
unfirom samplerCube cubemap; // 立方體貼圖紋理取樣器
void main()
{
FragColor = texture(cubemap, textureDir);
}
2. 天空盒(Skybox)
- 一個天空盒就是一個包含整個場景和6張周圍環(huán)境圖像的立方體,能夠給予觀察者更大視覺空間的錯覺权谁。一般天空盒圖像具有下面圖像所展示的模式:(圖片取自書中)
2.1 加載天空盒
- 因為天空盒其實就是個立方體貼圖剩檀,因此加載與立方體貼圖相似,下面我們將加載封裝成一個函數(shù)旺芽。
unsigned int loadCubemap(std::vector<std::string> faces)
{
unsigned int textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
int width, height, nrChannels;
unsigned char* data;
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 failed to load at path: " << faces[i] << std::endl;
stbi_image_free(data);
}
}
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);
return textureID;
}
- 接下來沪猴,我們只要指定圖像路徑,就可以將天空盒加載為紋理數(shù)據(jù)進行使用采章。
std::vector<std::string> faces = {
"right.jpg",
"left.jpg",
"top.jpg",
"bottom.jpg",
"front.jpg",
"back.jpg"
};
unsigned int cubemapTexture = loadCubemap(faces);
2.2 顯示天空盒
- 因為當(dāng)立方體使用立方體貼圖時可以使用本地位置作為紋理坐標(biāo)运嗜,因此我們只需提供位置矢量而無需設(shè)置紋理坐標(biāo)。要渲染天空盒我們可以定義如下頂點著色器悯舟。
#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);
}
- 片元著色器則從頂點著色器接收紋理位置進行紋理取樣担租。
#version 330 core
out vec4 FragColor;
in vec3 TexCoords;
uniform samplerCube skybox;
void main()
{
FragColor = texture(skybox, TexCoords);
}
- 要渲染天空盒,我們禁用深度測試抵怎,并將其首先在場景中進行繪制奋救。
glDepthMask(GL_FALSE);
skyboxShader.use();
// ... 設(shè)置視矩陣和投影矩陣
glBindVertexArray(skyboxVAO);
glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture);
glDrawArrays(GL_TRIANGLES, 0, 36);
glDepthMask(GL_TRUE);
// ... 繪制剩下的場景
-
這時候,我們運行程序并拉近視角便贵,會出現(xiàn)如下渲染效果菠镇。
- 上面的渲染問題,是因為當(dāng)前的視矩陣會對天空盒的位置進行變換承璃,我們需要移除視矩陣中的平移變換,只保留旋轉(zhuǎn)蚌本。從基本光照的章節(jié)我們知道盔粹,我們從4X4的矩陣中抽取左上角的3X3矩陣就可以去除平移變換。
glm::vec4 view = glm::mat4(glm::mat3(camera.GetViewMatrix()));
-
渲染效果程癌,這時移動視角將只有立方體發(fā)送變化舷嗡。
- 上面的渲染我們是先繪制天空盒然后繪制立方體,這樣我們需要渲染天空盒的每個片元嵌莉。通過使用早期深度測試(early depth testing)我們可以丟棄不可見的片元提高性能进萄。但是如果最后才繪制天空盒,則會遮擋立方體,不管是在有深度測試或無的情況下中鼠。因此可婶,我們需要使用一個小技巧來騙過深度緩沖區(qū),讓它認(rèn)為天空盒擁有最大的深度值1.0援雇,所以天空盒在有物體處在它前面時都會通不過深度測試矛渴。從坐標(biāo)系統(tǒng)章節(jié)我們知道頂點著色器運行后會執(zhí)行透視除法(perspective division),將坐標(biāo)的xyz分量除以w分量惫搏,那么我們將z分量的值設(shè)為與w分量一樣具温,則透視除法后標(biāo)準(zhǔn)設(shè)備坐標(biāo)的z分量的值則為1。最后我們將深度測試的運算符設(shè)置為
GL_LEQUAL
筐赔。
void main()
{
TexCoords = aPos;
vec4 pos = projection * view * vec4(aPos, 1.0);
gl_Position = pos.xyww;
}
3. 環(huán)境映射(Environment mapping)
- 像上一小節(jié)那樣使用環(huán)境立方體貼圖的技術(shù)我們稱為環(huán)境映射(environment mapping)技術(shù)铣猩,其中最流行的兩種:反射和折射。
3.1 反射
- 反射(Reflection)就是物體(或部分物體)反射周圍環(huán)境的屬性茴丰,即基于視角物體的顏色不同程度地等于環(huán)境的顏色剂习,例如鏡子就是一個反射物體。
- 下面的圖示展示了我們?nèi)绾斡嬎惴瓷涫噶拷匣Γ⑹褂迷撌噶坎蓸恿⒎襟w貼圖鳞绕。(圖片取自書中)
從上圖可知,我們可以基于視角矢量繞物體法向量來計算反射矢量. - GLSL有個內(nèi)置函數(shù)
reflect
可用于計算反射矢量尸曼,然后我們就可以使用反射矢量來索引/采樣立方體貼圖们何,獲取環(huán)境的顏色值,最終的渲染效果就是物體似乎反射了天空盒控轿。 - 基于前面渲染天空盒的場景冤竹,我們修改立方體的片元著色器,為其添加反射屬性茬射。
#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);
}
- 調(diào)整頂點著色器鹦蠕,輸出法向量和位置變量到片元著色器。
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 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 * vec4(Position, 1.0);
}
- 因為立方體使用到了法向量在抛,因此頂點數(shù)據(jù)原先的紋理坐標(biāo)需要替換成法向量钟病,具體可以參考前面章節(jié)的頂點數(shù)據(jù)。同時記得設(shè)置相機位置的uniform變量
cameraPos
刚梭。 - 最后我們渲染綁定立方體和天空盒肠阱。
glBindVertexArray(cubeVAO);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture);
glDrawArrays(GL_TRIANGLES, 0, 36);
-
渲染效果。
3.2 折射(Refraction)
- 另外一種形式的環(huán)境映射稱為折射(refraction)朴读,與反射相似屹徘。折射是由于光穿過不同材質(zhì)時光發(fā)生了方向的改變。如下圖所示:(圖片取自書中)
與反射相似衅金,我們有視角矢量噪伊,法向量簿煌,結(jié)果是折射矢量。 - 使用GLSL內(nèi)置函數(shù)
refract
可以很容易計算折射矢量鉴吹,函數(shù)需要一個法向量姨伟,一個視角矢量和兩種材質(zhì)折射率之間的比率值。 - 常見材質(zhì)折射率如下表所示:
材質(zhì) | 折射率 |
---|---|
空氣 | 1.00 |
水 | 1.33 |
冰 | 1.309 |
玻璃 | 1.52 |
鉆石 | 2.42 |
- 假設(shè)我們的立方體由玻璃制成拙寡,那么場景中光線由空氣進入玻璃發(fā)生折射授滓,其比率值為。
- 上面反射的渲染中我們已經(jīng)設(shè)定了法向量肆糕,和相機位置等數(shù)據(jù)般堆,對于折射,我們只需修改片元著色器即可將屬性變更為折射诚啃。
void main()
{
float ratio = 1.00 / 1.52;
vec3 I = normalize(Position - cameraPos);
vec3 R = refract(I, normalize(Normal), ratio);
FragColor = vec4(texture(skybox, R).rgb, 1.0);
}
-
渲染效果淮摔。
- 注意:對于精確的物理結(jié)果,我們還需對光線離開物體時計算一次折射始赎,現(xiàn)在我們只是使用單向折射和橙。