OpenGL学习总结(六)

来源:互联网 发布:java实现base64解码 编辑:程序博客网 时间:2024/05/17 04:08

高级OpenGL总结(一)

一、深度测试

OpenGL在进行投影矩阵变换之后,我们知道所有的三维点都会投影到同一个平面上,那么势必会有一个问题出现——在三维空间中的N个点会落到投影平面上的同一位置,那么OpenGL绘制图像的时候就往往会出现需要显示的顶点被不需要现实的顶点遮盖,为了解决这一问题,OpenGL使用了深度测试。OpenGL开启深度测试后,会开辟一个深度缓冲区,当要绘制一个顶点的时候,会将该顶点的深度值——Z值与深度缓冲区中的相应的深度值比较,若符合条件(一般是小于)则绘制顶点并更新深度缓冲区中相应的值,否则丢掉该顶点片段。
1、启用深度测试:

glEnable(GL_DEPTH_TEST);

2、清除深度缓冲区

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

3、在某些情况下我们需要进行深度测试并相应地丢弃片段,但我们不希望更新深度缓冲区,基本上,可以使用一个只读的深度缓冲区;OpenGL允许我们通过将其深度掩码设置为GL_FALSE禁用深度缓冲区写入

glDepthMask(GL_FALSE);

4、OpenGL 允许我们修改它深度测试使用的比较运算符(comparison operators)。这样我们能够控制OpenGL通过或丢弃碎片和如何更新深度缓冲区。我们可以通过调用glDepthFunc来设置比较运算符 (或叫做深度函数(depth function)):

glDepthFunc(GL_LESS);

5、该函数接受在下表中列出的几个比较运算符:

GL_ALWAYS       永远通过测试GL_NEVER        永远不通过测试GL_LESS         在片段深度值小于缓冲区的深度时通过测试GL_EQUAL        在片段深度值等于缓冲区的深度时通过测试GL_LEQUAL       在片段深度值小于等于缓冲区的深度时通过测试GL_GREATER      在片段深度值大于缓冲区的深度时通过测试GL_NOTEQUAL     在片段深度值不等于缓冲区的深度时通过测试GL_GEQUAL       在片段深度值大于等于缓冲区的深度时通过测试

默认情况下使用GL_LESS,这将丢弃深度值高于或等于当前深度缓冲区的值的片段。

6、深度值精度
在深度缓冲区中包含深度值介于0.0和1.0之间,从观察者看到其内容与场景中的所有对象的 z 值进行了比较。这些视图空间中的 z 值可以在投影平头截体的近平面和远平面之间的任何值。我们因此需要一些方法来转换这些视图空间 z 值到 [0,1] 的范围内,方法之一就是线性将它们转换为 [0,1] 范围内。下面的 (线性) 方程把 z 值转换为 0.0 和 1.0 之间的值 :
这里写图片描述
这里far和near是我们用来提供到投影矩阵设置可见视图截锥的远近值 (见坐标系)。方程带内锥截体的深度值 z,并将其转换到 [0,1] 范围。在下面的图给出 z 值和其相应的深度值的关系:
这里写图片描述
然而,在实践中是几乎从来不使用这样的线性深度缓冲区。正确的投影特性的非线性深度方程是和1/z成正比的 。这样基本上做的是在z很近是的高精度和 z 很远的时候的低精度。用几秒钟想一想: 我们真的需要让1000单位远的物体和只有1单位远的物体的深度值有相同的精度吗?线性方程没有考虑这一点。

由于非线性函数是和 1/z 成正比,例如1.0 和 2.0 之间的 z 值,将变为 1.0 到 0.5之间, 这样在z非常小的时候给了我们很高的精度。50.0 和 100.0 之间的 Z 值将只占 2%的浮点数的精度,这正是我们想要的。这类方程,也需要近和远距离考虑,下面给出:
这里写图片描述
该公式的图像是这样:
这里写图片描述

二、模板测试

