【UGUI源码分析】Mask的底层原理

Masking is implemented using the stencil buffer of the GPU.

Mask是通过GPU的模板缓冲实现的,关键代码只有一行,它的作用是为Mask对象生成一个特殊的材质,这个材质会将StencilBuffer的值置为1。

var maskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Replace, CompareFunction.Always);

同样,在Image,Text和RawImage的基类 MaskableGraphic 中,有一行代码,它的作用是为MaskableGraphic生成一个特殊的材质,这个材质在渲染时会取出StencilBuffer的值,判断是否为1,如果是才进行渲染。

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 optimization
    // 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;
}   

理解了原理,来看一下stencil test里的语法

Stencil
{
    Ref [Value] //表示要比较的值
    Comp [CompFunction] // 表示比较方法(等于/不等于/大于/小于等
    Pass [PassOp] // 表示当比较通过时对stencil buffer做什么操作(保留/替换/置0/增加/减少等
    Fail [FailOp] // 对buffer的操作,如果比较失败的话
    ReadMask [Value] // 表示取stencil buffer的值时用的mask
    WriteMask [Value]
}

简单的来说,是将stencil buffer的值与ReadMask与运算,然后与Ref值进行Comp比较,结果为true时进行Pass操作,否则进行Fail操作,操作值写入stencil buffer前先与WriteMask与运算。

我们再来看下前面的StencilMaterial.Add()的方法,从源代码里可以看出,它主要是帮助我们生成了一个材质并填充了Stencil相关的参数,

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.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("_UseAlphaClip", newEnt.useAlphaClip ? 1 : 0);
    m_List.Add(newEnt);
    return newEnt.customMat;
}

事实上我们可以用通过修改stencil buffer的属性来制造自定义材质,比如把上面例子中的CompareFunction.Equal 改为 CompareFunction.NotEqual 可实现遮罩的反效果,挖孔效果。

整个mask的实现流程可以看下这个flowchart,算是比较清楚了

 

 

这个博主总结的很不错,来自https://blog.csdn.net/NRatel/article/details/118252513

这里总结一下mask的实现原理:

  • mask会给image一个特殊的材质,这个材质会给image的每个像素点进行标记,将标记的结果放在一个缓存里(stencil buffer)
  • 当子级UI进行渲染的时候会去检查这个stencil buffer内的标记,如果当前覆盖的区域存在标记,就会进行渲染,otherwise不渲染

 

Reference:

  1. https://rugbbyli.gitlab.io/blog/post/2017-12-07-unity-stencil/
  2. https://www.daimajiaoliu.com/daima/479aedb9d900403
  3. https://blog.csdn.net/NRatel/article/details/118252513

 

posted @ 2022-04-05 14:12  cancantrbl  阅读(399)  评论(0编辑  收藏  举报