从0开始的OpenGL学习(十九)-模板缓存

本文主要解决两个问题:

1、模板测试的原理是什么?
2、如何利用模板缓存显示盒子的边框?

引言

前面的章节中我们介绍了深度测试,深度测试为我们呈现了物体前后遮挡的效果。不过,光有深度测试还不够,我们还需要别的东西来实现一些特殊的效果,例如水面上映照出天空,战争迷雾效果,汽车后视镜效果。于是,我们引入一个新的测试:模板测试。这个测试可以帮助我们实现上述的这些效果。

模板测试

和深度测试一样,模板测试也会决定片元是可以被渲染还是被丢弃。从渲染流程来说,模板测试在深度测试之前,通过模板测试之后才能进入深度测试。模板测试是基于另一个名叫模板缓存的东西来实现的,通过更新模板缓存,我们可以实现一些非常有趣的效果。

本质上,模板缓存不像深度缓存那样有实际意义(深度缓存表示物体的前后关系),它只是我们创造出来用于实现一些特殊效果的缓存。所以,你可能用不到模板缓存,但你肯定要用到深度缓存。模板缓存中的模板值是一个8位的整型数,所以,每个模板值就有256种不同的状态。通常我们不会用到那么多,用的最频繁的就是0和0xFF两种状态。

每个GUI库都需要设置一个模板缓存。GLFW已经自动创建好了,不过不是每个库都会自己创建,使用的时候记得查看用户手册确保模板缓存已经创建。

一个简单的模板缓存的示例:


模板缓存示例

模板缓存先被全部清0,然后在需要绘制的地方设置成1,然后场景中只有模板值为1的片元绘制出来了,其余全被丢弃。

模板缓存操作允许我们设置将模板缓存设置成特定的值。在渲染的时候,我们先写入模板值,然后在当前帧中读取这些值来测试片元是否丢弃。你可以尽情的开发模板缓存的使用方法,不过基本的流程是这样的:

  • 启用写入模板缓存功能
  • 渲染物体,更新模板缓存
  • 禁止写入模板缓存
  • 渲染物体,根据之前模板缓存中的内容进行渲染或丢弃

由此可见,使用模板缓存,我们可以根据已经渲染显示的物体去决定其他的物体时候显示。

启用模板测试的方法与启用深度测试的方法类似,只需要调用glEnable就行了,而且也需要在清屏的时候把模板缓存也清空了:

glEnable(GL_STENCIL_TEST);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

模板缓存提供一个glStencilMask函数来调整可以写入到缓存中的模板值。

glStencilMask(0xFF);  
glStencilMask(0x00);  

所有的模板值都会和glStencilMask中的参数进行与操作,这样就能起到对模板值的过滤作用。默认的参数为0xFF,一般也只需要用到0xFF和0x00就可以了。

模板函数

和深度测试一样,模板测试也有一些函数来控制如何进行测试以及测试之后进行什么操作。控制模板测试的函数是:glStencilFunc和glStencilOp。前面的函数用来控制如何进行测试,后面的函数用来控制测试之后进行什么操作。

glStencilFunc的函数原型是:glStencilFunc(GLenum func, GLint ref, GLuint mask),分别介绍一下参数的含义:

  • func:设置模板测试函数。允许的参数有:GL_NEVER, GL_LESS, GL_LEQUAL, GL_GREATER,GL_GEQUAL,GL_EQUAL,GL_NOTEQUAL和GL_ALWAYS。这些参数的含义和深度测试函数中参数含义一致。
  • ref:设置模板测试的引用值。模板缓存中的内容会和这个值比较。
  • mask:指定一个掩码值,这个值会引用值进行与运算,也会和模板缓存中的模板值进行与运算,这些运算操作是在模板测试之前完成的。默认会把掩码的所有位都置1.

在本章中我们会设置ref的值为1,mask的值为0xFF,像这样:

glStencilFunc(GL_EQUAL, 1, 0xFF);

如此调用的意思是,如果模板缓冲中的模板值等于1,则通过模板测试,否则就不通过。

glStencilFunc仅仅说明了OpenGL如何进行测试,并没有说测试完成之后该如何做。这个时候另一个函数就来了:glStencilOp。

glStencilOp(GLenum sFail, GLenum dpfail, GLenum dppass)包含了三个参数,这些参数的含义是:

  • sfail: 模板测试失败后进行的操作
  • dpfail: 模板测试成功,深度测试失败后进行的操作
  • dppass:模板测试和深度测试都成功后进行的操作
操作 描述
GL_KEEP 保留当前保存的模板值
GL_ZERO 模板值置0
GL_REPLACE 模板值设置成glStencilFunc函数中设置的引用值
GL_INCR 如果模板值小于最大值,将模板值加1
GL_INCR_WRAP 类似GL_INCR,当模板值超过最大值的时候设置成0
GL_DECR 如果模板值大于最小值,将模板值减1
GL_DECR_WRAP 类似GL_DECR,当模板值低于最小值时设置成最大值
GL_INVERT 对当前的模板值按位取反

