OpenGL学习(十)天空盒

这篇博客介绍了OpenGL中的天空盒技术,包括立方体贴图的创建、渲染立方体、立方体贴图着色器的编写,以及如何在场景中正确显示天空盒。通过将6张2D贴图组合成立方体贴图,再利用特殊的着色器,可以实现逼真的3D背景效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

写在前面

上一篇博客回顾:OpenGL学习(九)阴影映射(shadowMapping)

在昨天我们实现了非常简单的阴影映射特效,今天来更新立方体贴图的内容。这部分的内容相当简单,前提是要对 OpenGL 及其绘制过程有一个基本的理解。

相信我,仅 10 分钟足够你完成一个立方体贴图,并且用于天空盒的渲染。

天空盒简介

注意到我们之前的代码都是利用:

glClearColor(1.0, 1.0, 1.0, 1.0);   // 背景颜色

来填充一个纯色来作为背景图像。

事实上在现代计算机游戏中,天空盒是一个常见的填充背景的手段,并且往往能够起到好的效果。

通过立方体贴图我们得以实现天空盒的绘制。立方体贴图顾名思义就是将一个立方体的 6 个面贴上对应的纹理,然后用这个立方体将相机包裹住:

这样相机视线的背景就永远是立方体上的花样纹理,而不是纯白或者纯黑的 glClearColor 。

回想起小时候玩的大富翁的纸质骰子,我们用 6 张图片就可以将立方体变成一个被风景包围的立方体:

在这里插入图片描述

立方体贴图和普通 2D 贴图一样,只是在查询的时候,我们以三维坐标去查询,而不是普通纹理的二维坐标。我们通过 glsl 中的 samplerCube 类型的采样器,就可以访问到立方体贴图:

uniform samplerCube skybox;

...

color = textureCube(skybox, texcoord);

其中纹理坐标我们直接 利用立方体的坐标 即可完成查询。因为 glsl 的采样器会自动根据我们提供的方向向量,来返回视线触碰到的立方体贴图的颜色。

创建立方体贴图

创建一个立方体贴图也十分简单。我们直接循环进行 6 张 2D 贴图的创建即可,值得注意的是使用

GL_TEXTURE_CUBE_MAP_POSITIVE_X + 偏移量

来指定当前生成的是第几张贴图,在最后我们返回当前创建的纹理对象的索引。下面给出创建立方体贴图的函数:

GLuint loadCubemap(std::vector<const GLchar*> faces)
{
   
    GLuint textureID;
    glGenTextures(1, &textureID);
    glActiveTexture(GL_TEXTURE0);

    int width, height;
    unsigned char* image;

    glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
    for (GLuint i = 0; i < faces.size(); i++)
    {
   
        image = SOIL_load_image(faces[i], &width, &height, 0, SOIL_LOAD_RGB);
        glTexImage2D(
            GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0,
            GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image
        );
    }
    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);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
    glBindTexture(GL_TEXTURE_CUBE_MAP, 0);

    return textureID;
}

然后我们可以通过:

std::vector<const GLchar*> faces;
faces.push_back("skybox/right.jpg");
faces.push_back("skybox/left.jpg");
faces.push_back("skybox/top.jpg");
faces.push_back("skybox/bottom.jpg");
faces.push_back("skybox/back.jpg");
faces.push_back("skybox/front.jpg");
skyboxTexture = loadCubemap(faces);

来指定 6 张贴图的路径,并且调用 loadCubemap 进行创建。

渲染一个立方体

在获取了立方体贴图的纹理对象之后,我们还需要向场景中添加一个立方体,同时将纹理贴上去。立方体的添加也十分简单,我们指定 8 个顶点,然后指定 36 个三角面片索引即可。

注意因为我们直接使用立方体的坐标作为纹理采样的坐标(毕竟逻辑上也是直接获取对应点上的像素值),我们无需传递纹理坐标和法线等顶点属性

Model skybox;   // 渲染一个立方体用于立方体贴图绘制天空盒

...

// 生成一个立方体做天空盒的 “画布”
Mesh cube;
cube.vertexPosition = {
    // 立方体的 8 个顶点
    glm::vec3(-1, -1, -1),glm::vec3(1, -1, -1),glm::vec3(-1, 1, -1),glm::vec3(1, 1, -1),
    glm::vec3(-1, -1, 1),glm::vec3(1, -1, 1),glm::vec3(-1, 1, 1),glm::vec3(1, 1, 1)
};
cube.index = {
   0,3,1,0,2,3,1,5,4,1,4,0,4,2,0,4,6,2,5,6,4,5,7,6,2,6,7,2,7,3,1,7,5,1,3,7};
cube.bindData();
skybox.meshes.push_back(cube);

