基于Qt的OpenGL编程(3.x以上GLSL可编程管线版)---(二十二)立方体贴图(天空盒)

Vries的教程是我看过的最好的可编程管线OpenGL教程,没有之一其原地址如下,https://learnopengl-cn.github.io/04%20Advanced%20OpenGL/06%20Cubemaps/ 关于立方体的详细知识了解请看原教程,本篇旨在对Vires基于visual studio平台的编程思想与c++代码做纯Qt平台的移植,代码移植顺序基本按照原教程顺序,并附加一些学习心得,重在记录自身学习之用

Tip: 这章代码移植的难点就一个部分,Qt下用QOpenGLTexture生成样式为CubeMap的纹理。

程序源代码链接:https://pan.baidu.com/s/1pqajRIg2JkR9hNMNOWAx9g 提取码:rl6g 

编译环境:Qt5.9.4

编译器:Desktop Qt5.9.4 MSVC2017 64bit

IDE:QtCreator

一. 立方体贴图能用来干什么

  一言以概之,立方体贴图最广泛的用途,还是用来生成场景中的天空盒。

 1.1 笨方法

   以前在我只会,GL_TEXTURE_2D,这种2D纹理样式时,我是这样做天空盒的,很笨重的用六张矩形大纹理,分别分割载入下图所示的天空盒套图,然后拼成一个立方体。当这个立方体足够大时,这种方法当然是可行的,效果也还不错。但当我得知CubeMap这个纹理目标类型时,这个用拼接立方体生成天空盒的方法真的弱爆了。

1.2 CubeMap

    简单来说,CubeMap就是一个包含了6张2D纹理数据的纹理,并用方向向量对CubeMap纹理进行采样。

  Vries用了纹理载入库stbImage载入纹理,我们不需要这么麻烦,Qt的QOpenGLTexture类已经为我们提供了CubeMap纹理目标这个选项。

仔细分析QOpenGLTexture的帮助文档,文档清楚的说明除了

void setData(const QImage &image, MipMapGeneration genMipMaps = GenerateMipMaps) 函数

在生成2D纹理时会默认自动分配纹理物理内存,其余的setData()重载函数都必须手动分配内存空间

所以,如果我们需要生成纹理目标为CubeMap的纹理,必须提前手动分配内存空间。

在手动分配内存空间之前,必须确定纹理的尺寸,类型等样式。

最终在Qt下生成CubeMap纹理的过程如下所示。

