[OpenGL]使用OpenGL实现天空盒(skybox)

一、简介

本文介绍了如何使用OpenGL实现天空包围盒(skybox)效果。场景中包含一个模型,模型使用Blinn-Phong光照模型。本文最后给出了全部的代码和模型文件。
按照本文代码实现完成后,理论上可以得到如下结果:

渲染结果

二、使用OpenGL实现天空盒

0. 环境需要

  • Linux,或者 windos下使用wsl2。
  • 安装GLFW和GLAD。请参考[OpenGL] wsl2上安装使用cmake+OpenGL教程
  • 安装glmglm是个可以只使用头文件的库,因此可以直接下载release的压缩文件,然后解压到include目录下。例如,假设下载的release版本的压缩文件为glm-1.0.1-light.zip。将glm-1.0.1-light.zip复制include目录下,然后执行以下命令即可解压glm源代码:
    unzip glm-1.0.1-light.zip
    
  • 需要使用Assimp库加载obj模型,在 ubuntu 下可以使用以下命令安装 Assimp
    sudo apt-get update
    sudo apt-get install libassimp-dev
    
  • 需要下载 stb_image.h 作为加载.png图像的库。将 stb_image.h 下载后放入include/目录下。

1. 项目目录

项目目录

其中:

  • Mesh.hpp 包含了自定义的 Vertex, Texture, 和 Mesh 类。
  • Model.hpp 包含了自定义的Model类,用于加载obj模型。一个Model可以包含多个Mesh。在加载obj模型时使用Assimp库加载。
  • Shader.hpp 用于创建 shader 程序。
  • Skybox,hpp用于创建天空盒。
  • Blinn-Phong.vertBlinn-Phong.frag是使用Blinn-Phong光照模型 渲染场景 的 顶点着色器 和 片段着色器 代码。
  • skyBox.vertskyBox.frag是用于 渲染天空盒的 顶点着色器 和 片段着色器 代码。在顶点着色器中将skybox的各个点的w赋值给z分量,保证skybox模型各点的深度值为z/w=1.0,即skybox在场景的最后。

下面介绍各部分主要的代码:

2. CMakeLists.txt代码

cmake_minimum_required(VERSION 3.10)
set(CMAKE_CXX_STANDARD 14)

project(OpenGL_Cube_Map)

include_directories(include)

find_package(glfw3 REQUIRED)
find_package(assimp REQUIRED)
file(GLOB project_file main.cpp glad.c)
add_executable(${PROJECT_NAME} ${project_file})
target_link_libraries(${PROJECT_NAME} glfw assimp)

3. Model.hpp和Mesh.hpp 代码

Model.hpp 和 Mesh.hpp 代码与 LearnOpenGL-模型加载-模型 中的代码相同,使用Assimp库,基于递归的方式加载模型和纹理。读者可以参考LearnOpenGL-模型加载-模型

4. Skybox.hpp 代码

Skybox类主要有两个函数,函数loadCube()用于根据输入的纹理路径,创建 CUBE_MAP 类型的 纹理。函数 setupMesh() 函数,用于创建一个立方体模型 VAO和VBO,在显示天空盒时,将CUBE_MAP纹理贴到该立方体模型上。代码如下:

#pragma once
#include "stb_image.h"

#include <vector>
#include <string>

#include "glad/glad.h"
#include "Shader.hpp"
using namespace std;
class Skybox
{
  public:
    Skybox(vector<string> face_paths)
    {
        // 加载 cube texture的+X,-X,+Y,-Y,+Z,-Z方向的6个面
        loadCubemap(face_paths);
        // 加载 skybox 的顶点、顶点纹理坐标信息,设置 VAO, VBO
        setupMesh();
    };

    void Draw(Shader &shader)
    {
        glBindVertexArray(skyboxVAO);
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture);
        glDrawArrays(GL_TRIANGLES, 0, 36);
        glBindVertexArray(0);
    }

  private:
    // render data
    unsigned int skyboxVAO, skyboxVBO;
    unsigned int cubemapTexture;
    float skyboxVertices[108] = {
        // 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};

    void loadCubemap(vector<string> faces)
    {
        // 生成一个 cube map 纹理
        glGenTextures(1, &cubemapTexture);
        // 绑定该纹理
        glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture);

        int width, height, nrChannels;
        // 加载纹理图片
        for (unsigned int i = 0; i < faces.size(); i++)
        {
            // right(+X), left(-X), top(+Y), bottom(-Y), front(+Z), back(-Z)
            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);
    }

    // initializes all the buffer objects/arrays
    void setupMesh()
    {
        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);
    }
};