每一个参数都可以设定以下这些值:

操作 描述
GL_KEEP 保留当前保存的模板值
GL_ZERO 模板值置0
GL_REPLACE 模板值设置成glStencilFunc函数中设置的引用值
GL_INCR 如果模板值小于最大值,将模板值加1
GL_INCR_WRAP 类似GL_INCR,当模板值超过最大值的时候设置成0
GL_DECR 如果模板值大于最小值,将模板值减1
GL_DECR_WRAP 类似GL_DECR,当模板值低于最小值时设置成最大值
GL_INVERT 对当前的模板值按位取反

glStencilOp的默认设置值为(GL_KEEP, GL_KEEP, GL_KEEP),所以不管测试结果如何,模板缓存都会保存当前的值。所以,如果你想要改变模板缓存中的值,你就需要调用glStencilOp来改变其默认值。

有了glStencilFunc和glStencilOp函数的帮助,我们就可以利用模板缓存来实现一些效果了。

物体边框

光是解释原理和函数的使用方式不足以真正了解其使用方式,所以接下来的部分,我们会用模板缓存来实现物体边框的效果,就像下面这张图一样:


物体边框

根据上面学到的内容,我们大致可以理出一个如何实现的思路:

  1. 将模板测试方法设置成GL_ALWAYS,将物体片元位置的模板缓存设置为1
  2. 渲染物体
  3. 禁用模板写入和深度测试
  4. 将所有的物体都放大一点
  5. 使用一个输出边框颜色的片元着色器
  6. 绘制物体,但是只绘制模板值不为1的地方
  7. 启用模板写入和深度测试

在这个流程中,首先设置物体的每个片元位置的模板值为1,当我们想绘制边框的时候,只需要绘制边框的时候,只需要将物体都放大一点,然后在通过模板测试的位置绘制就可以,这样就只会绘制一个物体的框框了。

所以,我们先来创建绘制放大物体的片元着色器。这一步非常简单,只需输出一个固定颜色就行了。这个着色器我们命名为shaderSingleColor:

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

我们只想绘制物体的边框,所以地板的绘制还是老样子。我们要先绘制地板,然后绘制两个箱子(写入模板缓存),最后绘制大一点的箱子(根据之前的模板缓存值来丢弃片元)。

先启用模板测试,然后设置测试后的操作:

glEnable(GL_STENCIL_TEST);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);

如果模板测试和深度测试有不成功的,我们保留原有的值,如果测试都成功了,我们就用设置的引用值替换,我们会把引用值设置为1.在清空模板缓存值之后,我们将其设置为1:

glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilMask(0xFF);
normalShader.use();
//绘制两个正常的盒子
...

使用GL_ALWAYS参数确保盒子的片元位置会设置成1.因为片元总会通过模板测试,而我们之前设置通过模板测试和深度测试后会把模板值替换成引用值。接下来,我们采用另一种测试的方式来绘制大一点的盒子,并且我们不希望模板值写入到缓存中:

glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00);
glDisable(GL_DEPTH_TEST);;
shaderSingleColor.use();
//绘制两个稍大的盒子
...

代码非常简单,如果之前对模板缓存理解透彻的话,完全可以理解代码的意义。有一点你可能会觉得奇怪,就是为什么要禁用深度测试。因为我们的盒子是放在地板上的,有一部分的框框会延伸到地板下面,如果不禁用,你就只能到地板下面去看到框框了。像这样:


不禁用深度测试的后果

好,整个过程就像是这样子:

glEnable(GL_STRENCIL_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();
//绘制地板
...
  
glStencilFunc(GL_ALWAYS, 1, 0xFF); 
glStencilMask(0xFF); 
//绘制两个正常盒子
...
  
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00); 
glDisable(GL_DEPTH_TEST);
shaderSingleColor.use(); 
//绘制两个稍大的盒子
...

//恢复模板测试和深度测试
glStencilMask(0xFF);
glEnable(GL_DEPTH_TEST);  

按照这个思路补充完代码之后,编译运行,你会看到类似下面的效果:

运行效果

如果实现有困难,请参考这里的源码

怎么样,效果很赞吧?

这种边框的效果在很多游戏中都可以使用,比如,一些策略游戏需要选中某些东西就可以用这种方法。当然这种直接用一个颜色作为边框的显示方式有点生硬,你可以做一些过滤让边框效果柔和点,比如说高斯模糊(Gaussian Blur)。

总结

本文中,我们深入的理解了模板测试的原理,学到了如何使用glEnable函数启用/禁用模板测试,如何使用glStencilFunc函数设置模板测试的方式,如何使用glStencilOp函数设置模板测试后要进行的操作。并且,通过使用这些知识,我们绘制了两个盒子边框作为实践。成效斐然~

下一篇
目录
上一篇

参考资料

www.learnopengl.com(非常好的网站,建议学习)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容