LearnOpenGL 笔记(四)-高级OpenGL上

目录

一、深度测试

深度测试函数

深度值精度

深度冲突

二、模板测试

模板函数

物体轮廓

 三、混合

丢弃片段

 混合

渲染半透明纹理

不要打乱顺序

四、面剔除

环绕顺序

面剔除

五、帧缓冲-Framebuffer

创建一个帧缓冲

纹理附件

渲染缓冲对象附件

渲染到纹理

七、立方体贴图-天空盒

创建立方体贴图

天空盒

加载天空盒

显示天空盒

优化


一、深度测试

深度缓冲是由窗口系统自动创建的,它会以16、24或32位float的形式储存它的深度值。在大部分的系统中,深度缓冲的精度都是24位的。

深度缓冲是在片段着色器运行之后在屏幕空间中运行的。屏幕空间坐标与OpenGL的glViewport视口密切相关,可以直接使用GLSL内建变量gl_FragCoord从片段着色器中直接访问。gl_FragCoord中也包含了一个z分量,它包含了片段真正的深度值。z值就是需要与深度缓冲内容所对比的那个值。gl_FragCoord的x和y分量代表了片段的屏幕空间坐标(其中(0, 0)位于左下角)。

GPU都提供一个叫做提前深度测试(Early Depth Testing)的硬件特性。提前深度测试允许深度测试在片段着色器之前运行。

深度测试默认是禁用的,所以如果要启用深度测试的话,我们需要用GL_DEPTH_TEST选项来启用它:当它启用的时候,如果一个片段通过了深度测试的话,OpenGL会在深度缓冲中储存该片段的z值;如果没有通过深度缓冲,则会丢弃该片段。

glEnable(GL_DEPTH_TEST);

启用了深度缓冲,还应该在每个渲染迭代之前使用GL_DEPTH_BUFFER_BIT来清除深度缓冲:

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

在某些情况下你会需要对所有片段都执行深度测试并丢弃相应的片段,但希望更新深度缓冲。基本上来说,你在使用一个只读的(Read-only)深度缓冲。 OpenGL允许我们禁用深度缓冲的写入,只需要设置它的深度掩码(Depth Mask)为false。

glDepthMask(GL_FALSE);

深度测试函数

OpenGL允许我们修改深度测试中使用的比较运算符。调用glDepthFunc函数来设置比较运算符。

glDepthFunc(GL_LESS);//默认情况下使用的深度函数是GL_LESS

LearnOpenGL 笔记(四)-高级OpenGL上_第1张图片

模拟我们没有启用深度测试时所得到的结果。深度测试将会永远通过,所以最后绘制的片段将会总是会渲染在之前绘制片段的上面,所以地板覆盖盒子。重新设置为GL_LESS,场景还原。

//在源代码中,我们将深度函数改为GL_ALWAYS:
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_ALWAYS);

LearnOpenGL 笔记(四)-高级OpenGL上_第2张图片        LearnOpenGL 笔记(四)-高级OpenGL上_第3张图片

深度值精度

 深度缓冲包含了一个介于0.0和1.0之间的深度值,它将会与观察者视角所看见的场景中所有物体的z值进行比较。观察空间的z值可能是投影平截头体的近平面(Near)和远平面(Far)之间的任何值。

将这些观察空间的z值线性变换到[0, 1]范围之间:

 z值和对应的深度值之间的关系可以在下图中看到:非常近的物体的深度值设置为接近0.0的值,而当物体非常接近、远平面的时候,它的深度值会非常接近1.0。

LearnOpenGL 笔记(四)-高级OpenGL上_第4张图片

 然而,在实践中是几乎永远不会使用这样的线性深度缓冲(Linear Depth Buffer)的。要想有正确的投影性质,需要使用一个非线性的深度方程,它是与 1/z 成正比的。

 z值和最终的深度缓冲值之间的非线性关系:深度值很大一部分是由很小的z值所决定的,这给了近处的物体很大的深度精度。近处的物体比起远处的物体对深度值有着更大的影响。

LearnOpenGL 笔记(四)-高级OpenGL上_第5张图片

深度冲突

一个很常见的视觉错误会在两个平面或者三角形非常紧密地平行排列在一起时会发生,深度缓冲没有足够的精度来决定两个形状哪个在前面。结果就是这两个形状不断地在切换前后顺序,这会导致很奇怪的花纹。

