当前位置: 代码迷 >> 综合 >> LearnGL - 15 - Skybox - 天空盒
  详细解决方案

LearnGL - 15 - Skybox - 天空盒

热度:60   发布时间:2024-02-07 14:57:44.0

文章目录

  • 先看效果
  • 思路
  • 实践
    • 准备一个 Cube
    • 再准备好 CubeMap(立方体贴图)
    • 天空盒子的 Shader
    • 效果1
    • 在应用层设置传入的视图变化矩阵前,删除移动的量
    • 在GLSL shader层移动视图变化矩阵的移动量
    • 效果3
    • 添加其他几何体看看
    • 深度问题
    • 效果4
    • 效果5
    • 天空盒边界接缝处瑕疵问题
    • 边界缝隙解决
  • References


LearnGL - 学习笔记目录

前些篇:

  • LearnGL - 11.1 - 实现简单的Gouraud-Phong光照模型
  • LearnGL - 11.2 - 实现简单的Phong光照模型
  • LearnGL - 11.3 - 实现简单的Blinn-Phong光照模型
  • LearnGL - 11.4 - 实现简单的Flat BlinnPhong光照模型
  • LearnGL - 13 - PointLight - 点光源
  • LearnGL - 13.1 - SpotLight - 聚光灯
  • LearnGL - 14 - MultiLight - 多光源

这些演示光照计算先告一段落。

这一篇:实现 Sky Box (天空盒)

其实参考的学习资料已经学习到后面的大部分了,只不过写文章的速度比较慢,当作是给自己复习。

前几天用 Unity 也做了一个小游戏给家里人玩玩,哈哈,顺便熟悉一下 Unity。

本人才疏学浅,如有什么错误,望不吝指出。


先看效果

在这里插入图片描述


思路

  • 先准备一个 Cube(立方体),作为渲染 Skybox 天空盒的网格
  • 再准备好 CubeMap(立方体贴图)
  • 在 Shader 中使用 Cube 的 顶点坐标作为方向(从 原点顶点坐标 的方向) 作为对 CubeMap 采样用的方向

实践


准备一个 Cube

使用之前我们自己自定义的网格文件格式:Testing_Skybox.m

#vertices:8
-0.5,  0.5, -0.50.5,  0.5, -0.50.5, -0.5, -0.5
-0.5, -0.5, -0.5
-0.5,  0.5,  0.50.5,  0.5,  0.50.5, -0.5,  0.5
-0.5, -0.5,  0.5
#indices:36
0, 1, 2
0, 2, 3
4, 7, 6
4, 6, 5
4, 0, 3
4, 3, 7
5, 6, 2
5, 2, 1
0, 4, 5
0, 5, 1
7, 3, 6
3, 2, 6
#colors:0
#uv:0
#normals:0
#tangents:0

可以看到,vertices 的顶点数量就只有 8 个顶点,还有索引,就没有其他数据了

这些索引可以对应下图的内容:
在这里插入图片描述

OK,Mesh 网格数据都准备好了,下一步是纹理


再准备好 CubeMap(立方体贴图)

如果用到就版本的 API 的话,我们需要使用到一些枚举:

纹理目标 方位
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

在这里插入图片描述
这些枚举值是连续的,所以我们可以遍历的形式去使用,如下旧版 API 代码构建纹理:

int width, height, nrChannels;
unsigned char *data;  
for(unsigned int i = 0; i < textures_faces.size(); i++)
{data = stbi_load(textures_faces[i].c_str(), &width, &height, &nrChannels, 0);glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
}

上面哪些枚举对应下图的方向的纹理

在这里插入图片描述

添加上对应的纹理就是:
在这里插入图片描述

上面的代码使用的是旧版本的 API

下面我们使用 OpenGL 4.5 版本的 API,也正是我使用的方式:

