LearnOpenGL-高级OpenGL-6.天空盒

本文介绍了使用OpenGL进行天空盒渲染、环境映射(包括反射和折射)的技术。首先讲解了天空盒的概念和采样方法,然后通过代码示例展示了如何创建和应用立方体贴图实现天空盒效果。接着,文章详细阐述了反射的原理和实现,包括立方体反射及模型反射。最后讨论了折射的原理,并给出了相应的代码实现。整个过程中,深度测试的作用和调整是关键,确保了渲染的正确顺序和效果。
摘要由CSDN通过智能技术生成

本人刚学OpenGL不久且自学,文中定有代码、术语等错误,欢迎指正

我写的项目地址:https://github.com/liujianjie/LearnOpenGLProject

天空盒

介绍

立方体贴图就是一个包含了6个2D纹理的纹理,每个2D纹理都组成了立方体的一个面:一个有纹理的立方体

如何采样

  • 方向向量的大小并不重要,只要提供了方向,OpenGL就会获取方向向量(最终)所击中的纹素,并返回对应的采样纹理值。
  • 只要立方体的中心位于原点,我们就能使用立方体的实际位置向量来对立方体贴图进行采样了。
  • 我们可以将所有顶点的纹理坐标当做是立方体的顶点位置。最终得到的结果就是可以访问立方体贴图上正确面(Face)纹理的一个纹理坐标。

立方体有36个顶点位置,在顶点着色器后顶点属性进行线性插值,每个片段都有自己的顶点位置,采样天空盒时用这个顶点位置当做纹理坐标即可,立方体的一个面的正反面的天空盒图案都一样,实际是作为摄像机的我们处在一个采样6个贴图的盒子内部,以此实现游戏的远景。

OpenGL纹理目标

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

