Stencil Test的应用总结

0. 前言

一直以来,对Stencil的Operation知其然而不知其因此然,不太明白提供这些Operation更新Stencil有什么用。而GPU的Stencil更新机制实际上是根据应用的需求才这么设计的,理解好Stencil的应用状况,才能理解好Stencil Test的更新机制。所以,本文将对其主要的应用作下梳理,加强对Stencil Test的认知。html

1. Stencil Test简介

在OpenGL/Direct3D的流水线中,Stencil Test被纳入Pixel Shader以后的Output Merger Stage,其处理单位是像素(若是MSAA打开,则是Sample)。Stencil Test的有两个要点:node

  • Stencil值的测试,用于剔除像素
  • Stencil值的更新,用于产生实现特定效果的Stencil值

Stencil值的测试很简单——从Stencil Buffer 里读出该像素的Stencil值(8bit的UINT)与参考值比较,知足比较条件则pass最终画出(假设能经过Depth Test或其余剔除),不然fail直接剔除。比较函数以及参考值都是经过API设定,例如OpenGL的glStencilFunc(GLenum func, GLint ref, GLuint mask)函数。与Depth Test的比较函数相似,Stencil Test的比较函数包括NEVER, LESS, LEQUAL, GREATER, GEQUAL, EQUAL, NOTEQUAL和ALWAYS。 算法

经过Stencil值的测试咱们能够限制渲染的区域,好比下面的例子把渲染区域限制为Stencil值等于1的区域。 windows

clipboard.png
图1 给定中间图片中的Stencil值,将比较条件设为EQUAL,参考值设为1时,左侧图片的color经过Stencil Test后。 ide

咱们看到,只要Stencil Buffer里存储了指望的Stencil值,咱们就能够经过Stencil Test剔除像素来画出指望的区域,正如Stencil自己的含义(模板)。而事实上问题重点常在于如何构造出指望的Stencil值,除了少数应用使用特定已知的模板外,大部分是在渲染过程当中产生须要的模板,这就是要讲的第二个要点——Stencil值的更新,它是实现各类效果的关键。
在OpenGL中,写Stencil Buffer的开启与否是经过函数glStencilMask(GLuint mask)设置的,这个函数的参数mask对应Stencil值的各个bit是否容许写入,当mask设为0表示彻底关闭写Stencil Buffer。在开启写Stencil Buffer的状况下,不管像素是否被Stencil Test或Depth Test剔除,GPU都会执行Stencil值的更新。更新方式是跟Stencil Test和Depth Test的测试结果紧密联系的,OpenGL/D3D把测试结果分为三种状况:函数

  • sfail: Stencil Test fail
  • dpfail: Stencil Test pass但Depth Test fail
  • dppass: Stencil Test pass且Depth Test pass

经过API glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)能够分别为这三种测试结果指定更新该像素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在执行Stencil Test和Depth Test(没有Enable Depth Test的话将一直pass),按照测试结果(sfail,dpfail,dppass)对应的方式算出新的Stencil值,若有发生变化则写回Stencil Buffer里。
正是有上面的多种更新方式,以及Depth Test和Stencil Test的紧密联系使得Stencil Test能经过多个pass实现多种效果。网站

2. Stencil Test的应用

从上面能够看出,Stencil应用的过程大概是这样:ui

  • 开启写Stencil Buffer
  • 渲染物体,更新Stencil Buffer的内容
  • 关闭写Stencil Buffer
  • 渲染(其余)物体,经过Stencil Buffer的内容把部分像素剔除掉。

咱们看下不一样的更新机制如何实现特定需求的。spa

2.1 轮廓

给物体添加轮廓的思路很简单——把同一个物体画两遍,其中第一遍正常地渲染物体,第二遍将原物体作微小拉伸(比原来多出轮廓),并让Pixel Shader输出轮廓颜色。同时要使第一遍所画的像素位置上在第二遍渲染中不会再被画出新的像素,即须要使用一种剔除方法,使第二次渲染时只保留两次渲染物体的非重叠部分。
一开始咱们可能会想到用Depth Test——第一次渲染时打开Depth Write,在第二遍渲染时在Vertex Shader给构成网格的每一个顶点设一个足够大的深度值,这样第二次渲染时重叠部分会在GPU的Depth Test中由于遮挡而被剔除。然而,当场景里存在其余背景物体时,轮廓也会被遮挡住。所以,Depth Test并非过滤像素区域的好方法,而这样的需求场景,原本就是Stencil Test的舞台。