5. Blinn-Phong shader 代码

渲染场景的 Blinn-Phong shader 使用Blinn-Phong模型渲染场景,并且根据输入的 shadowMap 处理产生阴影效果。
Blinn-Phong shader的顶点着色器和片段着色器代码:
Blinn-Phong.vert:

#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNor;
layout(location = 2) in vec2 aTexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
uniform mat4 lightMVP;

out vec3 vertexPos;
out vec3 vertexNor;
out vec2 textureCoord;

void main() {
  textureCoord = aTexCoord;
  // 裁剪空间坐标系 (clip space) 中 点的位置
  gl_Position = projection * view * model * vec4(aPos, 1.0f);
  // 世界坐标系 (world space) 中 点的位置
  vertexPos = (model * vec4(aPos, 1.0f)).xyz;
  // 世界坐标系 (world space) 中 点的法向
  vertexNor = mat3(transpose(inverse(model))) * aNor;
}

Blinn-Phong.frag:

#version 330 core
out vec4 FragColor;

in vec3 vertexPos;
in vec3 vertexNor;
in vec2 textureCoord;


uniform vec3 cameraPos;
uniform vec3 lightPos;
uniform vec3 k;

uniform sampler2D texture0;

void main() {

  vec3 lightColor = vec3(1.0f, 1.0f, 1.0f);

  // Ambient
  // Ia = ka * La
  float ambientStrenth = k[0];
  vec3 ambient = ambientStrenth * lightColor;

  // Diffuse
  // Id = kd * max(0, normal dot light) * Ld
  float diffuseStrenth = k[1];
  vec3 normalDir = normalize(vertexNor);
  vec3 lightDir = normalize(lightPos - vertexPos);
  vec3 diffuse =
      diffuseStrenth * max(dot(normalDir, lightDir), 0.0) * lightColor;

  // Specular (Phong)
  // Is = ks * (view dot reflect)^s * Ls

  // float specularStrenth = k[2];
  // vec3 viewDir = normalize(cameraPos - vertexPos);
  // vec3 reflectDir = reflect(-lightDir, normalDir);
  // vec3 specular = specularStrenth *
  //                 pow(max(dot(viewDir, reflectDir), 0.0f), 2) * lightColor;

  // Specular (Blinn-Phong)
  // Is = ks * (normal dot halfway)^s Ls
  float specularStrenth = k[2];
  vec3 viewDir = normalize(cameraPos - vertexPos);
  vec3 halfwayDir = normalize(lightDir + viewDir);
  vec3 specular = specularStrenth *
                  pow(max(dot(normalDir, halfwayDir), 0.0f), 2) * lightColor;

  // Obejct color
  vec3 objectColor = vec3(0.8, 0.8, 0.8);
  if (textureCoord.x >= 0 && textureCoord.y >= 0) {
    objectColor = texture(texture0, textureCoord).xyz;
  }
  FragColor = vec4((ambient + diffuse + specular) * objectColor, 1.0f);

6. skyBox shader 代码

渲染天空盒的 skyBox shader可以简单视作一个渲染立方体的 shader。只不过该立方体各个面使用 天空盒央视的纹理,即 fragment 中的 samplerCube 类型的纹理。 samplerCude 纹理和 sampler2D 纹理没有什么主要区别,可以将 samplerCude 纹理视作一个立方体样式的,包含6个面的纹理贴图。
在 skyBox 顶点着色器中,为了保证 天空盒 总是是场景最后方,因此需要令各个顶点坐标的z分量等于w分量,这样可以保证各个顶点的深度值为 z/w=1.0,即在 view space的最后方。顶点着色器的代码如下:

skyBox.vert

#version 330 core
layout(location = 0) in vec3 aPos;
out vec3 TexCoords;

uniform mat4 view;
uniform mat4 projection;

void main() {
  TexCoords = aPos;
  vec4 pos = projection * view * vec4(aPos, 1.0);
  gl_Position = pos.xyww; // 令 gl_Position.z=gl_Position.w,
                          // 让 skybox 的深度值永远等于 z/w=1.0,
                          // 保证 skybox 永远在场景的后面
}

在片段着色器中使用texture()函数采样纹理不同坐标对应的颜色值。
skybox.frag

#version 330 core
out vec4 FragColor;

in vec3 TexCoords;

uniform samplerCube skybox;

void main() {
  // 采样 cube texture
  FragColor = texture(skybox, TexCoords);
}

7. main.cpp 代码

7.1). 代码整体流程

  1. 初始化glfw,glad,窗口
  2. 编译 shader 程序
  3. 加载obj模型、纹理图片、skybox
  4. 设置光源和相机位置,Blinn-Phong 模型参数
  5. 开始渲染
    5.1 使用 blinnPhongShader 渲染场景
    5.2 使用 skyBoxShader 渲染 skybox
  6. 释放资源