例子0:天空盒效果

  • 加载天空盒

    // 加载纹理// -------------
    unsigned int cubeTexture = loadTexture(FileSystem::getPath("assest/textures/container.jpg").c_str());
    // 加载天空盒
    vector<std::string> faces{
        FileSystem::getPath("assest/textures/skybox/right.jpg"),
        FileSystem::getPath("assest/textures/skybox/left.jpg"),
        FileSystem::getPath("assest/textures/skybox/top.jpg"),
        FileSystem::getPath("assest/textures/skybox/bottom.jpg"),
        FileSystem::getPath("assest/textures/skybox/front.jpg"),
        FileSystem::getPath("assest/textures/skybox/back.jpg")
    };
    unsigned int cubemapTexture = loadCubemap(faces);
    // 加载天空盒
    // 加载顺序
    // order:
    // +X (right)
    // -X (left)
    // +Y (top)
    // -Y (bottom)
    // +Z (front) 
    // -Z (back)
    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;
    }
    
  • 为天空盒创建立方体的六个面的顶点数据以及VAO VBO

    // skybox VAO
    unsigned int skyboxVAO, skyboxVBO;
    glGenVertexArrays(1, &skyboxVAO);
    glGenBuffers(1, &skyboxVBO);
    glBindVertexArray(skyboxVAO);
    glBindBuffer(GL_ARRAY_BUFFER, skyboxVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(skyboxVertices), &skyboxVertices, GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glBindVertexArray(0);
    
  • 渲染

    glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glEnable(GL_DEPTH_TEST);
    
    glm::mat4 model = glm::mat4(1.0f);
    glm::mat4 view = camera.GetViewMatrix();
    glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
    
    // 渲染立方体
    shader.use();
    view = camera.GetViewMatrix();
    shader.setMat4("model", model);// 不变,在中心
    shader.setMat4("view", view);
    shader.setMat4("projection", projection);
    glBindVertexArray(cubeVAO);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, cubeTexture);
    glDrawArrays(GL_TRIANGLES, 0, 36);
    glBindVertexArray(0);
    
    // 渲染天空盒
    // 重点代码:小于等于。由于深度缓冲区的默认值为1,而到顶点着色器里设置了天空盒的深度值为1,所以要为小于等于,1=1,测试才通过才到片段着色器采样颜色
    glDepthFunc(GL_LEQUAL);
    skyboxShader.use();
    //view = camera.GetViewMatrix();
    // 重点代码:取4x4矩阵左上角的3x3矩阵来移除变换矩阵的位移部分,再变回4x4矩阵。///
    // 防止摄像机移动,天空盒会受到视图矩阵的影响而改变位置,即摄像机向z后退,天空盒和cube向z前进
    view = glm::mat4(glm::mat3(camera.GetViewMatrix()));
    skyboxShader.setMat4("view", view);
    skyboxShader.setMat4("projection", projection);
    glBindVertexArray(skyboxVAO);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture);// 第一个参数从GL_TEXTURE_2D 变为GL_TEXTURE_CUBE_MAP
    glDrawArrays(GL_TRIANGLES, 0, 36);
    glBindVertexArray(0);
    glDepthFunc(GL_LESS);
    
  • glsl和采样

    #version 330 core
    layout (location = 0) in vec3 aPos;
    
    // 纹理坐标是3维的
    out vec3 TexCoords;
    // 不用model转换到世界矩阵
    uniform mat4 projection;
    uniform mat4 view;
    void main()
    {
        // 纹理坐标等于位置坐标/
        TexCoords = aPos;
        vec4 pos = projection * view * vec4(aPos, 1.0);
        // z为w,透视除法除后z=(z=w/w)=1,深度为最远///
        gl_Position = pos.xyww;
    }
    
    #version 330 core
    out vec4 FragColor;
    
    // 纹理坐标是3维的
    in vec3 TexCoords;// 纹理坐标
    
    // 天空盒纹理采样
    uniform samplerCube skybox;
    
    void main(){ 
        FragColor = texture(skybox, TexCoords);
    }
    
  • 关键地方

    • 天空盒不会跟随摄像机移动

      // 重点代码:取4x4矩阵左上角的3x3矩阵来移除变换矩阵的位移部分,再变回4x4矩阵。
      // 防止摄像机移动,天空盒会受到视图矩阵的影响而改变位置,即摄像机向z后退,天空盒和cube向z前进
      view = glm::mat4(glm::mat3(camera.GetViewMatrix()));
      
    • 天空盒后渲染,也不会覆盖先前绘制的物体

      • 先绘制其它物体

      • 设置深度测试为小于等于

      • 绘制天空盒

        在天空盒的顶点着色器运行后,会执行透视除法,将gl_Position的xyz坐标除以w分量,将gl_Position的xyz坐标除以w分量(透视除法所做)。

        所以我们设置天空盒的z为w,z=(z=w/w)=1

        gl_Position = pos.xyww;// z为w,透视除法除后z=(z=w/w)=1,深度为最远
        
      • 由于深度测试为小于等于(结合下面图示)

        • 在其他物体占据片段的深度缓冲值<=1

          天空盒的深度值1小于等于这些片段的缓冲值,所以不会通过深度测试,从而保持原有的物体片段颜色。

        • 其他物体占据片段深度缓冲的默认值为1

          天空盒的深度值1小于等于深度缓冲的值1,所以会通过深度测试,从而输出天空盒片段。

      • 错误做法,将深度测试为默认的小于

        • 其他物体占据片段深度缓冲的默认值为1

          天空盒的深度值1不小于深度缓冲区的默认值1不会通过深度测试,从而具有天空盒的颜色的片段不会输出到屏幕上。

  • 效果

环境映射

  • 什么是环境映射

    通过使用环境的立方体贴图,我们可以给物体反射折射的属性。

    这样使用环境立方体贴图的技术叫做环境映射(Environment Mapping),其中最流行的两个是反射(Reflection)和折射(Refraction)。

反射

  • 原理图

