立方体贴图由6个2D纹理(Texture2D)组成的立方体纹理,每个纹理对应一个面。可以通过一个方向向量进行索引采样,2D纹理是通过一个2D纹理坐标进行采样。
OpenGL_立方体贴图(Cubemap) - 图1
注意是方向向量,即不需要知道大小,OpenGL通过方向向量获取“击中”的纹理像素,并返回对应的采样纹理值。假设立方体的中心刚好位于坐标原点,则可由用立方体表面任意坐标值作为方向向量进行采样。代码示例如下:

  1. // 立方体贴图的加载,和普通2D纹理加载过程类似,有些许不同的地方
  2. GLuint createTextureCubemap( const std::vector<std::string> &path)
  3. {
  4. GLuint textureID;
  5. glGenTextures( 1, &textureID );
  6. glBindTexture( GL_TEXTURE_CUBE_MAP, textureID ); // 对应普通2D纹理的GL_TEXTURE_2D
  7. glTexParameteri( GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
  8. glTexParameteri( GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
  9. // 方向向量可能刚好指向两个纹理面之间,由于硬件限制,可能会导致无法击中这两个面。
  10. // 设置为GL_CLAMP_TO_EDGE,返回边界值。
  11. glTexParameteri( GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
  12. glTexParameteri( GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
  13. // 纹理第三个维度(位置的Z坐标)
  14. glTexParameteri( GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE );
  15. GLint width, height, nrChannels;
  16. for( size_t i = 0; i < path.size(); i++ ) // 需要加载6张2D纹理
  17. {
  18. unsigned char *data = stbi_load( path[i].c_str(),
  19. &width, &height,
  20. &nrChannels, 0 );
  21. if( data )
  22. {
  23. GLenum format = nrChannels == 1 ? GL_RED :
  24. nrChannels == 3 ? GL_RGB :
  25. nrChannels == 4 ? GL_RGBA :
  26. GL_RGB;
  27. // 6个纹理的ID
  28. // “右”纹理:GL_TEXTURE_CUBE_MAP_POSITIVE_X 或 GL_TEXTURE_CUBE_MAP_POSITIVE_X + 0
  29. // “左”纹理:GL_TEXTURE_CUBE_MAP_NEGATIVE_X 或 GL_TEXTURE_CUBE_MAP_POSITIVE_X + 1
  30. // “上”纹理:GL_TEXTURE_CUBE_MAP_POSITIVE_Y 或 GL_TEXTURE_CUBE_MAP_POSITIVE_X + 2
  31. // “下”纹理:GL_TEXTURE_CUBE_MAP_NEGATIVE_Y 或 GL_TEXTURE_CUBE_MAP_POSITIVE_X + 3
  32. // “前”纹理:GL_TEXTURE_CUBE_MAP_POSITIVE_Z 或 GL_TEXTURE_CUBE_MAP_POSITIVE_X + 4
  33. // “后”纹理:GL_TEXTURE_CUBE_MAP_NEGATIVE_Z 或 GL_TEXTURE_CUBE_MAP_POSITIVE_X + 5
  34. glTexImage2D( GL_TEXTURE_CUBE_MAP_POSITIVE_X + i,
  35. 0, // mipmap level,0表示一级,如果有多级,需要挨个单独设置
  36. format, // 告诉 OpenGL 我们希望把纹理储存为何种格式
  37. width, height, // 纹理宽高
  38. 0, // 总为0,历史遗留问题
  39. format, // 图像像素的分量组成
  40. GL_UNSIGNED_BYTE, // 每个像素分量的大小
  41. data ); // 内存中的图像数据
  42. }
  43. else
  44. {
  45. cout << "Failed to load texture::" << path[i] << endl;
  46. }
  47. stbi_image_free( data ); // 纹理数据已经上传到显存中,内存中的数据可以删除了。
  48. }
  49. glBindTexture( GL_TEXTURE_2D, 0 ); // 恢复成默认
  50. return textureID;
  51. }

在片段着色器中:

  1. in vec3 textureDir; // 代表3D纹理坐标的方向向量
  2. uniform samplerCube cubemap; // 立方体贴图的纹理采样器
  3. void main()
  4. {
  5. FragColor = texture(cubemap, textureDir);
  6. }

一、天空盒(Skybox)

立体贴图技术完全可以用2D纹理贴图来实现,那它有什么用?有一些技术通过立体贴图实现起来会非常简单,其中一个就是天空盒。
天空盒是一个包含了整个场景的(大)立方体,它包含周围环境的6个图像,让玩家以为他处在一个比实际大得多的环境当中。游戏中使用天空盒的例子有群山、白云或星空。下面这张截图中展示的是星空的天空盒,它来自于『上古卷轴3』
cubemaps_morrowind.jpg
立方体贴图能完美满足天空盒的需求:我们有一个6面的立方体,每个面都需要一个纹理。在上面的图片中,他们使用了夜空的几张图片,让玩家产生其位于广袤宇宙中的错觉,但实际上他只是在一个小小的盒子当中。
天空盒的实例代码见:https://github.com/JackieLong/OpenGL/tree/main/project_cubemap_test

二、环境映射

另外一个使用到立方体贴图的技术是环境映射(Environment Mapping),指的是将环境的颜色映射到物体表面,其中最流行的是:反射(Reflection)和折射(Refraction)。

1、反射

反射这个属性表现为物体(或物体的一部分)反射它周围环境,即根据观察者的视角,物体的颜色或多或少等于它的环境。镜子就是一个反射性物体:它会根据观察者的视角反射它周围的环境。物理模型如下:
cubemaps_reflection_theory.png
代码实现示例如下:
在片段着色器中:

  1. #version 330 core
  2. layout (location = 0) in vec3 aPos; // 顶点坐标
  3. layout (location = 1) in vec3 aNormal; // 顶点法向量
  4. out vec3 normal;
  5. out vec3 fragPos;
  6. uniform mat4 model;
  7. uniform mat4 view;
  8. uniform mat4 projection;
  9. void main()
  10. {
  11. normal = mat3(transpose(inverse(model))) * aNormal; // 消除物体不规则缩放对法向量的影响
  12. fragPos = vec3(model * vec4(aPos, 1.0)); // 统一转换到世界坐标中计算。
  13. gl_Position = projection * view * model * vec4(aPos, 1.0);
  14. }

在顶点着色器中:

  1. #version 330 core
  2. in vec3 normal; // 顶点法向量
  3. in vec3 fragPos; // 观察点(顶点或者片段)
  4. uniform vec3 viewPos; // 眼睛位置(摄像机位置)
  5. uniform samplerCube cubemap; // 立方体贴图
  6. out vec4 FragColor;
  7. void main()
  8. {
  9. vec3 I = normalize(fragPos - viewPos); // I向量
  10. normal = normalize(normal); // 单位法向量
  11. vec3 R = reflect(I, normal); // 立方体贴图方向向量
  12. FragColor = vec4(texture(cubemap, R).rgb, 1.0);
  13. }

2、折射

环境映射的另一种形式是折射,它和反射很相似。折射是光线由于传播介质的改变而产生的方向变化。折射是通过斯涅尔定律(Snell’s Law)来描述的:
cubemaps_refraction_theory.png
代码示例如下,在顶点着色器中:

  1. #version 330 core
  2. layout (location = 0) in vec3 aPos; // 顶点坐标
  3. layout (location = 1) in vec3 aNormal; // 顶点法向量
  4. out vec3 normal;
  5. out vec3 fragPos;
  6. uniform mat4 model;
  7. uniform mat4 view;
  8. uniform mat4 projection;
  9. void main()
  10. {
  11. normal = mat3(transpose(inverse(model))) * aNormal; // 消除物体不规则缩放对法向量的影响
  12. ragPos = vec3(model * vec4(aPos, 1.0)); // 统一转换到世界坐标中计算。
  13. gl_Position = projection * view * model * vec4(aPos, 1.0);
  14. }

在片段着色器中:

  1. #version 330 core
  2. in vec3 normal; // 顶点法向量
  3. in vec3 fragPos; // 观察点(顶点或者片段)
  4. uniform vec3 viewPos; // 眼睛位置(摄像机位置)
  5. uniform samplerCube cubemap; // 立方体贴图
  6. out vec4 FragColor;
  7. const float refract_air = 1.00; // 光在空气中的折射率
  8. const float refract_glass = 1.52; // 光在玻璃中的折射率
  9. const float ratio = refract_air / refract_glass; // 光从空气射入玻璃时的折射率
  10. void main()
  11. {
  12. vec3 I = normalize(fragPos - viewPos); // I向量
  13. normal = normalize(normal); // 单位法向量
  14. vec3 R = refract(I, normal, ratio); // 立方体贴图方向向量
  15. FragColor = vec4(texture(cubemap, R).rgb, 1.0);
  16. }

常见物质的折射率如下:

材质 折射率
空气 1.00
1.33
1.309
玻璃 1.52
钻石 2.42

折射率越大,弯曲程度越大。

三、动态环境贴图

现在我们使用的都是静态图像的组合来作为天空盒,看起来很不错,但它没有在场景中包括可移动的物体。我们一直都没有注意到这一点,因为我们只使用了一个物体。如果我们有一个镜子一样的物体,周围还有多个物体,镜子中可见的只有天空盒,看起来就像它是场景中唯一一个物体一样。
通过使用帧缓冲,我们能够为物体的6个不同角度创建出场景的纹理,并在每个渲染迭代中将它们储存到一个立方体贴图中。之后我们就可以使用这个(动态生成的)立方体贴图来创建出更真实的,包含其它物体的,反射和折射表面了。这就叫做动态环境映射(Dynamic Environment Mapping),因为我们动态创建了物体周围的立方体贴图,并将其用作环境贴图。
虽然它看起来很棒,但它有一个很大的缺点:我们需要为使用环境贴图的物体渲染场景6次,这是对程序是非常大的性能开销。现代的程序通常会尽可能使用天空盒,并在可能的时候使用预编译的立方体贴图,只要它们能产生一点动态环境贴图的效果。虽然动态环境贴图是一个很棒的技术,但是要想在不降低性能的情况下让它工作还是需要非常多的技巧的。