其实,代码移植的难点到这里也就完成了,主要就是CubeMap的生成方法,接下来的其余内容与Vries博客的内容一致。

  //注释变量类型,QMap<QString, QString> paths; 存储cubemap六个面的纹理路径
  QImage posX = QImage(paths["posX"]).convertToFormat(QImage::Format_RGB888); //Right,默认读取的纹理为32位RGB,不符合CubeMap的要求,必须转为24位RGB。
  QImage negX = QImage(paths["negX"]).convertToFormat(QImage::Format_RGB888); //Left
  QImage posY = QImage(paths["posY"]).convertToFormat(QImage::Format_RGB888); //Top
  QImage negY = QImage(paths["negY"]).convertToFormat(QImage::Format_RGB888); //Bottom
  QImage posZ = QImage(paths["posZ"]).convertToFormat(QImage::Format_RGB888); //Front
  QImage negZ = QImage(paths["negZ"]).convertToFormat(QImage::Format_RGB888); //Back

  //注释变量类型,QOpenGLTexture *texture;
  texture = new QOpenGLTexture(QOpenGLTexture::TargetCubeMap);

  texture->setSize(posX.width(), posX.height(), posX.depth()); //这个我猜测 是确定一面纹理的尺寸,然后allocate分配函数,根据TargeCubeMap,分配六面纹理的空间
  texture->setFormat(QOpenGLTexture::RGBFormat);  //Vries设置的就是GL_RGB,这里同步
  texture->allocateStorage(QOpenGLTexture::RGB, QOpenGLTexture::UInt8); //分配内存 ,UInt8等价于GL_UNSIGNED_BYTE

  texture->setData(0, 0, QOpenGLTexture::CubeMapPositiveX, QOpenGLTexture::RGB, QOpenGLTexture::UInt8, (const void*)posX.bits());
  texture->setData(0, 0, QOpenGLTexture::CubeMapPositiveY, QOpenGLTexture::RGB, QOpenGLTexture::UInt8, (const void*)posY.bits());
  texture->setData(0, 0, QOpenGLTexture::CubeMapPositiveZ, QOpenGLTexture::RGB, QOpenGLTexture::UInt8, (const void*)posZ.bits());
  texture->setData(0, 0, QOpenGLTexture::CubeMapNegativeX, QOpenGLTexture::RGB, QOpenGLTexture::UInt8, (const void*)negX.bits());
  texture->setData(0, 0, QOpenGLTexture::CubeMapNegativeY, QOpenGLTexture::RGB, QOpenGLTexture::UInt8, (const void*)negY.bits());
  texture->setData(0, 0, QOpenGLTexture::CubeMapNegativeZ, QOpenGLTexture::RGB, QOpenGLTexture::UInt8, (const void*)negZ.bits());

  texture->setMinificationFilter(QOpenGLTexture::Linear);     //纹理放大或缩小时,像素的取值方法 ,线性或就近抉择
  texture->setMagnificationFilter(QOpenGLTexture::Linear);
  texture->setWrapMode(QOpenGLTexture::DirectionS, QOpenGLTexture::ClampToEdge);   //设置纹理边缘的扩展方法
  texture->setWrapMode(QOpenGLTexture::DirectionT, QOpenGLTexture::ClampToEdge);
  //texture->setWrapMode(QOpenGLTexture::DirectionR, QOpenGLTexture::ClampToEdge); //Qt 显示不支持TargetCubeMap纹理类型R方向的扩展,不知道为什么,注释掉这个语句,一样可以正常运行

1.3 显示天空盒

   这里我们讲讲天空盒的着色器.

   最简单,最基本的着色器

   skybox.vert

#version 330 core
layout (location = 0) in vec3 aPos;

uniform mat4 view;
uniform mat4 projection;

out vec3 TexCoords;

void main(){
  TexCoords = aPos;
  gl_Position= projection * view * vec4(aPos, 1.0f);
}

   skybox.frag

#version 330 core

in vec3 TexCoords;
out vec4 FragColor;

uniform samplerCube skybox;

void main(){
  FragColor = texture(skybox, TexCoords);
}

    正如在本文开头所说,CubeMap纹理是使用方向向量进行采样的。所以在片段着色器中,我们只需要传入立方体的顶点数据,从而可顺利进行采样。在顶点着色器中,因为天空盒不再需要model矩阵控制移动或旋转,故干脆直接去掉model矩阵。在这个着色器下的渲染效果图如下所示:

  一个简单的立方体,就像我们的笨方法所做的那样。

优化(1)  修改着色器中view的变量值

  保证着色器,不变,我们修改顶点着色器中view矩阵的变化值,去掉控制矩阵移动的部分,在Vires的代码里,表现为:

glm::mat4 view = glm::mat4(glm::mat3(camera.GetViewMatrix()));

  因为QMatrix4x4 这个Qt的矩阵类不支持这么做,所以我使用以下方法等效替代之。

  QMatrix4x4 skyboxView;
  skyboxView.setRow(0, QVector4D(view(0, 0), view(0, 1), view(0, 2), 0.0f));
  skyboxView.setRow(1, QVector4D(view(1, 0), view(1, 1), view(1, 2), 0.0f));
  skyboxView.setRow(2, QVector4D(view(2, 0), view(2, 1), view(2, 2), 0.0f));
  skyboxView.setRow(3, QVector4D(0.0f,       0.0f,       0.0f,       1.0f)); 