LearnOpenGL 笔记(四)-高级OpenGL上_第6张图片

 防止深度冲突:不要把多个物体摆得太靠近,通过在两个物体之间设置一个用户无法注意到的偏移值,你可以完全避免这两个物体之间的深度冲突。尽可能将近平面设置远一些,精度在靠近平面时是非常高的。

二、模板测试

当片段着色器处理完一个片段之后,模板测试(Stencil Test)会开始执行,和深度测试一样,它也可能会丢弃片段。接下来,被保留的片段会进入深度测试,它可能会丢弃更多的片段。

一个模板缓冲中,(通常)每个模板值(Stencil Value)是8位的。所以每个像素/片段一共能有256种不同的模板值。我们可以将这些模板值设置为我们想要的值,然后当某一个片段有某一个模板值的时候,我们就可以选择丢弃或是保留这个片段了。

LearnOpenGL 笔记(四)-高级OpenGL上_第7张图片

 模板缓冲首先会被清除为0,之后在模板缓冲中使用1填充了一个空心矩形。场景中的片段将会只在片段的模板值为1的时候会被渲染(其它的都被丢弃了)。

  • 启用模板缓冲的写入。
  • 渲染物体,更新模板缓冲的内容。
  • 禁用模板缓冲的写入。
  • 渲染(其它)物体,这次根据模板缓冲的内容丢弃特定的片段。

启用GL_STENCIL_TEST来启用模板测试:

glEnable(GL_STENCIL_TEST);

在每次迭代之前清除模板缓冲:

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

模板缓冲有个和深度测试的glDepthMask一样的函数,将要写入缓冲的模板值进行与(AND)运算。

glStencilMask(0xFF); // 每一位写入模板缓冲时都保持原样
glStencilMask(0x00); // 每一位在写入模板缓冲时都会变成0(禁用写入)

模板函数

glStencilFunc和glStencilOp

glStencilFunc(GLenum func, GLint ref, GLuint mask)//一共包含三个参数
//func:设置模板测试函数(Stencil Test Function)
//ref:设置了模板测试的参考值(Reference Value)模板缓冲的内容将会与这个值进行比较。
//mask:设置一个掩码,它将会与参考值和储存的模板值在测试比较它们之前进行与(AND)运算。初始情况下所有位都为1

glStencilFunc(GL_EQUAL, 1, 0xFF)
//只要一个片段的模板值等于(GL_EQUAL)参考值1,片段将会通过测试并被绘制,否则会被丢弃

但是glStencilFunc仅仅描述了OpenGL应该对模板缓冲内容做什么,而不是我们应该如何更新缓冲。这就需要glStencilOp这个函数了.

glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)//一共包含三个选项
//sfail:模板测试失败时采取的行为。
//dpfail:模板测试通过,但深度测试失败时采取的行为。
//dppass:模板测试和深度测试都通过时采取的行为。
//默认情况下glStencilOp是设置为(GL_KEEP, GL_KEEP, GL_KEEP)

每个选项都可以选用以下的其中一种行为:

LearnOpenGL 笔记(四)-高级OpenGL上_第8张图片

 默认的行为不会更新模板缓冲,所以如果你想写入模板缓冲的话,你需要至少对其中一个选项设置不同的值。

物体轮廓

为每个(或者一个)物体在它的周围创建一个很小的有色边框:

  1. 在绘制(需要添加轮廓的)物体之前,将模板函数设置为GL_ALWAYS,每当物体的片段被渲染时,将模板缓冲更新为1。
  2. 渲染物体。
  3. 禁用模板写入以及深度测试。
  4. 将每个物体缩放一点点。
  5. 使用一个不同的片段着色器,输出一个单独的(边框)颜色。
  6. 再次绘制物体,但只在它们片段的模板值不等于1时才绘制。
  7. 再次启用模板写入和深度测试。

fs:

void main()
{
    FragColor = vec4(0.04, 0.28, 0.26, 1.0);
}

给那两个箱子加上边框,所以我们让地板不参与这个过程

//首先启用模板测试,并设置测试通过或失败时的行为:
glEnable(GL_STENCIL_TEST);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
//如果其中的一个测试失败了,我们什么都不做,我们仅仅保留当前储存在模板缓冲中的值。如果模板测试和深度测试都通过了,那么我们希望将储存的模板值设置为参考值

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); 

glStencilMask(0x00); // 记得保证我们在绘制地板的时候不会更新模板缓冲
normalShader.use();
DrawFloor()  

