Mask裁剪原理

Mask使用stencil来实现裁剪

要用Stencil方式来实现裁剪,主要涉及到2个步骤:

1) 裁剪区域设置stencil buffer: Mask在自己的GetModifiedMaterial函数中设置。

2) 被裁减物体将自己的stencil id与stencil buffer比较,如果相同,则绘制像素,否则丢弃像素。ugui中可被裁剪的ui都继承自MaskableGraphic,这部分的逻辑也是在MaskableGraphic的GetModifiedMaterial实现。

 

除此之外,Mask还做了一个额外的操作,就是在子节点渲染完毕后,恢复stencil buffer,这样就不会影响后续节点的渲染。

 

Mask.GetModifiedMaterial源码理解

public virtual Material GetModifiedMaterial(Material baseMaterial)
{
    if (!MaskEnabled())
        return baseMaterial;

    var rootSortCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
    var stencilDepth = MaskUtilities.GetStencilDepth(transform, rootSortCanvas); //Mask和Canvas之间还有几层Mask
    if (stencilDepth >= 8)
    {
        Debug.LogError("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) //没有嵌套的Mask
    {
        var maskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Replace, CompareFunction.Always, m_ShowMaskGraphic ? ColorWriteMask.All : 0);
        StencilMaterial.Remove(m_MaskMaterial);
        m_MaskMaterial = maskMaterial;

        //---------- 恢复stencil buffer
        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 stencilID = desiredStencilBit | (desiredStencilBit - 1);
    var writeMask = stencilID;
    var maskMaterial2 = StencilMaterial.Add(baseMaterial, stencilID, StencilOp.Replace, CompareFunction.Equal,
        m_ShowMaskGraphic ? ColorWriteMask.All : 0, //colorWriteMask
        desiredStencilBit - 1, //readMask
        writeMask //writeMask
        );
    StencilMaterial.Remove(m_MaskMaterial);
    m_MaskMaterial = maskMaterial2;

    //---------- 恢复stencil buffer
    graphic.canvasRenderer.hasPopInstruction = true;
    var unmaskMaterial2 = StencilMaterial.Add(baseMaterial, desiredStencilBit - 1, StencilOp.Replace, CompareFunction.Equal,
        0, //colorWriteMask
        desiredStencilBit - 1, //readMask
        writeMask //writeMask
        );
    StencilMaterial.Remove(m_UnmaskMaterial);
    m_UnmaskMaterial = unmaskMaterial2;

    graphic.canvasRenderer.popMaterialCount = 1;
    graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0);
    //----------

    return m_MaskMaterial;
}

里面主要涉及到2种情况

1) 没有嵌套的时候

Mask和Cavnas之间没有其他Mask,所以stencil参数会被设置为:StencilID:1, StencilComp: Always, StencilOp: Replace, StencilWriteMask: 255, StencilReadMask: 255

2) 存在嵌套的时候,比如嵌套3层

a) 最外层的Mask和Canvas之间没有其他Mask,所以stencil参数还是和上面的一样:StencilID:1, StencilComp: Always, StencilOp: Replace, StencilWriteMask: 255, StencilReadMask: 255

b) 第2层Mask和Canvas之间有1层Mask,stencil参数会被设置为:StencilID:3(0011b), StencilComp: Equal, StencilOp: Replace, StencilWriteMask: 3, StencilReadMask: 1(0001b)

注意这里的ReadMask,因为在绘制Mask_2的时候,stencil buffer的值已经被Mask设置为1,所以这边需要Mask的ID值1作为ReadMask,3&1=0011b&0001b=0001b,和stencil buffer中的值相同,此时就会执行Replace,将stencil buffer替换为3。

恢复stencil buffer时,会将stencil buffer恢复为Mask的stencilID值。

c) 第3层Mask和Canvas之间有2层Mask,stencil参数会被设置为:StencilID:7(0111b), StencilComp: Equal, StencilOp: Replace, StencilWriteMask: 7, StencilReadMask: 3(0011b)

这边会将Mask_2的ID值作为ReadMask,这样7&3才能等于3。

恢复stencil buffer时,会将stencil buffer恢复为Mask_2的stencilID值。

所以,可以看到StencilID的取值还是存在一些技巧的,使用bit的方式就是简洁高效。

 

MaskableGraphic.GetModifiedMaterial源码理解

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; //ui组件和Canvas之间有几层Mask
        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, //stencilID
            StencilOp.Keep, CompareFunction.Equal, ColorWriteMask.All,
            (1 << m_StencilValue) - 1, //readMask
            0 //writeMask, 不涉及写入, 所以直接总是0为也没事
        );
        StencilMaterial.Remove(m_MaskMaterial);
        m_MaskMaterial = maskMat;
        toUse = m_MaskMaterial;
    }
    return toUse;
}

这个比较简单,只要让StencilID值和它的上层Mask相同就好,其他的2个参数是不变的:StencilOp: Keep, StencilComp: Equal。

最外层的Mask的StencilID值为1

 第2层的StencilID为3

第3层的StencilID为7

 

GetModifiedMaterial什么时候被调用? 

SetMaterialDirty的时候就会被调用到了,主要看下面两个函数:

1) Graphic.UpdateMaterial函数

protected virtual void UpdateMaterial()
{
    if (!IsActive())
        return;

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

2) Graphic.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;
    }
}

 

粒子作为Mask的子节点也还是不能被裁剪 

就算把粒子的StencilID设置的和Mask一样,StencilComp:Equal, StencilOp:Keep也不行。

貌似是因为粒子和ugui渲染队列不同造成的,粒子就算作为Mask的子节点也不会和ugui在同一个渲染队列上渲染,也就造成了渲染粒子的时候,其实Mask已经把stencil buffer恢复了。

 

参考

<Unity-UGUI>使用Mask, 正确的裁减非默认材质UI对象_阆苑小书童的博客-CSDN博客_unity mask 多画布裁剪

【Unity源码学习】遮罩:Mask与Mask2D - 蚁丘 - 博客园 (cnblogs.com)

 

posted @ 2023-01-22 23:02  yanghui01  阅读(257)  评论(0编辑  收藏  举报