最近研究了下Shader里的StencilBuffer,之前一直是知道个大概,没有认真的研究过。

关于StencilBuffer的基础说明

StencilBuffer:模板缓存,是用于模板测试的一个专用缓冲区,每个像素点有八位的存储。
模板测试的位置大概在片元着色器中的最后几步,在深度测试和混合之前,引用一个别人的流程图
在这里插入图片描述
在这里插入图片描述
简单来说,希望遮罩别人的东西设置这个地方的StencilBuffer,然后后边绘制的东西通过条件比较(等于,不等于,大于小于等等)再加上各种参数来进行测试,测试失败就不能绘制,就没法进行后边的混合操作,也就是说这个物体在这个像素点上贡献不了颜色(进不了颜色缓冲区),从而达到了遮罩的效果。
关于stencil 的各种参数,网上有很多,我就不再详细一一介绍了,就大概说一下:

stencil
{
  Ref StencilID//当前片元的参考值(0-255)
  ReadMask readMask//读掩码
  WriteMask writeMask//写掩码
  Comp comprisonFunction//比较操作函数
  Pass stencilOperation//测试通过之后进行操作
  Fail stencilOperation//测试未通过进行的操作
  ZFail stencilOperation//模板测试通过,深度测试未通过执行的操作
}


我来手动实现一个UI的Mask来简单说明一下这些参数的用法
unity里结构大概是这样
在这里插入图片描述

