第二十五课 天空盒

背景

天空盒是用于增强场景表现力的一个常用技术,它一般通过在相机周围包裹一个纹理来实现。这个纹理通常是一些天空、山川或者摩天大楼等等,下面是游戏 Half-Life 中使用天空盒的例子:

天空盒的思想就是绘制一个大的立方体,然后将观察者放在立方体的中心,当相机移动时,这个立方体也跟着相机一起移动,这样相机就永远不会运动到场景的边缘。这与我们真实世界中的情况一样的,我们可以看见远处的天空接触到了地平线,但是不论我们怎么朝着那个方向移动,都不可能到达那个地方。

一个特殊类型的纹理会被映射到这个立方体上,这个纹理的创建是与其贴到立方体上的方式相关的,立方体上每个面的纹理与其相邻的面上的纹理的边缘都可以很好的贴合,就如下面这个情况一样:

如果我们去掉上面的纹理之间的白边,并且将上面的各部分纹理沿着白边折叠,这样我们就能得到我们想要的这样一个盒子,这就是 OpenGL 中所谓的立方体纹理。

为了从立方体纹理中进行采样,我们需要使用 3D 纹理坐标而不是我们之前用到的 2D 纹理坐标,纹理采样器将 3D 纹理坐标看做一个向量,它会先确定我们需要的纹素是存在于立方体的哪一个面上,之后再从这个面上得到纹素的数据,这一个过程可以用下面这个图片表示(从上往下看立方体)。

OpenGL 会根据纹理坐标中分量最大的那一个来选择合适的面进行采样,在上面的例子中我们可以看到其 Z 方向上的分量最大(虽然我们看不见 Y 轴上的情况,但是让我们假设它比 Z 分量小)。由于我们的 Z 分量沿着 Z 轴的正方向,所以纹理采样器会使用 “PosZ” 表面,并且从这个表面上获取纹素信息(其余的表面包括 ‘NegZ’, ‘PosX’, ‘NegX’, ‘PosY’ 以及 ‘NegY’)。

天空盒除了借助于一个立方体盒子来实现之外,还可以使用球体。他们之间的区别就在于球体中各个方向上的向量长度都是一样的(都为球体的半径),而在立方体盒子中则不是这样。但是他们的纹素获取机制都是一样的。通过球体来实现的天空盒有时也被叫做 skydome。这一节中我们就是用的这种天空盒。

代码

(ogldev_cubemap_texture.h:28)
class CubemapTexture
{
public:CubemapTexture(const string& Directory,const string& PosXFilename,const string& NegXFilename,const string& PosYFilename,const string& NegYFilename,const string& PosZFilename,const string& NegZFilename);~CubemapTexture();bool Load();void Bind(GLenum TextureUnit);
private:string m_fileNames[6];GLuint m_textureObj;
};

这个类封装了 OpenGL 对 cubemap 的实现,并且提供了一些简单的接口来导入和使用 cubemap。 其构造函数需要一个文件目录和 6 个图片文件名作为参数。为了简单起见我们假设这 6 个图片都位于同一个目录下。在开始的时候我们需要调用 Load() 函数从文件中导入图片数据并创建 OpenGL 纹理对象。这个类中定义了两个成员变量,一个字符串数组用于保存需要用到的所有纹理的绝对路径,和一个纹理对象句柄。在程序运行的时候,我们需要调用 Bind()函数将 cubemap 绑定到合适的纹理单元上以方便着色器程序调用。

(cubemap_texture.cpp:60)
bool CubemapTexture::Load()
{glGenTextures(1, &m_textureObj);glBindTexture(GL_TEXTURE_CUBE_MAP, m_textureObj);Magick::Image* pImage = NULL;Magick::Blob blob;for (unsigned int i = 0 ; i < ARRAY_SIZE_IN_ELEMENTS(types) ; i++) {pImage = new Magick::Image(m_fileNames[i]);try { pImage->write(&blob, "RGBA");}catch (Magick::Error& Error) {cout << "Error loading texture '" << m_fileNames[i] << "': " << Error.what() << endl;delete pImage;return false;}glTexImage2D(types[i], 0, GL_RGB, pImage->columns(), pImage->rows(), 0, GL_RGBA,GL_UNSIGNED_BYTE, blob.data());delete pImage;} 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);return true;
} 