1、模板缓冲区(Stencil Buffer):与颜色缓冲区和深度缓冲区类似,模板缓冲区可以为屏幕上的每个像素点保存一个无符号整数值。这个值的具体意义视程序的具体应用而定。在渲染的过程中,可以用这个值与一个预先设定的参考值相比较,根据比较的结果来决定是否更新相应的像素点的颜色值。这个比较的过程被称为模板测试。模板测试发生在透明度测试(alpha test)之后,深度测试(depth test)之前。如果模板测试通过,则相应的像素点更新,否则不更新。就像使用纸板和喷漆一样精确的混图一样,当启动模板测试时,通过模板测试的片段像素点会被替换到颜色缓冲区中,从而显示出来,未通过的则不会保存到颜色缓冲区中,从而达到了过滤的功能。下图描述了模板缓冲区的原理:
这里写图片描述
使用模板缓冲要遵循一下规定:
1、开启模板缓冲写入。
2、渲染物体,更新模板缓冲。
3、关闭模板缓冲写入。
4、渲染(其他)物体,这次基于模板缓冲内容丢弃特定片段。
2、开启模板测试

glEnable(GL_STENCIL_TEST);

3、清空模板缓冲

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

4、glStencilMask允许我们给模板值设置一个位遮罩(Bitmask),它与模板值进行按位与(AND)运算决定缓冲是否可写。默认设置的位遮罩都是1,这样就不会影响输出,但是如果我们设置为0x00,所有写入深度缓冲最后都是0。这和深度缓冲的glDepthMask(GL_FALSE)很类似:

// 0xFF == 0b11111111//此时,模板值与它进行按位与运算结果是模板值,模板缓冲可写glStencilMask(0xFF); // 0x00 == 0b00000000 == 0//此时,模板值与它进行按位与运算结果是0,模板缓冲不可写glStencilMask(0x00); 

5、模板函数:
一共有两种函数可供我们使用去配置模板测试:glStencilFunc和glStencilOp。
void glStencilFunc(GLenum func, GLint ref, GLuint mask)函数有三个参数:

func:设置模板测试操作。这个测试操作应用到已经储存的模板值和glStencilFunc的ref值上,可用的选项是:GL_NEVER、GL_LEQUAL、GL_GREATER、GL_GEQUAL、GL_EQUAL、GL_NOTEQUAL、GL_ALWAYS。它们的语义和深度缓冲的相似。ref:指定模板测试的引用值。模板缓冲的内容会与这个值对比。mask:指定一个遮罩,在模板测试对比引用值和储存的模板值前,对它们进行按位与(and)操作,初始设置为1

例如:

glStencilFunc(GL_EQUAL, 1, 0xFF)

它会告诉OpenGL,无论何时,一个片段模板值等于(GL_EQUAL)引用值1,片段就能通过测试被绘制了,否则就会被丢弃。

但是glStencilFunc只描述了OpenGL对模板缓冲做什么,而不是描述我们如何更新缓冲。这就需要glStencilOp登场了。
void glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)函数包含三个选项,我们可以指定每个选项的动作:

    sfail: 如果模板测试失败将采取的动作。    dpfail: 如果模板测试通过,但是深度测试失败时采取的动作。    dppass: 如果深度测试和模板测试都通过,将采取的动作。
GL_KEEP         保持现有的模板值GL_ZERO         将模板值置为0GL_REPLACE      将模板值设置为用glStencilFunc函数设置的ref值GL_INCR         如果模板值不是最大值就将模板值+1GL_INCR_WRAP    与GL_INCR一样将模板值+1,如果模板值已经是最大值则设为0GL_DECR         如果模板值不是最小值就将模板值-1GL_DECR_WRAP    与GL_DECR一样将模板值-1,如果模板值已经是最小值则设为最大值GL_INVERT       Bitwise inverts the current stencil buffer value.

glStencilOp函数默认设置为 (GL_KEEP, GL_KEEP, GL_KEEP) ,所以任何测试的任何结果,模板缓冲都会保留它的值。默认行为不会更新模板缓冲,所以如果你想写入模板缓冲的话,你必须像任意选项指定至少一个不同的动作。

三、混合

1、第一种混合方式——忽略片段
有些图像并不关心半透明度,但也想基于纹理的颜色值显示一部分。例如,创建像草这种物体你不需要花费很大力气,通常把一个草的纹理贴到2D四边形上,然后把这个四边形放置到你的场景中。可是,草并不是像2D四边形这样的形状,而只需要显示草纹理的一部分而忽略其他部分。
下面的纹理正是这样的纹理,它既有完全不透明的部分(alpha值为1.0)也有完全透明的部分(alpha值为0.0),而没有半透明的部分。你可以看到没有草的部分,图片显示了网站的背景色,而不是它自身的那部分颜色。
这里写图片描述
所以,当向场景中添加像这样的纹理时,我们不希望看到一个方块图像,而是只显示实际的纹理像素,剩下的部分可以被看穿。我们要忽略(丢弃)纹理透明部分的像素,不必将这些片段储存到颜色缓冲中。在此之前,我们还要学一下如何加载一个带有透明像素的纹理。