//我们将模板缓冲清除为0,对箱子中所有绘制的片段,将模板值更新为1
glStencilFunc(GL_ALWAYS, 1, 0xFF); // 所有的片段都应该更新模板缓冲
glStencilMask(0xFF); // 启用模板缓冲写入
DrawTwoContainers();

//现在模板缓冲在箱子被绘制的地方都更新为1了,我们将要绘制放大的箱子,但这次要禁用模板缓冲的写入
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);//只绘制箱子上模板值不为1的部分
glStencilMask(0x00); // 禁止模板缓冲的写入
glDisable(GL_DEPTH_TEST);
shaderSingleColor.use(); 
DrawTwoScaledUpContainers();
//完成之后重新启用深度缓冲
glStencilMask(0xFF);
glEnable(GL_DEPTH_TEST);  

LearnOpenGL 笔记(四)-高级OpenGL上_第9张图片

 三、混合

OpenGL中,混合(Blending)通常是实现物体透明度(Transparency)的一种技术。透明就是说一个物体(或者其中的一部分)不是纯色(Solid Color)的,它的颜色是物体本身的颜色和它背后其它物体的颜色的不同强度结合。

LearnOpenGL 笔记(四)-高级OpenGL上_第10张图片

 透明的物体可以是完全透明的(让所有的颜色穿过),或者是半透明的(它让颜色通过,同时也会显示自身的颜色)一个物体的透明度是通过它颜色的aplha值来决定的,Alpha颜色值是颜色向量的第四个分量。

丢弃片段

有些图片并不需要半透明,只需要根据纹理颜色值,显示一部分,或者不显示一部分,没有中间情况。比如说草。你需要将一个草的纹理贴在一个2D四边形(Quad)上,然后将这个四边形放到场景中。然而,草的形状和2D四边形的形状并不完全相同,所以你只想显示草纹理的某些部分,而忽略剩下的部分。它要么是完全不透明的(alpha值为1.0),要么是完全透明的(alpha值为0.0)

LearnOpenGL 笔记(四)-高级OpenGL上_第11张图片

 如何加载一个透明的纹理:

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);

//保证你在片段着色器中获取了纹理的全部4个颜色分量,而不仅仅是RGB分量
void main()
{
    // FragColor = vec4(vec3(texture(texture1, TexCoords)), 1.0);
    FragColor = texture(texture1, TexCoords);
}

LearnOpenGL 笔记(四)-高级OpenGL上_第12张图片LearnOpenGL 笔记(四)-高级OpenGL上_第13张图片

 出现这种情况是因为OpenGL默认是不知道怎么处理alpha值的,更不知道什么时候应该丢弃片段。我们需要自己手动来弄。幸运的是,有了着色器,这还是非常容易的。GLSL给了我们discard命令,一旦被调用,它就会保证片段不会被进一步处理,所以就不会进入颜色缓冲。

#version 330 core
out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D texture1;

void main()
{             
    vec4 texColor = texture(texture1, TexCoords);
    if(texColor.a < 0.1)
        discard;//保证片段不会被进一步处理
//检测一个片段的alpha值是否低于某个阈值,如果是的话,则丢弃这个片段
    FragColor = texColor;
}

GL_REPEAT:注意,当采样纹理的边缘的时候,OpenGL会对边缘的值和纹理下一个重复的值进行插值.由于我们使用了透明值,纹理图像的顶部将会与底部边缘的纯色值进行插值。

想避免这个,每当你alpha纹理的时候,请将纹理的环绕方式设置为

glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

 混合

然直接丢弃片段很好,但它不能让我们渲染半透明的图像。我们要么渲染一个片段,要么完全丢弃它。要想渲染有多个透明度级别的图像,我们需要启用混合(Blending)。

glEnable(GL_BLEND);

OpenGL中的混合是通过下面这个方程来实现的:

LearnOpenGL 笔记(四)-高级OpenGL上_第14张图片

 片段着色器运行完成后,并且所有的测试都通过之后,这个混合方程(Blend Equation)才会应用到片段颜色输出与当前颜色缓冲中的值(当前片段之前储存的之前片段的颜色)上。

LearnOpenGL 笔记(四)-高级OpenGL上_第15张图片LearnOpenGL 笔记(四)-高级OpenGL上_第16张图片

 我们有两个方形,我们希望将这个半透明的绿色方形绘制在红色方形之上。红色的方形将会是目标颜色。它应该先在颜色缓冲中。如果绿色方形对最终颜色贡献了60%,那么红色方块应该对最终颜色贡献了40%。最终的颜色将会被储存到颜色缓冲中,替代之前的颜色。