利用Stencil Test画轮廓的大概步骤是这样的:
1)将sfail, dfail, dpass的更新方式分别设为KEEP,KEEP,REPLACE

glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);

2)关闭写Stencil Buffer,按正常方式渲染背景。

glStencilMask(0x00);
    //draw the background
    ...

3)开启写Stencil Buffer,比较函数为ALWAYS,Stencil Test参考值设为1。渲染物体,这样渲染后物体每一个像素的Stencil值将等于1

glStencilFunc(GL_ALWAYS, 1, 0xFF);
    glStencilMask(0xFF);
    //draw the object
    ...

4)关闭写Stencil Buffer,比较函数设为NOTEQUAL,关闭Depth Test。将物体作微小拉伸并渲染物体,Pixel Shader输出轮廓颜色

glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
    glStencilMask(0x00);
    glDisable(GL_DEPTH_TEST);
    //draw the scaled object
    ...

clipboard.png
图 2 轮廓渲染

这个方法的思想很简单:第一次渲染物体后,最终全部画出的像素对应的Stencil值均为1,而第二次渲染时只画出Stencil值不等于1的轮廓,从而实现了指望的效果。图2是用learnopengl教程在Stencil这一章中画出的例子,我的以为这个网站的教程很适合初学OpenGL,里面对第三方库怎样build和使用有详细的解释,而且从最基本的例子开始展开按部就班,最重要的是每一个例子都有代码可参考。

2.2 Dissolve

在Graphics或Video领域,Dissolve用于描述一种过渡效果——一张图片渐渐地褪去,在同时另外一张图片替换原来的图片。Dissolve可以使用Stencil Buffer实现,在一开始将Stencil Buffer清零,经过设置不一样的比较函数,使第一张图片所有画出,而第二张图片所有不画。接着逐帧改变Stencil Buffer,逐渐增长1的个数,并以一样的方式画两张图片,直到最后Stencil Buffer全为1,只画出了第二张图片的全部像素。

clipboard.png

实现Dissolve的其中一帧的过程大概是
1) 开启Stencil,并将stencil比较函数设为GL_NEVER,参考值设为1,将sfail的更新方式设为GL_REPLACE,

glStencilFunc(GL_NEVER, 1, 1)
    glStencilOp(GL_REPLACE, GL_KEEP, GL_KEEP)

2)经过画几何体或glDrawPixels函数往Stencil Buffer里写入特定的Dissolve样式,因为Stencil Test一直fail,全部这些像素不会被画出
3)关闭写Stencil Buffer,将比较函数设为GL_EQUAL,参考值设为0,并画第一张图片,这样只有模板上为0值的地方才画出这张图片的像素

glStencilFuncGL_EQUAL, 0, 1(GL_EQUAL, 0, 1).
    //draw the 1st image
    ...

4)改变比较的参考值为1,并画第二张图片

glStencilFuncGL_EQUAL, 1, 1(GL_EQUAL, 1, 1).
    //draw the 2nd image
    ...

2.3 Shadow Volume

以上的更新机制比较简单,这里咱们继续看一个相对较复杂的应用——Shadow Volume,Shadow Volume最先是Frank Crow于1977年提出的一种为3D场景添加阴影的算法,后来也有其余研究者独立地提出一些变种算法。The Theory of Stencil Shadow Volumes给出了Shadow Volume的详细介绍。
Shadow Volume算法旨在光栅化的渲染中,确认出所渲染物体上那些受遮挡影响未能被光源照到像素,生成一个模板,而后剔除对应的像素不作lighting,从而实现阴影效果。该算法的第一步是构造一个Shadow Volume(这里不是指算法名字了,而是一个图3那样的Volume),其基本步骤是

  • 以光源为视点,找出遮挡物的全部轮廓边(那些同时被正面三角形和反面三角形包含的边)
  • 将轮廓边上的每一点向光源与其连线的方向延伸,全部边构成的多边形造成一个立体(即Shadow Volume,图3的阴影部分)的四周表面。
  • 另外可能要加上Front Cap或Back Cop,从而造成封闭的Shadow Volume。加何种Cap因不一样算法而异。

clipboard.png
图3 遮挡物在光源的延伸方向上造成的Shadow Volume

