進階OpenGL總結(三)
六、立方體貼圖
1、建立立方體貼圖
a、立方體貼圖和其他紋理一樣,是以要建立一個立方體貼圖,在進行任何紋理操作之前,需要生成一個紋理,激活相應紋理單元然後綁定到合适的紋理目标上。這次要綁定到 GL_TEXTURE_CUBE_MAP紋理類型:
GLuint textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
由于立方體貼圖包含6個紋理,立方體的每個面一個紋理,我們必須調用glTexImage2D函數6次,函數的參數和前面教程講的相似。然而這次我們必須把紋理目标(target)參數設定為立方體貼圖特定的面,這是告訴OpenGL我們建立的紋理是對應立方體哪個面的。是以我們便需要為立方體貼圖的每個面調用一次 glTexImage2D。
b、由于立方體貼圖有6個面,OpenGL就提供了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 前
c、和很多OpenGL其他枚舉一樣,對應的int值都是連續增加的,是以我們有一個紋理位置的數組或vector,就能以 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
);
}
d、這兒我們有個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);
e、在片段着色器中,我們必須使用一個不同的采樣器——samplerCube,用它來從texture函數中采樣,但是這次使用的是一個vec3方向向量,取代vec2。下面是一個片段着色器使用了立方體貼圖的例子:
in vec3 textureDir; // 用一個三維方向向量來表示立方體貼圖紋理的坐标
uniform samplerCube cubemap; // 立方體貼圖紋理采樣器
void main()
{
color = texture(cubemap, textureDir);
}
2、具體應用——天空盒
a、加載天空盒
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<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);
b、顯示天空盒
頂點着色器:
#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;
}
片斷着色器:
#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
3、環境映射
我們現在有了一個把整個環境映射到為一個單獨紋理的對象,我們利用這個資訊能做的不僅是天空盒。使用帶有場景環境的立方體貼圖,我們還可以讓物體有一個反射或折射屬性。像這樣使用了環境立方體貼圖的技術叫做環境貼圖技術,其中最重要的兩個是反射(reflection)和折射(refraction)。
a、反射
凡是是一個物體(或物體的某部分)反射(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));
}
然後在渲染箱子前我們還得綁定立方體貼圖紋理:
glBindVertexArray(cubeVAO);
glBindTexture(GL_TEXTURE_CUBE_MAP, skyboxTexture);
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);
b、折射
環境映射的另一個形式叫做折射(Refraction),它和反射差不多。折射是光線通過特定材質對光線方向的改變。我們通常看到像水一樣的表面,光線并不是直接通過的,而是讓光線彎曲了一點。它看起來像你把半隻手伸進水裡的效果。
折射遵守斯涅爾定律,使用環境貼圖看起來就像這樣:
我們有個觀察向量I,一個法線向量N,這次折射向量是R。就像你所看到的那樣,觀察向量的方向有輕微彎曲。彎曲的向量R随後用來從立方體貼圖上采樣。
折射可以通過GLSL的内建函數refract來實作,除此之外還需要一個法線向量,一個觀察方向和一個兩種材質之間的折射指數。
折射指數決定了一個材質上光線扭曲的數量,每個材質都有自己的折射指數。下表是常見的折射指數:
空氣 1.00
水 1.33
冰 1.309
玻璃 1.52
寶石 2.42
我們使用這些折射指數來計算光線通過兩個材質的比率。在我們的例子中,光線/視線從空氣進入玻璃(如果我們假設箱子是玻璃做的)是以比率是1.00/1.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);
}