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
*/