在构造Shadow Volume完成后,渲染过程大概以下:

  • 按无光照渲染整个场景,即全部物体都出于阴影中
  • 对于每一个光源,执行如下步骤:

    1. 渲染构造好的Volume,利用深度信息构造出一个模板,使出于光照中的像素在模板上有不一样的Stencil值
    2. 按有光照渲染整个场景,利用步骤1构造的模板区分阴影区域,使用额外的Blending把渲染结果添加到已有场景中

按照构造模板方法分类,Shadow Volume算法可分为两类

  • Depth pass
  • Depth fail

Depth pass和Depth fail分别在dppass和dpfail两种测试结果更新Stencil值。Wiki里还提到Exclusive-or的方法,这种方法也是在Depth pass时更新Stencil值,但它只采用了1bit的Stencil值,更新方式为INVERT,所以并不适用于有多个Shadow Volume重叠的状况。下面着重看戏这两种方法对于Stencil Buffer的使用,对二者的优缺点暂不作讨论。

2.3.1 Depth pass

Depth pass的思路是分两次分别渲染Shadow Volume的正面和反面,并用Stencil值记录位于物体前方的次数。若是正面和反面的次数相等,那么该位置出于光照中。若是正面的次数比反面多,那么该位置出于阴影中。由于Stencil值是在经过depth测试时更新的,因此这种方法较Depth pass。Depth pass构造应用模板的步骤为:

  • 关闭写Depth Buffer和Color Buffer,设置back-face culling,将dppass的更新方式设为GL_INCR.
  • 渲染Shadow Volume,因为Culling,只画了Shadow Volume的正面.
  • 设置front-face culling,将dppass的更新方式设为GL_DECR
  • 渲染Shadow Volume,因为Culling.只画了Shadow Volume的反面

clipboard.png
图4 Depth pass Shadow volume

如图4,箭头末端的数字分别对应每一个位置通过以上步骤后最终在Stencil Buffer里的数值,能够看到,出于阴影中的位置最终为1,由于它出于Shadow Volume的正面和反面之间,正面未被物体遮住depth pass以后Stencil值增1,而反面被物体遮住depth测试失败未能将Stencil值减1。当一个位置与眼睛的连线未闯过Shadow Volume(从左到右的第1条连线)或者穿过正反面(第2和第4条连线),那么意味着该位置在光照中。

2.3.2 Depth fail

另外一种方法Depth fail经过在dpfail时更新Stencil值来构造模板,Depth fail的步骤为:

  • 关闭写Depth Buffer和Color Buffer,设置front-face culling,将dpfail的更新方式设为GL_INCR.
  • 渲染Shadow Volume,因为Culling,只画了Shadow Volume的正面.
  • 设置back-face culling,将dpfail的更新方式设为GL_DECR
  • 渲染Shadow Volume,因为Culling.只画了Shadow Volume的反面

Depth fail实际上是depth pass的一个“翻转版本”——depth pass算出正面和反面在物体前方的次数,而depth fail则算反面和正面在物体后方的次数。这种差别致使了二者在实际应用中有各自的优点和不足,这些超出本文范围,就不深刻了。这里是一个提供代码的depth pass例子:Shadow Volume

2.3.3 Two-Sided Stencil

以上Shadow Volume的正反面是分两次渲染的,这无疑增长了Vertex Shader的带宽。事实上能够利用Two-Sided Stencil功能,对于OpenGL可经过下面两个函数分别为Front和Back设置不一样的更新方式,那么整个Shadow Volume实际上只须要画一次,同时画正面和背面,由GPU根据三角形的Face去选择更新Stencil值的方式。

void glStencilFuncSeparate(GLenum face​, GLenum func​, GLint ref​, GLuint mask​);
    void glStencilOpSeparate(GLenum face​, GLenum sfail​, GLenum dpfail​, GLenum dppass​);

2.3.4 总结

Shadow Volume算法是将Stencil Buffer的数值当作计数器来使用,用于统计物体每一个位置的正面和反面的数量,以之判断物体与Shadow Volume的关系。本质上,Stencil Buffer使用来记录物体与Shadow Volume两个面的遮挡关系,这也解释了Stencil值的更新为何要跟Depth Test的结果绑定在一块儿。

2.4 其余

除了上述提到的应用外,Wiki中提到的Stencil Test其余应用还有Decaling,portal rendering,Reflections,intersection highlighting等,留待慢慢消化。

相关文章
相关标签/搜索