模板缓冲(Stencil Buffer)

发布于 2021-01-13 18:01

1.Stencil Buffer是什么

    模版缓冲(stencil buffer)或印模缓冲,是在三维绘图等计算机图像硬件中常见的除颜色缓冲、像素缓冲、深度缓冲之外另一种数据缓冲。模版缓冲是以像素为单位的,整数数值的缓冲,通常给每个像素分配一个字节长度的数值。深度缓冲与模版缓冲经常在图形硬件的随机存取内存(RAM)中分享相同的区域。下图比较直观的展示了模板缓冲的原理。

2.Stencil Buffer的用处

    简单说:用来做模板测试(Stencil Test)

    详细说:最简单的情况,模版缓冲被用于限制渲染的区域。更多高级应用会利用深度缓冲与模版缓冲的在图形渲染流水线中的强关联关系。例如,模版的数值可以按每个像素是否通过深度测试来自动增加或减少。简单组合使用深度测试与模版修饰符可以使得大量的本来需要多次渲染过程的效果(例如阴影、外形的绘制或复合几何图元(Geometric primitive)的交叉部分的高光处理)可以简单实现,因此减轻了图形硬件的负担。

    我理解的StencilBuffer:一块和屏幕像素一一对应的Buffer,当前需要绘制到屏幕上的像素点可以通过用自身的某个值和缓冲区中对应位置的值作比较(Stencil Test),得到不同的测试结果(Pass,Fail,ZFail),根据不同的结果决定后续操作(保留像素,丢弃像素甚至改写Buffer值等,后续详细介绍),通过这些操作来实现某些效果。下图是Stencil Test的流程图,比文字更加便于理解:

模板测试流程:

注:Stencil Test是在片元着色器处理完之后进行,因此上图Samples可以理解成片元着色器处理后得到的物体对应的像素点色彩信息。下面是按照上图根据理解大致写出的伪代码。

伪代码:

//初始化stencilbuffer
int[,] stencil_buffer = { initvalue };
//当前需要绘制的像素
int[,] color_buffer = SamplePixels();
//设置当前绘制的物体的ref值,整数值
int refValue = 2;
//2 for example
//设置比较函数
delegate CompareFunc = SomeCompareFun;//大于、小于、等于或其他
//设置对应结果操作
delegate SFailOperation = SomeSFailOperation;//后续详解
delegate ZFailOperation = SomeZFailOperation;//后续详解
delegate PassOperation = SomePassOperation;//后续详解

foreach(var pixel in color_buffer )

{

//比较函数可以有很多种

 if(!CompareFunc(stencil_buffer[pixel.x,pixel.y],refValue))

{

        Discard(pixel);

SFailOperation();

}

else

{

if(!ZTest())

{

            Discard(pixel);

ZFailOperation();

}

else

{

            Keep(pixel);

PassOperation();

}

}

}


3.Unity中的StencilBuffer

3.1 定义说明

其实大部分人和我当初看StencilBuffer遇到是同样的问题,道理我都懂,但是到底是怎么应用的呢。带着这个问题,我们来研究一下Unity中是如何使用的。首先在Unity中是在Shader中设置属性的方式来启用StencilBuffer,ShaderLab语法设置如下:

Stencil
{
Ref [Value]
Comp [CompFunction]
Pass [PassOp]
Fail [FailOp]
ZFail [ZFailOp]
ReadMask [Value]
WriteMask [Value]
}

各字段含义:

  • Ref 表示要比较的值;

  • Comp 表示比较方法(等于/不等于/大于/小于等),具体说明看下方表格

  • Pass/Fail 表示当比较通过/不通过时对stencil buffer做什么操作(保留/替换/置0/增加/减少等),对应下方的Stencil Operation

  • ZFail 表示当ZTest不通过时对Stencil Buffer需要做什么操作,对应下方的Stencil Operation

  • ReadMask/WriteMask 表示取Stencil Buffer的值时用的mask(即可以忽略某些位);

官方文档关于Compparision Function和 Stencil Operation的说明

