Unity Mask Use Or Not


后续补充:
rectmask2d 在cpu侧做的优化是,如果被裁剪的矩形框不在rectmask的矩形框内了,那么直接在cpu侧裁剪掉,而不会提交给gpu去裁剪(不画图了 想象两个矩形 一个矩形再另一个矩形的外面 直接裁掉确实没有问题)
但是!! 当被裁的矩形有旋转的时候,他这个算法就不很合理了 !!! 想象一下再 = = 算画个图


左上角矩形直接被裁掉
错误的被裁掉了

这个时候你可能希望旋转的矩形在mask内部的部分还显示,但是抱歉,旋转的整个矩形内(比如图)直接在cpu侧给你裁掉了 = = 代码我大概看了一下没找到就没有细看,不过原理应该是这样的。

那mask最主要的坑:

就是当你使用ui自定义材质的时候,就比如image组件吧,给他上面放了一张alpha贴图做插值,但是如果你是后于mask加载完之后,修改image上面的材质,那么不生效,原因是mask已经在内存里面存了一份材质了,并不会被你修改的改变o
解决办法呢,先mask disable,赋值,然后再mask enable(具体就可以看正文内容)

ps:也就是没时间 不然就用自己写的了 =- =


前言:mask有很大的性能问题,所以在能够解决需求的情况下尽量不用或者少用,亦或者自己实现一套性能高效的mask组件。附 UI工程下载链接 Unity-Technologies/UI

mask 是什么

  在项目中,mask会经常被大量使用达到一些遮罩剔除的作用,看一下unity文档中说的

A Mask is not a visible UI
control but rather a way to modify the appearance of a control’s child elements. The mask restricts (ie, “masks”) the child elements to the shape of the parent. So, if the child is larger than the parent then only the part of the child that fits within the parent will be visible.

  大概就是说mask不是一个可以被看到的UI组件,作用是保证child的边缘不会超出parent的边缘,达到一个裁剪的效果,如下图

没有mask组件
有mask组件

mask的实现方法

   mask是基于模版缓冲来实现的

Masking is implemented using the stencil buffer of the GPU.
*The first Mask element writes a 1 to the stencil buffer *All elements below the mask check when rendering
, and only render to areas where there is a 1 in the stencil buffer *Nested Masks will write incremental bit masks into the buffer, this means that renderable children need to have the logical & of the stencil values to be rendered.

  在第一个mask元素进入渲染队列的时候,将所有的深度缓冲值写为1,在其之后的渲染元素都需要被监测,深度缓冲是否为1,为1才被渲染,否则discard。

什么是模版缓冲 (stencil buffer)

The stencil buffer can be used as a general purpose per pixel mask for saving or discarding pixels

The stencil buffer is usually an 8 bit integer per pixel. The value can be written to, increment or decremented. Subsequent draw calls can test against the value, to decide if a pixel should be discarded before running the pixel shader

  模版缓冲是以像素为单位的,整数数值的缓冲,通常给每个像素分配一个字节长度(0-255)的数值。
  模版测试呢,就是根据模版缓冲,通过位运算来判断当前片元能够进入到片元着色器
看一下官方UI-Default的shader

// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)

Shader "UI/Default"
{
    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 [_Stencil] // 要比较的值
            Comp [_StencilComp] //比较的函数
            Pass [_StencilOp] //通过的函数
            ReadMask [_StencilReadMask] //读mask的位限制 (referenceValue & readMask) comparisonFunction (stencilBufferValue & readMask)
            WriteMask [_StencilWriteMask] //写mask的位限制 WriteMask 0 means that no bits are affected and not that 0 will be written
        }
        //...
    }
}

  文档详见Unity Stencil 模版缓冲
  通过修改shader中的属性参数,配合Z-Test,应该可以解决一些奇怪的需求【我还没有遇到用Stencil来解决的需求 = =】

ui材质中模版缓冲的赋值

  也许你会发现一个问题,如果你在游戏里面给某个控件image组件一个自定义材质,并且他身上(或者父窗口)上拥有一个Mask组件,恰巧image上面的sprite的texture是异步加载的,那么你就有可能发现这个自定义材质上的后赋予的属性值没有变化(当你disable enable mask 的时候 就可以了 why?)

mask的具体实现

  来看一下mask类(顺便学习一些trick的写法 请看注释)

//继承3个类,UIBehaviour继承自MonoBehaviour, 剩下两个显而易见 处理响应逻辑和材质相关的
public class Mask : UIBehaviour, ICanvasRaycastFilter, IMaterialModifier
{
    //...
    [SerializeField]
    //FormerlySerializedAs 比如你想改变量的名字,但是又不想丢失引用,
    //那么将原来的名字打个标签 也许是个不错的选择
    [FormerlySerializedAs("m_ShowGraphic")]
    private bool m_ShowMaskGraphic = true;