这个函数用于导入 cubemap 要用到的纹理,在这个函数里面它先创建了一个纹理对象,并且这个纹理对象被绑定到了一个特殊的 GL_TEXTURE_CUBE_MAP 目标上,之后我们进入一个循环,为 cubemap 的每个面指定其对应的纹理数据, types 数组中的每一个元素都代表着 cubemap 中的一个面(GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X 等等)。因为这些枚举值与我们存放文件名的数组中的数据是对应的,这简化了我们的循环操作。我们借助于 ImageMagick 将图片文件一个一个的加载进来,之后通过 glTexImage2D() 函数将数据传递给 OpenGL,值得注意的是每次我们调用 glTexImage2D(),我们都会为其第一个参数指定不同的值,它对应着 cubemap 中不同的面。在 cubemap 导入之后,我们需要为其配置一些参数,除了 GL_TEXTURE_WRAP_R 之外,其他的你应该都比较熟悉了,这一个枚举值仅仅是对应了纹理坐标的第三个维度,我们只要参照上面的设置即可。

(cubemap_texture.cpp:95)
void CubemapTexture::Bind(GLenum TextureUnit)
{glActiveTexture(TextureUnit);glBindTexture(GL_TEXTURE_CUBE_MAP, m_textureObj);
}

在使用这个纹理对象来绘制天空盒之前,我们必须调用这个函数,同样我们将这个纹理对象绑定到 GL_TEXTURE_CUBE_MAP 目标上。

(skybox_technique.h:25)
class SkyboxTechnique : public Technique {
public:SkyboxTechnique();virtual bool Init();void SetWVP(const Matrix4f& WVP);void SetTextureUnit(unsigned int TextureUnit);
private:GLuint m_WVPLocation;GLuint m_textureLocation;
};

天空盒的渲染需要使用它自己的着色器程序,这个着色器我们只需要传入几个属性即可——一个用于对天空盒进行变换的 WVP 矩阵和其渲染需要用到的纹理单元。

(skybox.vs)#version 330
layout (location = 0) in vec3 Position;
uniform mat4 gWVP;
out vec3 TexCoord0;
void main()
{vec4 WVP_Pos = gWVP * vec4(Position, 1.0);gl_Position = WVP_Pos.xyww;TexCoord0 = Position;
}

它的顶点着色器十分简单,但是你还是需要注意其中的一些小技巧。首先我们使用 WVP 矩阵对传入的顶点数据进行变换,但需要注意的是我们并不是直接将变换之后的坐标传入 gl_Position 中,在传入之前我们用其 W 分量替换了 Z 分量的值。这是因为在光栅化阶段中光栅器会对 gl_Position 执行透视除法(除以其 W 分量),如果这里我们将 Z 分量替换成 W 分量的值,则在透视除法之后这个向量的 Z 分量的值就成为了 1.0 。这样这个片元就会始终位于远裁剪面上,这意味着在深度测试的过程中,天空盒的片元和场景中任意模型的片元比较都会失败。这样天空盒就只会是填满场景模型留下的背景空隙,这也是我们希望得到的效果。

这里的第二个技巧就是我们使用位于模型局部坐标系下的位置坐标作为 3D 纹理坐标,这样做可行的原因是在对 cubemap 采样的过程中也是通过从圆心发出一条射线到立方体或者球体表面上,所以这里天空盒的位置坐标就可以直接作为我们的纹理坐标。之后我们将这些数据都传递到片元着色器中。

(skybox.fs)#version 330
in vec3 TexCoord0;
out vec4 FragColor;
uniform samplerCube gCubemapTexture;
void main()
{FragColor = texture(gCubemapTexture, TexCoord0);
}

这里的片元着色器也十分简单,这里唯一需要注意的就是我们使用 samplerCube 而不是 sampler2D 来从我们的 cubemap 进行采样。

(skybox.h:27)
class SkyBox
{
public:SkyBox(const Camera* pCamera, const PersProjInfo& p);~SkyBox();bool Init(const string& Directory,const string& PosXFilename,const string& NegXFilename,const string& PosYFilename,const string& NegYFilename,const string& PosZFilename,const string& NegZFilename);void Render();
private: SkyboxTechnique* m_pSkyboxTechnique;const Camera* m_pCamera;CubemapTexture* m_pCubemapTex;Mesh* m_pMesh;PersProjInfo m_persProjInfo;
};

