================================ 前序===============================
AndroidLearnOpenGL是本博主自己实现的LearnOpenGL练习集合:
Github地址:https://github.com/wangyongyao1989/AndroidLearnOpenGL
系列文章:
7、LearnOpenGL之高级OpenGL(1)
一、深度测试:
深度缓冲就像颜色缓冲(Color Buffer)(储存所有的片段颜色:视觉输出)一样,在每个片段中储存了信息,并且(通常)和颜色缓冲有着一样的宽度和高度。深度缓冲是由窗口系统自动创建的,它会以16、24或32位float的形式储存它的深度值。在大部分的系统中,深度缓冲的精度都是24位的。
当深度测试(Depth Testing)被启用的时候,OpenGL会将一个片段的深度值与深度缓冲的内容进行对比。OpenGL会执行一个深度测试,如果这个测试通过了的话,深度缓冲将会更新为新的深度值。如果深度测试失败了,片段将会被丢弃。
深度缓冲是在片段着色器运行之后在屏幕空间中运行的。屏幕空间坐标与通过OpenGL的glViewport所定义的视口密切相关,并且可以直接使用GLSL内建变量gl_FragCoord从片段着色器中直接访问。gl_FragCoord的x和y分量代表了片段的屏幕空间坐标(其中(0, 0)位于左下角)。gl_FragCoord中也包含了一个z分量,它包含了片段真正的深度值。z值就是需要与深度缓冲内容所对比的那个值。
深度测试默认是禁用的,所以如果要启用深度测试的话,我们需要用GL_DEPTH_TEST选项来启用它:
glEnable(GL_DEPTH_TEST);
当它启用的时候,如果一个片段通过了深度测试的话,OpenGL会在深度缓冲中储存该片段的z值;如果没有通过深度缓冲,则会丢弃该片段。如果你启用了深度缓冲,你还应该在每个渲染迭代之前使用GL_DEPTH_BUFFER_BIT来清除深度缓冲,否则你会仍在使用上一次渲染迭代中的写入的深度值:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
可以想象,在某些情况下你会需要对所有片段都执行深度测试并丢弃相应的片段,但不希望更新深度缓冲。基本上来说,你在使用一个只读的(Read-only)深度缓冲。OpenGL允许我们禁用深度缓冲的写入,只需要设置它的深度掩码(Depth Mask)设置为GL_FALSE
就可以了:
glDepthMask(GL_FALSE);
深度测试函数:
OpenGL允许我们修改深度测试中使用的比较运算符。这允许我们来控制OpenGL什么时候该通过或丢弃一个片段,什么时候去更新深度缓冲。我们可以调用glDepthFunc函数来设置比较运算符(或者说深度函数(Depth Function)):
glDepthFunc(GL_LESS);
深度值精度:
深度缓冲包含了一个介于0.0和1.0之间的深度值,它将会与观察者视角所看见的场景中所有物体的z值进行比较。观察空间的z值可能是投影平截头体的近平面(Near)和远平面(Far)之间的任何值。我们需要一种方式来将这些观察空间的z值变换到[0, 1]范围之间,其中的一种方式就是将它们线性变换到[0, 1]范围之间。下面这个(线性)方程将z值变换到了0.0到1.0之间的深度值:
这里的nearnear和farfar值是我们之前提供给投影矩阵设置可视平截头体的(见坐标系统)那个 near 和 far 值。这个方程需要平截头体中的一个z值,并将它变换到了[0, 1]的范围中。z值和对应的深度值之间的关系可以在下图中看到:
然而,在实践中是几乎永远不会使用这样的线性深度缓冲(Linear Depth Buffer)的。要想有正确的投影性质,需要使用一个非线性的深度方程,它是与 1/z 成正比的。它做的就是在z值很小的时候提供非常高的精度,而在z值很远的时候提供更少的精度。
由于非线性方程与 1/z 成正比,在1.0和2.0之间的z值将会变换至1.0到0.5之间的深度值,这就是一个float提供给我们的一半精度了,这在z值很小的情况下提供了非常大的精度。在50.0和100.0之间的z值将会只占2%的float精度,这正是我们所需要的。这样的一个考虑了远近距离的方程是这样的:
下图中看到z值和最终的深度缓冲值之间的非线性关系:
深度值很大一部分是由很小的z值所决定的,这给了近处的物体很大的深度精度。这个(从观察者的视角)变换z值的方程是嵌入在投影矩阵中的,所以当我们想将一个顶点坐标从观察空间至裁剪空间的时候这个非线性方程就被应用了。如果你想深度了解投影矩阵究竟做了什么,我建议阅读这篇文章。
深度缓冲的可视化:
我们知道片段着色器中,内建gl_FragCoord向量的z值包含了那个特定片段的深度值。如果我们将这个深度值输出为颜色,我们可以显示场景中所有片段的深度值。我们可以根据片段的深度值返回一个颜色向量来完成这一工作:
void main()
{
FragColor = vec4(vec3(gl_FragCoord.z), 1.0);
}
将屏幕空间中非线性的深度值变换至线性深度值的完整片段着色器如下:
#version 330 core
out vec4 FragColor;
float near = 0.1;
float far = 100.0;
float LinearizeDepth(float depth)
{
float z = depth * 2.0 - 1.0; // back to NDC
return (2.0 * near * far) / (far + near - z * (far - near));
}
void main()
{
float depth = LinearizeDepth(gl_FragCoord.z) / far; // 为了演示除以 far
FragColor = vec4(vec3(depth), 1.0);
}
深度冲突:
一个很常见的视觉错误会在两个平面或者三角形非常紧密地平行排列在一起时会发生,深度缓冲没有足够的精度来决定两个形状哪个在前面。结果就是这两个形状不断地在切换前后顺序,这会导致很奇怪的花纹。这个现象叫做深度冲突(Z-fighting),因为它看起来像是这两个形状在争夺(Fight)谁该处于顶端。
防止深度冲突:
- 第一个也是最重要的技巧是永远不要把多个物体摆得太靠近,以至于它们的一些三角形会重叠。通过在两个物体之间设置一个用户无法注意到的偏移值,你可以完全避免这两个物体之间的深度冲突。
- 第二个技巧是尽可能将近平面设置远一些。在前面我们提到了精度在靠近近平面时是非常高的,所以如果我们将近平面远离观察者,我们将会对整个平截头体有着更大的精度。然而,将近平面设置太远将会导致近处的物体被裁剪掉,所以这通常需要实验和微调来决定最适合你的场景的近平面距离。
- 另外一个很好的技巧是牺牲一些性能,使用更高精度的深度缓冲。大部分深度缓冲的精度都是24位的,但现在大部分的显卡都支持32位的深度缓冲,这将会极大地提高精度。所以,牺牲掉一些性能,你就能获得更高精度的深度测试,减少深度冲突。
二、模版测试:
当片段着色器处理完一个片段之后,模板测试(Stencil Test)会开始执行,和深度测试一样,它也可能会丢弃片段。接下来,被保留的片段会进入深度测试,它可能会丢弃更多的片段。模板测试是根据又一个缓冲来进行的,它叫做模板缓冲(Stencil Buffer),我们可以在渲染的时候更新它来获得一些很有意思的效果。
一个模板缓冲中,(通常)每个模板值(Stencil Value)是8位的。所以每个像素/片段一共能有256种不同的模板值。我们可以将这些模板值设置为我们想要的值,然后当某一个片段有某一个模板值的时候,我们就可以选择丢弃或是保留这个片段了。
模板缓冲操作允许我们在渲染片段时将模板缓冲设定为一个特定的值。通过在渲染时修改模板缓冲的内容,我们写入了模板缓冲。在同一个(或者接下来的)渲染迭代中,我们可以读取这些值,来决定丢弃还是保留某个片段。使用模板缓冲的时候你可以尽情发挥,但大体的步骤如下:
- 启用模板缓冲的写入。
- 渲染物体,更新模板缓冲的内容。
- 禁用模板缓冲的写入。
- 渲染(其它)物体,这次根据模板缓冲的内容丢弃特定的片段。
可以启用GL_STENCIL_TEST来启用模板测试。在这一行代码之后,所有的渲染调用都会以某种方式影响着模板缓冲。
glEnable(GL_STENCIL_TEST);
注意,和颜色和深度缓冲一样,你也需要在每次迭代之前清除模板缓冲。
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
和深度测试的glDepthMask函数一样,模板缓冲也有一个类似的函数。glStencilMask允许我们设置一个位掩码(Bitmask),它会与将要写入缓冲的模板值进行与(AND)运算。默认情况下设置的位掩码所有位都为1,不影响输出,但如果我们将它设置为0x00
,写入缓冲的所有模板值最后都会变成0.这与深度测试中的glDepthMask(GL_FALSE)是等价的。
glStencilMask(0xFF); // 每一位写入模板缓冲时都保持原样
glStencilMask(0x00); // 每一位在写入模板缓冲时都会变成0(禁用写入)
模版函数:
和深度测试一样,我们对模板缓冲应该通过还是失败,以及它应该如何影响模板缓冲,也是有一定控制的。一共有两个函数能够用来配置模板测试:glStencilFunc和glStencilOp。
glStencilFunc:
仅仅描述了OpenGL应该对模板缓冲内容做什么。
glStencilFunc(GLenum func, GLint ref, GLuint mask)
包含三个参数:
- func:设置模板测试函数(Stencil Test Function)。这个测试函数将会应用到已储存的模板值上和glStencilFunc函数的
ref
值上。可用的选项有:GL_NEVER、GL_LESS、GL_LEQUAL、GL_GREATER、GL_GEQUAL、GL_EQUAL、GL_NOTEQUAL和GL_ALWAYS。它们的语义和深度缓冲的函数类似。 - ref:设置了模板测试的参考值(Reference Value)。模板缓冲的内容将会与这个值进行比较。
- mask:设置一个掩码,它将会与参考值和储存的模板值在测试比较它们之前进行与(AND)运算。初始情况下所有位都为1。
glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)
glStencilOp:
如何更新缓冲。
glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)
包含三个选项:
- sfail:模板测试失败时采取的行为。
- dpfail:模板测试通过,但深度测试失败时采取的行为。
- dppass:模板测试和深度测试都通过时采取的行为。
每个选项都可以选用以下的其中一种行为:
物体轮廓:
物体轮廓所能做的事情正如它名字所描述的那样。我们将会为每个(或者一个)物体在它的周围创建一个很小的有色边框。当你想要在策略游戏中选中一个单位进行操作的,想要告诉玩家选中的是哪个单位的时候,这个效果就非常有用了。为物体创建轮廓的步骤如下:
- 在绘制(需要添加轮廓的)物体之前,将模板函数设置为GL_ALWAYS,每当物体的片段被渲染时,将模板缓冲更新为1。
- 渲染物体。
- 禁用模板写入以及深度测试。
- 将每个物体缩放一点点。
- 使用一个不同的片段着色器,输出一个单独的(边框)颜色。
- 再次绘制物体,但只在它们片段的模板值不等于1时才绘制。
- 再次启用模板写入和深度测试。
场景中物体轮廓的完整步骤会看起来像这样:
glEnable(GL_DEPTH_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()
glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilMask(0xFF);
DrawTwoContainers();
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00);
glDisable(GL_DEPTH_TEST);
shaderSingleColor.use();
DrawTwoScaledUpContainers();
glStencilMask(0xFF);
glEnable(GL_DEPTH_TEST);
三、混合:
OpenGL中,混合(Blending)通常是实现物体透明度(Transparency)的一种技术。透明就是说一个物体(或者其中的一部分)不是纯色(Solid Color)的,它的颜色是物体本身的颜色和它背后其它物体的颜色的不同强度结合。一个有色玻璃窗是一个透明的物体,玻璃有它自己的颜色,但它最终的颜色还包含了玻璃之后所有物体的颜色。这也是混合这一名字的出处,我们混合(Blend)(不同物体的)多种颜色为一种颜色。所以透明度能让我们看穿物体。
一个物体的透明度是通过它颜色的alpha值来决定的。Alpha颜色值是颜色向量的第四个分量。
丢弃片段:
有些图片并不需要半透明,只需要根据纹理颜色值,显示一部分,或者不显示一部分,没有中间情况。想要丢弃(Discard)显示纹理中透明部分的片段,不将这些片段存储到颜色缓冲中。
GLSL给了我们discard
命令,一旦被调用,它就会保证片段不会被进一步处理,所以就不会进入颜色缓冲。有了这个指令,我们就能够在片段着色器中检测一个片段的alpha值是否低于某个阈值,如果是的话,则丢弃这个片段,就好像它不存在一样:
#version 320 es
precision mediump float;
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D texture1;
void main()
{
vec4 texColor = texture(texture1, TexCoords);
//discard命令,一旦被调用,它就会保证片段不会被进一步处理,所以就不会进入颜色缓冲。
if(texColor.a < 0.1)
discard;
FragColor = texture(texture1, TexCoords);
}
混合:
要想渲染有多个透明度级别的图像,我们需要启用混合(Blending)。和OpenGL大多数的功能一样,我们可以启用GL_BLEND来启用混合:
glEnable(GL_BLEND);
OpenGL中的混合是通过下面这个方程来实现的:
这个混合方程(Blend Equation)才会应用到片段颜色输出与当前颜色缓冲中的值(当前片段之前储存的之前片段的颜色)上。源颜色和目标颜色将会由OpenGL自动设定,但源因子和目标因子的值可以由我们来决定。
glBlendFunc(GLenum sfactor, GLenum dfactor)函数接受两个参数,来设置源和目标因子。OpenGL为我们定义了很多个选项,我们将在下面列出大部分最常用的选项。注意常数颜色向量C¯constantC¯constant可以通过glBlendColor函数来另外设置。
OpenGL甚至给了我们更多的灵活性,允许我们改变方程中源和目标部分的运算符。当前源和目标是相加的,但如果愿意的话,我们也可以让它们相减。glBlendEquation(GLenum mode)允许我们设置运算符,它提供了三个选项:
渲染半透明纹理:
在初始化时我们启用混合,并设定相应的混合函数:
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
要想让混合在多个物体上工作,我们需要最先绘制最远的物体,最后绘制最近的物体。普通不需要混合的物体仍然可以使用深度缓冲正常绘制,所以它们不需要排序。但我们仍要保证它们在绘制(排序的)透明物体之前已经绘制完毕了。当绘制一个有不透明和透明物体的场景的时候,大体的原则如下:
- 先绘制所有不透明的物体。
- 对所有透明的物体排序。
- 按顺序绘制所有透明的物体。
排序透明物体的一种方法是,从观察者视角获取物体的距离。这可以通过计算摄像机位置向量和物体的位置向量之间的距离所获得。接下来我们把距离和它对应的位置向量存储到一个STL库的map数据结构中。map会自动根据键值(Key)对它的值排序,所以只要我们添加了所有的位置,并以它的距离作为键,它们就会自动根据距离值排序了。
std::map<float, glm::vec3> sorted;
for (unsigned int i = 0; i < windows.size(); i++)
{
float distance = glm::length(camera.Position - windows[i]);
sorted[distance] = windows[i];
}
结果就是一个排序后的容器对象,它根据distance键值从低到高储存了每个窗户的位置。
之后,这次在渲染的时候,我们将以逆序(从远到近)从map中获取值,之后以正确的顺序绘制对应的窗户:
for(std::map<float,glm::vec3>::reverse_iterator it = sorted.rbegin();
it != sorted.rend(); ++it)
{
model = glm::mat4();
model = glm::translate(model, it->second);
shader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 6);
}
四、面剔除:
如果我们想象任何一个闭合形状,它的每一个面都有两侧,每一侧要么面向用户,要么背对用户。如果我们能够只绘制面向观察者的面呢?
这正是面剔除(Face Culling)所做的。OpenGL能够检查所有面向(Front Facing)观察者的面,并渲染它们,而丢弃那些背向(Back Facing)的面,节省我们很多的片段着色器调用(它们的开销很大!)。但我们仍要告诉OpenGL哪些面是正向面(Front Face),哪些面是背向面(Back Face)。OpenGL使用了一个很聪明的技巧,分析顶点数据的环绕顺序(Winding Order)。
环绕顺序:
当我们定义一组三角形顶点时,我们会以特定的环绕顺序来定义它们,可能是顺时针(Clockwise)的,也可能是逆时针(Counter-clockwise)的。每个三角形由3个顶点所组成,我们会从三角形中间来看,为这3个顶点设定一个环绕顺序。
定义了顶点1,之后我们可以选择定义顶点2或者顶点3,这个选择将定义了这个三角形的环绕顺序。下面的代码展示了这点:
float vertices[] = {
// 顺时针
vertices[0], // 顶点1
vertices[1], // 顶点2
vertices[2], // 顶点3
// 逆时针
vertices[0], // 顶点1
vertices[2], // 顶点3
vertices[1] // 顶点2
};
每组组成三角形图元的三个顶点就包含了一个环绕顺序。OpenGL在渲染图元的时候将使用这个信息来决定一个三角形是一个正向三角形还是背向三角形。默认情况下,逆时针顶点所定义的三角形将会被处理为正向三角形。
当你定义顶点顺序的时候,你应该想象对应的三角形是面向你的,所以你定义的三角形从正面看去应该是逆时针的。这样定义顶点很棒的一点是,实际的环绕顺序是在光栅化阶段进行的,也就是顶点着色器运行之后。这些顶点就是从观察者视角所见的了。
观察者所面向的所有三角形顶点就是我们所指定的正确环绕顺序了,而立方体另一面的三角形顶点则是以相反的环绕顺序所渲染的。这样的结果就是,我们所面向的三角形将会是正向三角形,而背面的三角形则是背向三角形。下面这张图显示了这个效果:
在顶点数据中,我们将两个三角形都以逆时针顺序定义(正面的三角形是1、2、3,背面的三角形也是1、2、3(如果我们从正面看这个三角形的话))。然而,如果从观察者当前视角使用1、2、3的顺序来绘制的话,从观察者的方向来看,背面的三角形将会是以顺时针顺序渲染的。虽然背面的三角形是以逆时针定义的,它现在是以顺时针顺序渲染的了。这正是我们想要剔除(Cull,丢弃)的不可见面了。
面剔除
OpenGL能够丢弃那些渲染为背向三角形的三角形图元。既然已经知道如何设置顶点的环绕顺序了,我们就可以使用OpenGL的面剔除选项了,它默认是禁用状态的。
确认在每个三角形中它们都是以逆时针定义的,这是一个很好的习惯。
要想启用面剔除,我们只需要启用OpenGL的GL_CULL_FACE选项:
glEnable(GL_CULL_FACE);
glCullFace函数有三个可用的选项:
- GL_BACK:只剔除背向面。
- GL_FRONT:只剔除正向面。
- GL_FRONT_AND_BACK:剔除正向面和背向面。
面剔除是一个提高OpenGL程序性能的很棒的工具。但你需要记住哪些物体能够从面剔除中获益,而哪些物体不应该被剔除。
五、帧缓冲:
用于写入颜色值的颜色缓冲、用于写入深度信息的深度缓冲和允许我们根据一些条件丢弃特定片段的模板缓冲。这些缓冲结合起来叫做帧缓冲(Framebuffer),它被储存在GPU内存中的某处。OpenGL允许我们定义我们自己的帧缓冲,也就是说我们能够定义我们自己的颜色缓冲,甚至是深度缓冲和模板缓冲。
帧缓冲的创建:
和OpenGL中的其它对象一样,我们会使用一个叫做glGenFramebuffers的函数来创建一个帧缓冲对象(Framebuffer Object, FBO):
unsigned int fbo;
glGenFramebuffers(1, &fbo);
首先我们创建一个帧缓冲对象,将它绑定为激活的(Active)帧缓冲,做一些操作,之后解绑帧缓冲。我们使用glBindFramebuffer来绑定帧缓冲。
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
在绑定到GL_FRAMEBUFFER目标之后,所有的读取和写入帧缓冲的操作将会影响当前绑定的帧缓冲。我们也可以使用GL_READ_FRAMEBUFFER或GL_DRAW_FRAMEBUFFER,将一个帧缓冲分别绑定到读取目标或写入目标。绑定到GL_READ_FRAMEBUFFER的帧缓冲将会使用在所有像是glReadPixels的读取操作中,而绑定到GL_DRAW_FRAMEBUFFER的帧缓冲将会被用作渲染、清除等写入操作的目标。大部分情况你都不需要区分它们,通常都会使用GL_FRAMEBUFFER,绑定到两个上。
完整的帧缓冲条件:
- 附加至少一个缓冲(颜色、深度或模板缓冲)。
- 至少有一个颜色附件(Attachment)。
- 所有的附件都必须是完整的(保留了内存)。
- 每个缓冲都应该有相同的样本数(sample)。
帧缓冲操作:
为帧缓冲创建一些附件,并将附件附加到帧缓冲上。在完成所有的条件之后,我们可以以GL_FRAMEBUFFER为参数调用glCheckFramebufferStatus,检查帧缓冲是否完整。它将会检测当前绑定的帧缓冲,并返回规范中这些值的其中之一。如果它返回的是GL_FRAMEBUFFER_COMPLETE,帧缓冲就是完整的了。
//帧缓冲是否完整
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);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
给纹理的data
参数传递了NULL
。对于这个纹理,我们仅仅分配了内存而没有填充它。填充这个纹理将会在我们渲染到帧缓冲之后来进行。同样注意我们并不关心环绕方式或多级渐远纹理,我们在大多数情况下都不会需要它们。
现在我们已经创建好一个纹理了,要做的最后一件事就是将它附加到帧缓冲上了:
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0
, GL_TEXTURE_2D, texture, 0);
glFrameBufferTexture2D有以下的参数:
- target:帧缓冲的目标(绘制、读取或者两者皆有)。
- attachment:我们想要附加的附件类型。当前我们正在附加一个颜色附件。注意最后的
0
意味着我们可以附加多个颜色附件。 - textarget:希望附加的纹理类型。
- texture:要附加的纹理本身。
- level:多级渐远纹理的级别。我们将它保留为0。
除了颜色附件之外,我们还可以附加一个深度和模板缓冲纹理到帧缓冲对象中。要附加深度缓冲的话,我们将附件类型设置为GL_DEPTH_ATTACHMENT。注意纹理的格式(Format)和内部格式(Internalformat)类型将变为GL_DEPTH_COMPONENT,来反映深度缓冲的储存格式。要附加模板缓冲的话,你要将第二个参数设置为GL_STENCIL_ATTACHMENT,并将纹理的格式设定为GL_STENCIL_INDEX。
也可以将深度缓冲和模板缓冲附加为一个单独的纹理。纹理的每32位数值将包含24位的深度信息和8位的模板信息。要将深度和模板缓冲附加为一个纹理的话,我们使用GL_DEPTH_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);
渲染缓冲对象附件:
渲染缓冲对象(Renderbuffer Object)是在纹理之后引入到OpenGL中,作为一个可用的帧缓冲附件类型的,所以在过去纹理是唯一可用的附件。和纹理图像一样,渲染缓冲对象是一个真正的缓冲,即一系列的字节、整数、像素等。渲染缓冲对象附加的好处是,它会将数据储存为OpenGL原生的渲染格式,它是为离屏渲染到帧缓冲优化过的。
渲染缓冲对象直接将所有的渲染数据储存到它的缓冲中,不会做任何针对纹理格式的转换,让它变为一个更快的可写储存介质。然而,渲染缓冲对象通常都是只写的,所以你不能读取它们(比如使用纹理访问)。当然你仍然还是能够使用glReadPixels来读取它,这会从当前绑定的帧缓冲,而不是附件本身,中返回特定区域的像素。
因为它的数据已经是原生的格式了,当写入或者复制它的数据到其它缓冲中时是非常快的。所以,交换缓冲这样的操作在使用渲染缓冲对象时会非常快。我们在每个渲染迭代最后使用的glfwSwapBuffers,也可以通过渲染缓冲对象实现:只需要写入一个渲染缓冲图像,并在最后交换到另外一个渲染缓冲就可以了。渲染缓冲对象对这种操作非常完美。
- 创建渲染缓冲对象:
unsigned int rbo;
glGenRenderbuffers(1, &rbo);
- 绑定这个渲染缓冲对象,让之后所有的渲染缓冲操作影响当前的rbo:
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
- 创建一个深度和模板渲染缓冲对象可以通过调用glRenderbufferStorage函数来完成:
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);
- 最后一件事就是附加这个渲染缓冲对象:
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT
, GL_RENDERBUFFER, rbo);
渲染到纹理:
帧缓冲对象的初始化
- 首先要创建一个帧缓冲对象,并绑定它,这些都很直观:
- 接下来我们需要创建一个纹理图像,我们将它作为一个颜色附件附加到帧缓冲上。我们将纹理的维度设置为窗口的宽度和高度,并且不初始化它的数据:
- 创建渲染缓冲对象:
- 将渲染缓冲对象附加到帧缓冲的深度和模板附件上:
- 绑定到帧缓冲区,像往常一样绘制场景以着色纹理。
bool GLSeniorFBO::setupGraphics(int w, int h) {
screenW = w;
screenH = h;
LOGI("setupGraphics(%d, %d)", w, h);
GLuint fBOProgram = fBOShader->createProgram();
if (!fBOProgram) {
LOGE("Could not create fBOProgram shaderId.");
return false;
}
GLuint screenProgram = screenShader->createProgram();
if (!screenProgram) {
LOGE("Could not create screenProgram shaderId.");
return false;
}
glViewport(0, 0, w, h);
checkGlError("glViewport");
LOGI("glViewport successed!");
//清屏
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
checkGlError("glClear");
//开启深度测试
glEnable(GL_DEPTH_TEST);
// cube VAO
glGenVertexArrays(1, &cubeVAO);
glGenBuffers(1, &cubeVBO);
glBindVertexArray(cubeVAO);
glBindBuffer(GL_ARRAY_BUFFER, cubeVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(FBOVertices), &FBOVertices
, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float)
, (void *) 0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float),
(void *) (3 * sizeof(float)));
glBindVertexArray(0);
// plane VAO
glGenVertexArrays(1, &planeVAO);
glGenBuffers(1, &planeVBO);
glBindVertexArray(planeVAO);
glBindBuffer(GL_ARRAY_BUFFER, planeVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(fBOPlaneVertices), &fBOPlaneVertices,
GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float)
, (void *) 0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float),
(void *) (3 * sizeof(float)));
glBindVertexArray(0);
// screen quad VAO
glGenVertexArrays(1, &quadVAO);
glGenBuffers(1, &quadVBO);
glBindVertexArray(quadVAO);
glBindBuffer(GL_ARRAY_BUFFER, quadVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices)
, &quadVertices, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE
, 4 * sizeof(float), (void *) 0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float),
(void *) (2 * sizeof(float)));
// load and create a texture
LOGI("load and create a texture!");
GLenum format;
if (nrChannels1 == 1) {
format = GL_RED;
} else if (nrChannels1 == 3) {
format = GL_RGB;
} else if (nrChannels1 == 4) {
format = GL_RGBA;
}
// LOGI("texture1 format==%d", format);
if (data1) {
cubeTexture = loadTexture(data1, width1, height1, format);
}
if (nrChannels2 == 1) {
format = GL_RED;
} else if (nrChannels2 == 3) {
format = GL_RGB;
} else if (nrChannels2 == 4) {
format = GL_RGBA;
}
if (data2) {
floorTexture = loadTexture(data2, width2, height2, format);
}
// shader configuration
// --------------------
fBOShader->use();
fBOShader->setInt("texture1", 0);
screenShader->use();
screenShader->setInt("screenTexture", 0);
//1.首先要创建一个帧缓冲对象,并绑定它,这些都很直观
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
//2.接下来我们需要创建一个纹理图像,我们将它作为一个颜色附件附加到帧缓冲上。
// 我们将纹理的维度设置为窗口的宽度和高度,并且不初始化它的数据
glGenTextures(1, &textureColorbuffer);
glBindTexture(GL_TEXTURE_2D, textureColorbuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, screenW, screenH
, 0, GL_RGB, GL_UNSIGNED_BYTE, 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, textureColorbuffer,0);
//3.创建渲染缓冲对象
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, screenW,
screenH);
//4.将渲染缓冲对象附加到帧缓冲的深度和模板附件上
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT,
GL_RENDERBUFFER,rbo);
//5.最后,我们希望检查帧缓冲是否是完整的,如果不是,我们将打印错误信息
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
return true;
}
渲染帧缓冲场景:
要想绘制场景到一个纹理上,我们需要采取以下的步骤:
- 将新的帧缓冲绑定为激活的帧缓冲,和往常一样渲染场景
- 绑定默认的帧缓冲
- 绘制一个横跨整个屏幕的四边形,将帧缓冲的颜色缓冲作为它的纹理。
void GLSeniorFBO::renderFrame() {
//绑定到帧缓冲区,像往常一样绘制场景以着色纹理
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
//启用深度测试(禁用渲染屏幕空间四边形)
glEnable(GL_DEPTH_TEST);
// 确保清除帧缓冲区的内容
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
fBOShader->use();
glm::mat4 model = glm::mat4(1.0f);
glm::mat4 view = mCamera.GetViewMatrix();
glm::mat4 projection = glm::perspective(glm::radians(mCamera.Zoom),
(float) screenW / (float) screenH, 0.1f, 100.0f);
fBOShader->setMat4("view", view);
fBOShader->setMat4("projection", projection);
// cubes
glBindVertexArray(cubeVAO);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, cubeTexture);
model = glm::translate(model, glm::vec3(-1.0f, 0.0f, -1.0f));
fBOShader->setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
model = glm::mat4(1.0f);
model = glm::translate(model, glm::vec3(2.0f, 0.0f, 0.0f));
fBOShader->setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
// floor
glBindVertexArray(planeVAO);
glBindTexture(GL_TEXTURE_2D, floorTexture);
fBOShader->setMat4("model", glm::mat4(1.0f));
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
//现在绑定回默认帧缓冲区,并使用附加的帧缓冲区颜色纹理绘制一个四边形平面
glBindFramebuffer(GL_FRAMEBUFFER, 0);
//禁用深度测试,这样屏幕空间四边形就不会因为深度测试而被丢弃。
glDisable(GL_DEPTH_TEST);
// 清除所有相关缓冲区
// 将透明颜色设置为白色(实际上并没有必要,因为我们无论如何都看不到四边形后面)
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
screenShader->use();
glBindVertexArray(quadVAO);
//使用颜色附着纹理作为四边形平面的纹理
glBindTexture(GL_TEXTURE_2D, textureColorbuffer);
glDrawArrays(GL_TRIANGLES, 0, 6);
checkGlError("glDrawArrays");
}
后期处理:
既然整个场景都被渲染到了一个纹理上,我们可以简单地通过修改纹理数据创建出一些非常有意思的效果。
#version 320 es
precision mediump float;
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D screenTexture;
void main()
{
//灰度加权
FragColor = texture(screenTexture, TexCoords);
float average = 0.2126 * FragColor.r + 0.7152
* FragColor.g + 0.0722 * FragColor.b;
FragColor = vec4(average, average, average, 1.0);
}
反相、灰度、核效果、模糊、边缘检测等有趣效果。
六、立方体贴图:
多个纹理组合起来映射到一张纹理上的一种纹理类型:立方体贴图(Cube Map)。简单来说,立方体贴图就是一个包含了6个2D纹理的纹理,每个2D纹理都组成了立方体的一个面:一个有纹理的立方体。
创建立方体贴图:
- 生成一个纹理,并将其绑定到纹理目标上,之后再做其它的纹理操作。这次要绑定到GL_TEXTURE_CUBE_MAP:
unsigned int textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
- 纹理目标:有6个面,OpenGL给我们提供了6个特殊的纹理目标,专门对应立方体贴图的一个面。
- 遍历目标纹理:和OpenGL的很多枚举(Enum)一样,它们背后的int值是线性递增的,所以如果我们有一个纹理位置的数组或者vector,我们就可以从GL_TEXTURE_CUBE_MAP_POSITIVE_X开始遍历它们,在每个迭代中对枚举值加1,遍历了整个纹理目标。
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);
- 在绘制使用立方体贴图的物体之前,我们要先激活对应的纹理单元,并绑定立方体贴图,这和普通的2D纹理没什么区别。
- 在片段着色器中,我们使用了一个不同类型的采样器,
samplerCube
,我们将使用texture函数使用它进行采样,但这次我们将使用一个vec3
的方向向量而不是vec2
。使用立方体贴图的片段着色器会像是这样的:
in vec3 textureDir; // 代表3D纹理坐标的方向向量
uniform samplerCube cubemap; // 立方体贴图的纹理采样器
void main()
{
FragColor = texture(cubemap, textureDir);
}
天空盒:
天空盒是一个包含了整个场景的(大)立方体,它包含周围环境的6个图像。
加载天空盒:
因为天空盒本身就是一个立方体贴图,加载天空盒和之前加载立方体贴图时并没有什么不同。为了加载天空盒,我们将使用下面的函数,它接受一个包含6个纹理路径的vector:
unsigned int loadCubemap(vector<std::string> faces)
{
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;
}
显示天空盒:
代码 如下:
#include <iostream>
#include "../includes/GLSeniorCubeMap.h"
bool GLSeniorCubeMap::setupGraphics(int w, int h) {
screenW = w;
screenH = h;
LOGI("setupGraphics(%d, %d)", w, h);
GLuint CubeMapProgram = cubeMapShader->createProgram();
if (!CubeMapProgram) {
LOGE("Could not create CubeMapProgram shaderId.");
return false;
}
GLuint skyboxProgram = skyboxShader->createProgram();
if (!skyboxProgram) {
LOGE("Could not create skyboxProgram shaderId.");
return false;
}
glViewport(0, 0, w, h);
checkGlError("glViewport");
LOGI("glViewport successed!");
//清屏
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
checkGlError("glClear");
//开启深度测试
glEnable(GL_DEPTH_TEST);
// cube VAO
glGenVertexArrays(1, &cubeVAO);
glGenBuffers(1, &cubeVBO);
glBindVertexArray(cubeVAO);
glBindBuffer(GL_ARRAY_BUFFER, cubeVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(CubeMapVertices)
, &CubeMapVertices, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float)
, (void *) 0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float),
(void *) (3 * sizeof(float)));
// skybox VAO
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);
// load and create a texture
LOGI("load and create a texture!");
GLenum format;
if (nrChannels1 == 1) {
format = GL_RED;
} else if (nrChannels1 == 3) {
format = GL_RGB;
} else if (nrChannels1 == 4) {
format = GL_RGBA;
}
// LOGI("texture1 format==%d", format);
if (data1) {
cubeTexture = loadTexture(data1, width1, height1, format);
}
cubemapTexture = loadCubemap(faces);
if (nrChannels2 == 1) {
format = GL_RED;
} else if (nrChannels2 == 3) {
format = GL_RGB;
} else if (nrChannels2 == 4) {
format = GL_RGBA;
}
// shader configuration
// --------------------
cubeMapShader->use();
cubeMapShader->setInt("texture1", 0);
skyboxShader->use();
skyboxShader->setInt("screenTexture", 0);
return true;
}
void GLSeniorCubeMap::renderFrame() {
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// draw scene as normal
cubeMapShader->use();
glm::mat4 model = glm::mat4(1.0f);
model = glm::rotate(model, glm::radians(45.0f), glm::vec3(1.0f, 0.3f, 0.5f));
glm::mat4 view = mCamera.GetViewMatrix();
glm::mat4 projection = glm::perspective(glm::radians(mCamera.Zoom)
,(float) screenW / (float) screenH, 0.1f, 100.0f);
cubeMapShader->setMat4("model", model);
cubeMapShader->setMat4("view", view);
cubeMapShader->setMat4("projection", projection);
// cubes
glBindVertexArray(cubeVAO);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, cubeTexture);
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);
// draw skybox as last
// change depth function so depth test passes when values are equal to depth buffer's content
glDepthFunc(GL_LEQUAL);
skyboxShader->use();
// remove translation from the view matrix
view = glm::mat4(glm::mat3(mCamera.GetViewMatrix()));
skyboxShader->setMat4("view", view);
skyboxShader->setMat4("projection", projection);
// skybox cube
glBindVertexArray(skyboxVAO);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture);
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);
glDepthFunc(GL_LESS); // set depth function back to default
checkGlError("glDrawArrays");
}
bool GLSeniorCubeMap::setSharderPath(const char *vertexPath
, const char *fragmentPath) {
cubeMapShader->getSharderPath(vertexPath, fragmentPath);
return 0;
}
bool
GLSeniorCubeMap::setSharderScreenPath(const char *vertexScreenPath,
const char *fragmentScreenPath) {
skyboxShader->getSharderPath(vertexScreenPath, fragmentScreenPath);
return 0;
}
void GLSeniorCubeMap::setPicPath(const char *pic1) {
LOGI("setPicPath pic1==%s", pic1);
data1 = stbi_load(pic1, &width1, &height1, &nrChannels1, 0);
}
void
GLSeniorCubeMap::setSkyBoxPicPath(const char *rightPic, const char *letfPic, const char *topPic,
const char *bottomPic, const char *frontPic,
const char *backPic) {
faces.push_back(rightPic);
faces.push_back(letfPic);
faces.push_back(topPic);
faces.push_back(bottomPic);
faces.push_back(frontPic);
faces.push_back(backPic);
}
void GLSeniorCubeMap::setMoveXY(float dx, float dy, int actionMode) {
LOGI("setMoveXY dx:%f,dy:%f,actionMode:%d", dy, dy, actionMode);
float xoffset = dx - lastX;
float yoffset = lastY - dy; // reversed since y-coordinates go from bottom to top
lastX = dx;
lastY = dy;
mActionMode = actionMode;
mCamera.ProcessXYMovement(xoffset, yoffset);
}
void GLSeniorCubeMap::setOnScale(float scaleFactor, float focusX, float focusY, int actionMode) {
float scale;
if (actionMode == 1 || actionMode == 3) {
scale = 45.0f;
} else {
if (scaleFactor > 1) {
scale = (scaleFactor - 1) * 1000 + 45;
} else {
scale = 50 - (1 - scaleFactor) * 1000;
}
}
LOGI("setOnScale scale:%f", scale);
mCamera.ProcessScroll(scale);
}
GLSeniorCubeMap::GLSeniorCubeMap() {
cubeMapShader = new GLSeniorShader();
skyboxShader = new GLSeniorShader;
}
GLSeniorCubeMap::~GLSeniorCubeMap() {
cubeTexture = 0;
cubemapTexture = 0;
//析构函数中释放资源
glDeleteVertexArrays(1, &cubeVAO);
glDeleteVertexArrays(1, &skyboxVAO);
glDeleteBuffers(1, &cubeVBO);
glDeleteBuffers(1, &skyboxVBO);
cubeMapShader = nullptr;
skyboxShader = nullptr;
if (data1) {
stbi_image_free(data1);
data1 = nullptr;
}
faces.clear();
colorVertexCode.clear();
colorFragmentCode.clear();
}
void GLSeniorCubeMap::printGLString(const char *name, GLenum s) {
const char *v = (const char *) glGetString(s);
LOGI("OpenGL %s = %s\n", name, v);
}
void GLSeniorCubeMap::checkGlError(const char *op) {
for (GLint error = glGetError(); error; error = glGetError()) {
LOGI("after %s() glError (0x%x)\n", op, error);
}
}
/**
* 加载纹理
* @param path
* @return
*/
int GLSeniorCubeMap::loadTexture(unsigned char *data, int width, int height, GLenum format) {
unsigned int textureID;
glGenTextures(1, &textureID);
// LOGI("loadTexture format =%d", format);
if (data) {
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
stbi_image_free(data);
} else {
checkGlError("Texture failed to load at path: ");
stbi_image_free(data);
}
return textureID;
}
// loads a cubemap texture from 6 individual texture faces
// order:
// +X (right)
// -X (left)
// +Y (top)
// -Y (bottom)
// +Z (front)
// -Z (back)
// -------------------------------------------------------
int GLSeniorCubeMap::loadCubemap(vector<std::string> faces) {
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 {
LOGE("Cubemap texture failed to load at path: %s ", faces[i].c_str());
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;
}
环境映射:
使用环境的立方体贴图,可以给物体反射和折射的属性。这样使用环境立方体贴图的技术叫做环境映射(Environment Mapping),其中最流行的两个是反射(Reflection)和折射(Refraction)。
反射:
这张图展示了我们如何计算反射向量,并如何使用这个向量来从立方体贴图中采样:
我们根据观察方向向量I¯和物体的法向量N¯,来计算反射向量R¯。我们可以使用GLSL内建的reflect函数来计算这个反射向量。最终的R¯向量将会作为索引/采样立方体贴图的方向向量,返回环境的颜色值。最终的结果是物体看起来反射了天空盒。
已经在场景中配置好天空盒了,创建反射效果并不会很难。我们将会改变箱子的片段着色器,让箱子有反射性:
#version 330 core
out vec4 FragColor;
in vec3 Normal;
in vec3 Position;
uniform vec3 cameraPos;
uniform samplerCube skybox;
void main()
{
vec3 I = normalize(Position - cameraPos);
vec3 R = reflect(I, normalize(Normal));
FragColor = vec4(texture(skybox, R).rgb, 1.0);
}
先计算了观察/摄像机方向向量I
,并使用它来计算反射向量R
,之后我们将使用R
来从天空盒立方体贴图中采样。注意,我们现在又有了片段的插值Normal和Position变量,所以我们需要更新一下顶点着色器。
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
out vec3 Normal;
out vec3 Position;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
Normal = mat3(transpose(inverse(model))) * aNormal;
Position = vec3(model * vec4(aPos, 1.0));
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
折射:
折射是光线由于传播介质的改变而产生的方向变化。在常见的类水表面上所产生的现象就是折射,光线不是直直地传播,而是弯曲了一点。
折射是通过斯涅尔定律(Snell’s Law)来描述的,使用环境贴图的话看起来像是这样:
同样,我们有一个观察向量I¯,一个法向量N¯,而这次是折射向量R¯。可以看到,观察向量的方向轻微弯曲了。弯折后的向量R¯将会用来从立方体贴图中采样。
折射可以使用GLSL的内建refract函数来轻松实现,它需要一个法向量、一个观察方向和两个材质之间的折射率(Refractive Index)。
折射率决定了材质中光线弯曲的程度,每个材质都有自己的折射率。一些最常见的折射率可以在下表中找到:
已经绑定了立方体贴图,提供了顶点数据和法线,并设置了摄像机位置的uniform。唯一要修改的就是片段着色器:
void main()
{
float ratio = 1.00 / 1.52;
vec3 I = normalize(Position - cameraPos);
vec3 R = refract(I, normalize(Normal), ratio);
FragColor = vec4(texture(skybox, R).rgb, 1.0);
}
动态环境贴图:
通过使用帧缓冲,我们能够为物体的6个不同角度创建出场景的纹理,并在每个渲染迭代中将它们储存到一个立方体贴图中。之后我们就可以使用这个(动态生成的)立方体贴图来创建出更真实的,包含其它物体的,反射和折射表面了。这就叫做动态环境映射(Dynamic Environment Mapping),因为我们动态创建了物体周围的立方体贴图,并将其用作环境贴图。
Github地址:https://github.com/wangyongyao1989/AndroidLearnOpenGL
参考资料:中文版LearnOpenGL