Stencil Test的应用总结
文章推薦指數: 80 %
Stencil值的测试很简单——从Stencil Buffer 里读出该像素的Stencil值(8bit的UINT)与参考值比较,满足比较条件则pass最终画出(假设能通过Depth Test或 ...
注册登录问答专栏课程招聘活动发现✓使用“Bing”搜本站使用“Google”搜本站使用“百度”搜本站站内搜索注册登录首页专栏3D图形流水线文章详情0StencilTest的应用总结Rhino发布于2019-03-08
0.前言
一直以来,对Stencil的Operation知其然而不知其所以然,不太明白提供这些Operation更新Stencil有什么用。
而GPU的Stencil更新机制其实是根据应用的需求才这么设计的,理解好Stencil的应用情况,才能理解好StencilTest的更新机制。
因此,本文将对其主要的应用做下梳理,增强对StencilTest的认知。
1.StencilTest简介
在OpenGL/Direct3D的流水线中,StencilTest被归入PixelShader之后的OutputMergerStage,其处理单位是像素(如果MSAA打开,则是Sample)。
StencilTest的有两个要点:
Stencil值的测试,用于剔除像素
Stencil值的更新,用于产生实现特定效果的Stencil值
Stencil值的测试很简单——从StencilBuffer里读出该像素的Stencil值(8bit的UINT)与参考值比较,满足比较条件则pass最终画出(假设能通过DepthTest或其他剔除),否则fail直接剔除。
比较函数以及参考值都是通过API设定,例如OpenGL的glStencilFunc(GLenumfunc,GLintref,GLuintmask)函数。
与DepthTest的比较函数类似,StencilTest的比较函数包括NEVER,LESS,LEQUAL,GREATER,GEQUAL,EQUAL,NOTEQUAL和ALWAYS。
通过Stencil值的测试我们可以限制渲染的区域,比如下面的例子把渲染区域限制为Stencil值等于1的区域。
图1给定中间图片中的Stencil值,将比较条件设为EQUAL,参考值设为1时,左侧图片的color通过StencilTest后。
我们看到,只要StencilBuffer里存储了期望的Stencil值,我们就可以通过StencilTest剔除像素来画出期望的区域,正如Stencil本身的含义(模板)。
而事实上问题重点常在于如何构造出期望的Stencil值,除了少数应用使用特定已知的模板外,大部分是在渲染过程中产生需要的模板,这就是要讲的第二个要点——Stencil值的更新,它是实现各种效果的关键。
在OpenGL中,写StencilBuffer的开启与否是通过函数glStencilMask(GLuintmask)设置的,这个函数的参数mask对应Stencil值的各个bit是否允许写入,当mask设为0表示完全关闭写StencilBuffer。
在开启写StencilBuffer的情况下,无论像素是否被StencilTest或DepthTest剔除,GPU都会执行Stencil值的更新。
更新方式是跟StencilTest和DepthTest的测试结果紧密联系的,OpenGL/D3D把测试结果分为三种情况:
sfail:StencilTestfail
dpfail:StencilTestpass但DepthTestfail
dppass:StencilTestpass且DepthTestpass
通过APIglStencilOp(GLenumsfail,GLenumdpfail,GLenumdppass)可以分别为这三种测试结果指定更新该像素Stencil数值的方式,可选的方式包括
Action
Description
GL_KEEP
当前的Stencil值保持不变
GL_ZERO
将Stencil值更新为0.
GL_REPLACE
将Stencil值替换为参考值
GL_INCR
若当前Stencil值小于最大值,则加1
GL_INCR_WRAP
Stencil值加1,若超过最大值则wrap为0
GL_DECR
若当前Stencil值大于最小值,则减1
GL_DECR_WRAP
Stencil值减1,若小于0则wrap为最大值
GL_INVERT
按位反转当前Stencil值
GPU在执行StencilTest和DepthTest(没有EnableDepthTest的话将一直pass),按照测试结果(sfail,dpfail,dppass)对应的方式算出新的Stencil值,如有发生变化则写回StencilBuffer里。
正是有上面的多种更新方式,以及DepthTest和StencilTest的紧密联系使得StencilTest能通过多个pass实现多种效果。
2.StencilTest的应用
从上面可以看出,Stencil应用的过程大概是这样:
开启写StencilBuffer
渲染物体,更新StencilBuffer的内容
关闭写StencilBuffer
渲染(其他)物体,通过StencilBuffer的内容把部分像素剔除掉。
我们看下不同的更新机制如何实现特定需求的。
2.1轮廓
给物体添加轮廓的思路很简单——把同一个物体画两遍,其中第一遍正常地渲染物体,第二遍将原物体做微小拉伸(比原来多出轮廓),并让PixelShader输出轮廓颜色。
同时要使第一遍所画的像素位置上在第二遍渲染中不会再被画出新的像素,即需要使用一种剔除方法,使第二次渲染时只保留两次渲染物体的非重叠部分。
一开始我们可能会想到用DepthTest——第一次渲染时打开DepthWrite,在第二遍渲染时在VertexShader给构成网格的每个顶点设一个足够大的深度值,这样第二次渲染时重叠部分会在GPU的DepthTest中因为遮挡而被剔除。
然而,当场景里存在其他背景物体时,轮廓也会被遮挡住。
因此,DepthTest并不是过滤像素区域的好方法,而这样的需求场景,本来就是StencilTest的舞台。
利用StencilTest画轮廓的大概步骤是这样的:1)将sfail,dfail,dpass的更新方式分别设为KEEP,KEEP,REPLACE
glStencilOp(GL_KEEP,GL_KEEP,GL_REPLACE);
2)关闭写StencilBuffer,按正常方式渲染背景。
glStencilMask(0x00);
//drawthebackground
...
3)开启写StencilBuffer,比较函数为ALWAYS,StencilTest参考值设为1。
渲染物体,这样渲染后物体每个像素的Stencil值将等于1
glStencilFunc(GL_ALWAYS,1,0xFF);
glStencilMask(0xFF);
//drawtheobject
...
4)关闭写StencilBuffer,比较函数设为NOTEQUAL,关闭DepthTest。
将物体做微小拉伸并渲染物体,PixelShader输出轮廓颜色
glStencilFunc(GL_NOTEQUAL,1,0xFF);
glStencilMask(0x00);
glDisable(GL_DEPTH_TEST);
//drawthescaledobject
...
图2轮廓渲染
这个方法的思想很简单:第一次渲染物体后,最终所有画出的像素对应的Stencil值均为1,而第二次渲染时只画出Stencil值不等于1的轮廓,从而实现了期望的效果。
图2是用learnopengl教程在Stencil这一章中画出的例子,个人觉得这个网站的教程很适合初学OpenGL,里面对第三方库怎样build和使用有详细的解释,并且从最基本的例子开始展开循序渐进,最重要的是每个例子都有代码可参考。
2.2Dissolve
在Graphics或Video领域,Dissolve用于描述一种过渡效果——一张图片渐渐地褪去,在同时另一张图片替换原来的图片。
Dissolve可使用StencilBuffer实现,在一开始将StencilBuffer清零,通过设置不同的比较函数,使第一张图片全部画出,而第二张图片全部不画。
接着逐帧改变StencilBuffer,逐渐增加1的个数,并以同样的方式画两张图片,直到最后StencilBuffer全为1,只画出了第二张图片的所有像素。
实现Dissolve的其中一帧的过程大概是1)开启Stencil,并将stencil比较函数设为GL_NEVER,参考值设为1,将sfail的更新方式设为GL_REPLACE,
glStencilFunc(GL_NEVER,1,1)
glStencilOp(GL_REPLACE,GL_KEEP,GL_KEEP)
2)通过画几何体或glDrawPixels函数往StencilBuffer里写入特定的Dissolve样式,由于StencilTest一直fail,所有这些像素不会被画出3)关闭写StencilBuffer,将比较函数设为GL_EQUAL,参考值设为0,并画第一张图片,这样只有模板上为0值的地方才画出这张图片的像素
glStencilFuncGL_EQUAL,0,1(GL_EQUAL,0,1).
//drawthe1stimage
...
4)改变比较的参考值为1,并画第二张图片
glStencilFuncGL_EQUAL,1,1(GL_EQUAL,1,1).
//drawthe2ndimage
...
2.3ShadowVolume
以上的更新机制比较简单,这里我们继续看一个相对较复杂的应用——ShadowVolume,ShadowVolume最早是FrankCrow于1977年提出的一种为3D场景添加阴影的算法,后来也有其他研究者独立地提出一些变种算法。
TheTheoryofStencilShadowVolumes给出了ShadowVolume的详细介绍。
ShadowVolume算法旨在光栅化的渲染中,确认出所渲染物体上那些受遮挡影响未能被光源照到像素,生成一个模板,然后剔除对应的像素不做lighting,从而实现阴影效果。
该算法的第一步是构造一个ShadowVolume(这里不是指算法名字了,而是一个图3那样的Volume),其基本步骤是
以光源为视点,找出遮挡物的所有轮廓边(那些同时被正面三角形和反面三角形包含的边)
将轮廓边上的每一点向光源与其连线的方向延伸,所有边构成的多边形形成一个立体(即ShadowVolume,图3的阴影部分)的四周表面。
另外可能要加上FrontCap或BackCop,从而形成封闭的ShadowVolume。
加何种Cap因不同算法而异。
图3遮挡物在光源的延伸方向上形成的ShadowVolume
在构造ShadowVolume完成后,渲染过程大概如下:
按无光照渲染整个场景,即所有物体都出于阴影中
对于每个光源,执行以下步骤:
渲染构造好的Volume,利用深度信息构造出一个模板,使出于光照中的像素在模板上有不同的Stencil值
按有光照渲染整个场景,利用步骤1构造的模板区分阴影区域,使用额外的Blending把渲染结果添加到已有场景中
按照构造模板方法分类,ShadowVolume算法可分为两类
Depthpass
Depthfail
Depthpass和Depthfail分别在dppass和dpfail两种测试结果更新Stencil值。
Wiki里还提到Exclusive-or的方法,这种方法也是在Depthpass时更新Stencil值,但它只采用了1bit的Stencil值,更新方式为INVERT,因此并不适用于有多个ShadowVolume重叠的情况。
下面着重看戏这两种方法对于StencilBuffer的使用,对两者的优缺点暂不做讨论。
2.3.1Depthpass
Depthpass的思路是分两次分别渲染ShadowVolume的正面和反面,并用Stencil值记录位于物体前方的次数。
如果正面和反面的次数相等,那么该位置出于光照中。
如果正面的次数比反面多,那么该位置出于阴影中。
因为Stencil值是在通过depth测试时更新的,所以这种方法较Depthpass。
Depthpass构造应用模板的步骤为:
关闭写DepthBuffer和ColorBuffer,设置back-faceculling,将dppass的更新方式设为GL_INCR.
渲染ShadowVolume,由于Culling,只画了ShadowVolume的正面.
设置front-faceculling,将dppass的更新方式设为GL_DECR
渲染ShadowVolume,由于Culling.只画了ShadowVolume的反面
图4DepthpassShadowvolume
如图4,箭头末端的数字分别对应每个位置经过以上步骤后最终在StencilBuffer里的数值,可以看到,出于阴影中的位置最终为1,因为它出于ShadowVolume的正面和反面之间,正面未被物体遮住depthpass之后Stencil值增1,而反面被物体遮住depth测试失败未能将Stencil值减1。
当一个位置与眼睛的连线未闯过ShadowVolume(从左到右的第1条连线)或者穿过正反面(第2和第4条连线),那么意味着该位置在光照中。
2.3.2Depthfail
另一种方法Depthfail通过在dpfail时更新Stencil值来构造模板,Depthfail的步骤为:
关闭写DepthBuffer和ColorBuffer,设置front-faceculling,将dpfail的更新方式设为GL_INCR.
渲染ShadowVolume,由于Culling,只画了ShadowVolume的正面.
设置back-faceculling,将dpfail的更新方式设为GL_DECR
渲染ShadowVolume,由于Culling.只画了ShadowVolume的反面
Depthfail其实是depthpass的一个“翻转版本”——depthpass算出正面和反面在物体前方的次数,而depthfail则算反面和正面在物体后方的次数。
这种差异导致了两者在实际应用中有各自的优势和不足,这些超出本文范围,就不深入了。
这里是一个提供代码的depthpass例子:ShadowVolume。
2.3.3Two-SidedStencil
以上ShadowVolume的正反面是分两次渲染的,这无疑增加了VertexShader的带宽。
事实上可以利用Two-SidedStencil功能,对于OpenGL可通过下面两个函数分别为Front和Back设置不同的更新方式,那么整个ShadowVolume实际上只需要画一次,同时画正面和背面,由GPU根据三角形的Face去选择更新Stencil值的方式。
voidglStencilFuncSeparate(GLenumface,GLenumfunc,GLintref,GLuintmask);
voidglStencilOpSeparate(GLenumface,GLenumsfail,GLenumdpfail,GLenumdppass);
2.3.4总结
ShadowVolume算法是将StencilBuffer的数值当做计数器来使用,用于统计物体每个位置的正面和反面的数量,以之判断物体与ShadowVolume的关系。
本质上,StencilBuffer使用来记录物体与ShadowVolume两个面的遮挡关系,这也解释了Stencil值的更新为什么要跟DepthTest的结果绑定在一起。
2.4其他
除了上述提到的应用外,Wiki中提到的StencilTest其他应用还有Decaling,portalrendering,Reflections,intersectionhighlighting等,留待慢慢消化。
gpuopengl计算机图形学阅读7.1k更新于2019-03-09赞收藏分享本作品系原创,采用《署名-非商业性使用-禁止演绎4.0国际》许可协议3D图形流水线介绍3d图形流水线原理和细节关注专栏Rhino0声望1粉丝关注作者0条评论得票最新提交评论Rhino0声望1粉丝关注作者文章目录跟随宣传栏▲
延伸文章資訊
- 1Stencil testing - LearnOpenGL
A stencil buffer (usually) contains 8 bits per stencil value that amounts to a total of 256 diffe...
- 2Stencil buffer - Wikipedia
A stencil buffer is an extra data buffer, in addition to the color buffer and Z-buffer, found on ...
- 3Unit Testing - Stencil.js
Stencil makes it easy to unit test components and app utility functions using Jest. Unit tests va...
- 4Stencil Test - OpenGL Wiki
The Stencil Test is a per-sample operation performed after the Fragment Shader. The fragment's st...
- 5Stencil Test的应用总结
Stencil值的测试很简单——从Stencil Buffer 里读出该像素的Stencil值(8bit的UINT)与参考值比较,满足比较条件则pass最终画出(假设能通过Depth Test或...