一.立方体贴图
将多个纹理组合起来映射到一张纹理上的一种纹理类型:立方体贴图(Cube Map)。
而使用立方体贴图而不是使用六张单独的纹理的作用就是为了方便我们通过一个方向向量来进行索引/采样。如图:方向向量的原点位于它的中心,我们只需要提供一个方向,不论大小多少就可以通过方向向量来进行纹理值采样。
二.创建立方体贴图
- 创建纹理对象
立方体贴图的创建和其他纹理是一样的,只不过我们这次要绑定到GL_TEXTURE_CUBE_MAP
上。
unsigned int textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
- 绑定纹理数据
因为立方体贴图有6个纹理,所以需要调用glTexImage2D
函数6次,并且在纹理目标的参数设置为立方体贴图的一个特定的面。
注意,纹理目标作为OpenGl中的枚举类型,背后的int是线性递增的,如果有一个纹理位置的数组类型,我们就可以通过递增从
GL_TEXTURE_CUBE_MAP_POSITIVE_X
开始遍历。
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);
- 绘制立方体贴图的物体之前,激活对应的纹理单元,并绑定立方体贴图。
PS:对立方体贴图进行采样的时候,片段着色器中的采样器类型为samplerCube
,因为这时我们不再通过vec2
的纹理坐标,而是通过vec3
的方向向量对立方体贴图进行采样。
in vec3 textureDir; // 代表3D纹理坐标的方向向量
uniform samplerCube cubemap; // 立方体贴图的纹理采样器
void main()
{
FragColor = texture(cubemap, textureDir);
}
三.天空盒
天空盒是一个包含了整个场景的(大)立方体,它包含周围环境的6个图像,让玩家以为他处在一个比实际大得多的环境当中,实际上玩家是在这个盒子里面。
如下:
1.加载天空盒
实际上也就是加载立方体贴图的代码。
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;
}
我们把立方体贴图代码拟合到了一个函数中方便进行调用,调用时只需要输入一个纹理贴图路径数组即可。
vector<std::string> faces
{
"right.jpg",
"left.jpg",
"top.jpg",
"bottom.jpg",
"front.jpg",
"back.jpg"
};
unsigned int cubemapTexture = loadCubemap(faces);
2.显示天空盒
- 由于天空盒仍然是一个立方体,那么我们仍然需要定义这个立方体的顶点数据,VAO、VBO。
float skyboxVertices[] = {
// positions
-1.0f, 1.0f, -1.0f,
-1.0f, -1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
1.0f, 1.0f, -1.0f,
-1.0f, 1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
-1.0f, -1.0f, -1.0f,
-1.0f, 1.0f, -1.0f,
-1.0f, 1.0f, -1.0f,
-1.0f, 1.0f, 1.0f,
-1.0f, -1.0f, 1.0f,
1.0f, -1.0f, -1.0f,
1.0f, -1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f, 1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
-1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f, -1.0f, 1.0f,
-1.0f, -1.0f, 1.0f,
-1.0f, 1.0f, -1.0f,
1.0f, 1.0f, -1.0f,
1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
-1.0f, 1.0f, 1.0f,
-1.0f, 1.0f, -1.0f,
-1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
1.0f, -1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
1.0f, -1.0f, 1.0f
};
正如我们之前讲的,我们可以直接用立方体的位置作为纹理坐标来对立方体贴图进行采样。当立方体位于原点(0,0,0)时,每一个位置值都是一个方向向量,都代表一个纹素值。
- 配置顶点着色器
#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);
}
- 配置片段着色器(将顶点着色器传来的位置向量作为纹理的方向向量来对立方体贴图采样
samplerCube
)
#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);
}
- 绑定立方体贴图,
skybox
采样器就可以访问对应的纹理贴图,绘制时我们首先绘制天空盒并禁用深度写入,这样天空盒就一直在其他物体后了。
glDepthMask(GL_FALSE);
skyboxShader.use();
// ... 设置观察和投影矩阵
glBindVertexArray(skyboxVAO);
glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture);
glDrawArrays(GL_TRIANGLES, 0, 36);
glDepthMask(GL_TRUE);
// ... 绘制剩下的场景
注意,此时我们的视角移动的时候天空盒也会发生移动,而我们想要的效果是不论玩家移动了多远,天空盒都不会变近,所以我们应该把天空盒的4x4视图矩阵中取其3x3矩阵来移除位移部分,再将其变换为4x4矩阵,达到类似的效果。
四.环境映射
现在,我们可以将环境映射到一个纹理对象上,然而利用这个信息,我们可以给物体反射和折射的属性。
1.反射
反射这个属性表现为物体(或物体的一部分)反射它周围环境,即根据观察者的视角,物体的颜色或多或少等于它的环境。
原理如图:设我们的观察方向为I,物体的法向量为N,根据这两个向量以及reflect
函数来计算反射向量R。然后我们就可以利用这个反射向量作为采样立方体贴图的方向向量,并将采样的纹素结果赋给该物体的这个片段上。最后的结果就像是物体反射了天空盒。
- 片段着色器
#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);
}
- 顶点着色器(为了给片段着色器提供顶点数据以便计算向量信息)
#version 330 core
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);
}
注意:在渲染箱子之前要先绑定立方体贴图
glBindVertexArray(cubeVAO); glBindTexture(GL_TEXTURE_CUBE_MAP, skyboxTexture); glDrawArrays(GL_TRIANGLES, 0, 36);
效果:
2.折射
如图,折射只是最后的光的方向向量改变了,采样方法都是通过这个光的方向对立方体贴图进行采样。
而得到折射后的方向向量我们可以使用reflect函数。
我们已经绑定了立方体贴图,提供了顶点数据和法线,并设置了摄像机位置的uniform,唯一修改的就是片段着色器:
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);
}
效果如图:
3.动态环境贴图
现在我们使用的天空盒都是静态图像的组合,而要得到一个包含移动物体的环境贴图就需要用到动态环境贴图。
通过使用帧缓冲,我们能够为物体的6个不同角度创建出场景的纹理,并在每个渲染迭代中将它们储存到一个立方体贴图中。
之后我们就可以使用这个(动态生成的)立方体贴图来创建出更真实的,包含其它物体的,反射和折射表面了。这就叫做动态环境映射。