a、加载带有ALPHA的图像:
注:SOIL能以RGBA的方式加载大多数没有alpha值的纹理,它会将这些像素的alpha值设为了1.0。

unsigned char * image = SOIL_load_image(path, &width, &height, 0, SOIL_LOAD_RGBA);

不要忘记还要改变OpenGL生成的纹理:

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

b、由于草纹理被添加到四边形物体上,我们需要再次创建另一个VAO,向里面填充VBO,以及设置合理的顶点属性指针。在我们绘制完地面和两个立方体后,我们就来绘制草叶:

glBindVertexArray(vegetationVAO);glBindTexture(GL_TEXTURE_2D, grassTexture);  for(GLuint i = 0; i < vegetation.size(); i++){    model = glm::mat4();    model = glm::translate(model, vegetation[i]);    glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));    glDrawArrays(GL_TRIANGLES, 0, 6);}  glBindVertexArray(0);

c、在着色器代码中,使用discard命令,它保证了片段不会被进一步处理,这样就不会进入颜色缓冲。有了这个命令我们就可以在片段着色器中检查一个片段是否有在一定的阈限下的alpha值,如果有,那么丢弃这个片段,就好像它不存在一样:

#version 330 corein vec2 TexCoords;out vec4 color;uniform sampler2D texture1;void main(){    vec4 texColor = texture(texture1, TexCoords);    if(texColor.a < 0.1)        discard;    color = texColor;}

2、第二种混合方式——混合颜色,渲染半透明颜色

a、上述丢弃片段的方式,不能使我们获得渲染半透明图像,我们要么渲染出像素,要么完全地丢弃它。为了渲染出不同的透明度级别,我们需要开启混合(Blending)。像大多数OpenGL的功能一样,我们可以开启GL_BLEND来启用混合(Blending)功能:

glEnable(GL_BLEND);

开启混合后,我们还需要告诉OpenGL它该如何混合。

OpenGL以下面的方程进行混合:
这里写图片描述
b、使用void glBlendFunc(GLenum sfactor, GLenum dfactor)接收两个参数,来设置源(source)和目标(destination)因子。OpenGL为我们定义了很多选项,我们把最常用的列在下面。注意,颜色常数向量C¯constant可以用glBlendColor函数分开来设置。
这里写图片描述

c、也可以为RGB和alpha通道各自设置不同的选项,使用glBlendFuncSeperate:

    glBlendFuncSeparate(GLenum srcRGB, GLenum destRGB, GLenum srcAlpha, GLenum destAlpha);      //参数srcRGB表示颜色值的源混合因子,参数destRGB表示颜色在的目标混合因子,参数srcAlpha表示Alpha值的源混合因子,参数destAlpha表示Alpha值的目标混合因子  

d、OpenGL给了我们更多的自由,我们可以改变方程源和目标部分的操作符。现在,源和目标元素已经相加了。如果我们愿意的话,我们还可以把它们相减。

void glBlendEquation(GLenum mode)允许我们设置这个操作,有3种可行的选项:

GL_FUNC_ADD:默认的,彼此元素相加:C¯result=Src+DstGL_FUNC_SUBTRACT:彼此元素相减: C¯result=Src−DstGL_FUNC_REVERSE_SUBTRACT:彼此元素相减,但顺序相反:C¯result=Dst−Src

通常我们可以简单地省略glBlendEquation因为GL_FUNC_ADD在大多数时候就是我们想要的,但是如果你如果你真想尝试努力打破主流常规,其他的方程或许符合你的要求。

e、使用:

glEnable(GL_BLEND);glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

如果不做任何处理会出现如下结果:
这里写图片描述

f、按照先远后近的顺序来渲染物体
要让混合在多物体上有效,我们必须先绘制最远的物体,最后绘制最近的物体。普通的无混合物体仍然可以使用深度缓冲正常绘制,所以不必给它们排序。我们一定要保证它们在透明物体前绘制好。当无透明度物体和透明物体一起绘制的时候,通常要遵循以下原则:

