一、简介
本文介绍了如何使用OpenGL实现天空包围盒(skybox)效果。场景中包含一个模型,模型使用Blinn-Phong光照模型。本文最后给出了全部的代码和模型文件。
按照本文代码实现完成后,理论上可以得到如下结果:
二、使用OpenGL实现天空盒
0. 环境需要
- Linux,或者 windos下使用wsl2。
- 安装GLFW和GLAD。请参考[OpenGL] wsl2上安装使用cmake+OpenGL教程。
- 安装glm。glm是个可以只使用头文件的库,因此可以直接下载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.vert
和Blinn-Phong.frag
是使用Blinn-Phong光照模型渲染场景
的 顶点着色器 和 片段着色器 代码。skyBox.vert
和skyBox.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). 代码整体流程
- 初始化glfw,glad,窗口
- 编译 shader 程序
- 加载obj模型、纹理图片、skybox
- 设置光源和相机位置,Blinn-Phong 模型参数
- 开始渲染
5.1 使用 blinnPhongShader 渲染场景
5.2 使用 skyBoxShader 渲染 skybox - 释放资源
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)中下载。