要渲染一个天空盒这里需要一些组件——一个着色器对象、一个 cubemap 纹理对象和一个立方体盒子(或者球体)模型。为了简化天空盒的使用,我们将这些组件都封装在同一个类中。我们只需要在开始的时候使用纹理文件的路径对其进行初始化,之后我们就可以在运行期间调用 Render()函数进行渲染。这个类同时也可以访问相机对象和透视投影的相关信息(FOV,Z 以及屏幕尺寸)。

void SkyBox::Render()
{m_pSkyboxTechnique->Enable();GLint OldCullFaceMode;glGetIntegerv(GL_CULL_FACE_MODE, &OldCullFaceMode);GLint OldDepthFuncMode;glGetIntegerv(GL_DEPTH_FUNC, &OldDepthFuncMode);glCullFace(GL_FRONT);glDepthFunc(GL_LEQUAL);Pipeline p; p.Scale(20.0f, 20.0f, 20.0f);p.Rotate(0.0f, 0.0f, 0.0f);p.WorldPos(m_pCamera->GetPos().x, m_pCamera->GetPos().y, m_pCamera->GetPos().z);p.SetCamera(m_pCamera->GetPos(), m_pCamera->GetTarget(), m_pCamera->GetUp());p.SetPerspectiveProj(m_persProjInfo);m_pSkyboxTechnique->SetWVP(p.GetWVPTrans());m_pCubemapTex->Bind(GL_TEXTURE0);m_pMesh->Render(); glCullFace(OldCullFaceMode); glDepthFunc(OldDepthFuncMode);
}

这个函数负责天空盒的渲染。首先我们启用天空盒的着色器,这里我们用到了一个新的 OpenGL API——glGetIntegerv()。这个函数用于获取 OpenGL 的状态,它需要两个参数,第一个参数是一个 OpenGL 状态枚举值,表示要获得的 OpenGL 状态;第二个参数是用于存放这个状态的值的数组的地址(我们这里只是返回一个整型变量)。对于不同的状态,我们可以使用不同的 Get* 函数来获取其状态,比如 glGetIntegerv(), glGetBooleanv(), glGetInteger64v(), glGetFloatv() 以及 glGetDoublev()。这里我们使用 glGetIntegerv() 是因为我们想要改变在 glut_backend.cpp 中为 OpenGL 设置的一些状态,由于渲染天空盒的时候我们需要改变 OpenGL 的一些状态,但是为了不影响场景中其他对象的渲染,所以在天空盒进行渲染的时候我们先将需要改变的 OpenGL 状态保存下来,等天空盒渲染完成之后再将这些状态还原回去。

首先我们需要改变剔除模式,一般情况下我们希望将三角形背向相机的面剔除,但是对于天空盒,由于相机是放在天空盒内部的,所以我们希望看到的是天空盒的内部,但是一般情况下,球体的模型表面被定义为正面(根据顶点定义的顺序确定),我们可以修改我们的模型或者修改 OpenGL 的状态来解决这个问题。这里我们使用后者,所以我们告诉 OpenGL 对正面进行剔除。其次我们需要改变深度测试函数,默认情况下,我们告诉 OpenGL 当新的片元的 Z 值小于之前存放的片元时,则新的片元覆盖之前的片元。对于天空盒,其片元的 Z 值始终等于远裁剪面的值,当我们将深度测试函数设置为 “less than” 时,这样的片元在深度测试的时候不能保存下来,所以这里我们将深度测试函数设置为 GL_LEQUAL(小于或者等于)

接下来我们需要做的事就是计算天空盒的 WVP 矩阵,需要注意的是天空盒的中心应该是被放置在相机的位置处的,这样才能保证相机一直位于天空盒的中心,之后我们将 cubemap 纹理绑定到 0 号纹理单元上(在 SkyboxTechnique 着色器中,我们向着色器中传入的纹理单元也是 0)。之后我们渲染这个球体模型,最后我们将 OpenGL 的相关设置还原。

这里有一个有趣的现象,我们总是在最后才渲染天空盒(所有其他模型渲染完之后)。这是因为我们知道天空盒肯定是在场景中的所有模型的后面的,一些 GPU 有一些优化机制可以在执行片元着色器之前就进行一些早期的深度测试,从而将那些在深度测试中失败的片元丢弃,这对于天空盒的渲染时非常有利的,因为这样的话就只用对那些没有被场景模型覆盖的背景片元执行片元着色器了。