LearnOpenGL 笔记(四)-高级OpenGL上_第17张图片

 glBlendFunc:glBlendFunc(GLenum sfactor, GLenum dfactor)函数接受两个参数,来设置源和目标因子。

//使用源颜色向量的alpha作为源因子使用1−alpha作为目标因子
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

//也可以使用glBlendFuncSeparate为RGB和alpha通道分别设置不同的选项
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO);
这个函数和我们之前设置的那样设置了RGB分量,但这样只能让最终的alpha分量被源颜色向量的alpha值所影响到。

渲染半透明纹理

//在初始化时我们启用混合,并设定相应的混合函数
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

片段着色器
#version 330 core
out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D texture1;

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

它都会将当前片段的颜色和当前颜色缓冲中的片段颜色根据alpha值来进行混合。由于窗户纹理的玻璃部分是半透明的,我们应该能通窗户中看到背后的场景了。

LearnOpenGL 笔记(四)-高级OpenGL上_第18张图片LearnOpenGL 笔记(四)-高级OpenGL上_第19张图片

 最前面窗户的透明部分遮蔽了背后的窗户?深度测试和混合一起使用的话会产生一些麻烦.当写入深度缓冲时,深度缓冲不会检查片段是否是透明的,所以透明的部分会和其它值一样写入到深度缓冲中。

不要打乱顺序

要想让混合在多个物体上工作,我们需要最先绘制最远的物体,最后绘制最近的物体。普通不需要混合的物体仍然可以使用深度缓冲正常绘制,所以它们不需要排序。

当绘制一个有不透明和透明物体的场景的时候,大体的原则如下:

  1. 先绘制所有不透明的物体。
  2. 对所有透明的物体排序。(从观察者视角获取物体的距离)
  3. 按顺序绘制所有透明的物体。

四、面剔除

任何一个闭合形状,它的每一个面都有两侧,每一侧要么面向用户,要么背对用户。我们能够只绘制面向观察者的面。OpenGL能够检查所有面向(Front Facing)观察者的面,并渲染它们,而丢弃那些背向(Back Facing)的面,节省我们很多的片段着色器调用。

环绕顺序

逆时针顶点所定义的三角形将会被处理为正向三角形。

LearnOpenGL 笔记(四)-高级OpenGL上_第20张图片LearnOpenGL 笔记(四)-高级OpenGL上_第21张图片

在顶点数据中,我们定义的是两个逆时针顺序的三角形。然而,从观察者的方面看,后面的三角形是顺时针的。 

float vertices[] = {
    // 顺时针
    vertices[0], // 顶点1
    vertices[1], // 顶点2
    vertices[2], // 顶点3
    // 逆时针
    vertices[0], // 顶点1
    vertices[2], // 顶点3
    vertices[1]  // 顶点2  
};
//OpenGL在渲染图元的时候将使用这个信息来决定一个三角形是一个正向三角形还是背向三角形
//逆时针顶点所定义的三角形将会被处理为正向三角形。

面剔除

OpenGL能够丢弃那些渲染为背向三角形的三角形图元,需要启用OpenGL的GL_CULL_FACE选项:从这一句代码之后,所有背向面都将被丢弃

glEnable(GL_CULL_FACE);

目前我们在渲染片段的时候能够节省50%以上的性能,但注意这只对像立方体这样的封闭形状有效。OpenGL允许我们改变需要剔除的面的类型。除了需要剔除的面之外,我们也可以通过调用glFrontFace,告诉OpenGL我们希望将顺时针的面(而不是逆时针的面)定义为正向面 。

glCullFace(GL_FRONT);//初始值是GL_BACK
//glCullFace函数有三个可用的选项

GL_BACK:只剔除背向面。
GL_FRONT:只剔除正向面。
GL_FRONT_AND_BACK:剔除正向面和背向面。

glFrontFace(GL_CCW);

//默认值是GL_CCW,它代表的是逆时针的环绕顺序,另一个选项是GL_CW代表的是顺时针顺序

五、帧缓冲-Framebuffer

创建一个帧缓冲

//创建一个帧缓冲对象(
unsigned int fbo;
glGenFramebuffers(1, &fbo);