先绘制所有不透明物体。 为所有透明物体排序。 按顺序绘制透明物体。 一种排序透明物体的方式是,获取一个物体到观察者透视图的距离。这可以通过获取摄像机的位置向量和物体的位置向量来得到。接着我们就可以把它和相应的位置向量一起储存到一个map数据结构(STL库)中。map会自动基于它的键排序它的值,所以当我们把它们的距离作为键添加到所有位置中后,它们就自动按照距离值排序了:

std::map<float, glm::vec3> sorted;for (GLuint i = 0; i < windows.size(); i++) // windows contains all window positions{    GLfloat distance = glm::length(camera.Position - windows[i]);    sorted[distance] = windows[i];}

最后产生了一个容器对象,基于它们距离从低到高储存了每个窗子的位置。

随后当渲染的时候,我们逆序获取到每个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);    glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));    glDrawArrays(GL_TRIANGLES, 0, 6);}

我们从map得来一个逆序的迭代器,迭代出每个逆序的条目,然后把每个窗子的四边形平移到相应的位置。这个相对简单的方法对透明物体进行了排序,修正了前面的问题,现在场景看起来像这样:
这里写图片描述

四、面剔除

a、面剔除主要是用于将一些用户不可能看见的面“删除”——不进行渲染。
OpenGL允许检查所有正面朝向(Front facing)观察者的面,并渲染它们,而丢弃所有背面朝向(Back facing)的面,这样就节约了我们很多片段着色器的命令(它们很昂贵!)。我们必须告诉OpenGL我们使用的哪个面是正面,哪个面是反面。OpenGL使用一种聪明的手段解决这个问题——分析顶点数据的连接顺序(Winding order)。
b、顶点连接顺序
当我们定义一系列的三角顶点时,我们会把它们定义为一个特定的连接顺序(Winding Order),它们可能是顺时针的或逆时针的。每个三角形由3个顶点组成,我们从三角形的中间去看,从而把这三个顶点指定一个连接顺序。
这里写图片描述
每三个顶点都形成了一个包含着连接顺序的基本三角形。OpenGL使用这个信息在渲染你的基本图形的时候决定这个三角形是三角形的正面还是三角形的背面。默认情况下,逆时针的顶点连接顺序被定义为三角形的正面。

当定义你的顶点顺序时,你如果定义能够看到的一个三角形,那它一定是正面朝向的,所以你定义的三角形应该是逆时针的,就像你直接面向这个三角形。把所有的顶点指定成这样是件炫酷的事,实际的顶点连接顺序是在光栅化阶段(Rasterization stage)计算的,所以当顶点着色器已经运行后。顶点就能够在观察者的观察点被看到。

我们指定了它们以后,观察者面对的所有的三角形的顶点的连接顺序都是正确的,但是现在渲染的立方体另一面的三角形的顶点的连接顺序被反转。最终,我们所面对的三角形被视为正面朝向的三角形,后部的三角形被视为背面朝向的三角形。下图展示了这个效果:
这里写图片描述

c、开启面剔除

glEnable(GL_CULL_FACE);

从这儿以后,所有的不是正面朝向的面都会被丢弃(尝试飞入立方体看看,里面什么面都看不见了)。目前,在渲染片段上我们节约了超过50%的性能,但记住这只对像立方体这样的封闭形状有效。当我们绘制上个教程中那个草的时候,我们必须关闭面剔除,这是因为它的前、后面都必须是可见的。

OpenGL允许我们改变剔除面的类型。要是我们剔除正面而不是背面会怎样?我们可以调用glCullFace来做这件事:

glCullFace(GL_BACK);glCullFace函数有三个可用的选项    GL_BACK:只剔除背面。    GL_FRONT:只剔除正面。    GL_FRONT_AND_BACK:剔除背面和正面。

另外,我们还可以告诉OpenGL使用顺时针而不是逆时针来表示正面,这通过glFrontFace来设置:

glFrontFace(GL_CCW);

我们可以做个小实验,告诉OpenGL现在顺时针代表正面:

glEnable(GL_CULL_FACE);glCullFace(GL_BACK);glFrontFace(GL_CW);

最后的结果只有背面被渲染了:
这里写图片描述

0 0