立方体贴图

撰写于 2016-10-27 修改于 2018-01-29 分类 OpenGL 标签 高级OpenGL

什么是立方体贴图?

立方体贴图(CubeMap),就是6张2D纹理,组成一个立方体,但这并不是一个简单的立方体盒子,它有自己特有的属性,其中最重要的一点就是可以使用方向向量对6个面的纹理进行采样和索引,不再使用纹理坐标,以下的示意图就很简单明了(教程中图片拿来用一下):

如何创建一个立方体贴图?

绑定纹理

立方体贴图只是一种纹理类型而已,只需在生成绑定纹理时,告诉OpenGL我要绑定的是立方体贴图(GL_TEXTURE_CUBE_MAP),简单的代码如下:

1
2
3
GLuint skyBoxTextureID;
glGenTextures(1, &skyBoxTextureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, skyBoxTextureID);

纹理数据

以前的2D纹理(GL_TEXTURE_2D)只需加载一张纹理的数据,而立方体贴图有6个面,所以要加载六张纹理数据,当然必须要告诉OpenGL,当前加载的是哪个面的纹理数据,六个面对应六个枚举值,分别如下:

1
2
3
4
5
6
7
/*立方体贴图纹理目标*/
GL_TEXTURE_CUBE_MAP_POSITIVE_X //右
GL_TEXTURE_CUBE_MAP_NEGATIVE_X //左
GL_TEXTURE_CUBE_MAP_POSITIVE_Y //上
GL_TEXTURE_CUBE_MAP_NEGATIVE_Y //下
GL_TEXTURE_CUBE_MAP_POSITIVE_Z //后
GL_TEXTURE_CUBE_MAP_NEGATIVE_Z //前

此处要读取图片文件,所以我写了一个文件名和文件路径读取的类,该类会自动搜索传入的文件夹下面的带有front,back,left,right,up,bottom关键字的文件,并把该文件的文件名和文件路径存起来,方便使用 SOIL_load_image 函数加载文件,读取文件类的 头文件 源文件
由于立方体贴图纹理目标的枚举是连续的,所以可以使用 GL_TEXTURE_CUBE_MAP_POSITIVE_X + i 的方式进行加载数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*获取文件加载类的实例*/
BrowseDir* browseDir = BrowseDir::getInstance("skybox");
map<string, string> filePath = browseDir->getAllFilePath();
vector<string> fileNames = browseDir->getAllFileNames();
/*加载纹理数据*/
unsigned char* image = nullptr;
int width, height, channels;
for (size_t i = 0 ; i < fileNames.size(); i ++ )
{
cout << filePath[fileNames[i]].c_str() << endl;
image = SOIL_load_image(filePath[fileNames[i]].c_str(), &width, &height, &channels, SOIL_LOAD_RGB);
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
SOIL_free_image_data(image);
}

其他的纹理参数设置,跟以前2D纹理没有什么区别:

1
2
3
4
5
6
7
/*大小过滤器*/
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_R, GL_CLAMP_TO_EDGE);
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);

同2D纹理一样,使用立方体贴图也需要传入到片段着色器,此处使用的采样器是 samplerCube,并且不需要使用纹理坐标,而是使用顶点的方向向量,具体的片段着色器源码可在这里获取。

渲染立方体贴图

立方体贴图最重要的一个应用就是绘制天空盒子,绘制天空盒子跟绘制普通的2D的纹理没有什么区别,加载纹理,设置顶点数据,编译着色器,绘制即可。运行结果如下:



好了,到此为止一个完美的天空盒子已经显示出来了,but还有一个问题,设想如果摄像机往前移动(任意方向),往前走一段距离后就走出了天空盒子,跑出了“天空”,是不是很尴尬,所以在摄像机移动的时候,应该去除掉位移,保留旋转,这样就永远走不出“天空”了,那么如何保留旋转,去除位移呢?只需要用view原来的矩阵(4X4矩阵)的3×3部分去除平移,代码如下:
1
2
mat4 view = this->camera->GetLookAt();
view = mat4(mat3(view));

现在就是一个完美的显示出一个天空盒子了,具体的源码可以在这里看到:头文件源文件

优化

虽然说天空盒子已经显示出来了,但是有一个性能上的问题,由于我们是先渲染天空盒子,后渲染物体(为什么物体没有被挡着呢,是因为我们在绘制天空盒子是没有向深度缓冲区写入数据 glDepthMask(GL_FALSE)),就会造成即使物体挡到了天空盒子,挡住的那部分的天空盒子还是会被渲染,造成性能的浪费。那我们可以先绘制物体,后绘制天空盒子,但是会造成天空盒子遮挡到z值比他大的物体(个人理解)。所以我们可以这样处理,因为透视除法是在顶点着色器后面执行的,我们可以把想办法把z值搞成1.0(最大值),这样就会让OpenGL认为天空盒子的z值是最大的,永远被其他物体遮挡。如何让透视除法后z值最大呢,因为透视除法只是x/w,y/w,z/w,所以只需让 z=w 即可,我们可以在顶点着色器中这样做:

1
2
vec4 tempPosition = projection * view * vec4(position, 1.0f);
gl_Position = vec4(tempPosition.x, tempPosition.y, tempPosition.w, tempPosition.w);

这样性能就会有稍微的提升!


参考教程:https://learnopengl-cn.github.io/
学习交流:我的OpenGL工程

Site by ZHJ using Hexo & Random

Hide