很简单,新建两个Image, 一个父物体做遮罩本身,一个子物体当被遮罩的东西
实际我们用遮罩的时候直接在parentGo上挂一个Mask组件就好了,但是为了学习和演示,我们不这么做。
我们新建两个材质球mat1和mat2 . 然后分别新建两个shader shader1和shader2 分别挂到两个材质球上。
ParentGo用mat1 , ChildGo用mat2
shader部分,我们从网上找到UIdefault的shader源码,复制一份,然后对其中stencil部分进行修改
shader1(遮罩部分的shader)

 Properties
 {
     [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
     _Color ("Tint", Color) = (1,1,1,1)

   //  _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
   //当然实际上在这些暴漏的属性里直接设置就可,但是我为了用枚举直接写可以更清晰的表达出含义所以先注释掉
     _ColorMask ("Color Mask", Float) = 15

     [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
 }

 SubShader
 {
     Stencil
     {
         Ref 1
         Comp Always
         Pass Replace
         ReadMask 255
         WriteMask 255
     }

shader2(被遮罩部分的shader)

  Stencil
  {
      Ref 1
      Comp Equal
      Pass Keep
      ReadMask 255
      WriteMask 255
  }

列出了stencil的改动部分,这样就实现了mask一样的效果,(alphaclip要勾选上)
在这里插入图片描述
过程大概是这样
按照UGUI从上到下的渲染顺序(简单的这么说一下)
先进行父物体的渲染
1.先看他的Comp(测试条件) 是always那就是永远都通过,好,再加上我们这个图片选了alphacilp,就是说这个图片中透明度不是0的像素,全都通过了模板测试,进行下一步
2.通过了之后怎么办呢,看这个Pass(通过之后的操作) 是Replace ,就是把当前的stencilbuffer换成我自己的id(就是ref 1),当然,在换之前要先把你的id 跟我们的writemask(写入遮罩)来做一个&的位运算,其实就是为了对这个id写入做了一些掩码限制,不能说id是什么就完全替换成什么,有些情况可能有很多层遮罩的时候我们希望一个遮罩只能写入影响stencilbuffer的其中一位,那就把这个WriteMask设置成那一位。我这里是255(即1111 1111)所有位都中门大开,好了,那现在这个黑色箭头有像素的部分的stencilbuffer 全都变成 1 了。注意stencilbuffer是屏幕空间上的变量哦,不是我这张箭头的image上所拥有的,是这个屏幕上,这个箭头所占的位置,stencilbuffer都变成1了,这样才会影响别的图片渲染。
然后到子物体
3.还是先看Comp(测试条件)Equal ,哦就是说我的id要和此时stencilBuffer里的相等才能通过测试,但是在比较之前,我们同时也要把我们的id先经过ReadMask来过滤一下,这样只比较我们想要的一位,之后再比较。
4.之后就如我们所见,蓝色按钮只有在黑色箭头内部才有显示,因为那里面通过了模板测试得以渲染,而外边的没通过的像素就被丢弃了。
5.在之后,通过了之后还要有什么操作呢,我们还要看Pass(通过之后还对stencilBuffer有什么操作) 是Keep ,哦就是保持不变,子物体不会影响stencilbuffer的值。

一个最简单的遮罩流程就完了,如果说要实现不显示遮罩本身的图片的话,那么我们在mat1的colormask属性直接设置为0,这样他本身的颜色就被阻挡住了。
在这里插入图片描述

在这里插入图片描述
这样就实现了 Mask里 这个效果
在这里插入图片描述
怎么样, 我讲的简单吧~哈哈哈。

关于UGUI的Mask浅析

好 简单的模板测试原理捋清楚了之后我们来直接研究Mask源码,先贴一下吧

using System;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Rendering;
using UnityEngine.Serialization;

namespace UnityEngine.UI
{
    [AddComponentMenu("UI/Mask", 13)]
    [ExecuteAlways]
    [RequireComponent(typeof(RectTransform))]
    [DisallowMultipleComponent]
    /// <summary>
    /// A component for masking children elements.
    /// </summary>
    /// <remarks>
    /// By using this element any children elements that have masking enabled will mask where a sibling Graphic would write 0 to the stencil buffer.
    /// </remarks>
    public class Mask : UIBehaviour, ICanvasRaycastFilter, IMaterialModifier
    {
        [NonSerialized]
        private RectTransform m_RectTransform;
        public RectTransform rectTransform
        {
            get { return m_RectTransform ?? (m_RectTransform = GetComponent<RectTransform>()); }
        }

        [SerializeField]
        private bool m_ShowMaskGraphic = true;

        /// <summary>
        /// Show the graphic that is associated with the Mask render area.
        /// </summary>
        public bool showMaskGraphic
        {
            get { return m_ShowMaskGraphic; }
            set
            {
                if (m_ShowMaskGraphic == value)
                    return;

                m_ShowMaskGraphic = value;
                if (graphic != null)
                    graphic.SetMaterialDirty();
            }
        }

        [NonSerialized]
        private Graphic m_Graphic;

        /// <summary>
        /// The graphic associated with the Mask.
        /// </summary>
        public Graphic graphic
        {
            get { return m_Graphic ?? (m_Graphic = GetComponent<Graphic>()); }
        }

        [NonSerialized]
        private Material m_MaskMaterial;

        [NonSerialized]
        private Material m_UnmaskMaterial;

        protected Mask()
        {}

        public virtual bool MaskEnabled() { return IsActive() && graphic != null; }

        [Obsolete("Not used anymore.")]
        public virtual void OnSiblingGraphicEnabledDisabled() {}

        protected override void OnEnable()
        {
            base.OnEnable();
            if (graphic != null)
            {
                graphic.canvasRenderer.hasPopInstruction = true;
                graphic.SetMaterialDirty();

                // Default the graphic to being the maskable graphic if its found.
                if (graphic is MaskableGraphic)
                    (graphic as MaskableGraphic).isMaskingGraphic = true;
            }

            MaskUtilities.NotifyStencilStateChanged(this);
        }

        protected override void OnDisable()
        {
            // we call base OnDisable first here
            // as we need to have the IsActive return the
            // correct value when we notify the children
            // that the mask state has changed.
            base.OnDisable();
            if (graphic != null)
            {
                graphic.SetMaterialDirty();
                graphic.canvasRenderer.hasPopInstruction = false;
                graphic.canvasRenderer.popMaterialCount = 0;

                if (graphic is MaskableGraphic)
                    (graphic as MaskableGraphic).isMaskingGraphic = false;
            }

            StencilMaterial.Remove(m_MaskMaterial);
            m_MaskMaterial = null;
            StencilMaterial.Remove(m_UnmaskMaterial);
            m_UnmaskMaterial = null;

            MaskUtilities.NotifyStencilStateChanged(this);
        }

#if UNITY_EDITOR
        protected override void OnValidate()
        {
            base.OnValidate();

            if (!IsActive())
                return;

            if (graphic != null)
            {
                // Default the graphic to being the maskable graphic if its found.
                if (graphic is MaskableGraphic)
                    (graphic as MaskableGraphic).isMaskingGraphic = true;

                graphic.SetMaterialDirty();
            }

            MaskUtilities.NotifyStencilStateChanged(this);
        }

#endif

        public virtual bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
        {
            if (!isActiveAndEnabled)
                return true;

            return RectTransformUtility.RectangleContainsScreenPoint(rectTransform, sp, eventCamera);
        }

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

想要全部弄懂是有些难度的,因为里面涉及的东西很多,尤其是MaskUtilities.NotifyStencilStateChanged相关的,我就浅析几个我觉得学到了的地方吧

1.核心函数

public virtual Material GetModifiedMaterial(Material baseMaterial)

这是接口
IMaterialModifier
的函数,简单来说就是修改当前组件所用材质球的函数,Mask这个类的核心操作都在这里,传入参数是一个baseMateril,然后我们通过种种设置和修改,最后返回了m_MaskMaterial,也就是把当前材质baseMateril–>m_MaskMaterial的过程。

2.计算遮罩深度

var rootSortCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
var stencilDepth = MaskUtilities.GetStencilDepth(transform, rootSortCanvas);

注意这里要通过MaskUtilities.MaskUtilities.FindRootSortOverrideCanvas()来找到父级里最近的一个“根Canvas”,因为我们渲染是以canvas为单位渲染的,比如这样的结构

在这里插入图片描述

如果说,MaskC物体上挂了一个Canvas(并且选中了override sorting) 那比如我从maskD来找“跟Canvas”就会找到的是MaskC,就不会再往上边找了,换言之,在上边的遮罩就不会对item产生作用。
下边的函数用来查找遮罩的深度,从0开始的,假如大家都没有挂canvas的情况下,那么MaskA的depth是0 ,MaskB是1, 以此类推。

3.两个材质

 [NonSerialized]
 private Material m_MaskMaterial;

 [NonSerialized]
 private Material m_UnmaskMaterial;

首先,这两个材质都是通过这样设置的

 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;

这里的StencilMaterial就是一个工具类,这里的Add()和Remove都是维护里面材质池的方法,不用太关心(主要我也看不懂),就知道他是通过后边那一长串参数来设置了新的材质球就行了。
这两个材质,我当时看了许久也没懂这个UnMaskMatrial到底是干嘛的,因为我看上边的MaskMatrial应该就是作为Mask的材质,给他设置stencil信息了,但是UnMaskMatrial是什么时候用到的呢?代码里只有graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0);这样操作,这个SetPopMaterial()里面的代码也不给看了。 经过查询和询问AI,我分析出来了,这个UnMaskMaterial是记录了这是模板缓冲之前的材质球。而graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0)这个函数很厉害,这里相当于是把这个材质球存到了里面,在渲染该物体以及其所有子物体之后,再次弹出他,再用他渲染一遍。
那为什么要这么做呢,你想想啊,模板缓冲区只有一份,你这个MaskMaterial渲染完之后,给stencilBuffer一顿霍霍,对于你的子物体来说,是正确的。但是总有不是你子物体的吧?我一个其他层级上的东西,只要在渲染顺序在你之后,都会受到现在被霍霍的stencilBuffer的影响,也给一顿裁剪,这对吗?这不对,我们Mask的初心肯定是只影响自己的子物体。于是,这个UnMaskMaterial就发挥作用了,他最后会用原先的材质球,给stencilBuffer恢复现场(至于他是怎么做到渲染完所有子物体再弹出这个材质球的你别问,问就是我是菜鸡我也不知道)

4.关于材质球的参数设置

仔细看代码,我们发现在计算遮罩信息的时候,他分情况写的,一种是如果是如果他是第一层遮罩(即他是“根”遮罩),一种是他不是第一层,他上边还有遮罩。我们直接看第二种复杂的(因为其实第一种根遮罩也可以套到第二种的算法里来,只是一种特殊情况)

 var maskMaterial2 = StencilMaterial.Add(baseMaterial, desiredStencilBit | (desiredStencilBit - 1), StencilOp.Replace, CompareFunction.Equal, m_ShowMaskGraphic ? ColorWriteMask.All : 0, desiredStencilBit - 1, desiredStencilBit | (desiredStencilBit - 1));

这样看不直观,我们直接把他的参数整理竖着列出来

  stencilId:       desiredStencilBit | (desiredStencilBit - 1)
  stencil Op:      Replace
  CompareFunction: Equal
  ColorMask:       m_ShowMaskGraphic ? ColorWriteMask.All : 0
  ReadMask:        desiredStencilBit - 1
  WriteMask:       desiredStencilBit | (desiredStencilBit - 1)

这样是不是清晰多了? 我们来配合例子讲解,这样清楚一些。假设这个mask是第三层mask,也就是说他父级以及父级以上还有两个Mask
那么此时这个desiredStencilBit 根据前文int desiredStencilBit = 1 << stencilDepth;这里第三层mask,stencilDepth是从0开始的,也就是说stencilDepth得出的应该是2。从而 1<<2 ,是向左移动两位 就是0000 0100(即十进制的4)
好接下来进行参数设置,我们先看CompareFunction (测试条件)不是always那么简单了,变成equal了,那就是说我本身作为遮罩,还需要和stencilbuffer里的id进行比较相等才能继续操作了,这很合理,因为你上边还有别的遮罩。比较之前,我们先看看我们的stencil id
desiredStencilBit | (desiredStencilBit - 1) 说实话,一开始看到这种写法,我反应是 ”当前遮罩和当前的上层遮罩“这种感觉。但后来一想不对,这样不还得整递归了吗?肯定不是。我看不出来,但是对位运算敏感的同学肯定一眼看出来,这是什么操作

desiredStencilBit   										0000 0100
desiredStencilBit - 1										0000 0011
desiredStencilBit | (desiredStencilBit - 1)				    0000 0111

哈哈,其实就是相当于把当前位以及右侧的位数全部变成1的一个操作,当然仅限于“二进制里的整数”才可以有这种效果
这么做的目的是什么呢,我所理解的就是,你是第三层遮罩么,意味着在你区域内能显示的东西,肯定也要经过了前两层遮罩的考验,所以右边两位也设置成1也是合理的。
而ReadMask的 desiredStencilBit - 1也很好理解了,0000 0011 这个就是只看你父级的遮罩部分,即右侧两个,就是说,我只是比较父级的两层 ,经过这个过滤 ,我将以 0000 0011 的形态进行接下来的模板测试.用一个公式来说就是这样
(referenceValue & readMask) comparisonFunction (stencilBufferValue & readMask)
即 缓冲区的后两位和当前物体的后两位相等,就通过测试。
想象一下,就是这个意思,如果该像素在父级的两个遮罩内,我们去操作,如果都不在父级的遮罩内,那不管在不在我本级的遮罩里,他都不会显示了,也就不用考虑了。
通过测试后的像素,一定是在父级和本级的遮罩内了 ,所以直接把 stencilId 0000 0111后三位写进去就行了。

核心逻辑大概就是这样~希望我讲清楚啦!
哦对了还有一个关键问题就是,这些遮罩是如何把子物体的stencil信息设置好的,这个应该就比较复杂了,涉及到好几个UI系统比较关键的类,我们以后有时间再说~

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