LearnGL - 学习笔记目录
前些篇:
这些演示光照计算先告一段落。
这一篇:实现 Sky Box (天空盒)
其实参考的学习资料已经学习到后面的大部分了,只不过写文章的速度比较慢,当作是给自己复习。
前几天用 Unity 也做了一个小游戏给家里人玩玩,哈哈,顺便熟悉一下 Unity。
本人才疏学浅,如有什么错误,望不吝指出。
使用之前我们自己自定义的网格文件格式:Testing_Skybox.m
#vertices:8
-0.5, 0.5, -0.5
0.5, 0.5, -0.5
0.5, -0.5, -0.5
-0.5, -0.5, -0.5
-0.5, 0.5, 0.5
0.5, 0.5, 0.5
0.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 网格数据都准备好了,下一步是纹理
如果用到旧版本的 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 API
tex, // 纹理对象
mipmap_level , // 10 个层级的 mipmap
GL_RGBA8, // OpenGL 纹理对象内部使用的格式:RGBA四个分量,每个分量8个 bit
1024, 102); // 纹理宽、高尺寸
// 已经分配了纹理对象的存储空间,我们可以设置纹素数组中的纹理数据了
for (int face = 0; face < 6; face++)
{
glTextureSubImage3D( // 4.5 API
tex, // 纹理对象
0, // 要设置的mipmap 层级
0, 0, // 每个纹理的x,y像素位置偏移
face, // z 偏移相当于上面旧版中对应的 GL_TEXTURE_CUBE_MAP_POSITIVE_X 等,的每个方向
1024, 1024, // 每个面向纹理的尺寸
1, // 每次一个面(深度)
GL_RGBA, // 纹理外部数据的格式,RGBA 四个分量
GL_UNSIGNED_BYTE, // 每个分量是一个无符号的 byte,即:0~255
texture_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 层mipmap
GL_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 offset
cube_index, // 对应第几个 cube
1024, 1024, // 每个面向纹理的尺寸
1, // 面数
GL_RGBA, // 纹理数据的外部数据格式,四个分量
GL_UNSIGNED_BYTE, // 每个分量为一个无符号的 byte
texture_data[face][cube_index]); // 数据
}
}
// 如果需要 mipmap 多层级的话,这里可以调用 OpenGL API生成 mipmap 数据
if (mipmap_level > 1) {
glGenerateTextureMipmap(tex);// opengl 4.5 API,生成指定纹理对象的mipmaps
}
网格、纹理我们都准备好了
那么可以开始 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 compatibility
uniform 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
的处理
可以看到,这不是我们想要的效果,这个问题是因为我们将相机返回的视图矩阵中的位移也应用到了天空盒上面去了
而我们的天空盒子是假设一个超级远的内容,当做是不会移动的一个超级远的内容。(当然真实情况并不是这样的,这里只是模拟)
所以我们需要想办法移除相机移动的数值,有两种方式:
glm::mat4 vMat = glm::mat4(glm::mat3(camera->getViewMatrix()));
思路是先 将 mat4
转型为 mat3
丢弃第四列第四行的内容,再转回 mat4
补回默认的第四列第四行的内容,我们的移动分量在第四列的前三个分量,默认值是0,所以这样是可以移除移动量的。
这种方式相比上面在应用层来说性能会损耗一点点,不过可以忽略不计,但是对于应用层代码来说就不用修改了,可读性也高一些,不用为了天空盒而专门特殊处理代码。
所以这也是我采用的方式:
// 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_o
Y_x Y_y Y_z Y_o
Z_x Z_y Z_z Z_o
0 0 0 1
将 X_o = Y_o = Z_o = 0
矩阵变成
X_x X_y X_z 0
Y_x Y_y Y_z 0
Z_x Z_y Z_z 0
0 0 0 1
*/
//将 X_o = Y_o = Z_o = 0
new_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;
具体可以查看注释说明
因为我们在 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.5
0.5, 0.5, -0.5
0.5, -0.5, -0.5
-0.5, -0.5, -0.5
-0.5, 0.5, 0.5
0.5, 0.5, 0.5
0.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_o
Y_x Y_y Y_z Y_o
Z_x Z_y Z_z Z_o
0 0 0 1
将 X_o = Y_o = Z_o = 0
矩阵变成
X_x X_y X_z 0
Y_x Y_y Y_z 0
Z_x Z_y Z_z 0
0 0 0 1
*/
// cube 的采样方向
fSkybox_sample_vec = vPos;
// 复制
mat4 new_vMat = vMat;
//将 X_o = Y_o = Z_o = 0
new_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,因为非零的数除以本身等于1
gl_Position = outPos.xyww;
}
主要查看:
gl_Position = outPos.xyww;
然后详细的说明可以看注释,这里不在重复说明
上面将天空盒的深度设置为 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; // 不用写深度,因为本身为最大深度了
将 mat->depthCompare = DrawState_DepthTestingType::LEqual;
后,运行效果为:
嗯,这看起来还不错的效果!
看起来上面的运行情况还想没啥问题了,其实,如果仔细看天空盒的每个面向的边界接缝处,都有一些缝隙,看起来很不舒服,如下图:
这是 底部 的边界缝隙的问题:
这是 顶部 的边界缝隙的问题:
OpenGL 还专门提供了一个 API:
glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);
只要在相机渲染对应的 Renderer 之前对其按渲染队列值排序一下即可实现。
我还特地给天空盒添加了一个专用的队列值
一般我们可能会将天空盒在最早的时候就渲染了。
然后其他的几何体可以后面再渲染来挡住原来天空盒渲染过的片元内容。(覆盖深度)
但是这样会浪费效率,所以我们决定将天空和放在了渲染完所有的不透明几何体之后在渲染它。
这样就可以利用之前不透明物体的深度值来剔除天空盒不必要的片段,从而提升性能。