UGUI内核大探究(八)MaskableGraphic

原创 2016年09月16日 17:25:37

MaskableGraphic是UGUI的核心组件,它继承自Graphic。MaskableGraphic是一个抽象类,它的派生类有RawImage、Image、Text。顾名思义,MaskableGraphic是可遮罩的图像。本文就介绍一下MaskableGraphic的实现原理。

按照惯例,附上UGUI源码下载地址


MaskableGraphic重写了OnEnable、OnDisable(调用时机参见Untiy3D组件小贴士(一)OnEnabled与OnDisabledOnTransformParentChanged(当父对象改变)和OnCanvasHierarchyChanged(当画布层次改变)方法,这些方法间接继承自UIBehavior。这些方法里都设置了m_ShouldRecalculateStencil(是否需要重新计算模板)为true,并调用UpdateClipParent(更新裁剪的父对象)和SetMaterialDirty(设置材质为脏的)。OnDisable方法里还额外从StencilMaterial(静态方法,用于管理模板材质)移除了m_MaskMaterial(遮罩材质),并设置m_MaskMaterial为null。

UpdateClipParent方法使用MaskUtilities的GetRectMaskForClippable方法从父对象中找到RectMask2D组件。我们知道RectMask2D是一个可以根据RectTransform裁剪子对象的组件。如图:


我们为Canvas添加了RectMask2D组件,超出Canvas矩形范围的部分就被裁剪掉了。

SetMaterialDirty是Graphic的方法,可以参考UGUI内核大探究(七)Graphic

StencilMaterial是一个静态类,负责管理模板材质。它维护了一个类型为MatEntry的List,外部可以调用Add、Remove和ClearAll方法来对这个List进行操作。

Add方法,会创建一个MatEntry,并将输入的baseMat以及其他参数赋值给MatEntry的变量,并创建一个赋值baseMat新的材质customMat。并将stencilID、operation等参数设置给customMat(实际上是shader的参数)。


MaskableGraphic继承了IClippable,需要实现RecalculateClipping、Cull和SetClipRect方法。这些方法在MaskUtilitiesRectMask2D被调用,用于图像的裁剪。

RecalculateClipping方法调用了上文提到过的UpdateClipParent,找到父对象中的RectMask2D组件。它在MaskUtilities中的Notify2DMaskStateChanged被调用遍历RectMask2D的所有子对象,找到IClippable接口的组件,调用RecalculateClipping方法,这个方法是在RectMask2D的OnEnable、OnDisable以及编辑器模式下的OnValidate方法中被调用。

Cull方法:

        public virtual void Cull(Rect clipRect, bool validRect)
        {
            if (!canvasRenderer.hasMoved)
                return;

            var cull = !validRect || !clipRect.Overlaps(canvasRect);
            var cullingChanged = canvasRenderer.cull != cull;
            canvasRenderer.cull = cull;

            if (cullingChanged)
            {
                m_OnCullStateChanged.Invoke(cull);
                SetVerticesDirty();
            }
        }

如果输入参数validRect为false或者输入的clipRect与所属Canvas的矩形区域不重合,cull为true,并设置给canvasRenderercanvasRenderer是Graphic里的属性,表示本对象上的CanvasRenderer组件。如果cull的值发生变化回调m_OnCullStateChanged(发送事件),并设置顶点为脏的(参考UGUI内核大探究(七)Graphic),等待重新绘制。

SetClipRect方法很简单,只是根据输入validRectcanvasRenderer开启或关闭矩形裁剪(开启时传递参数clipRect)。


MaskableGraphic继承了IMaskable,需要实现RecalculateMasking方法。这个方法会在MaskUtilities中调用,用于图像的遮罩。方法名为NotifyStencilStateChanged遍历Mask的所有子对象,找到IMaskable接口的组件,调用RecalculateMasking方法,在Mask的OnEnable、OnDisable以及编辑器模式下的OnValidate方法中被调用。

Mask是可以通过图片裁剪图片的组件,如图


一个圆形图像的对象包含了一个子对象,是一个方形图像(实际上是默认图像),稍微错开一点。我们为圆形图像添加Mask组件,就看到方形图像超出圆形图像的部分没有被渲染出来。

RecalculateMasking这个方法只是设置了m_ShouldRecalculateStencil为true并SetMaterialDirty。


MaskableGraphic还继承了IMaterialModifier,需要实现GetModifiedMaterial方法。这个方法在Graphic中被调用,用于重建图像时,或许修改过的材质。

        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 Mask component then it will
            // generate the mask material. This is an optimisation
            // it adds some coupling between components though :(
            if (m_StencilValue > 0 && GetComponent<Mask>() == null)
            {
                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;
        }

如果需要重算模板,便会从根画布(或者父对象中overrideSorting为true的Canvas)获取模板的深度。

如果深度大于零,且本对象含有Mask组件,便将baseMaterial(即当前的material)加入StencilMaterial,并移除旧的m_MaskMaterial。最后,替换m_MaskMaterial并返回(似乎m_MaskMaterial这个变量只是为了从StencilMaterial移除才存在的

深度获取方法:

        public static int GetStencilDepth(Transform transform, Transform stopAfter)
        {
            var depth = 0;
            if (transform == stopAfter)
                return depth;

            var t = transform.parent;
            var components = ListPool<Component>.Get();
            while (t != null)
            {
                t.GetComponents(typeof(Mask), components);
                for (var i = 0; i < components.Count; ++i)
                {
                    if (components[i] != null && ((Mask)components[i]).IsActive() && ((Mask)components[i]).graphic.IsActive())
                    {
                        ++depth;
                        break;
                    }
                }

                if (t == stopAfter)
                    break;

                t = t.parent;
            }
            ListPool<Component>.Release(components);
            return depth;
        }
简单来讲就是从本对象往上找,找到一个图像有效的Mask组件,便深度加1,直到找到stopAfter(也就是上面说的根画布或者父对象中overrideSorting为true的画布)。

接着顺带介绍一下RectMask2D和Mask两个类。

RectMask2D维护了一个类型为IClippable的List——m_ClipTargets。MaskableGraphic会在UpdateClipParent的时候,将自己从旧的RectMask2D的RemoveClippable方法,将自己从m_ClipTargets中移除,并调用新的RectMask2D方法,加入m_ClipTargets。而m_ClipTargets会在PerformClipping方法中被调用。

PerformClipping是继承自IClipper接口的方法。RectMask2D会在OnEnable的时候将自己注册到ClipperRegistry,并再OnDisable的时候从后者移除。ClipperRegistry是一个单例,是裁剪器的注册处。我们在UGUI内核大探究(六)CanvasUpdateRegistry中提到过它,当Layout(布局)重建结束后,会调用它的Cull方法,遍历每一个Clipper执行PerformClipping方法。

        public virtual void PerformClipping()
        {
            // if the parents are changed
            // or something similar we
            // do a recalculate here
            if (m_ShouldRecalculateClipRects)
            {
                MaskUtilities.GetRectMasksForClip(this, m_Clippers);
                m_ShouldRecalculateClipRects = false;
            }

            // get the compound rects from
            // the clippers that are valid
            bool validRect = true;
            Rect clipRect = Clipping.FindCullAndClipWorldRect(m_Clippers, out validRect);
            if (clipRect != m_LastClipRectCanvasSpace)
            {
                for (int i = 0; i < m_ClipTargets.Count; ++i)
                    m_ClipTargets[i].SetClipRect(clipRect, validRect);

                m_LastClipRectCanvasSpace = clipRect;
                m_LastClipRectValid = validRect;
            }

            for (int i = 0; i < m_ClipTargets.Count; ++i)
                m_ClipTargets[i].Cull(m_LastClipRectCanvasSpace, m_LastClipRectValid);
        }

MaskUtilities中还剩一个方法没讲,就是GetRectMasksForClip,这个方法将自己和父对象中的所有有效的RectMask2D组件打包给m_Clippers.

Clipping的FindCullAndClipWorldRect方法将这些m_Clippers的canvasRect(最近的Canvas父对象的矩形区域)求交集,得到一个最小的裁剪区域(如果这个裁剪区域不合理,validRect便为false)。接着,如果新的出来的clipRect与旧的不同,那么遍历m_ClipTargets,为他们设置裁剪区域,就是我们前面讲的SetClipRect。最后调用所有IClippable的剔除方法(前面讲的Cull)。


Mask与MaskableGraphic一样,继承了IMaterialModifier接口。Mask的GetModifiedMaterial方法:

        public virtual Material GetModifiedMaterial(Material baseMaterial)
        {
            if (graphic == null)
                return baseMaterial;

            var rootSortCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
            var stencilDepth = MaskUtilities.GetStencilDepth(transform, rootSortCanvas);
            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)
            {
                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;
        }


与MaskableGraphic相比,多了stencilDepth大于等于8时的警告,以及desiredStencilBit等于1(stencilDepth等于0)时的特殊处理(无图可遮),并根据m_ShowMaskGraphic来设置遮罩图像是否显示出来,此外额外canvasRenderer设置了一个材质。(似乎是为了上一级的CanvasRenderer渲染时方便调用,Unity3D官方喜欢藏着掖着,本来属于UI的CanvasRenderer却非要放在UnityEngine里面,所以无法知道SetPopMaterial具体实现了什么逻辑。docs.unity3d.com里关于这个方法的介绍也很含糊,我也没有搜索到相关的资料。等我研究出了结果,再把这一段介绍补完。


另外RectMask2D和Mask都实现了IsRaycastLocationValid方法,继承自UnityEngine.ICanvasRaycastFilter,实现是判断矩形(RectTransform)区域是否包含输入的点。它在Graphic中被调用,用于筛选出被射线照射到的图像。

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ecidevilin/article/details/52555253

解决你MCSE最大的困惑

解决你MCSE最大的困惑2001年4月12日9:32:2 启迪网   在NT 4.0系列考试中有三门课程可以使用中文考试,分别为70-58 Networking Essentials、70-59 TC...
  • stanely
  • stanely
  • 2001-05-11 14:54:00
  • 943

<Unity-UGUI>使用Mask, 正确的裁减非默认材质UI对象

使用UGUI中的Mask的时候,如何正确裁减非默认材质的UI对象 欢迎使用Markdown编辑器写博客 本Markdown编辑器使用StackEdit修改而来,用它写博客,将会带来全新的体验哦: ...
  • ykmzy
  • ykmzy
  • 2016-05-10 20:43:46
  • 4322

水煮TCPMP(不得不转的好文)

http://www.cnblogs.com/jrong/articles/1381954.html 这段时间在研究TCPMP(The Core Pocket Media Player),这是一个应用...
  • fg313071405
  • fg313071405
  • 2010-05-15 12:28:00
  • 279

UGUI基础

Canvas的渲染渲染基本层次基本渲染层次是根据可视化对象在继承窗口中的显示顺序来渲染的,在此有两种方式可以调节对象的顺序,第一种:直接在继承窗口拖拽。第二种:通过Transform的SetAsFir...
  • zw514159799
  • zw514159799
  • 2016-04-14 23:36:41
  • 2795

UGUI技巧

UGUI技巧 Text中的可以单独指定某些文字的颜色,只需将想要变色的文本放在之间即可,如“吃橙色物品有机会获得红色宝石”,同样适用于NGUI。两张图片,如头像框,头像在...
  • zhaoguanghui2012
  • zhaoguanghui2012
  • 2016-09-13 17:11:12
  • 1717

从UGUI源码中想到的UI动态”挖洞”效果的实现(蒙版过滤)

一、需求描述:项目中有时候会用到一些“”挖洞“”效果,比如新手教程出现时,整个画面变暗(蒙版),但是突出突出个别的按钮,供玩家点击,但是别的UI元素不能响应点击事件。常见的做法可能是在最上层加一个蒙版...
  • hjzyzr
  • hjzyzr
  • 2017-08-29 11:25:24
  • 1019

UGUI内核大探究(七)Graphic

Graphic是UGUI的核心组件,负责显示图像。它是一个抽象类,是MaskableGraphic(可遮罩图像)的基类(同样是抽象类),而后者是RawImage、Image和Text的基类。虽然Sel...
  • ecidevilin
  • ecidevilin
  • 2016-09-15 12:38:47
  • 7196

UGUI学习手记-Graphic

关于源码 关于源码可以参考这篇博客下载或调试。 Graphic简介 继承自UIBehaviour(所有UI组件的基类,负责接收来自UnityEngine或者UnityEditor的事件)...
  • Admin_Jhon
  • Admin_Jhon
  • 2017-05-31 14:54:16
  • 925

如何快速优化手游性能问题?从UGUI优化说起

本文作者从自身多年的Unity项目UI开发及优化的经验出发,从UGUI,CPU,GPU以及unity特有资源等几个维度,介绍了unity手游性能优化的一些方法。...
  • wetest_tencent
  • wetest_tencent
  • 2016-12-08 21:39:54
  • 1211

UGUI内核大探究(零)

UGUI是Unity3D官方推出的UI系统,为了更好的使用UGUI,我们就需要去了解它。 UGUI代码开源,我们可以从bitbucket下载到源码。...
  • ecidevilin
  • ecidevilin
  • 2016-10-10 22:31:33
  • 4144
收藏助手
不良信息举报
您举报文章:UGUI内核大探究(八)MaskableGraphic
举报原因:
原因补充:

(最多只允许输入30个字)