回头看一眼pipeline,基础的绘制部分都已经说完了,就剩下Alpha Test、Depth/Stencil Test、Blend、Dither等。下面开始进行依次介绍。


Alpha Test

Alpha Test名头很大,其实很简单,就是在PS中判断alpha值,如果alpha小于阈值,则discard,当然,Alpha Test会引起性能问题,所以能避开则避开。


Scissor Test

开始的时候我没准备把Scissor Test放在这里,因为按照pipeline它不应该出现在这里。但是由于下图的原因,发现把Scissor放在这里比较合适。(这个图应该也有一点问题,理论上是先depth test,再进行stencil test)我个人觉得到这个时候再做Scissor有点晚了,应该在PS之前就做的,否则辛苦PS出来的,就要被discard了。Scissor就好比一把剪刀,把framebuffer这个画纸剪掉一部分。

opengles

void glScissor(GLint x, GLint y, GLsizei width, GLsizei height);

Scissor Test会判断当前fragment是否处于Scissor规定的矩形区域中,矩形区域是通过glScissor这个API传入的四个参数确定的。

这个函数有4个输入参数,用于确定一个矩形区域。如果 x 小于 Xw 小于 x + width 且 y 小于 Yw 小于 y + height,则说明(Xw, Yw)确定的这个fragment在矩形区域内,则Scissor Test通过。反之,如果不通过,这个fragment则会被抛弃。Scissor Test是通过glEnableglDisable这个API,传入参数GL_SCISSOR_TEST来进行开关。如果关闭的话,则Scissor Test一直pass。如果第三个参数width和第四个参数height小于0,则出现GL_INVALID_VALUE的错误。默认状态下第一个参数x和第二个参数y初始值为0,第三个参数width和第四个参数height为GL Window的尺寸。而Scissor Test默认为关闭状态。

这个函数没有输出参数。


Depth/Stencil Test

由于OpenGL ES为3D API,那么绘制的物件当然也是3D的,但是framebuffer是2D的,而其实我们传入的顶点是3D的,也就是说其实有一张depth buffer(不管是window-system-provided framebuffer还是application-created framebuffer),物件的Z值是写在这张buffer中。当绘制开始的时候,会通过glClear这个API,传入GL_DEPTH_BUFFER_BIT,将depth buffer中的值统一为glClearDepthf设定的值。通过VS和光栅化计算好顶点3D坐标后,将Z值与depth buffer中的值进行Test,Test方法由glDepthFunc确定,如果失败,则丢弃该像素,如果通过,则算是通过了Depth Test,下一步再进行Stencil Test。Stencil Test与Depth Test类似而又稍有不同,具体情况下面通过API串讲来进行说明。


OpenGL ES API 详解

void glClearDepthf(GLclampf depth);

不管是window-system-provided framebuffer还是application-created framebuffer,其关联的buffer,不管是color buffer还是depth buffer还是stencil buffer,在刚被创建好的时候,并不会被初始化,那么根据平台不同,里面保存的东西可能不同,就好比我们虽然准备了一张画纸,但是这个画纸上面可能原本就有东西,这些东西是不确定的,所以我们先要把这张画图涂上底色,而我们可能希望这个画纸的底色是白色,也有可能希望是黑色的,或者其他颜色,而glClearDepthf这个API,就是设定一种Z值,作为统一 depth buffer所使用的Z值。

这个函数只有1个输入参数,用于确定Z值,供统一 depth buffer使用的。这个输入参数初始值为1。在输入的时候这个值可以随意填写,但是会被闭区间[0,1]进行clamp。

这个函数没有输出参数,通过这个API确定的Z值,会被用于glClear,用于统一 depth buffer使用。

void glDepthFunc(GLenum func);

如果Depth Test失败,该pixel将被discard,depth buffer也不会更新。Depth Test是由glEnableglDisable这个API,传入参数GL_DEPTH_TEST来进行开关。如果disable了,Depth Test和Depth buffer的写入操作将会被跳过,直接进行下一个操作。而Stencil值是否会被在Stencil buffer中进行更新,则以下文为准(Depth Test当做通过Depth Test处理)。如果enable,Depth Test则正常进行。