7.2). main.cpp代码

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include "Skybox.hpp"
#include "Shader.hpp"
#include "Mesh.hpp"
#include "Model.hpp"

#include "glm/ext.hpp"
#include "glm/mat4x4.hpp"

#include <random>
#include <iostream>
// 用于处理窗口大小改变的回调函数
void framebuffer_size_callback(GLFWwindow *window, int width, int height);
// 用于处理用户输入的函数
void processInput(GLFWwindow *window);

// 指定窗口默认width和height像素大小
unsigned int SCR_WIDTH = 800;
unsigned int SCR_HEIGHT = 600;

/************************************/

int main()
{
    /****** 1.初始化glfw, glad, 窗口 *******/
    // glfw 初始化 + 配置 glfw 参数
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    // 在创建窗口之前
    glfwWindowHint(GLFW_SAMPLES, 4); // 设置多重采样级别为4
    // glfw 生成窗口
    GLFWwindow *window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        // 检查是否成功生成窗口,如果没有成功打印出错信息并且退出
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }

    // 设置窗口window的上下文
    glfwMakeContextCurrent(window);
    // 配置window变化时的回调函数
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    // 使用 glad 加载 OpenGL 中的各种函数
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }
    // 启用 深度测试
    glEnable(GL_DEPTH_TEST);
    // 启用 多重采样抗锯齿
    glEnable(GL_MULTISAMPLE);
    // glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // 使用线框模式,绘制时只绘制 三角形 的轮廓
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); // 使用填充模式,绘制时对 三角形 内部进行填充

    /************************************/

    /****** 2.编译 shader 程序 ******/

    // 渲染场景的shader
    Shader blinnPhongShader("../resources/Blinn-Phong.vert", "../resources/Blinn-Phong.frag");

    // 渲染skybox的shader
    Shader skyBoxShader("../resources/skyBox.vert", "../resources/skyBox.frag");

    /************************************/

    /****** 3.加载obj模型、纹理图片、skybox ******/

    // 3.1 scene mesh
    Model ourModel("../resources/models/spot/spot.obj");
    // Model ourModel("../resources/models/nanosuit/nanosuit.obj");

    // skybox 的6个面纹理图片
    vector<std::string> faces{"../resources/models/skybox/right.jpg", "../resources/models/skybox/left.jpg",
                              "../resources/models/skybox/top.jpg",   "../resources/models/skybox/bottom.jpg",
                              "../resources/models/skybox/front.jpg", "../resources/models/skybox/back.jpg"};
    Skybox skybox(faces);

    /************************************/

    /****** 4.设置光源和相机位置,Blinn-Phong 模型参数 ******/
    // I = Ia + Id + Is
    // Ia = ka * La
    // Id = kd * (normal dot light) * Ld
    // Is = ks * (reflect dot view)^s * Ls
    // 模型参数 ka, kd, ks
    float k[] = {0.1f, 0.7f, 0.2f}; // ka, kd, ks
    // 光源位置
    glm::vec3 light_pos = glm::vec3(-2.0f, 2.0f, 0.0f);
    // 相机位置
    glm::vec3 camera_pos = glm::vec3(0.0f, 0.0f, 1.5f);
    /************************************/

    /****** 5.开始渲染 ******/

    float rotate = 45.0f;
    while (!glfwWindowShouldClose(window))
    {
        rotate += 0.05f;
        // input
        // -----
        processInput(window);

        // render
        // ------
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        // 清除颜色缓冲区 并且 清除深度缓冲区
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        // 5.1 使用 blinnPhongShader 渲染场景
        blinnPhongShader.use();

        // 设置 camera_MVP 矩阵, 假设以 camera 为视角,渲染 camera 视角下的场景深度图
        // camera model 矩阵
        glm::mat4 camera_model = glm::mat4(1.0f);
        camera_model = glm::translate(camera_model, glm::vec3(0.0f, 0.0f, 0.0f));
        camera_model = glm::rotate(camera_model, glm::radians(0.0f), glm::vec3(1.0f, 0.0f, 0.0f));
        camera_model = glm::rotate(camera_model, glm::radians(rotate), glm::vec3(0.0f, 1.0f, 0.0f));
        camera_model = glm::rotate(camera_model, glm::radians(0.0f), glm::vec3(0.0f, 0.0f, 1.0f));

        // 计算 光源的位置,光源只有平移和缩放两种变换
        glm::vec4 temp_light_pos = camera_model * glm::vec4(light_pos, 1.0f);
        glm::vec3 cur_light_pos = glm::vec3(temp_light_pos.x / temp_light_pos.w, temp_light_pos.y / temp_light_pos.w,
                                            temp_light_pos.z / temp_light_pos.w);

        camera_model = glm::scale(camera_model, glm::vec3(0.5f, 0.5f, 0.5f));

        // camera view 矩阵
        glm::mat4 camera_view = glm::mat4(1.0f);
        camera_view = glm::lookAt(camera_pos, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));

        // camera projection 矩阵
        glm::mat4 camera_projection = glm::mat4(1.0f);
        camera_projection = glm::perspective(glm::radians(60.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
        blinnPhongShader.setMat4("model", camera_model);
        blinnPhongShader.setMat4("view", camera_view);
        blinnPhongShader.setMat4("projection", camera_projection);
        blinnPhongShader.setVec3("k", k[0], k[1], k[2]);
        blinnPhongShader.setVec3("cameraPos", camera_pos);
        blinnPhongShader.setVec3("lightPos", cur_light_pos);

        ourModel.Draw(blinnPhongShader);

        // 5.2 使用 skyBoxShader 渲染 skybox
        // 设置深度值小于等于深度缓冲中的值时 深度测试通过
        // 因为 深度缓冲中默认设置 各像素的深度值为 1.0, 同时,我们在 skyBox.vert 中将 skybox 的z分量设为w,
        // 那么,skybox 各点的深度值即为 z/w=1.0,为了绘制 skybox ,我们将 深度测试对比方式设为 GL_LEQUAL,
        // 即保证 skybox 在场景的最后面(深度值为1.0),也保证可以顺利的渲染出 skybox
        glDepthFunc(GL_LEQUAL);
        skyBoxShader.use();

        // 设置 skybox_MVP 矩阵
        // skybox view*modle 矩阵
        // 令skybox 与场景有相同的旋转、缩放变换,同时 去除 camera_model 矩阵中的 平移项
        glm::mat4 skybox_view_model = glm::mat4(glm::mat3(camera_view * camera_model));
        // skybox projection 矩阵
        glm::mat4 skybox_projection = camera_projection;

        skyBoxShader.setMat4("view", skybox_view_model);
        skyBoxShader.setMat4("projection", skybox_projection);
        skybox.Draw(skyBoxShader);

        // 将深度测试对比模式改回 GL_LESS 用于绘制场景
        glDepthFunc(GL_LESS);

        glfwSwapBuffers(window); // 在gfw中启用双缓冲,确保绘制的平滑和无缝切换
        glfwPollEvents(); // 用于处理所有挂起的事件,例如键盘输入、鼠标移动、窗口大小变化等事件
    }

    /************************************/
    /****** 6.释放资源 ******/
    // glfw 释放 glfw使用的所有资源
    glfwTerminate();
    /************************************/
    return 0;
}

// 用于处理用户输入的函数
void processInput(GLFWwindow *window)
{
    // 当按下 Esc 按键时调用 glfwSetWindowShouldClose() 函数,关闭窗口
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
    {
        glfwSetWindowShouldClose(window, true);
    }
}

// 在使用 OpenGL 和 GLFW 库时,处理窗口大小改变的回调函数
// 当窗口大小发生变化时,确保 OpenGL 渲染的内容能够适应新的窗口大小,避免图像被拉伸、压缩或出现其他比例失真的问题
void framebuffer_size_callback(GLFWwindow *window, int width, int height)
{
    SCR_WIDTH = width;
    SCR_HEIGHT = height;
    glViewport(0, 0, width, height);
}

8. 编译运行及结果

编译运行:

cd ./build
cmake ..
make
./OpenGL_Cube_Map

渲染结果:
渲染结果

三、全部代码及模型文件

全部代码以及模型文件可以在使用OpenGL实现天空盒(skybox)中下载。

四、参考

[1].LearnOpenGL-高级OpenGL-立方体贴图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值