Comparison Function说明
GreaterOnly render pixels whose reference value is greater than the value in the buffer.
GEqualOnly render pixels whose reference value is greater than or equal to the value in the buffer.
LessOnly render pixels whose reference value is less than the value in the buffer.
LEqualOnly render pixels whose reference value is less than or equal to the value in the buffer.
EqualOnly render pixels whose reference value equals the value in the buffer.
NotEqualOnly render pixels whose reference value differs from the value in the buffer.
AlwaysMake the stencil test always pass.
NeverMake the stencil test always fail.
Stencil Operation说明
KeepKeep the current contents of the buffer.
ZeroWrite 0 into the buffer.
ReplaceWrite the reference value into the buffer.
IncrSatIncrement the current value in the buffer. If the value is 255 already, it stays at 255.
DecrSatDecrement the current value in the buffer. If the value is 0 already, it stays at 0.
InvertNegate all the bits.
IncrWrapIncrement the current value in the buffer. If the value is 255 already, it becomes 0.
DecrWrapDecrement the current value in the buffer. If the value is 0 already, it becomes 255.


3.2 示例

关于示例,可以参考官方文档有一个简单的示例,这里简单说一下,就不全贴出来了,文末有引用地址。

示例给出了红绿蓝三个球,他们三个的StencilBuffer设置如下:

//红色球
Stencil
{
Ref 2
Comp always
Pass replace
}

//绿色球
Stencil
{
Ref 2
Comp equal
Pass keep
ZFailRef 1
Comp equal
//不写就是默认值
}

渲染过程说明:

  • 红球:Ref=2,always:总是通过测试,因此渲染红球后,Pass操作replace,将红球的Ref值2替换到StencilBuffer对应位置的值。

  • 绿球:Ref=2,equal:值相等通过测试,通过则保持StencilBuffer中的值,ZFail会StencilBuffer中的值-1,此时StencilBuffer中值在红球范围内的像素点是2,因此绿球和红球重叠且没有被红球挡住的部分就会被渲染成绿色,而绿球和红球重叠且被红球挡住的部分的StencilBuffer,因为ZFail,此部分的StencilBuffer值-1后值变成了1。

  • 最终效果如下: 

4.UGUI中的Mask

    在Unity中StencilBuffer最常见的一个应用就是UGUI中的Mask组件,这东西的实现就是用的StencilBuffer。具体UGUI如何使用的StencilBuffer,UGUI是开源的,我们可以看下他的代码。这里为了简洁我只贴出来相关的关键部分的代码了,在Unity中Mask组件主要是对于继承MaskableGraphic类的UI组件起作用,因此我们着重看的有三个类,Mask,Graphic和MaskableGraphic。

    这里需要说明的是,Unity渲染UI组件时默认使用的Shader默认是:UI/Default,是支持StencilBuffer的:

Shader "UI/Default"
{
Properties
{
...
_StencilComp ("Stencil Comparison", Float) = 8
_Stencil ("Stencil ID", Float) = 0
_StencilOp ("Stencil Operation", Float) = 0
_StencilWriteMask ("Stencil Write Mask", Float) = 255
_StencilReadMask ("Stencil Read Mask", Float) = 255
...
}

SubShader
{
...
Stencil
{
Ref [_Stencil]
Comp [_StencilComp]
Pass [_StencilOp]
ReadMask [_StencilReadMask]
WriteMask [_StencilWriteMask]
}
...
Pass
{
...
}
}
}

下面是Graphic.cs,Mask.cs,MaskableGraphic.cs文件中的相关代码:

//code from graphic
public abstract class Graphic:UIBehaviour,ICanvasElement
{
...

/// <summary>
/// Call to update the Material of the graphic onto the CanvasRenderer.
/// </summary>
protected virtual void UpdateMaterial()
{
if (!IsActive())
return;

canvasRenderer.materialCount = 1;
canvasRenderer.SetMaterial(materialForRendering, 0);
canvasRenderer.SetTexture(mainTexture);
}


//在Rebuild->UpdateMaterial->materialForRendering
public virtual Material materialForRendering
{
get
{
var components = ListPool<Component>.Get();
GetComponents(typeof(IMaterialModifier), components);

var currentMat = material;
for (var i = 0; i < components.Count; i++)
currentMat = (components[i] as IMaterialModifier).GetModifiedMaterial(currentMat);
ListPool<Component>.Release(components);
return currentMat;
}
}

...
}