立方体贴图着色器

因为立方体贴图直接利用立方体的坐标作为方向向量进行纹理采样,而无需纹理坐标,于是我们的着色器发生了一些变换。我们最好利用一组新的着色器来管理这些特殊情况。我们创建 skybox 系列着色器。

其中顶点着色器仍然负责完成 mvp 变换,值得注意的是,我们直接将变换后的坐标作为采样的方向向量,传递到片元着色器中:

#version 330 core

// 顶点着色器输入
layout (location = 0) in vec3 vPosition;

out vec3 texcoord;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
   
	gl_Position = projection * view * model * vec4(vPosition, 1.0);  
	texcoord = vPosition;	// 坐标作为cubeMap采样坐标
}

片元着色器则更为简单,我们利用传入的 cubmap 采样器,和从顶点着色器中获取的 “纹理坐标” 进行立方体贴图的采样,并且输出最终的结果:

#version 330 core

in vec3 texcoord;
out vec4 fColor;

uniform samplerCube skybox;

void main()
{
   
    fColor = textureCube(skybox, texcoord);
}

与此同时,在 c++ 中别忘记创建我们的着色器对象:

GLuint skyboxProgram;   // 天空盒绘制

...

skyboxProgram = getShaderProgram("shaders/skybox.fsh", "shaders/skybox.vsh");

开始绘制天空盒

我们正式开始绘制天空盒。这一部分我们放到 display 也就是每一帧的回调函数中进行。首先我们使用着色器,并且做一些清空窗口等杂活:

// 绘制天空盒
glUseProgram(skyboxProgram);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glViewport(0, 0, windowWidth, windowHeight);

注:
我是在正常渲染之前进行天空盒的绘制,所以必须提前 glClear
而后续的正常渲染则不需要调用 glClear 了
因为渲染的像素是覆盖在天空盒之上的

然后我们传送相机的变换矩阵,同时传送我们天空盒的立方体贴图纹理,最后调用 draw call:

// 传视图,投影矩阵
glUniformMatrix4fv(glGetUniformLocation(skyboxProgram, "view"), 1, GL_FALSE, glm::value_ptr(camera.getViewMatrix()));
glUniformMatrix4fv(glGetUniformLocation(skyboxProgram, "projection"), 1, GL_FALSE, glm::value_ptr(camera.getProjectionMatrix()));

// 传cubemap纹理
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_CUBE_MAP, skyboxTexture);
glUniform1i(glGetUniformLocation(skyboxProgram, "skybox"), 1);

// 立方体永远跟随相机
skybox.translate = camera.position;

glDepthMask(GL_FALSE);
skybox.draw(skyboxProgram);
glDepthMask(GL_TRUE);

值得注意的是,立方体必须时刻跟随相机
因为我们的立方体默认在 (0,0,0) 位置,但是相机发生移动之后,立方体就无法包裹住相机了
就会出现。。。唔 单独的一个立方体的情况,如下图:

在这里插入图片描述

好,如果一切顺利,那么我们会得到一个十分逼真的效果:

在这里插入图片描述
这下牛大了,这不把之前的白色背景干的碎碎的

完整代码

着色器

c++

// std c++
#include <iostream>
#include <string>
#include <fstream>
#include <vector>
#include <map>
#include <sstream>
#include <iostream>

// glew glut
#include <GL/glew.h>
#include <GL/freeglut.h>

// glm
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

// SOIL
#include <SOIL2/SOIL2.h>

// assimp
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>

// --------------------- end of include --------------------- //

class Mesh
{
   
public:
    // OpenGL 对象
    GLuint vao, vbo, ebo;
    GLuint diffuseTexture;  // 漫反射纹理

    // 顶点属性
    std::vector<glm::vec3> vertexPosition;
    std::vector<glm::vec2> vertexTexcoord;
    std::vector<glm::vec3> vertexNormal;

    // glDrawElements 函数的绘制索引
    std::vector<int> index;

    Mesh() {
   }
    void bindData()
    {
   
        // 创建顶点数组对象
        glGenVertexArrays(1, &vao); // 分配1个顶点数组对象
        glBindVertexArray(vao);  	// 绑定顶点数组对象

        // 创建并初始化顶点缓存对象 这里填NULL 先不传数据
        glGenBuffers(1, &vbo);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        glBufferData(GL_ARRAY_BUFFER,
            vertexPosition.size() * sizeof(glm::vec3) +
            vertexTexcoord.size() * sizeof(glm::vec2) +
            vertexNormal.size() * sizeof(glm::vec3),
            NULL, GL_STATIC_DRAW);

        // 传位置
        GLuint offset_position = 0;
        GLuint size_position = vertexPosition.size() *
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值