在光栅化阶段(包括纹理和雾)之后,数据就不再是像素,而是成为片断。每个片断都具有与像素对应的坐标数据以及颜色值和深度值,然后,每个片断都经历一系列的测试和操作。如果顺利通过这些测试和操作,片断值便可以转换为像素。为了绘制这些像素,我们需要知道它们的颜色(即存储在颜色缓冲区中的信息)。当每个像素的数据按照统一的方式存储时,存储所有像素的存储空间就叫做缓冲区。不同的缓冲区为每个像素存储的数据量可能不同。但是,在一个特定的缓冲区内,为每个像素存储的数据量是相同的。为每个像素存储了1位信息的缓冲区又称为位平面。
颜色缓冲区只是用于存储像素信息的许多缓冲区之一,还有很多其他缓冲区。例如,深度缓冲区存储了每个像素的深度信息。颜色缓冲区本身也可能包括几个子缓冲区。系统的帧缓冲区是由所有这些缓冲区组成的。除了颜色缓冲区之外,其他缓冲区都无法直接查看,它们用于实现诸如隐藏表面消除、场景抗锯齿、模板测试、绘制平滑的运动图像等功能。
片断测试和操作
在屏幕上绘制几何图形、文本或图像时,OpenGL会执行一些计算,进行旋转、移动和缩放,以便确定光照效果以及物体投影到窗口坐标的方式,并确定哪些像素的颜色应该绘制。在OpenGL确定了每个片断是否应该生成以及应该使用什么颜色之后,这个片断仍然需要经历一些处理阶段,这些操作控制是否需要这个片断以及如何把它写入到帧缓冲区中。
片断在进入帧缓冲区之前要经历一定的测试,这些测试和操作是按照下面的顺序来的,如果片断在其中某个测试中被排除,后面的测试或操作就不再进行。
(1) 裁剪测试
(2) alpha测试
(3) 模板测试
(4) 深度测试
(5) 混合
(6) 抖动
(7) 逻辑操作
-
裁剪测试
可以使用glScissor()函数,在窗口中定义一个矩形区域,并把绘图限制在这个区域之内。如果一个片断位于这个区域的内部,那么它就能够通过裁剪测试。
裁剪测试只是模板测试的一个版本,它使用的时屏幕上的一个矩形区域。用硬件实现裁剪测试相当容易,而模板测试的实现则可能相当慢,也许是因为模板测试是用软件执行的。
裁剪测试的一个高级应用就是执行非线性投影。首先,把窗口分为常规的子区域网格,指定视口和裁剪参数,并在某一时刻把渲染限制在一个区域内部。然后,使用不同的投影矩阵,把整个场景投影到每个区域中。
-
alpha测试
在RGBA模式下,alpha测试允许根据一个片断的alpha值接受或拒绝它。可以通过向glEnable()和glDisable()函数传递GL_ALPHA_TEST,分别启用和禁用alpha测试。
如果alpha测试被启用,它就把源片断的alpha值与一个参考值进行比较,并根据比较结果接受或拒绝这个片断。可以用glAlphaFunc()函数设置参考值和比较函数。在默认情况下,这个参考值是0,比较函数是GL_ALWAYS,alpha测试被禁用。
alpha测试的一种应用就是实现一种透明算法。可以分两次渲染整个场景,第一次只接受alpha值为1的片断,第二次接受alpha值不为1的片断。在这两次渲染时都打开深度缓冲区,但是在第二道渲染时禁止写入到深度缓冲区。
alpha测试的另一种用途就是用纹理图像制作贴花,并且可以看透贴花的某些部分。可以把能够看透的贴花部分的alpha值设置为0.0,其他地方设置为1.0,并且把参考值设置为0.5,并且把比较函数设置为GL_GREATER。这样,贴花就具有能够看透的部分,并且深度缓冲区中的值也不会受到影响。这个技巧又称为“billboarding”。
-
模板测试
模板测试只有在存储模板缓冲区的情况下才会执行(如果不存在模板缓冲区,模板测试总是能够通过)。模板测试把像素存储在模板缓冲区的值与一个参考只进行比较。根据测试结果,对模板缓冲区中的这个值进行相应的修改。可以使用glStencilFunc()和glStencilOp()函数选择需要使用的特定比较函数、参考值以及对模板缓冲区所执行的修改操作。
“使用饱和”表示模板值将会限制在极端值之下。如果在使用饱和的情况下对模板值0进行减1操作,它的值仍然保持为0。“不使用饱和”表示模板值在超出了限制范围后会回绕。如果在不适用饱和的情况下对模板值0执行减1操作,它的值便会回绕为最大的无符号整数值(这将是一个相当大的值)。
- 模板查询
- 模板示例
模板测试最常见的用途就是屏蔽掉屏幕中一些不规则的区域,以免在这些区域中进行绘图。为此,可以把模板掩码设置为全0,并且在模板缓冲区中用1绘制所需要的形状。我们无法把几何图形直接绘制到模板缓冲区,但是可以通过把它写入到颜色缓冲区中,并为zpass参数选择一个适当的值(例如GL_REPLACE)来实现这个目的。(可以使用glDrawPixels()函数把像素数据直接绘制到模板缓冲区。)在进行绘图时,模板缓冲区中也会写入一个值。为了防止模板缓冲区的绘图影响颜色缓冲区,可以把颜色掩码设置为0.另外,还可以禁止对深度缓冲区的写入。
在定义了模板区域之后,就可以把参考值设置为1,并设置比较函数,如果模板平面值等于参考值就通过测试。在绘图过程中,不要对模板平面的内容进行修改。
模板测试的其他用途:
-
加盖:假如绘制的是一个闭合的由几个多边形组成的凸形物体(或者几个,只要它们彼此并不相交或包围),并且裁剪平面可能会切除物体的一部分,也可能并不切除。假设裁剪平面与物体相交,我们想用某种颜色为物体加上盖,以免看到物体的内部。为此,可以用0清除模板缓冲区,然后在绘图之前启用模板测试,并把模板比较函数设置为GL_ALWAYS,并且在片断通过模板测试时将模板缓冲区的值取反。在绘制完所有的物体之后,不需要加盖的屏幕区域对应的模板缓冲区值为0,需要加盖的区域则为非零值。接着,可以重新设置模板函数,只绘制模板缓冲区不为0的区域,并使用加盖颜色绘制一个穿越整个屏幕的多边形。
-
重叠的半透明多边形:假设有一个半透明表面,它由多个稍微重叠的多边形组成。如果简单地使用alpha混合,底层的那部分物体被多个半透明表面所覆盖,看起来效果不佳。可以使用模板缓冲区,确保每个片断最多只被组成半透明表面的1个多边形所覆盖。为此,可以使用0清除模板缓冲区,并且在模板平面为0的地方才绘制片断,在绘制片断之后将模板缓冲区的值增加1。
-
点画模式:假设我们想使用点画模式来绘制图像,为此,可以把点画模式写入到模板缓冲区,然后根据模板缓冲区的内容进行绘图。写入点画模式之后,在绘制图像时,模板缓冲区的内容不会改变,因此物体将会根据模板缓冲区中的点画模式进行绘制。
-
深度测试
对于屏幕上的每个像素,深度缓冲区负责记录观察点和占据这个像素的物体之间的距离。然后,如果能够通过指定的深度测试,源片断的深度值就会替换深度缓冲区中已经存在的值。
深度缓冲区一般用于隐藏表面消除。如果像素出现了一种新的候选颜色,只有当对应的新物体比原来的物体更靠近观察点时,它才会被绘制。最初,深度缓冲区的清除值表示一个与观察点尽可能远的距离,因此任何物体的深度值都要小于它。如果这就是我们所需要的深度缓冲区使用方式,只要简单地向glEnable()函数传递GL_DEPTH_TEST,并记得在重绘每一帧之前清除深度缓冲区。
-
遮挡查询
深度缓冲区决定了每个像素是否可见,为了提高性能,在渲染几何物体(有可能非常复杂)之前判断它是否可见是一个不错的思路(如果不可见就不绘制它)。对于由大量多边形组成的复杂几何形状,这个技巧显得尤为重要。
为了使用遮挡查询,需要执行如下步骤:
- 为每个所需的遮挡查询生成一个查询ID;
- 调用glBeginQuery(),表示开始一个遮挡查询;
- 渲染需要进行遮挡查询的几何图形;
- 调用glEndQuery(),表示已经完成了遮挡查询;
- 提取通过了遮挡查询的样本数量。
为了使遮挡查询的处理尽量保持高效,可以禁用所有可能增加渲染时间但又不会影响像素可见性的渲染模式。
-
生成查询对象
遮挡查询对象标识符只不过是一个无符号整数。尽管不是严格必需,但是让OpenGL生成一组ID仍然是一种很好的做法。
-
对遮挡查询对象进行初始化
为了指定一个遮挡查询所使用的几何图形,只要在glBeginQuery()和glEndQuery()之间包括需要进行的渲染操作。
-
判断遮挡查询的结果
在完成对需要进行遮挡查询的几何物体的渲染之后,需要提取遮挡查询的结果。可以通过调用glGetQueryObject[u]iv()函数来完成。这个函数将返回片断或样本的数量。
-
清除遮挡查询对象
-
条件渲染
遮挡查询的问题之一是它们需要OpenGL暂停对几何图元和片断的处理,计算深度缓冲区受影响的样本的数目,并把这个值返回给应用程序。为了避免不必要的暂停,条件渲染允许图形服务器判断一个遮挡查询是否产生任何片断以及是否渲染所涉及到的命令。使用glGetQuery*()的形式把想要条件执行的渲染操作包围起来,这样就可以打开条件渲染。
-
混合、抖动和逻辑操作
当一个源片断通过了所有测试之后,它就可以按照不同的方式与颜色缓冲区的当前内容进行组合。最简单的方式就是覆盖原先的值。另外,如果使用的是RGBA模式,并且想对片断进行半透明或抗锯齿处理,可以把它的值与缓冲区中已有的值求平均(混合)。在可用颜色较少的系统中,可能需要对颜色值进行抖动,在适当损失颜色质量的情况下增加可使用的颜色数量。最后,可以使用各种位操作符,对源片断和已经写入的片断进行组合。
-
1)混合
-
混合操作把源片断的R、G、B和alpha值与已经存储在这个位置的像素的对应值进行组合。可以使用不同的混合操作,混合的执行取决于源片断的alpha值和这个像素已经存储的alpha值(如果有的话)。
-
2)抖动
在那些只有少量颜色位平面的系统中,可以通过对图像中的颜色进行抖动,在略微损失颜色质量的前提下,增加可用颜色的数量。抖动类似于报纸所使用的半调技巧。
抖动操作是与硬件相关的。所有的OpenGL实现都允许打开或关闭这个操作。事实上,在有些计算机上,启动抖动根本不会引起抖动操作,因为计算机已经具有很高的颜色分辨率,根本不需要通过抖动来增加颜色。为了启用和禁用抖动,可以分别向glEnable()和glDisable()函数传递GL_DITHER参数。在默认情况下,抖动是启用的。
-
3)逻辑操作
片断执行的最后一种操作就是逻辑操作,例如OR、XOR或INVERT,它们作用于源片断和颜色缓冲区中的目标片断。这类逻辑操作在bit-bit类型的计算机上尤其实用。在这种类型的计算机上,主要的图形操作就是把一块数据矩形从窗口上的一个地方复制到另一个地方,从窗口复制到处理器内存,或者从内存复制到窗口。一般而言,这种复制并不是把数据直接写入到内存,而是允许在源数据和已经存在的数据之间执行任意的逻辑操作。然后,再用操作结果、替换原有的数据。
累计缓冲区作用
累计缓冲区可以用于场景抗锯齿、运动模糊、景深模拟以及计算多个光源所产生的柔和阴影等。此外,它还有一些其他应用,尤其是当它与其他缓冲区联合使用时。
OpenGL的图形操作并不是直接写入到累积缓冲区。一般情况下,OpenGL在一个标准颜色缓冲区中产生一系列的图像,然后以每次一幅的方式把它们累积到累积缓冲区中。在累积操作完成后,再把结果复制到颜色缓冲区中以便查看。为了减少舍入误差,累积缓冲区的精度(每种颜色成分的位数)可能比标准的颜色缓冲区更高。显示,多次渲染场景所需要的时间比一次渲染要长,但是它的渲染质量更高。
可以按照摄影师对胶片进行多次曝光的方式使用累计缓冲区。通常,摄像师在未前推胶卷的情况下对同一场景进行多次拍摄,以实现多次曝光。如果场景中存在移动的物体,它们看上去就会显得比较模糊。与摄像师使用相机所做的事情相比,我们可以使用计算机对图像进行更多的处理。
-
运动模糊
可以按照相同的方式设置累积缓冲区,不是对图像进行空间上的微移,而是进行时间上的微移。可以按照如下方式调用glAccum()函数:glAccum(GL_MULT, decayFactor);
这样,随着场景被绘制到累积缓冲区中,整个场景将越来越模糊。其中decayFactor是一个0.0~1.0之间的数字,其值越小,物体的运动速度就越快。然后,可以使用下面这个调用,将完成后的图像(显示了物体的当前位置及以前位置的“汽化痕迹”)从累计缓冲区转移到标准的颜色缓冲区:glAccum(GL_RETURN, 1.0);即使物体以不同的速度移动,或者有些物体进行的是加速运动,图像看上去仍是正确的。和前面一样,微移点越多,最终图像的质量就越高,直到由于累计缓冲区的有限精度而无法进一步提高分辨率为止。可以同时在时间和空间上进行微移,同时实现运动模糊和场景抗锯齿效果。但是,图像渲染质量的提高总是以增加渲染时间为代价的。
-
景深
在正常情况下,OpenGL绘制的所有物体都进行了正确的聚焦(除非用户使用的显示器坏了,在这种情况下,所有的物体看上去都很模糊)。累积缓冲区可以模拟我们在照片中所看到的效果,物体和聚焦平面的距离越远,它们看上去就越模糊。这个技巧并不是对照相机所产生的这种效果进行准确模拟,但是它的结果看上去有点类似。
为了实现这个效果,可以通过glFrustum()函数中使用不同的参数值,对场景进行反复绘制。可以选择这样的参数:当观察点的位置发生稍微的变化时,每个视景体都覆盖了聚焦平面上的同一个矩形,另外,可以使用累积缓冲区,按照常规的方法,对所有的渲染结果求平均值。
由于累积缓冲区会带来大内存的开销,所以在实时应用程序中比较少用。但在非实时的应用程序中,可以产生实时应用程序无法做到的效果。例如,你可以多次渲染场景,并在每次渲染时进行抖动零点几个像素,这样就可以产生整个场景的反走样的效果,比多重采样的效果还要好。还可以模糊前景或背景,然后清晰的渲染一个物体来模拟,照相机景深的效果。
-
柔和阴影
为了累积多个光源所产生的柔和阴影,可以多次渲染场景,每次只打开一个光源,并将渲染结果累积起来。这个操作可以与使用空间微移的场景抗锯齿操作一起进行。
-
微移
如果打算选取9个或16个样本对图像进行抗锯齿,我们可能会认为这些样本均匀地分布在图像中是最好的选择。令人吃惊的是,这种想法不一定正确。事实上,有时候在相邻的像素上进行采样的效果更好。我们可能希望采样点在像素中心的周围均匀分布(规范化分布)。
帧缓冲区对象
在帧缓冲区内的各种操作常常需要在缓冲区之间大量移动数据,这就是引入帧缓冲区对象的原因所在,可以创建自己的帧缓冲区,并且使用它们附带的渲染缓冲区来最小化数据复制并优化性能。
对于执行离屏渲染、更新纹理图像和进行缓冲区ping-ponging(GPGPU中用到的一种数据传输技术),帧缓冲区对象都非常有用。
窗口系统所提供的帧缓冲区是你的图形服务器的显示系统可用的唯一的帧缓冲区,也就是说,它是你在自己的屏幕上可以看到的唯一的帧缓冲区。当窗口打开的时候所创建的缓冲区,其使用上也有所限制。相比较而言,应用程序创建的帧缓冲区不能在显示器上显示,它们只支持离屏渲染。
窗口系统提供的帧缓冲区和我们所创建的帧缓冲区的另一个不同之处在于,当窗口创建的时候,窗口系统管理的那些缓冲区就分配它们的缓冲区——颜色、深度、模板和累积等。当创建应用管理的帧缓冲区对象的时候,需要创建与所创建的帧缓冲区对象相关的其他渲染缓冲区。带有窗口系统提供的缓冲区的那些缓冲区,不能与应用程序创建的缓冲区对象相关联,反之亦然。
要分配一个应用程序生成的帧缓冲区对象的名字,需要调用glGenFramebuffers(),它将为帧缓冲区对象生成一个未使用的标识符。和OpenGL中的一些其他对象相比较(例如,纹理对象和显示列表),总是需要使用glGenFramebuffers()返回的一个名字。
分配一个帧缓冲区对象名字并不会真正地创建帧缓冲区对象或者为其分配任何存储空间。这些任务都是通过调用glBindFramebuffer()来完成的。glBindFramebuffer()操作与我们在OpenGL中所见到的众多其他glBind*()函数工作方式相似。当初次针对一个特定的帧缓冲区调用它的时候,它会为该对象分配存储空间并初始化。任何后续的调用,将会把所提供的帧缓冲区对象名字作为一个活跃的对象绑定。
一旦创建了一个帧缓冲区对象,仍然不能用它做太多事情。需要提供一个地方,作为绘制的目的地和读取操作的源,这些地方叫做帧缓冲区附加。渲染缓冲区是可以添加到帧缓冲区对象的类型之一。
-
渲染缓冲区
渲染缓冲区是OpenGL管理的有效内存,其中包含了格式化的图像数据。假设图像缓冲区的格式与OpenGL期望向其中渲染的格式一致(例如,不能向深度缓冲区中渲染颜色),一旦渲染缓冲区附加到一个帧缓冲区对象,它所保存的数据就变得有意义了。
与OpenGL种的很多其他缓冲区一样,分配和删除缓冲区的过程也和我们前面见到的类似。要创建一个新的渲染缓冲区,可以调用glGenRenderbuffers()。
-
创建渲染缓冲区存储
当初次针对一个未使用的渲染缓冲区名字调用glBindRenderbuffer()的时候,OpenGL服务器创建一个渲染缓冲区,其所有状态信息都设置为默认值。在这个配置过程中,并没有分配存储空间来存储图像数据。我们需要分配存储空间并指定其图像格式,然后才可以把渲染缓冲区附加到一个帧缓冲区并向其中渲染。一旦为渲染缓冲区创建了存储空间,需要将其附加到一个帧缓冲区对象,然后才能够向其中渲染。
-
帧缓冲区附加
渲染的时候,可以把渲染的结果发送到如下位置:
- 发送到颜色缓冲区以创建图像,甚至发送到多个颜色缓冲区(如果使用多个渲染目标);
- 发送到深度缓冲区以存储遮挡信息;
- 用来存储基于像素掩码的模板缓冲区,以控制渲染。
这些缓冲区的每一个都表示为一个帧缓冲区附加,我们可以向其附加合适的图像缓冲区,以便稍后向其中渲染或者从其中读取。
当前,有两种类型的渲染表面可以与这些附加之一关联起来:渲染缓冲区和纹理图像的一层。首先介绍一个渲染缓冲区附加到帧缓冲区对象,这通过调用glFramebufferRenderbuffer()来实现。
样例创建并附加了两个渲染缓冲区:一个用于颜色,另一个用于深度。然后,我们处理渲染,最后,把结果复制回窗口系统提供的帧缓冲区以显示结果。可以使用这种技术为一个视频的离屏渲染生成帧,这样,就不必担心由于窗口重叠或者某人调整窗口大小并中断渲染而导致可见的帧缓冲区被破坏。
要记住重要的一点是,在渲染之前可能需要为每个帧缓冲区设置视口,特别是应用程序定义的帧缓冲区和窗口系统提供的帧缓冲区的大小不同的时候。
帧缓冲区对象的另一个常见用法是动态地更新纹理。你可能想要表示表面外观的变化(例如,游戏中墙上的弹孔),或者在进行类似GPGPU的计算,要更新一个查找表中的值。在这些情况下,和渲染缓冲区相对比,把纹理图像的一个层绑定为帧缓冲区附加。渲染之后,纹理图像可以从帧缓冲区解除,以便它可以用于后续的渲染。
可以从一个同时为写入而绑定为帧缓冲区附加的纹理中读取,这种情况叫做帧缓冲区渲染循环。对于两种操作来说,结果都是未定义的。也就是说,取样绑定的纹理图像所返回的值,也是绑定的时候写入到纹理层的值,这可能会出错。
-
帧缓冲区完整性
由于纹理和缓冲区格式以及帧缓冲区附加之间有无数种组合,当使用应用定义的帧缓冲区对象的时候,可能出现各种阻碍完成渲染的情况。在修改了一个帧缓冲区对象的附加之后,最好通过调用glCheckFramebufferStatus()来检查帧缓冲区的状态。
-
-
复制像素矩形
glBlitFramebuffer()以单个的、增强的调用方式,包含了glCopyPixels()和glPixelZoom()的操作。glBlitFramebuffer()允许在复制操作中更大的像素过滤,和纹理映射的方式有很大的相同之处(实际上,在复制过程中,GL NEAREST和GL LINEAR使用同样的过滤操作)。此外,这个程序对多重采样缓冲区也有意义,并且支持在不同帧缓冲区之间复制(由帧缓冲区对象控制)。
-