//这个去掉位移的4x4矩阵,使天空盒vertices的尺寸的改变,不再影响渲染效果

  效果如下图所示,天空盒已经固定,不再随着视角的移动而移动,用数学知识解释,这时起主要作用只有projection投影矩阵。

优化(2) 天空盒置于最底层渲染

   本来只要我们在绘制顺序里,我们只要第一个绘制天空盒,剩余绘制的所有物体都会遮住天空盒的片段,但这样做太费劲了,后续绘制的每一个片段都要在深度里与天空盒的片段做一次比较。所以Vries在着色器中将天空盒的深度信息全设为1.0,然后就可以在总绘制顺序中自由绘制天空盒,不再要求必须是第一个绘制,略微提高了一些性能。

修改顶点着色器

skybox.vert

#version 330 core
layout (location = 0) in vec3 aPos;

uniform mat4 view;
uniform mat4 projection;

out vec3 TexCoords;

void main(){
  TexCoords = aPos;
  vec4 pos = projection * view * vec4(aPos, 1.0f);
  gl_Position = pos.xyww;
}

这个pos.xyww特别魔性,一般默认传递值,都是pos.xyzw。而分量(z/w)的值即代表片段的深度值。所以当改将z改为w时,w/w=1.0f,则天空盒所有的片段深度皆为1.0f

在天空盒绘制之前,重新设置深度测试的比较条件。因为深度测试的默认比较条件是小于,而深度换冲的默认值皆为1.0。如果不修改比较条件,天空盒的所有片段均不可能通过测试。

  core->glDepthFunc(GL_LEQUAL);
 //深度换冲默认的值为1.0, 如果不加上小于等于的比较条件,那深度值为1.0的天空盒在小于深度值的条件下永远无法通过深度测试
  ResourceManager::getShader("skybox").use();
  skybox->draw();
  core->glDepthFunc(GL_LESS);

二. 光的反射与折射

  2.1反射

   因为CubeMap由向量采样的特点,很容易可以做出物体反射的效果,详细知识看Vries的教程。

    

简单来说,根据上图这个反射的示意图,使用着色器进行物理模仿。

reflection.vert

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 2) in vec3 aNormal;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

out vec3 Position;
out vec3 Normal;

void main(){
  Normal = mat3(transpose(inverse(model))) * aNormal;
  Position = vec3(model * vec4(aPos, 1.0));
  gl_Position = projection * view * model * vec4(aPos, 1.0f);

}

reflection.frag

#version 330 core
out vec4 FragColor;

in vec3 Position;
in vec3 Normal;

uniform samplerCube skybox;
uniform vec3 cameraPos;

void main(){
  vec3 I = normalize(Position - cameraPos);
  vec3 R = reflect(I, normalize(Normal));
  FragColor = vec4(texture(skybox, R).rgb, 1.0);
}

  其中,片段着色器的reflect()函数为GLSL自带函数。

  2.2 折射

  折射现象在反射的着色器上,仅修改两行代码即可实现,主要在管理器中提前输入折射的折射率ratio。

  refraction.frag

#version 330 core
out vec4 FragColor;

in vec3 Position;
in vec3 Normal;

uniform samplerCube skybox;
uniform vec3 cameraPos;
uniform float ratio;

void main(){
  vec3 I = normalize(Position - cameraPos);
  vec3 R = refract(I, normalize(Normal), ratio);
  FragColor = vec4(texture(skybox, R).rgb, 1.0);
}

  折射率的使用如下所示,如果我们表示光从空气进入玻璃时,则需使用(1.0f/1.52f)作为ratio的参考值。

  /*
   * 折射率:
   * 空气      1.00
   * 水        1.33
   * 冰        1.309
   * 玻璃      1.52
   * 钻石      2.42
  */

猜你喜欢

转载自blog.csdn.net/z136411501/article/details/84332434