//code from mask
public class Mask : UIBehaviour, ICanvasRaycastFilter, IMaterialModifier
{
...

/// Stencil calculation time!
public virtual Material GetModifiedMaterial(Material baseMaterial)
{
if (!MaskEnabled())
return baseMaterial;

var rootSortCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
var stencilDepth = MaskUtilities.GetStencilDepth(transform, rootSortCanvas);
if (stencilDepth >= 8)
{
Debug.LogWarning("Attempting to use a stencil mask with depth > 8", gameObject);
return baseMaterial;
}

int desiredStencilBit = 1 << stencilDepth;

// if we are at the first level...
// we want to destroy what is there
if (desiredStencilBit == 1)
{
var maskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Replace, CompareFunction.Always, m_ShowMaskGraphic ? ColorWriteMask.All : 0);
StencilMaterial.Remove(m_MaskMaterial);
m_MaskMaterial = maskMaterial;

var unmaskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Zero, CompareFunction.Always, 0);
StencilMaterial.Remove(m_UnmaskMaterial);
m_UnmaskMaterial = unmaskMaterial;
graphic.canvasRenderer.popMaterialCount = 1;
graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0);

return m_MaskMaterial;
}

//otherwise we need to be a bit smarter and set some read / write masks
var maskMaterial2 = StencilMaterial.Add(baseMaterial, desiredStencilBit | (desiredStencilBit - 1), StencilOp.Replace, CompareFunction.Equal, m_ShowMaskGraphic ? ColorWriteMask.All : 0, desiredStencilBit - 1, desiredStencilBit | (desiredStencilBit - 1));
StencilMaterial.Remove(m_MaskMaterial);
m_MaskMaterial = maskMaterial2;

graphic.canvasRenderer.hasPopInstruction = true;
var unmaskMaterial2 = StencilMaterial.Add(baseMaterial, desiredStencilBit - 1, StencilOp.Replace, CompareFunction.Equal, 0, desiredStencilBit - 1, desiredStencilBit | (desiredStencilBit - 1));
StencilMaterial.Remove(m_UnmaskMaterial);
m_UnmaskMaterial = unmaskMaterial2;
graphic.canvasRenderer.popMaterialCount = 1;
graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0);

return m_MaskMaterial;
}
...

}

/// <summary>
/// A Graphic that is capable of being masked out.
/// </summary>
public abstract class MaskableGraphic : Graphic, IClippable, IMaskable, IMaterialModifier
{
...
/// <summary>
/// See IMaterialModifier.GetModifiedMaterial
/// </summary>
public virtual Material GetModifiedMaterial(Material baseMaterial)
{
var toUse = baseMaterial;

if (m_ShouldRecalculateStencil)
{
var rootCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
m_StencilValue = maskable ? MaskUtilities.GetStencilDepth(transform, rootCanvas) : 0;
m_ShouldRecalculateStencil = false;
}

// if we have a enabled Mask component then it will
// generate the mask material. This is an optimisation
// it adds some coupling between components though :(
Mask maskComponent = GetComponent<Mask>();
if (m_StencilValue > 0 && (maskComponent == null || !maskComponent.IsActive()))
{
var maskMat = StencilMaterial.Add(toUse, (1 << m_StencilValue) - 1, StencilOp.Keep, CompareFunction.Equal, ColorWriteMask.All, (1 << m_StencilValue) - 1, 0);
StencilMaterial.Remove(m_MaskMaterial);
m_MaskMaterial = maskMat;
toUse = m_MaskMaterial;
}
return toUse;
}
...
}

