OpenGL学习(十)天空盒

写在前面

上一篇博客回顾: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
  • 15
    点赞
  • 78
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
以下是一个简单的OpenGL天空完整代码,其中包括了加载纹理和渲染天空的过程: ```c++ #include <glad/glad.h> #include <GLFW/glfw3.h> #include <iostream> #include <vector> #include <stb_image.h> #include "shader.h" // 顶点数据 float skyboxVertices[] = { // 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 }; // 加载纹理 unsigned int loadCubemap(std::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; } int main() { // 初始化GLFW glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 创建窗口 GLFWwindow* window = glfwCreateWindow(800, 600, "OpenGL", NULL, NULL); if (window == NULL) { std::cout << "Failed to create GLFW window" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); // 初始化GLAD if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Failed to initialize GLAD" << std::endl; return -1; } // 创建着色器程序 Shader skyboxShader("skybox.vs", "skybox.fs"); // 加载天空纹理 std::vector<std::string> faces { "right.jpg", "left.jpg", "top.jpg", "bottom.jpg", "front.jpg", "back.jpg" }; unsigned int cubemapTexture = loadCubemap(faces); // 配置全局OpenGL状态 glEnable(GL_DEPTH_TEST); // 渲染循环 while (!glfwWindowShouldClose(window)) { // 处理输入 if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, true); // 渲染天空 glDepthFunc(GL_LEQUAL); // 更改深度函数,以便深度测试在值等于深度缓冲区最大值时通过 skyboxShader.use(); glm::mat4 view = glm::mat4(glm::mat3(glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, -1.0f), glm::vec3(0.0f, 1.0f, 0.0f))))); glm::mat4 projection = glm::perspective(glm::radians(45.0f), (float)800 / (float)600, 0.1f, 100.0f); skyboxShader.setMat4("view", view); skyboxShader.setMat4("projection", projection); glBindVertexArray(skyboxVAO); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture); glDrawArrays(GL_TRIANGLES, 0, 36); glBindVertexArray(0); glDepthFunc(GL_LESS); // 设置回深度函数 // 交换缓冲区和轮询IO事件 glfwSwapBuffers(window); glfwPollEvents(); } // 清理 glDeleteVertexArrays(1, &skyboxVAO); glDeleteBuffers(1, &skyboxVBO); // 关闭GLFW glfwTerminate(); return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值