GLuint tex; // 要创建的纹理extern const GLvoid* texture_data[6];
GLsizei mipmap_level = 10;// 生成、绑定和初始化纹理对象,使用 GL_TEXTURE_CUBE_MAP 目标
glCreateTextures(GL_TEXTURE_CUBE_MAP, 1, &tex); // 创建纹理对象
glTextureStorage2D(			// 4.5 APItex, 					// 纹理对象mipmap_level ,			// 10 个层级的 mipmapGL_RGBA8,				// OpenGL 纹理对象内部使用的格式:RGBA四个分量,每个分量8个 bit1024, 102);				// 纹理宽、高尺寸// 已经分配了纹理对象的存储空间,我们可以设置纹素数组中的纹理数据了
for (int face = 0; face < 6; face++)
{glTextureSubImage3D(	// 4.5 APItex,				// 纹理对象0,					// 要设置的mipmap 层级0, 0,				// 每个纹理的x,y像素位置偏移face,				// z 偏移相当于上面旧版中对应的 GL_TEXTURE_CUBE_MAP_POSITIVE_X 等,的每个方向10241024// 每个面向纹理的尺寸1,					// 每次一个面(深度)GL_RGBA,			// 纹理外部数据的格式,RGBA 四个分量GL_UNSIGNED_BYTE,	// 每个分量是一个无符号的 byte,即:0~255texture_data[face]);// 每个面向纹理的数据
}
// 如果需要 mipmap 多层级的话,这里可以调用 OpenGL API生成 mipmap 数据
if (mipmap_level > 1) {glGenerateTextureMipmap(tex);// opengl 4.5 API,生成指定纹理对象的mipmaps
}

还有另一种版本的,可以一次对 5个 cube_map 的生成:

GLuint tex;	// 要创建的纹理extern const GLvoid* texture_data[6][5]; // 各个方面的数据
GLsizei mipmap_level = 10;// 生成、绑定和初始化纹理对象,使用 GL_TEXTURE_CUBE_MAP_ARRAY 目标
glGenTextures(1, &tex); // 创建纹理对象
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, tex); // 绑定到 CUBE_MAP 目标上
glTexStorage3D( // 对当前绑定到 CUBE_MAP 目标上的纹理设置格式、分配大小GL_TEXTURE_CUBE_MAP_ARRAY, 	// 绑定到的目标类型mipmap_level ,				// 10 层mipmapGL_RGBA8,					// 内部格式1024, 1024,					// 宽、高尺寸5);							// 把深度当做 Cube Map 的数据数量,5 个 cube// 已经分配了纹理对象的存储空间,可以设置纹素数组中的纹理数据了
for (int cube_index = 0; cube_index < 5; cube_index++)
{for (int face = 0; face < 6; face++){GLenum target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + face; // 对应要设置的面向glTexSubImage3D(target,								// 面mipmap_level,						// mipmap 层级0, 0,								// x,y offsetcube_index,							// 对应第几个 cube1024, 1024,							// 每个面向纹理的尺寸1,									// 面数GL_RGBA,							// 纹理数据的外部数据格式,四个分量GL_UNSIGNED_BYTE,					// 每个分量为一个无符号的 bytetexture_data[face][cube_index]);	// 数据}
}
// 如果需要 mipmap 多层级的话,这里可以调用 OpenGL API生成 mipmap 数据
if (mipmap_level > 1) {glGenerateTextureMipmap(tex);// opengl 4.5 API,生成指定纹理对象的mipmaps
}

天空盒子的 Shader

网格、纹理我们都准备好了

那么可以开始 shader 部分内容了

// jave.lin - testing_skybox.vert
#version 450 compatibility
uniform mat4 mMat; 
uniform mat4 vMat; 
uniform mat4 pMat; in vec3 vPos;out vec3 fSkybox_sample_vec;void main() {// cube 的采样方向fSkybox_sample_vec  = vPos;gl_Position = pMat * vMat * mMat * vec4(vPos, 1.0f);
}// jave.lin - testing_skybox.frag
#version 450 compatibilityuniform samplerCube main_tex;	// 天空盒 纹理
in vec3 fSkybox_sample_vec;		// 天空盒 的采样方向,可以不用归一化void main() {gl_FragColor 	= vec4(texture(main_tex, fSkybox_sample_vec).rgb, 1.0);
}

可以看到我们使用的是一个 vec3 的向量值来采样的,它的作用如下图:

黄·色的向量,是原点到顶点方向的向量,而我们 texture(cube_map, vec3) 就是根据这个向量来采样的,只要碰撞到Cube 的边界上的点对应的面向的纹理的纹素,就是采样到的内容:
如下图
在这里插入图片描述
然后我们将它传入到 片段着色器,主要看 顶点着色器 的:

// cube 的采样方向
fSkybox_sample_vec  = vPos;

片段着色器 采样就一句话,完事!

gl_FragColor 	= vec4(texture(main_tex, fSkybox_sample_vec).rgb, 1.0);

为何传入一个向量就完事了呢?如果你还记得我们从顶点着色器传到片段着色器的数据是会插值处理的(除非你声明了 flat ),就像下图一样:
在这里插入图片描述
所以如果我们传入四个顶点的坐标,那么他们对应的每个片段上的方向都是插值的,这样刚好完美的对应用于 texture(cube_map, vec3) 中的 vec3 参数,而该 GLSL 中的 API texture(cube_map, vec3) 对采样向量是否归一化不是必要的,所以我们的代码中也没有 normalize 的处理


效果1

来看看运行效果:
在这里插入图片描述
在这里插入图片描述

可以看到,这不是我们想要的效果,这个问题是因为我们将相机返回的视图矩阵中的位移也应用到了天空盒上面去了

而我们的天空盒子是假设一个超级远的内容,当做是不会移动的一个超级远的内容。(当然真实情况并不是这样的,这里只是模拟)

所以我们需要想办法移除相机移动的数值,有两种方式:

  • 在应用层设置传入的 视图变化 矩阵前,删除移动的量:
  • 在GLSL shader层移动 视图变化 矩阵的移动量

在应用层设置传入的视图变化矩阵前,删除移动的量

glm::mat4 vMat = glm::mat4(glm::mat3(camera->getViewMatrix()));

思路是先 将 mat4 转型为 mat3 丢弃第四列第四行的内容,再转回 mat4 不会默认的第四列第四行的内容,我们的移动分量在第四列的前三个分量,默认值是0,所以这样是可以移除移动量的。


在GLSL shader层移动视图变化矩阵的移动量

这种方式相比上面在应用层来说性能会损耗一点点,不过可以忽略不计,但是对于应用层代码来说就不用修改了,可读性也高一些,不用为了天空盒而专门特殊处理代码。

所以这也是我采用的方式:

// jave.lin - testing_skybox.vert
#version 450 compatibility
uniform mat4 mMat; 
uniform mat4 vMat; 
uniform mat4 pMat; in vec3 vPos;out vec3 fSkybox_sample_vec;void main() {// cube 的采样方向fSkybox_sample_vec  = vPos;// 复制mat4 new_vMat = vMat;/*X_x X_y X_z X_oY_x Y_y Y_z Y_oZ_x Z_y Z_z Z_o0 0 0 1将 X_o = Y_o = Z_o = 0矩阵变成X_x X_y X_z 0Y_x Y_y Y_z 0Z_x Z_y Z_z 00 0 0 1*///将 X_o = Y_o = Z_o = 0new_vMat[3][0] = new_vMat[3][1] = new_vMat[3][2] = 0;vec4 outPos = pMat * new_vMat * mMat * vec4(vPos, 1.0);gl_Position = outPos;
}

主要看、理解这么一句:

new_vMat[3][0] = new_vMat[3][1] = new_vMat[3][2] = 0;

具体可以查看注释说明


效果3

在这里插入图片描述
为何啥都看不到了?

因为我们在 shader 中移除了 view matrix 视图矩阵的移动量了,所以我们的镜头相当于一只都像 cube 立方体的中心点,那么我们看到的都是立方体内部的面,即:背面

而我开了剔除面向的功能,并且剔除的是:背面,所以就不显示了

所以我们只要将剔除的面向该为:剔除:正面

//mat->wire_frame = false; // 默认就是非线框模式,不用设置
//mat->enabledCullFace = true; // 默认启用面向剔除 : true,不用设置
mat->cullFace = DrawState_FaceCullingType::Front; // 剔除面向为:正面
//mat->enabledDepthTest = true; // 默认启用深度测试 : true,不用设置
//mat->depthCompare = DrawState_DepthTestingType::Less; // 默认为 Less 不用设置
mat->enabledDepthWrite = false;	// 不用写深度,因为本身为最大深度了

主要看两句:

mat->cullFace = DrawState_FaceCullingType::Front; // 剔除面向为:正面
mat->enabledDepthWrite = false;	// 不用写深度,因为本身为最大深度了

剔除正面

也不需要些深度,因为天空和只会被挡,而不会挡住其他东西(除非你要制作一些比天空盒还要远的东西,太空?那也只能动态过度不同的天空盒了)

在这里插入图片描述


添加其他几何体看看

添加上其他的几何体后,发现效果不对了?

天空盒都挡住了我们的几何体了?怎么办呢?

在这里插入图片描述

然后我们将相机往前移动里球体和气球猫网格几何体都近一些,会显示部分球体与气球猫的网格

这是因为深度的问题


深度问题

还记得我们之前看的 sky box 的 cube 的顶点数据吗?

#vertices:8
-0.5,  0.5, -0.50.5,  0.5, -0.50.5, -0.5, -0.5
-0.5, -0.5, -0.5
-0.5,  0.5,  0.50.5,  0.5,  0.50.5, -0.5,  0.5
-0.5, -0.5,  0.5

所以明显天空盒渲染出来的深度有些比球体和气球猫几何体的网格的深度要还小导致的

那么我们要想办法将天空盒的深度都渲染到最大的深度

之前说过的,深度的范围:0.0~1.0,深度越小,就越靠近镜头,越大,就越远离镜头

所以我们要想办法将天空盒渲染到最远的深度:1.0的值

shader 如下:

// jave.lin - testing_skybox.vert
#version 450 compatibility
uniform mat4 mMat; 
uniform mat4 vMat; 
uniform mat4 pMat; in vec3 vPos;out vec3 fSkybox_sample_vec;void main() {/*X_x X_y X_z X_oY_x Y_y Y_z Y_oZ_x Z_y Z_z Z_o0 0 0 1将 X_o = Y_o = Z_o = 0矩阵变成X_x X_y X_z 0Y_x Y_y Y_z 0Z_x Z_y Z_z 00 0 0 1*/// cube 的采样方向fSkybox_sample_vec  = vPos;// 复制mat4 new_vMat = vMat;//将 X_o = Y_o = Z_o = 0new_vMat[3][0] = new_vMat[3][1] = new_vMat[3][2] = 0;vec4 outPos = pMat * new_vMat * mMat * vec4(vPos, 1.0);// 注意我们将 z 改为了 w,即:每个顶点都设置深度最大,因为天空盒都假设是最远的背景内容// 注意我们的深度范围是:0~1 (最小~最大)// 为何用w深度就可以为最大呢(1)// 因为顶点着色器之后,会执行透视除法// 透视除法:假设 pos(x,y,z,w) 是顶点变化后的坐标点// 透视除法将之前的坐标除以他第四个分量 w pos /= pos.w; // 相当于:pos.xyzw = pos.xyzw / pos.wwww;// 为何是第四分量,因为我们故意在 Projection 矩阵的[2][3] 设置为 -1,这样即可获取原本透视前的 z 值,即可:view space 下的 z// 然后结合齐次坐标的表示:(1,2,3,1)=(2,4,6,2)// 即可:(x,y,z,w)/w=(x/w,y/w,z/w,w/w)=(x/w,y/w,z/w,1)// 这就是 齐次坐标 透视除法 的由来// 那么回想刚刚我们前面设置的:pos = pos.xyww; // 注意后面两个分量是 ww// 相当于: pos.xyzw = pos.xyww// 如果:pos.xyzw / pos.wwww = 后面两个参数肯定都是1,因为非零的数除以本身等于1gl_Position = outPos.xyww;
}

主要查看:

gl_Position = outPos.xyww;

然后详细的说明可以看注释,这里不在重复说明


效果4

上面将天空盒的深度设置为 1.0 的越大值后,发现啥都不显示了,如下图:
在这里插入图片描述

这是因为我们的默认的深度比较是 Less 的方式导致的

Less 的方式意味着,必须比深度缓存中的值要小,才能通过,而我们的深度缓存值默认每帧渲染前都先清理深度缓存值为 1.0 导致的,这样天空盒渲染的也是深度为 1.0 ,所以天空盒的深度沒比緩存的值要小,而是相等,所以我们可以将天空盒的渲染状态的:深度比较调整为:LEqual 即可,LEqual 是小于、等于的意思(LEqual == Less or Equal)。

那么我们再调整好深度比较方式为:LEqual(不要用 Equal,因为会有精度问题,造成类似 z-fighting 的问题),再看看效果:

//mat->wire_frame = false; // 默认就是非线框模式,不用设置
//mat->enabledCullFace = true; // 默认启用面向剔除 : true,不用设置
mat->cullFace = DrawState_FaceCullingType::Front; // 剔除面向为:正面
//mat->enabledDepthTest = true; // 默认启用深度测试 : true,不用设置
//mat->depthCompare = DrawState_DepthTestingType::Less; // 默认为 Less 不用设置
mat->depthCompare = DrawState_DepthTestingType::LEqual;
mat->enabledDepthWrite = false;	// 不用写深度,因为本身为最大深度了

效果5

mat->depthCompare = DrawState_DepthTestingType::LEqual; 后,运行效果为:
在这里插入图片描述

嗯,这看起来还不错的效果!


天空盒边界接缝处瑕疵问题

看起来上面的运行情况还想没啥问题了,其实,如果仔细看天空盒的每个面向的边界接缝处,都有一些缝隙,看起来很不舒服,如下图:

这是 底部 的边界缝隙的问题:
在这里插入图片描述
这是 顶部 的边界缝隙的问题:

在这里插入图片描述


边界缝隙解决

OpenGL 还专门提供了一个 API:

glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);

在这里插入图片描述


References

  • Cubemap Texture
  • 立方体贴图