例子1:Cube反射

  • 代码

    立方体的shader,天空盒的shader不变(还是和上面例子:天空盒效果的一样)

    #version 330 core
    layout (location = 0) in vec3 aPos;
    layout (location = 1) in vec3 aNormal;
    
    out vec3 Normal;
    out vec3 Position;
    
    uniform mat4 projection;
    uniform mat4 model;
    uniform mat4 view;
    void main()
    {
       // 法线矩阵
       Normal = mat3(transpose(inverse(model))) * aNormal;
       // 到世界空间
       Position = vec3(model * vec4(aPos, 1.0));
       // 这里不再是gl_Position = pos.xyww;因为这是中间立方体的,不是天空盒的shader
       gl_Position = projection * view * vec4(aPos, 1.0); 
    }
    
    #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));
      // 采样天空盒的uv坐标是3维的
      FragColor = vec4(texture(skybox, R).rgb, 1.0);// FragColor = texture(skybox, R); 这个效果一样
    }
    

    cpp

    Shader shader("assest/shader/4高级OpenGL/6.2.1.cube-反射天空盒.vs", "assest/shader/4高级OpenGL/6.2.1.cube-反射天空盒.fs");
    Shader skyboxShader("assest/shader/4高级OpenGL/6.1.1.天空盒-普通效果.vs", "assest/shader/4高级OpenGL/6.1.1.天空盒-普通效果.fs");
    .....
    // shader configuration
    // --------------------
    shader.use();
    shader.setInt("skybox", 0);
    
    skyboxShader.use();
    skyboxShader.setInt("skybox", 0);
    
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture);// 第一个参数从GL_TEXTURE_2D 变为GL_TEXTURE_CUBE_MAP
    
    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
      // 渲染立方体
      shader.use();
      view = camera.GetViewMatrix();
      shader.setMat4("model", model);// 不变,在中心
      shader.setMat4("view", view);
      shader.setMat4("projection", projection);
      // 为了反射传入
      shader.setVec3("cameraPos", camera.Position);
      glBindVertexArray(cubeVAO);
      glDrawArrays(GL_TRIANGLES, 0, 36);
      glBindVertexArray(0);
      // 其它和天空盒的代码一样
    
  • 效果

    请添加图片描述

    箱子上的贴图是后面的天空盒贴图

例子2:模型反射

  • 代码

    立方体的shader,天空盒的shader不变(还是和上面例子:天空盒效果的一样)

    #version 330 core
    layout (location = 0) in vec3 aPos;
    layout (location = 1) in vec3 aNormal;
    layout (location = 2) in vec2 aTexCoords;
    
    out vec3 Normal;
    out vec3 Position; // 片段的坐标-世界空间
    out vec2 TexCoords;// 纹理坐标
    
    uniform mat4 model;
    uniform mat4 view;
    uniform mat4 projection;
    
    void main()
    {
        gl_Position = projection * view * model * vec4(aPos, 1.0);
        TexCoords = aTexCoords;
        // 到世界空间
        Position = vec3(model * vec4(aPos, 1.0));
        // 这里不再是gl_Position = pos.xyww;因为这是中间立方体的,不是天空盒的shader
        Normal = mat3(transpose(inverse(model))) * aNormal;
    }
    
    #version 330 core
    out vec4 FragColor;
    
    in vec3 Normal;
    in vec3 Position; // 片段的坐标-世界空间
    in vec2 TexCoords;// 纹理坐标
    
    uniform vec3 cameraPos;
    uniform sampler2D texture_diffuse1;
    uniform sampler2D texture_specular1;
    uniform sampler2D texture_height1;
    
    // 天空盒纹理采样
    uniform samplerCube skybox;
    
    void main(){ 
        vec3 I = normalize(Position - cameraPos);
        vec3 R = reflect(I, normalize(Normal));
        // 采样镜面光贴图颜色(uv坐标是2维的)
        vec4 specular4 = texture(texture_specular1, TexCoords); // 采样出来的颜色是4维的
        vec3 specular3 = specular4.rgb;
        // 采样天空盒颜色(uv坐标是3维的)并乘以镜面光贴图颜色
        FragColor = vec4(texture(skybox, R).rgb * specular3, 1.0) ;
        // FragColor = vec4(texture(skybox, R).rgb, 1.0) ;// 未乘以镜面光贴图颜色
    }
    

    cpp

    // 加载模型
    Model ourModel(FileSystem::getPath("assest/model/nanosuit/nanosuit.obj"));
    
    // shader configuration
    // --------------------
    shader.use();
    shader.setInt("skybox", 4);
    
    skyboxShader.use();
    skyboxShader.setInt("skybox", 4);
    // 设置的天空盒的纹理单元位置,好像不会与普通的纹理冲突,但保险起见还是设为4
    glActiveTexture(GL_TEXTURE4);
    glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture);// 第一个参数从GL_TEXTURE_2D 变为GL_TEXTURE_CUBE_MAP
    
    while (!glfwWindowShouldClose(window))
    {
        // 渲染这个模型
        // 为了反射传入
        model = glm::translate(model, glm::vec3(0.0f, 0.0f, 0.0f));
        model = glm::scale(model, glm::vec3(0.1f, 0.1f, 0.1f));
        shader.use();
        shader.setVec3("cameraPos", camera.Position);
        shader.setMat4("model", model);
        shader.setMat4("view", view);
        shader.setMat4("projection", projection);
        ourModel.Draw(shader);
    }
    
  • 效果

    采样天空盒颜色,未乘以镜面光贴图颜色

    采样天空盒颜色,并乘以镜面光贴图颜色

    请添加图片描述

