立方体贴图

我们将讨论的是将多个纹理组合起来映射到一张纹理上的一种纹理类型:立方体贴图(Cube Map)。

  1. 创建立方体贴图 > 立方体贴图是和其它纹理一样的,所以如果想创建一个立方体贴图的话,我们需要生成一个纹理,并将其绑定到纹理目标上,之后再做其它的纹理操作。这次要绑定到GL_TEXTURE_CUBE_MAP:

        unsigned int textureID;
        glGenTextures(1, &textureID);
        glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);

    因为立方体贴图包含有6个纹理,每个面一个,我们需要调用glTexImage2D函数6次,参数和之前教程中很类似。但这一次我们将纹理目标(target)参数设置为立方体贴图的一个特定的面,告诉OpenGL我们在对立方体贴图的哪一个面创建纹理。这就意味着我们需要对立方体贴图的每一个面都调用一次glTexImage2D。

    纹理目标 方位
    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

    和OpenGL的很多枚举(Enum)一样,它们背后的int值是线性递增的,所以如果我们有一个纹理位置的数组或者vector,我们就可以从GL_TEXTURE_CUBE_MAP_POSITIVE_X开始遍历它们,在每个迭代中对枚举值加1,遍历了整个纹理目标:

        int width, height, nrChannels;
        unsigned char *data;  
        //textures_faces 贴图路径的 vector
        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
            );
        }

    因为立方体贴图和其它纹理没什么不同,我们也需要设定它的环绕和过滤方式: cpp 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); //GL_TEXTURE_WRAP_R:纹理的R坐标设置了环绕方式,它对应的是纹理的第三个维度(和位置的z一样) glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);

    立方体贴图的Shader 采样器数据类型 samplerCube,并且使用vec3的方向向量进行采样

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

    立方体的一个重要用途:天空盒(Skybox)

  2. 加载天空盒子 > 和其他纹理一样, 只是对了一个轴向 R cpp 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);

  3. 显示天空盒子 > 天空盒和一般的立方体一样,只是渲染在对底层的背景上

    1. 顶点着色器 cpp #version 330 core layout (location = 0) in vec3 aPos; //立方体贴图的uv坐标 `vec3` out vec3 TexCoords; uniform mat4 projection; uniform mat4 view; void main() { TexCoords = aPos; gl_Position = projection * view * vec4(aPos, 1.0); }

    2. 片段着色器 cpp #version 330 core out vec4 FragColor; in vec3 TexCoords; //立方体贴图的采样器 uniform samplerCube skybox; void main() { FragColor = texture(skybox, TexCoords); }

    3. 绘制天空盒时,我们需要将它变为场景中的第一个渲染的物体,并且禁用深度写入。这样子天空盒就会永远被绘制在其它物体的背后了。 cpp glDepthMask(GL_FALSE); skyboxShader.use(); // ... 设置观察和投影矩阵 glBindVertexArray(skyboxVAO); glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture); glDrawArrays(GL_TRIANGLES, 0, 36); glDepthMask(GL_TRUE); // ... 绘制剩下的场景

  4. 优化点 > 目前我们是首先渲染天空盒,之后再渲染场景中的其它物体。这样子能够工作,但不是非常高效。如果我们先渲染天空盒,我们就会对屏幕上的每一个像素运行一遍片段着色器,即便只有一小部分的天空盒最终是可见的。可以使用提前深度测试(Early Depth Testing)轻松丢弃掉的片段能够节省我们很多宝贵的带宽。 所以,我们将会最后渲染天空盒,以获得轻微的性能提升。这样子的话,深度缓冲就会填充满所有物体的深度值了,我们只需要在提前深度测试通过的地方渲染天空盒的片段就可以了,很大程度上减少了片段着色器的调用。问题是,天空盒只是一个1x1x1的立方体,它很可能会不通过大部分的深度测试,导致渲染失败。不用深度测试来进行渲染不是解决方案,因为天空盒将会复写场景中的其它物体。我们需要欺骗深度缓冲,让它认为天空盒有着最大的深度值1.0,只要它前面有一个物体,深度测试就会失败。

    在坐标系统小节中我们说过,透视除法是在顶点着色器运行之后执行的,将gl_Position的xyz坐标除以w分量。我们又从深度测试小节中知道,相除结果的z分量等于顶点的深度值。使用这些信息,我们可以将输出位置的z分量等于它的w分量,让z分量永远等于1.0,这样子的话,当透视除法执行之后,z分量会变为w / w = 1.0。

        void main()
        {
            TexCoords = aPos;
            vec4 pos = projection * view * vec4(aPos, 1.0);
            // w 替代z, 得到 1.0 的深度值。 将它从默认的GL_LESS改为GL_LEQUAL。深度缓冲将会填充上天空盒的1.0值,所以我们需要保证天空盒在值小于或等于深度缓冲而不是小于时通过深度测试。
            gl_Position = pos.xyww;
        }
    1. 反向,颜色取反

          void main()
          {
              FragColor = vec4(vec3(1.0 - texture(screenTexture, TexCoords)), 1.0);
          }
    2. 灰度 rgb 平均值

          void main()
          {
              FragColor = texture(screenTexture, TexCoords);
              //float average = (FragColor.r + FragColor.g + FragColor.b) / 3.0;
              float average = (FragColor.r + FragColor.g + FragColor.b) * 0.333333;
              FragColor = vec4(average, average, average, 1.0);
          }

      这已经能创造很好的结果了,但人眼会对绿色更加敏感一些,而对蓝色不那么敏感,所以为了获取物理上更精确的效果,我们需要使用加权的(Weighted)通道: r 0.2126、 g 0.7152 、 b 0.0722 cpp void main() { FragColor = texture(screenTexture, TexCoords); float average = 0.2126 * FragColor.r + 0.7152 * FragColor.g + 0.0722 * FragColor.b; FragColor = vec4(average, average, average, 1.0); }

    3. 核效果

          void main()
          {
              vec2 offsets[9] = vec2[](
                  vec2(-offset,  offset), // 左上
                  vec2( 0.0f,    offset), // 正上
                  vec2( offset,  offset), // 右上
                  vec2(-offset,  0.0f),   // 左
                  vec2( 0.0f,    0.0f),   // 中
                  vec2( offset,  0.0f),   // 右
                  vec2(-offset, -offset), // 左下
                  vec2( 0.0f,   -offset), // 正下
                  vec2( offset, -offset)  // 右下
              );
      
              float kernel[9] = float[](
                  -1, -1, -1,
                  -1,  9, -1,
                  -1, -1, -1
              );
      
              vec3 sampleTex[9];
              for(int i = 0; i < 9; i++)
              {
                  sampleTex[i] = vec3(texture(screenTexture, TexCoords.st + offsets[i]));
              }
              vec3 col = vec3(0.0);
              for(int i = 0; i < 9; i++)
                  col += sampleTex[i] * kernel[i];
      
              FragColor = vec4(col, 1.0);
          }
    4. 模糊 cpp float kernel[9] = float[]( 1.0 / 16, 2.0 / 16, 1.0 / 16, 2.0 / 16, 4.0 / 16, 2.0 / 16, 1.0 / 16, 2.0 / 16, 1.0 / 16 );

    5. 边缘检测

          float kernel[9] = float[](
              1, 1, 1,
              1, -8, 1,
              1, 1, 1  
          );

文章作者: Yonggang Long
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Yonggang Long !
 上一篇
2022-08-10 Yonggang Long
下一篇 
2022-08-10 Yonggang Long
  目录