//code from StencilMaterial.cs
/// <summary>
/// Dynamic material class makes it possible to create custom materials on the fly on a per-Graphic basis,
/// and still have them get cleaned up correctly.
/// </summary>
public static class StencilMaterial
{
...
/// <summary>
/// Add a new material using the specified base and stencil ID.
/// </summary>
public static Material Add(Material baseMat, int stencilID, StencilOp operation, CompareFunction compareFunction, ColorWriteMask colorWriteMask, int readMask, int writeMask)
{
...
//略过不重要代码

var newEnt = new MatEntry();
newEnt.count = 1;
newEnt.baseMat = baseMat;
newEnt.customMat = new Material(baseMat);
newEnt.customMat.hideFlags = HideFlags.HideAndDontSave;
newEnt.stencilId = stencilID;
newEnt.operation = operation;
newEnt.compareFunction = compareFunction;
newEnt.readMask = readMask;
newEnt.writeMask = writeMask;
newEnt.colorMask = colorWriteMask;
newEnt.useAlphaClip = operation != StencilOp.Keep && writeMask > 0;

newEnt.customMat.name = string.Format("Stencil Id:{0}, Op:{1}, Comp:{2}, WriteMask:{3}, ReadMask:{4}, ColorMask:{5} AlphaClip:{6} ({7})", stencilID, operation, compareFunction, writeMask, readMask, colorWriteMask, newEnt.useAlphaClip, baseMat.name);

newEnt.customMat.SetInt("_Stencil", stencilID);
newEnt.customMat.SetInt("_StencilOp", (int)operation);
newEnt.customMat.SetInt("_StencilComp", (int)compareFunction);
newEnt.customMat.SetInt("_StencilReadMask", readMask);
newEnt.customMat.SetInt("_StencilWriteMask", writeMask);
newEnt.customMat.SetInt("_ColorMask", (int)colorWriteMask);
newEnt.customMat.SetInt("_UseUIAlphaClip", newEnt.useAlphaClip ? 1 : 0);

if (newEnt.useAlphaClip)
newEnt.customMat.EnableKeyword("UNITY_UI_ALPHACLIP");
else
newEnt.customMat.DisableKeyword("UNITY_UI_ALPHACLIP");

m_List.Add(newEnt);
return newEnt.customMat;
}
...
}

    可以看出,Mask的实现基本原理,渲染过程中,遍历每个Graphic对象,如果这个对象包含IMaterialModifier组件(Mask和MaskableGraphic都实现了该接口),则通过代码来动态设置该对象的Renderer上对应的Material中关于StencilBuffer相关的属性,从而使Mask自身以及其下的MaskableGripic相关的UI组件,在渲染时会进行Stencil Test,以此来实现被Mask遮罩的效果。StencilTest过程如下:

  1. Mask类中的GetModifiedMaterial方法中,设置自身Material的Ref值,设置通过测试后操作为Replace,即替换StencilBuffer中对应的值

  2. MaskableGraphic中的GetModifiedMaterial方法,设置对应的Material的Ref值(与Mask的Ref值相等)以及Comparision Func设置为Equal。

  3. 渲染过程中,Mask会将自身图片对应的区域的StencilBuffer的值替换成自身设置的Ref值,而渲染Mask下UI组件时,根据模板测试,只有在被Mask刷过的StencilBuffer的区域的Ref值和UI组件对应的Ref值相等,因此只有在Mask区域内的像素才会通过测试被渲染出来。这样就得到了应有的效果。

    注:这里说的是主要逻辑和思路,并不包含比如嵌套mask以及mask本身Graphic是否渲染等其他细节问题,如有兴趣可以去阅读UGUI源码。

引用

  • 维基百科:https://zh.m.wikipedia.org/zh-sg/%E6%A8%A1%E7%89%88%E7%B7%A9%E8%A1%9D

  • Unity 官方文档:https://docs.unity3d.com/Manual/SL-Stencil.html

  • Stencil testing:https://learnopengl.com/Advanced-OpenGL/Stencil-testing

本文来自网络或网友投稿,如有侵犯您的权益,请发邮件至:aisoutu@outlook.com 我们将第一时间删除。

相关素材