折射

  • 原理

  • 折射率表

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

    例子中,光线/视线从空气(折射率1)进入玻璃(如果我们假设箱子是玻璃制的),所以比值为1.00/1.52=0.658

例子1:Cube折射

  • 代码

    和反射的代码差不多,就是中间立方体的glsl片段着色器代码不一样

    void main(){ 
        float ratio = 1.00 / 1.52;
        vec3 I = normalize(Position - cameraPos);
        vec3 R = refract(I, normalize(Normal), ratio);// refract,第三个参数是折射率
    
        // 采样天空盒颜色(uv坐标是3维的)
        FragColor = vec4(texture(skybox, R).rgb, 1.0);// FragColor = texture(skybox, R); 这个效果一样
    }
    
  • 效果

    请添加图片描述

例子2:模型折射

  • 代码

    void main(){ 
        float ratio = 1.00 / 1.52;
        vec3 I = normalize(Position - cameraPos);
        vec3 R = refract(I, normalize(Normal), ratio);// refract,第三个参数是折射率
    
        // 采样天空盒颜色(uv坐标是3维的)
        FragColor = vec4(texture(skybox, R).rgb, 1.0);// FragColor = texture(skybox, R); 这个效果一样
    }
    
  • 效果

    请添加图片描述

测试-先渲染天空盒再渲染物体,默认深度LESS比较方式

  • 代码

    // 将天空盒的盒子长宽为20
    float skyboxVertices[] = {
        // positions          
        -10.0f,  10.0f, -10.0f,
        -10.0f, -10.0f, -10.0f,
        .....
    };
    // 渲染天空盒
    //glDepthFunc(GL_LEQUAL); // 不用LEQUAL而是默认的LESS
    skyboxShader.use();
    // 重点代码:取4x4矩阵左上角的3x3矩阵来移除变换矩阵的位移部分,再变回4x4矩阵。
    // 防止摄像机移动,天空盒会受到视图矩阵的影响而改变位置,即摄像机向z后退,天空盒和cube向z前进
    view = glm::mat4(glm::mat3(camera.GetViewMatrix()));
    skyboxShader.setMat4("view", view);
    skyboxShader.setMat4("projection", projection);
    glBindVertexArray(skyboxVAO);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture);// 第一个参数从GL_TEXTURE_2D 变为GL_TEXTURE_CUBE_MAP
    glDrawArrays(GL_TRIANGLES, 0, 36);
    glBindVertexArray(0);
    //glDepthFunc(GL_LESS);
    
    // 渲染立方体
    shader.use();
    view = camera.GetViewMatrix();
    shader.setMat4("model", model);// 不变,在中心
    shader.setMat4("view", view);
    shader.setMat4("projection", projection);
    glBindVertexArray(cubeVAO);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, cubeTexture);
    glDrawArrays(GL_TRIANGLES, 0, 36);
    glBindVertexArray(0);
    

    天空盒的顶点位置z透视除法后不为1

    //gl_Position = pos.xyww;// z=w,透视除法除后还是1,深度为最远
    gl_Position= projection * view * vec4(aPos, 1.0);
    
  • 解释代码顺序

    • 天空盒的盒子长宽为20

    • 先绘制天空盒,再绘制箱子

    • 这代码天空盒将不会受摄像机的观察矩阵的位移部分影响

      所以虽然glsl天空盒的深度值z未设置w,但是视觉上依旧是无限远

      不过实际上现在代码造成的影响是,不论摄像机所在什么位置,以摄像机为原点,处在一个20*20大小的立方体盒子,在20*20范围内的物体被显示,20*20外的物体被天空盒颜色所覆盖。

      换句话说:注意摄像机在原点,所以20*20的盒子半径为10,于是原点为出发点距离摄像机长度小于10的物体会显示,大于10的物体会被天空盒颜色所覆盖。

  • 进一步解释(结合下方图)

    • 箱子离摄像机的距离 <10(第一幅图)

      箱子的深度值小于天空盒,所以天空盒同箱子所占的片段区域会被丢弃,显示箱子的片段颜色

    • 箱子离摄像机的距离 > 10(第二幅图)

      箱子的深度值大于天空盒,所以天空盒同箱子所占的片段区域会覆盖箱子,显示天空盒的片段颜色

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘建杰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值