这个函数只有1个输入参数,用于确定Depth Test的对比函数。只接受GL_NEVER, GL_LESS, GL_EQUAL, GL_LEQUAL, GL_GREATER, GL_NOTEQUAL, GL_GEQUAL, GL_ALWAYS。否则,则会出现GL_INVALID_ENUM的错误。默认为GL_LESS,也就是说如果这次Draw Call获得的Z值小于Depth buffer中对应位置存储的Z值,则Depth Test通过,Z值将写入Depth buffer。否则Depth Test失败,Z值将不被写入Depth Test,但是有一点,无论Depth Test成功失败,Stencil 都会根据设定判断是否需要写入Stencil Buffer。

这个函数没有输出参数。默认情况下Depth Test是处于关闭状态。如果没有depth buffer(比如通过APIeglCreateWindowSurface创建的surface config不包含depth bit或者使用FBO的时候压根没有attach depth attachment),则认定Depth Test Always Pass。

void glDepthMask(GLboolean flag);

glDepthMask 是用于控制 depth buffer 的写入权限。

这个函数只有1个输入参数,用于确定depth buffer是否有写入权限,如果传入的为GL_TRUE,则说明depth buffer有写入权限。初始状态下,depth buffer是可写的。当传入GL_FALSE的时候,depth buffer不可写。

这个函数没有输出参数。

void glDepthRangef(GLclampf nearVal, GLclampf farVal);

将Depth值从NDC(normalized device coordinate)转到window坐标系下,然后再写入depth buffer中。计算公式为Zw = (f-n)*Zd/2+(n+f)/2。

这个函数有2个输入参数。第一个参数nearVal用于确定近裁剪面在window坐标系下的坐标。初始值为0。第二个参数farVal用于确定远裁剪面在window坐标系下的坐标。初始值为1。Depth在NDC空间是经过了除以W的操作,所以depth值为-1到1,考虑到远近裁剪面,glDepthRangef将Depth线性的从NDC转到了window坐标系下。不考虑depth buffer的实际构造,window坐标系下depth值被认为是0到1。所以两个输入参数都会被闭区间[0,1]进行clamp,这样得到的depth值也将会是0到1。nearVal不一定比farVal小,比如nearVal=1,farVal=0也可以。

这个函数没有输出参数。

void glPolygonOffset(GLfloat factor, GLfloat units);

经过glDepthRangef算出来的window坐标系下的Z值,经过光栅化后,可以通过glPolygonOffset做整体偏移。这个操作将在光栅化之后,以及depth test以及写入depth buffer之前。这个API主要是用于绘制hidden-line图片(比如墙后面的人,用线框表示),或者表面上的贴花,或者描边。

这个函数有2个输入参数。第一个参数factor和第二个参数units将会用于一个公式,算出一个offset,然后加到depth值上。offset = factor × DZ + r × units。DZ 与屏幕空间中整个polygon的depth值的变化范围有关。范围为[0, 1]。units是个平台相关的一个常数,代表着depth偏移量的最小单位。第一个参数factor和第二个参数units的初始值都为0,两个值正负都可以。

这个函数没有输出参数。而这个函数的执行受到 GL_POLYGON_OFFSET_FILL 的限制,如果通过APIglEnable打开GL_POLYGON_OFFSET_FILL,那么光栅化生成的polygon上的每个depth值都要加上这个offset。但是无论如何offset,depth值无论如何都在[0, 1]。

void glClearStencil(GLint s);

glClearStencil这个API,就是设定一种Stencil,作为统一 stencil buffer所使用的Stencil值。

这个函数只有1个输入参数,用于确定Stencil值,供统一 stencil buffer使用的。这个输入参数初始值为0。这个输入参数虽然是一个int值,但是其实是用于执行位操作,也就是说s最大为2的m次方-1,其中m为stencil buffer的位数。s可能为0,可能为10,可能为101,可能为1010101。

这个函数没有输出参数,通过这个API确定的Stencil值,会被用于glClear,用于统一 stencil buffer使用。

void glStencilFunc(GLenum func, GLint ref, GLuint mask);

void glStencilFuncSeparate(GLenum face, GLenum func, GLint ref, GLuint mask);

Stencil Test是将stencil buffer上的值与ref值做对比,如果Stencil Test失败,该pixel将被discard。Stencil Test是由glEnableglDisable这个API,传入参数GL_STENCIL_TEST来进行开关。如果disable了,Stencil Test将会被跳过,直接进行下一个操作。而Stencil值是否会被在Stencil buffer中进行更新,则以下文为准。如果enable,Stencil Test则正常进行。

