![52f681f886d4317b7024a9046e22f6a9.png](https://i-blog.csdnimg.cn/blog_migrate/2fa183f4459e21aae1b964f8d9ed5c0e.jpeg)
# 四月的天空
三月底新项目启动,估计整个Q2都会忙死。
上篇搭了一个Input的架子,但是输入很别扭,另外场景上就一个模型显得空荡荡的,今天我们就把天空盒加上。
参考 https://learnopengl.com/Advanced-OpenGL/Cubemaps 非常棒的Tutorial.
中间场景的一章还没有时间补齐,不过我们先过天空盒吧,以OpenGL为例。
# 原理
## CubeMap
天空盒需要用到立方体贴图,与2D纹理不同,一个立方体贴图(Cube Map)包含6个2D纹理,每个2D纹理是一个立方体的面。可以直接通过一个方向向量对CubeMap采样。
![0d5c8c7a84507556eb4366ac65f5beed.png](https://i-blog.csdnimg.cn/blog_migrate/a31dc29ba72ef51bf7673d9b85c3b539.jpeg)
方向向量的大小无关紧要。一旦提供了方向,OpenGL就会获取方向向量触碰到立方体表面上的相应的纹理像素(texel),这样就返回了正确的纹理采样值。
方向向量触碰到立方体表面的一点也就是立方体贴图的纹理位置,这意味着只要立方体的中心位于原点上,我们就可以使用立方体的位置向量来对立方体贴图进行采样。然后我们就可以获取所有顶点的纹理坐标,就和立方体上的顶点位置一样。所获得的结果是一个纹理坐标,通过这个纹理坐标就能获取到立方体贴图上正确的纹理。
## SkyBox
立方体贴图最著名的应用就是天空盒。
![5f6ec80e0f279ab56ceb028db6f59881.png](https://i-blog.csdnimg.cn/blog_migrate/71530b2fc3a1acd96d3d265c2c5bdbac.jpeg)
Skybox本质上是一个位置(不包括旋转)固定在相机坐标系原点的立方体,用CubeMap给立方体贴上纹理后,可以用立方体的位置作为纹理坐标进行采样。
看下顶点着色器,与参考中不同,我把视图矩阵中对天空盒本身的平移在shader里做舍弃了。
#version 330 core
layout (location = 0) in vec3 aPos;
out vec3 TexCoords;
uniform mat4 projection;
uniform mat4 view;
void main()
{
TexCoords = aPos;
mat4 _matrix = view;
_matrix[3].x = 0.0;
_matrix[3].y = 0.0;
_matrix[3].z = 0.0;
vec4 pos = projection * _matrix * vec4(aPos, 1.0);
gl_Position = pos.xyww;
}
片段着色器 CubeMap的标准采样
#version 330 core
in vec3 TexCoords;
out vec4 color;
uniform samplerCube skybox;
void main()
{
color = texture(skybox, TexCoords);
}
我们直接采纳了参考1中的优化部分,使用前置深度测试(early depth testing),节省片段着色。
坐标系教程,透视除法(perspective division)是在顶点着色器运行之后执行的,把gl_Position
的xyz坐标除以w元素。我们从深度测试教程了解到除法结果的z元素等于顶点的深度值。利用这个信息,我们可以把输出位置的z元素设置为它的w元素,这样就会导致z元素等于1.0了,因为,当透视除法应用后,它的z元素转换为w/w = 1.0:
gl_Position = pos.xyww;
最终,标准化设备坐标就总会有个与1.0相等的z值了,1.0就是深度值的最大值。只有在没有任何物体可见的情况下天空盒才会被渲染(只有通过深度测试才渲染,否则假如有任何物体存在,就不会被渲染,只去渲染物体)。
我们必须改变一下深度方程,把它设置为GL_LEQUAL
,原来默认的是GL_LESS
。深度缓冲会为天空盒用1.0这个值填充深度缓冲,所以我们需要保证天空盒是使用小于等于深度缓冲来通过深度测试的,而不是小于。
# 实战
我们在OpenGLGraphicsManager中添加方法初始化天空盒所需顶点数据、立方体贴图等并设置。
bool OpenGLGraphicsManager::initializeSkyBox()
{
//==============================================
static const float skyboxVertices[] = {
//此处省略,请去github上copy。
};
unsigned int skyboxVAO, skyboxVBO;
glGenVertexArrays(1, &skyboxVAO);
glGenBuffers(1, &skyboxVBO);
glBindVertexArray(skyboxVAO);
glBindBuffer(GL_ARRAY_BUFFER, skyboxVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(skyboxVertices), &skyboxVertices, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
GLenum target;
target = GL_TEXTURE_CUBE_MAP;
uint32_t texture_id;
glGenTextures(1, &texture_id);
glBindTexture(target, texture_id);
vector<std::string> names = { "Textures/right.jpg","Textures/left.jpg","Textures/top.jpg",
"Textures/bottom.jpg" ,"Textures/front.jpg","Textures/back.jpg" };
for (int picIdx = 0; picIdx < 6; picIdx++)
{
Buffer buf = g_pAssetLoader->SyncOpenAndReadBinary(names[picIdx].c_str());
JpegParser parser;
auto imgptr = std::make_shared<Image>(parser.Parse(buf));
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + picIdx, 0, GL_RGB, imgptr->Width, imgptr->Height,
0, GL_RGB, GL_UNSIGNED_BYTE, imgptr->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);
for (GLenum err; (err = glGetError()) != GL_NO_ERROR;)
{
printf("gl error = %d", err);
}
m_textures["SkyBox"] = texture_id;
m_Frame.frameContext.skybox = texture_id;
m_skyboxShader->bind();
m_skyboxShader->setUniform1i("skybox", 0);
m_skyboxContext.vao = skyboxVAO;
m_skyboxContext.mode = GL_TRIANGLES;
m_skyboxContext.type = GL_UNSIGNED_BYTE;
m_skyboxContext.indexCount = 36;
return true;
}
- 创建顶点数据
- 创建并设置VAO、VBO 像常规绘制常规立方体一样
- 创建CubeMap,利用GL_TEXTURE_CUBE_MAP_POSITIVE_X 的枚举关系++,来循环设置立方体六张纹理贴图。
- 设置Drawcall参数以供渲染调用。
****Draw
void OpenGLGraphicsManager::DrawSkyBox()
{
glDepthMask(GL_FALSE);
glDepthFunc(GL_LEQUAL); // change depth function so depth test passes when values are equal to depth buffer's content
m_skyboxShader->bind();
int32_t texture_id = m_Frame.frameContext.skybox;
m_skyboxShader->setUniformMat4("projection", m_Frame.frameContext.projectionMatrix);
m_skyboxShader->setUniformMat4("view", m_Frame.frameContext.viewMatrix);
glBindVertexArray(m_skyboxContext.vao);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, texture_id);
//glDrawElements(m_skyboxContext.mode, m_skyboxContext.indexCount, m_skyboxContext.type, 0x00);
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);
glDepthFunc(GL_LESS);
glDepthMask(GL_TRUE);
}
效果如下:
![b223b6aa20ebd215f58bb3927532cd4e.gif](https://i-blog.csdnimg.cn/blog_migrate/2dab2f5c32d81c5abca819520bb71df1.gif)
为了更好的观察,顺路我们把Input也优化了下,具体参考github修改。
# 进阶 反射映射
进阶的SkyBox 反射映射(Reflection mapping),在计算机图形学领域是用预先计算的纹理图像模拟复杂镜面的以中高效方法。纹理用来存储被渲染物体周围环境的图像。 最常见的有两种实现
- 标准环境映射 环境在一个镜面球体上反射得到单一纹理图像。
- 立方环境映射 环境保存在立方体的六个表面纹理上。(最高效)
这种实现方法比传统的光线追踪算法更高效,但是不够真实。典型的缺点是没有考虑自反射。
SkyBox是立方环境映射,通过确定观察物体的向量就可以进行立方映射反射,照相机光线在照相机向量与物体相交的位置按照曲面法线方向进行反射,这样传到立方图(cube map)取得纹素(texel)的反射光线在照相机看来好像位于物体表面,这样就得到了物体的反射效果。
参考
LearnOpenGL - Cubemapslearnopengl.com![408c9483a47603c2295ffece18701835.png](https://i-blog.csdnimg.cn/blog_migrate/116736c130bc76819bdcf2f6f96ccde4.png)