//使用glBindFramebuffer来绑定帧缓冲
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
//读取和写入帧缓冲的操作将会影响当前绑定的帧缓冲

也可以使用GL_READ_FRAMEBUFFER或GL_DRAW_FRAMEBUFFER,将一个帧缓冲分别绑定到读取目标或写入目标。

一个完整的帧缓冲需要满足以下的条件:

  • 附加至少一个缓冲(颜色、深度或模板缓冲)。
  • 至少有一个颜色附件(Attachment)。
  • 所有的附件都必须是完整的(保留了内存)。
  • 每个缓冲都应该有相同的样本数。

我们需要为帧缓冲创建一些附件,并将附件附加到帧缓冲上.可以以GL_FRAMEBUFFER为参数调用glCheckFramebufferStatus,检查帧缓冲是否完整,之后所有的渲染操作将会渲染到当前绑定帧缓冲的附件中。

if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)

由于我们的帧缓冲不是默认帧缓冲,渲染指令将不会对窗口的视觉输出有任何影响。渲染到一个不同的帧缓冲被叫做离屏渲染(Off-screen Rendering)。

//我们需要再次激活默认帧缓冲,将它绑定到0
glBindFramebuffer(GL_FRAMEBUFFER, 0);

//在完成所有的帧缓冲操作之后,不要忘记删除这个帧缓冲对象
glDeleteFramebuffers(1, &fbo);

纹理附件

当把一个纹理附加到帧缓冲的时候,所有的渲染指令将会写入到这个纹理中。使用纹理的优点是,所有渲染操作的结果将会被储存在一个纹理图像中,我们之后可以在着色器中很方便地使用它。

//为帧缓冲创建一个纹理
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
//将维度设置为了屏幕大小
//给纹理的data参数传递了NULL
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

附加到帧缓冲上:
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
//帧缓冲的目标
//我们想要附加的附件类型

//纹理类型
//纹理本身
//多级渐远纹理的级别

如果你想将你的屏幕渲染到一个更小或更大的纹理上,你需要(在渲染到你的帧缓冲之前)再次调用glViewport。

除了颜色附件之外,我们还可以附加一个深度和模板缓冲纹理到帧缓冲对象中:

  • GL_DEPTH_ATTACHMENT
  • GL_STENCIL_ATTACHMENT

将一个深度和模板缓冲附加为一个纹理到帧缓冲:

glTexImage2D(
  GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, 800, 600, 0, 
  GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, NULL
);

glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, texture, 0);

渲染缓冲对象附件

渲染缓冲对象直接将所有的渲染数据储存到它的缓冲中,不会做任何针对纹理格式的转换,让它变为一个更快的可写储存介质。通常都是只写的,所以你不能读取它们(glReadPixels)。

//创建一个渲染缓冲对象
unsigned int rbo;
glGenRenderbuffers(1, &rbo);

//绑定这个渲染缓冲对象
glBindRenderbuffer(GL_RENDERBUFFER, rbo);

//创建一个深度和模板渲染缓冲对象
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);
//GL_DEPTH24_STENCIL8作为内部格式,它封装了24位的深度和8位的模板缓冲。

//附加这个渲染缓冲对象
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);

渲染到纹理

// 第一处理阶段(Pass)
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 我们现在不使用模板缓冲
glEnable(GL_DEPTH_TEST);
DrawScene();    

// 第二处理阶段
glBindFramebuffer(GL_FRAMEBUFFER, 0); // 返回默认
glClearColor(1.0f, 1.0f, 1.0f, 1.0f); 
glClear(GL_COLOR_BUFFER_BIT);

screenShader.use();  
glBindVertexArray(quadVAO);
glDisable(GL_DEPTH_TEST);
glBindTexture(GL_TEXTURE_2D, textureColorbuffer);
glDrawArrays(GL_TRIANGLES, 0, 6);  

LearnOpenGL 笔记(四)-高级OpenGL上_第22张图片

七、立方体贴图-天空盒

立方体贴图就是一个包含了6个2D纹理的纹理,每个2D纹理都组成了立方体的一个面:一个有纹理的立方体。

LearnOpenGL 笔记(四)-高级OpenGL上_第23张图片

 立方体贴图有一个非常有用的特性,它可以通过一个方向向量来进行索引/采样。假设我们有一个1x1x1的单位立方体,方向向量的原点位于它的中心。只要立方体的中心位于原点,我们就能使用立方体的实际位置向量来对立方体贴图进行采样了。