glStencilFunc只有3个输入参数。glStencilFuncSeparate有4个参数。由于Stencil Test分为两种模式,front 和 back。针对non-polygon(比如point、line等)以及front-facing polygon(通过glFrontFace确定什么是front-facing polygon),使用front模式,针对back-facing polygon,则使用back模式。glStencilFuncSeparateglStencilOpSeparate会去通过第一个参数face来区分这两种模式,可以是GL_FRONT, GL_BACK, GL_FRONT_AND_BACK。否则,则会出现GL_INVALID_ENUM的错误,当使用GL_FRONT_AND_BACK参数时,等同于glStencilFunc和glStencilOp。而glStencilFunc会针对这两种模式进行同样的处理方式。glStencilFunc的三个输入参数用于决定stencil test是否能够pass。其中第二个参数ref是一个int值,用于进行unsigned stencil test。ref值将会先被clamp在[0,2的s次方-1],s为stencil buffer的位数,默认为0。第三个参数mask则为参与对比的stencil的位数,默认全为1。第一个参数func确定测试的方法,只能是GL_NEVER, GL_LESS, GL_LEQUAL, GL_GREATER, GL_GEQUAL, GL_EQUAL, GL_NOTEQUAL, GL_ALWAYS。否则,则会出现GL_INVALID_ENUM的错误。默认为GL_ALWAYS。如果第一个参数func是GL_LESS,则代表着如果第二个参数ref比stencil buffer中存储的值小,也就是if ( ref & mask ) 小于 ( stencil & mask ),则stencil test pass。

这个函数没有输出参数。默认情况下Stencil Test是处于关闭状态。如果没有stencil buffer(比如通过APIeglCreateWindowSurface创建的surface config不包含stencil bit或者使用FBO的时候压根没有attach stencil attachment),则认定Stencil Test Always Pass。

stencil一般是先被绘制到屏幕上,然后再在屏幕上绘制物件,一般用在multipass绘制中,用于达到一些特殊效果,比如decal贴花、描边等。

void glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass);

void glStencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass);

glStencilOp和glStencilOpSeparate决定特定情况下stencil value如何被处理。第一个参数sfail代表党stencil test fail的时候如何操作。只能是GL_KEEP, GL_ZERO, GL_REPLACE, GL_INCR, GL_INCR_WRAP, GL_DECR, GL_DECR_WRAP, GL_INVERT。默认为GL_KEEP。意思是:保持、归零、用reference value替换掉stencil buffer对应的值、incrementing with saturation, decrementing with saturation、按位取反、incrementing without saturation, and decrementing without saturation。其中incrementing with saturation, decrementing with saturation意思是+1或者-1,但是结果会被clamp到2的n次方-1,n为GL_STENCIL_BITS,不会造成循环。而incrementing without saturation, and decrementing without saturation则是+1或者-1,但是结果可以造成循环,也就是最大值+1=0,0-1=最大值。第二个参数dpfail代表stencil test pass,但是depth test fail的结果,也只能是GL_KEEP, GL_ZERO, GL_REPLACE, GL_INCR, GL_INCR_WRAP, GL_DECR, GL_DECR_WRAP, GL_INVERT。默认为GL_KEEP。第三个参数dppass代表着stencil和depth test都pass的结果,或者stencil test pass,depth test被disable了或者压根没有depth buffer。也只能是GL_KEEP, GL_ZERO, GL_REPLACE, GL_INCR, GL_INCR_WRAP, GL_DECR, GL_DECR_WRAP, GL_INVERT。默认为GL_KEEP。如果使用其他值,则会出现GL_INVALID_ENUM的错误。

这个函数没有输出参数。

void glStencilMask(GLuint mask);

void glStencilMaskSeparate(GLenum face, GLuint mask);

glDepthMask 是用于控制 stencil buffer 的写入权限。

glStencilMask函数只有1个输入参数,是一个int类型的变量,一共有s位,s为stencil buffer的位数。用于确定stencil buffer的对应位是否有写入权限,如果传入的对应位为1,则说明stencil buffer的对应位有写入权限。初始状态下,stencil buffer的所有位都是可写的。当传入的对应位为0的时候,stencil buffer的对应位不可写。

glStencilMaskSeparate有2个参数,第一个参数为stencil test的模式,可以为GL_FRONT, GL_BACK, GL_FRONT_AND_BACK。否则,则出现GL_INVALID_ENUM的错误。也就是针对stencil test 的front/back两种模式设定mask位。glClear的时候,是采用front模式的mask位进行stencil buffer的clear的。

这个函数没有输出参数。

Blend