    public bool showMaskGraphic
    {
        get { return m_ShowMaskGraphic; }
        set
        {
            if (m_ShowMaskGraphic == value)
                return;

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

    //... 下文会说这两个值
    [NonSerialized] private Material m_MaskMaterial;
    [NonSerialized] private Material m_UnmaskMaterial;

    //让我们来看一下enable 和disable函数里面做了些什么
    protected override void OnEnable()
    {
        base.OnEnable();
        if (graphic != null)
        {
            //这个hasPopInstruction,【The pop instruction is executed after 
            //all children have been rendered】 会导致再画一次,也就是为什么
            //mask会有两次draw call性能不好的原因
            graphic.canvasRenderer.hasPopInstruction = true;
            graphic.SetMaterialDirty();
        }

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

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

        MaskUtilities.NotifyStencilStateChanged(this);
    }

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

        var rootSortCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
        //根据嵌套mask层数拿到深度值 比如只有一个mask 那么就是1
        var stencilDepth = MaskUtilities.GetStencilDepth(transform, rootSortCanvas);
        //还记得上文说的一个像素是一个整数类型的8bit值 所以当然不能比8大
        if (stencilDepth >= 8)
        {
            Debug.LogError("Attempting to use a stencil mask with depth > 8", gameObject);
            return baseMaterial;
        }

        //算出要比较的值(shader中的Ref)
        int desiredStencilBit = 1 << stencilDepth;
        //下面的代码: 如果是第一层 那么就不需要 readmask和writemask 
        //否则需要加上,给shader赋值的过程,大家简单分析一下就懂了
        // 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;
    }
}

  在StencilMaterial.Add方法里面,把材质自己存了一份。当以下条件满足的时候,那么就从队列里去取,请注意,只要以下几个属性一致,那么就认为是同一个材质

    for (int i = 0; i < m_List.Count; ++i)
    {
        MatEntry ent = m_List[i];
        //判断是否使用同一个材质的条件
        if (ent.baseMat == baseMat
            && ent.stencilId == stencilID
            && ent.operation == operation
            && ent.compareFunction == compareFunction
            && ent.readMask == readMask
            && ent.writeMask == writeMask
            && ent.colorMask == colorWriteMask)
        {
            ++ent.count;
            return ent.customMat;
        }
    }

    var newEnt = new MatEntry();
    newEnt.count = 1;
    newEnt.baseMat = baseMat;
    //new了一个材质出来
    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);

    // left for backwards compatability
    if (newEnt.customMat.HasProperty("_UseAlphaClip"))
        newEnt.customMat.SetInt("_UseAlphaClip", 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组件的材质,其下所有子组件的材质都只在enable的时候被拷贝了一份呢?有人和我遇到了一样的问题看下面的代码就可以知道了,当mask组件enable的时候,会调用 RecalculateMasking会让image等继承IMaskAble的组件,设置一个标志位

public static void NotifyStencilStateChanged(Component mask)
{
    var components = ListPool<Component>.Get();
    mask.GetComponentsInChildren(components);
    for (var i = 0; i < components.Count; i++)
    {
        if (components[i] == null || components[i].gameObject == mask.gameObject)
            continue;

        var toNotify = components[i] as IMaskable;
        if (toNotify != null)
        //设置标志位
        toNotify.RecalculateMasking();
    }    
    ListPool<Component>.Release(components);
}

  当这个标志位为true的时候,子组件调用GetModifiedMaterial方法的时候,就会根据这个标志位去选择mask的方法,去储存一份材质,所以这份材质是拷贝出来的

public virtual Material GetModifiedMaterial(Material baseMaterial)
{
    var toUse = baseMaterial;
    //又去调用了父的mask里面的取材质方法
    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;
}

  为什么说他坑呢,因为很多时候你以为内存中的材质和面板上的是一个,但是并不是。

mask的性能问题

  1.上面的分析很明确,DrawCall上会多一次,所以xxxxx
  2.会打断UI的Batch,同样是增加DrawCall

如何改善:根据实际情况 妥善使用

  1.尝试用Rect Mask 替代Mask (rectmask 内的ui节点不可以和外面的ui节点合并批次, 多个rectmask之间也不可以合并批次,但是多个mask之间内的ui节点是可以合并批次的!并且多个mask首尾dc如果满足条件也是可以分别合并的!)
  2.自己实现mask组件 Unity手游开发札记——使用Shader进行UGUI的优化
  3.忍了!

关于如何优化大家就根据项目情况不断优化ba!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354

推荐阅读更多精彩内容