创建立方体贴图

//生成一个纹理,并将其绑定到纹理目标上
unsigned int textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);

//包含有6个纹理,每个面一个,我们需要调用glTexImage2D函数6次
//将纹理目标(target)参数设置为立方体贴图的一个特定的面
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
    );
}

//设定它的环绕和过滤方式
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);

LearnOpenGL 笔记(四)-高级OpenGL上_第24张图片

 在片段着色器中,我们使用了一个不同类型的采样器,samplerCube,我们将使用texture函数使用它进行采样,但这次我们将使用一个vec3的方向向量而不是vec2

in vec3 textureDir; // 代表3D纹理坐标的方向向量
uniform samplerCube cubemap; // 立方体贴图的纹理采样器

void main()
{             
    FragColor = texture(cubemap, textureDir);
}

天空盒

天空盒是一个包含了整个场景的(大)立方体,它包含周围环境的6个图像,让玩家以为他处在一个比实际大得多的环境当中。

这个网站就提供了很多天空盒,如果你将这六个面折成一个立方体,你就会得到一个完全贴图的立方体,模拟一个巨大的场景。

LearnOpenGL 笔记(四)-高级OpenGL上_第25张图片

加载天空盒

unsigned int loadCubemap(vector faces)//它接受一个包含6个纹理路径的vector
{
    unsigned int textureID;
    glGenTextures(1, &textureID);
    glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);

    int width, height, nrChannels;
    for (unsigned int i = 0; i < faces.size(); i++)
    {
        unsigned char *data = stbi_load(faces[i].c_str(), &width, &height, &nrChannels, 0);
        if (data)
        {
            glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 
                         0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data
            );
            stbi_image_free(data);
        }
        else
        {
            std::cout << "Cubemap texture failed to load at path: " << faces[i] << std::endl;
            stbi_image_free(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);

    return textureID;
}

//在调用这个函数之前,我们需要将合适的纹理路径按照立方体贴图枚举指定的顺序加载到一个vector中
vector faces
{
    "right.jpg",
    "left.jpg",
    "top.jpg",
    "bottom.jpg",
    "front.jpg",
    "back.jpg"
};
unsigned int cubemapTexture = loadCubemap(faces);

显示天空盒

由于天空盒是绘制在一个立方体上的,和其它物体一样,我们需要另一个VAO、VBO以及新的一组顶点。

当立方体处于原点(0, 0, 0)时,它的每一个位置向量都是从原点出发的方向向量。这个方向向量正是获取立方体上特定位置的纹理值所需要的。正是因为这个,我们只需要提供位置向量而不用纹理坐标了。

#version 330 core
out vec4 FragColor;

in vec3 TexCoords;

uniform samplerCube skybox;

void main()
{    
    FragColor = texture(skybox, TexCoords);
}
//我们将顶点属性的位置向量作为纹理的方向向量,并使用它从立方体贴图中采样纹理值

绘制天空盒时,我们需要将它变为场景中的第一个渲染的物体,并且禁用深度写入。天空盒就会永远被绘制在其它物体的背后了。

glDepthMask(GL_FALSE);
skyboxShader.use();
// ... 设置观察和投影矩阵
//
glm::mat4 view = glm::mat4(glm::mat3(camera.GetViewMatrix()));
//将观察矩阵转换为3x3矩阵(移除位移),再将其转换回4x4矩阵
//让移动不会影响天空盒的位置向量

glBindVertexArray(skyboxVAO);
glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture);
glDrawArrays(GL_TRIANGLES, 0, 36);
glDepthMask(GL_TRUE);
// ... 绘制剩下的场景

优化

提前深度测试(Early Depth Testing)-我们将会最后渲染天空盒,以获得轻微的性能提升。

透视除法是在顶点着色器运行之后执行的,将gl_Position的xyz坐标除以w分量。相除结果的z分量等于顶点的深度值。

void main()
{
    TexCoords = aPos;
    vec4 pos = projection * view * vec4(aPos, 1.0);
    gl_Position = pos.xyww;
//将输出位置的z分量等于它的w分量,让z分量永远等于1.0,这样子的话,当透视除法执行之后,z分量会变为w / w = 1.0
}

结果就是天空盒只会在没有可见物体的地方渲染了,我们还要改变一下深度函数,将它从默认的GL_LESS改为GL_LEQUAL。

你可能感兴趣的:(OpenGL,游戏引擎,c++,几何学,opengl)