Blend名头仅次于Alpha Test,其实也很简单,就是把当前帧绘制的颜色根据设定的方式与framebuffer上原有颜色进行混合。而Blend中的Alpha Blend就是传说中的半透明,也就是特效堆积游戏中最大的性能陷阱,因为它将百分之百带来overdraw。下面,就通过几个API来介绍Blend。

Blend其实就是将当前DC得到的颜色与存储在framebuffer上的目标颜色进行混合。混合方式取决于glBlendEquation,混合因子取决于glBlendFuncglBlendColor。混合得到的结果会被clamp到[0, 1]。Blend中最常见的就是根据src或者dest的alpha值作为因子进行的alpha blend。Blend通过 glEnableglDisable传入参数GL_BLEND来打开和关闭。初始状态下Blend是关闭的,如果关闭的话,则会直接跳到下一个环节。


OpenGL ES API 详解

void glBlendColor(GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha);

定义一个Blend Color,这个Blend Color将有可能作为Blend因子,详细作用见glBlendFunc

这个函数有4个输入参数,用于确定GL_BLEND_COLOR值,在输入的时候这个值可以随意填写,但是会被闭区间[0,1]进行clamp。初始状态下 GL_BLEND_COLOR 为(0, 0, 0, 0)。

这个函数没有输出参数,通过这个API确定GL_BLEND_COLOR值,会根据glBlendFunc的设定,进行Blend。

void glBlendEquation(GLenum mode);

void glBlendEquationSeparate(GLenum modeRGB, GLenum modeAlpha);

混合的操作是由这两个API设定的。

glBlendEquationSeparate有2个输入参数,第一个参数modeRGB用于确定RGB三通道的混合模式,第二个参数modeAlpha用于确定alpha通道的混合模式。glBlendEquation只有一个输入参数,代表着RGBA四个通道的混合模式。以上涉及到的三个参数都必须是GL_FUNC_ADD, GL_FUNC_SUBTRACT, or GL_FUNC_REVERSE_SUBTRACT。否则就会出现GL_INVALID_ENUM 的错误。初始状态下,这三个参数的默认值均为GL_FUNC_ADD,GL_FUNC_ADD 也是最常用的混合算法,常用于比如AA或者半透明计算等(Patrick:没用过Blend做AA)。而这三个参数的意义如下:

opengles

这张图片是从OpenGL ES 2.0 Spec中截取出来的。代表了RGBA四个通道针对三种混合模式的计算公式。其中s下标代表着src的RGBA,src也就是当前DC得到的颜色。d下标代表着dest的RGBA,dest也就是目标framebuffer的颜色。而S和D则代表着src和dest的混合因子,RGBA四个通道的混合因子也可能不同,具体混合因子是有API 设定。

这个函数没有输出参数,通过这个API确定Blend模式,进行Blend。混合得到的结果会被clamp到[0, 1]。

void glBlendFunc(GLenum sfactor, GLenum dfactor);

void glBlendFuncSeparate(GLenum srcRGB, GLenum dstRGB, GLenum srcAlpha, GLenum dstAlpha);

混合的因子是由这两个API设定的。

glBlendFuncSeparate 有4个输入参数,第一个参数 srcRGB 和第二个参数 dstRGB 用于确定RGB三通道src和dest的混合因子,第三个参数 srcAlpha 和第四个参数 dstAlpha 用于确定alpha通道src和dest的混合因子。 glBlendFunc 只有2个输入参数,第一个参数sfactor代表着src中RGBA四个通道的混合因子,第二个参数dfactor代表着dest中RGBA四个通道的混合因子。以上涉及到的六个参数中,三个参数是针对src的,三个参数是针对dest的。针对dest的参数必须是GL_ZERO, GL_ONE, GL_SRC_COLOR, GL_ONE_MINUS_SRC_COLOR, GL_DST_COLOR, GL_ONE_MINUS_DST_COLOR, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_DST_ALPHA, GL_ONE_MINUS_DST_ALPHA. GL_CONSTANT_COLOR, GL_ONE_MINUS_CONSTANT_COLOR, GL_CONSTANT_ALPHA, and GL_ONE_MINUS_CONSTANT_ALPHA。初始状态下默认值为 GL_ZERO 。针对src的参数比针对dest的参数可选择性多一个 GL_SRC_ALPHA_SATURATE 。初始状态下默认值为GL_ONE。否则就会出现GL_INVALID_ENUM 的错误。而这些参数的意义如下:

opengles

