写在前面
上一篇博客回顾: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) 位置,但是相机发生移动之后,立方体就无法包裹住相机了
就会出现。。。唔 单独的一个立方体的情况,如下图:
好,如果一切顺利,那么我们会得到一个十分逼真的效果:
这下牛大了,这不把之前的白色背景干的碎碎的
完整代码
着色器
- debug 和 shadow 和正常渲染的着色器见:上一篇博客
- 天空盒着色器见:上文 立方体贴图着色器部分
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() *