操作结果

OpenGL教程翻译 第二十五课 天空盒相关推荐

  1. 用OpenInventor实现的NeHe OpenGL教程-第二十五课

    用OpenInventor实现的NeHe OpenGL教程-第二十五课           NeHe教程在这节课中向我们介绍了如何从文件加载3D模型,并且平滑的从一个模型变换为另一个模型.两个模型之间 ...

  2. OpenGL教程翻译 第二十六课 法线纹理

    第二十六课 法线纹理 背景 我们之前使用的光照技术还算不错,光线在模型表面得到了很好的插值,为场景营造出真实感.但是这种效果还能够有非常大的提升.事实上,我们以前使用的这种插值方式在某种程度上来说是对 ...

  3. OpenGL教程翻译 第二十二课 使用Assimp加载模型

    第二十二课 使用Assimp加载模型 原文地址:http://ogldev.atspace.co.uk/(源码请从原文主页下载) 背景 到现在为止我们都在使用手动生成的模型.正如你所想的,指明每个顶点 ...

  4. NeHe OpenGL教程 第四十五课:顶点缓存

    转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线 ...

  5. OpenGL教程翻译 第十二课 透视投影

    第十二课 透视投影 原文地址:http://ogldev.atspace.co.uk/(源码请从原文主页下载) 背景 在这一节中我们将会介绍如何在保持深度外观的情况下将三维世界中的物体投影到二维平面上 ...

  6. OpenGL教程翻译 第十八课 漫反射光

    第十八课 漫反射光 原文地址:http://ogldev.atspace.co.uk/(源码请从原文主页下载) 背景 环境光和漫反射光的主要不同是,漫反射光的计算需要依靠光线方向而环境光完全忽略了它! ...

  7. NeHe OpenGL第二十五课:变形

    NeHe OpenGL第二十五课:变形 变形和从文件中加载3D物体: 在这一课中,你将学会如何从文件加载3D模型,并且平滑的从一个模型变换为另一个模型.   欢迎来到这激动人心的一课,在这一课里,我们 ...

  8. NeHe OpenGL教程 第三十六课:从渲染到纹理

    转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线 ...

  9. NeHe OpenGL教程 第四十四课:3D光晕

    转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线 ...

最新文章

  1. C++中模板template typename T
  2. 无人驾驶之车道线检测简易版
  3. 使用HTML5的WebSocket实现服务端和客户端数据通信(有演示和源码)
  4. php代码结构,如何优化php代码结构
  5. JVM之内存结构图文详解
  6. 关于SELECT 逻辑的执行顺序问题
  7. create-react-app 构建的项目使用 mobx (说到底就是为了使用装饰器语法对 babel 做些配置...
  8. Linux系统openssl测试指导,Linux管理员必用:OpenSSL服务器测试技巧
  9. ICBU可控文本生成技术详解
  10. python-format格式化专题介绍1909
  11. UVA10474 Where is the Marble?【排序】
  12. 关键词之间用分号_如何选择关键词?快看这几个要点
  13. atitit.错误:找不到或无法加载主类 的解决 v4 qa15.doc 艾提拉总结 attilax总结 1.1. 修改此java文件,让他启动编译,还是不能生成了新的class, 1 1.2. 查
  14. FPGA 入门 (一)
  15. 1、Struts2基本入门
  16. 微信小程序-图片放大与缩小
  17. Sass Module 介绍
  18. 2022年,AI将给网络安全领域带来什么?
  19. 旺旺qq html,如何在网页HTML中嵌入QQ、MSN、旺旺、Gtalk快速对话框代码
  20. .net core 3.1JWT用户权限认证(二)获取token

热门文章

  1. java手机论坛_Java论坛系统巡云轻论坛 v1.2
  2. Nds-IR780 近红外荧光探针IR780纳米粒子
  3. Node.js.不要堵塞线程(一)
  4. 字体大宝库:30款英文大写字体免费下载
  5. 啊,CET6 2020年12月
  6. Cross Site Tracing (XST) Attacks
  7. [pwn]调试:gdb+pwndbg食用指南
  8. 视频教程-2019视频营销实战教程-网络营销
  9. 奥塔在线:MySql数据库定时备份脚本
  10. RecyclerView的万能分割线