这张图片是从OpenGL ES 2.0 Spec中截取出来的。代表了RGBA四个通道对应的N种混合因子。其中s下标代表着src的RGBA,src也就是当前DC得到的颜色。d下标代表着dest的RGBA,dest也就是目标framebuffer的颜色。而c下标代表着Blend Color的RGBA,Blend Color就是由API glBlendColor设定的GL_BLEND_COLOR, 而S和D则代表着src和dest的混合因子,RGBA四个通道的混合因子也可能不同,具体混合因子是有API 设定。其中需要注意的就是GL_SRC_ALPHA_SATURATE因子只适用于src,而其中涉及到的f= min(As, 1 - Ad)。

这个函数没有输出参数,通过这个API确定Blend因子,进行Blend。混合得到的结果会被clamp到[0, 1]。

混合将在所有的draw buffer中发生,使用这些buffer的颜色作为dest。如果color buffer中没有alpha值,则alpha为1。

void glEnable(GLenum cap);

void glDisable(GLenum cap);

开启或者关闭GPU的特性。

glEnable 和 glDisable函数都是只有1个输入参数,用于指定一个GL的特性。可以传入GL_BLEND(将PS算出的颜色与color buffer上的颜色进行混合)、GL_CULL_FACE(根据三角形在window坐标系的顶点顺序进行剔除)、GL_DEPTH_TEST(进行depth test并根据test结果更新depth buffer,需要注意即使存在depth buffer且depth mask为非0,如果depth test被disable了,那么也不会进行depth test)、GL_DITHER(在写入color buffer之前对color进行dither,默认开启)、GL_POLYGON_OFFSET_FILL(将光栅化产生的polygon fragment的depth进行offset的偏移)、GL_SAMPLE_ALPHA_TO_COVERAGE(通过alpha值计算出来一个临时的coverage值,然后与fragment的coverage做与操作)(Patrick:没用过)、GL_SAMPLE_COVERAGE(将fragment的coverage做和临时 coverage 做与操作,如果GL_SAMPLE_COVERAGE_INVERT 为GL_TRUE,则反转coverage)(Patrick:没用过)、GL_SCISSOR_TEST(丢弃scissor外面的像素)、GL_STENCIL_TEST(进行 stencil test并根据test结果更新 stencil buffer)。如果传入其他值,则会出现GL_INVALID_ENUM 的错误。

这个函数没有输出参数。

Dither

Dither抖动算法,在OpenGL ES API层次只能打开或者关闭这个算法,默认是打开的。

Dither的功能:对于可用颜色较少的系统,可以以牺牲分辨率为代价,通过颜色值的抖动来增加可用颜色数量。抖动操作是和硬件相关的,OpenGL允许程序员所做的操作就只有打开或关闭抖动操作。实际上,若机器的分辨率已经相当高,激活抖动操作根本就没有任何意义。要激活或取消抖动,可以用glEnable(GL_DITHER)和glDisable(GL_DITHER)函数。默认情况下,抖动是激活的。

Dither的算法我也没具体研究过,就放一个链接在这里吧:http://blog.csdn.net/grimraider/article/details/7449278


还剩下最后两个API,不适合在之前的任何一个章节讲解,glFlushglFlush。这两个API是用于同步GL命令。

void glFlush(void);

这个API的功能是:指示之前所有的GL命令都必须在一定时间内完成。

GL命令可能会被存放在不同的command buffer中,比如网络buffer和硬件加速器之类。glFlush这个API会清空这些buffer,使得所有被调用的GL命令都会被真正的绘制引擎尽快的执行。尽管这些命令不一定能在一个特定的时间内执行完毕,但是也会在一定时间内完毕的。

正是由于GL命令可能在这些command buffer中,所以当程序需要确认这些GL已经被执行的时候,需要手动调用glFlush这个API。比如在用户需要针对生成的图片进行输入之前,需要调用这个API。

这个函数没有输入和输出参数。glFlush可以在任意时间返回,不需要等待所有的GL命令执行完毕。

void glFinish(void);

这个API的功能是:强制所有的GL命令去完成(包括设置状态、影响framebuffer等等)。而且不完成,不返回。

这个函数没有输入和输出参数。

本节教程就到此结束,希望大家继续阅读我之后的教程。

谢谢大家,再见!


原创技术文章,撰写不易,转载请注明出处:电子设备中的画家|王烁 于 2017 年 12 月 19 日发表,原文链接(http://geekfaner.com/shineengine/blog13_OpenGLESv2_12.html)