UGUI源码解析(RectMask2D,Mask与模板缓存原理)

编程入门 行业动态 更新时间:2024-10-12 18:17:35

UGUI源码解析(RectMask2D,Mask与模板<a href=https://www.elefans.com/category/jswz/34/1771061.html style=缓存原理)"/>

UGUI源码解析(RectMask2D,Mask与模板缓存原理)

RectMask2D

RectMask2D继承了UIBehaviour, IClipper, ICanvasRaycastFilter,

是一个二维矩形遮罩,可以对遮罩外部区域进行剪切/遮罩。与Mask组件相比,它的使用有一些限制,只能在2D空间中使用,要求面片上的元素是共面的。不需要模板缓冲区/额外的dc。

 

重要属性和字段

private readonly RectangularVertexClipper m_VertexClipper = new RectangularVertexClipper();

RectangularVertexClipper 类只有一个方法

public Rect GetCanvasRect(RectTransform t, Canvas c)

通过调用这个方法,获取在Canvas c下的指定的RectTransform  t的Rect区域范围

 

private HashSet<MaskableGraphic> m_MaskableTargets = new HashSet<MaskableGraphic>();

被当前这个RectMask2D组件遮罩住的MaskableGraphic组件列表

 

维护了一个IClippable类型的列表:

private HashSet<IClippable> m_ClipTargets = new HashSet<IClippable>();

 

private List<RectMask2D> m_Clippers = new List<RectMask2D>();

用于缓存给定裁剪平面上的所有的RectMask2D组件


关键方法

OnEnable方法

设置m_ShouldRecalculateClipRects为true,把自己注册到ClipperRegistry,并调用MaskUtilities.Notify2DMaskStateChanged,重新计算裁剪

 

OnDisable方法

清空m_ClipTargets,清空m_Clippers,把自己从ClipperRegistry中解除注册,并调用MaskUtilities.Notify2DMaskStateChanged,重新计算裁剪

 

OnValidate方法

仅在Editor下调用,设置m_ShouldRecalculateClipRects为true,如果自身处于隐藏状态直接返回,调用MaskUtilities.Notify2DMaskStateChanged,重新计算裁剪

 

IsRaycastLocationValid方法

继承自ICanvasRaycastFilter,返回RectTransformUtility.RectangleContainsScreenPoint,RectTransform是否包含从相机看到的点。

 

PerformClipping方法

PerformClipping是继承自IClipper接口的方法。

  • 如果m_ShouldRecalculateClipRects为true,会调用 MaskUtilities.GetRectMasksForClip,把本对象和父对象中所有有效的RectMask2D,放入到m_Clippers中。
  • Clipping的FindCullAndClipWorldRect方法,遍历m_Clippers的canvasRect,取交集,取得一个最小的裁剪区域,如果这个裁剪区域不合理,validRect便为false,否则根据裁剪区域,返回一个Rect,同时设置validRect为true。
  • 新的clipRect如果与之前的不同,或者m_ForceClip为true,遍历m_ClipTargets,调用 clipTarget.SetClipRect,为他们设置裁剪区域,记录新的clipRect和validRect。
  • 最后,遍历m_ClipTargets,调用所有IClippable的Cull剔除方法。

 

AddClippable方法

设置m_ShouldRecalculateClipRects为true,把clippable类型的组件添加到m_ClipTargets列表中,设置m_ForceClip为true,这个参数会在PerformClipping被用到。这个方法在MaskableGraphic中的UpdateClipParent方法中被调用。

 

RemoveClippable方法

设置m_ShouldRecalculateClipRects为true,调用clippable.SetClipRect(new Rect(), false)关闭矩形裁剪,把clippable类型的组件从m_ClipTargets列表中移除,设置m_ForceClip为true。这个方法在MaskableGraphic中的UpdateClipParent方法中被调用。

 

ClipperRegistry

ClipperRegistry是一个单例,是裁剪器的注册处。在CanvasUpdateRegistry的PerformUpdate方法中,布局重建完成后,会调用组件的裁剪方法,ClipperRegistry.instance.Cull(),在Cull方法中,会遍历每一个IClipper执行PerformClipping方法。

内部方法:

 public void Cull()

 public static void Register(IClipper c)

 public static void Unregister(IClipper c)

字段:

readonly IndexedSet<IClipper> m_Clippers = new IndexedSet<IClipper>();

public static ClipperRegistry instance

 

Clipping

静态类,只提供一个方法,

public static Rect FindCullAndClipWorldRect(List<RectMask2D> rectMaskParents, out bool validRect)

遍历m_Clippers的canvasRect,取交集,取得一个最小的有效裁剪区域validRect,如果有这样一个区域那么validRect为true,否则为false;


  •  

遮罩原理剖析

实际上只是矩形区域进行裁剪,通过设置m_ShouldRecalculateClipRects的值,在方法PerformClipping中调用

MaskUtilities.GetRectMasksForClip,获取本对象和父对象中所有有效的RectMask2D;

再调用Clipping的FindCullAndClipWorldRect方法,遍历m_Clippers的canvasRect,取交集,取得一个最小的有效裁剪区域validRect;

新的clipRect如果与之前的不同,或者m_ForceClip为true,遍历m_ClipTargets,调用 clipTarget.SetClipRect,为他们设置裁剪区域,记录新的clipRect和validRect;

最后,遍历m_ClipTargets,调用所有IClippable的Cull剔除方法。

 

Mask

Mask继承了UIBehaviour, ICanvasRaycastFilter,IMaterialModifier。

 

重要属性和字段

public bool showMaskGraphic

是否显示用来完成遮罩的区域的Graphic

public Graphic graphic

用来完成遮罩的区域的Graphic

 

关键方法

OnEnable方法

如果含有Graphic组件,设置graphic.canvasRenderer.hasPopInstruction为true,调用graphic.SetMaterialDirty()方法。然后调用MaskUtilities.NotifyStencilStateChanged,重新计算遮罩。

 

OnDisable方法

  • 如果含有Graphic组件,调用graphic.SetMaterialDirty,设置graphic.canvasRenderer.hasPopInstruction为false,设置graphic.canvasRenderer.popMaterialCount为0。
  • 从StencilMaterial中移除m_MaskMaterial和m_UnmaskMaterial,并设置m_MaskMaterial和m_UnmaskMaterial为空,
  • 最后调用MaskUtilities.NotifyStencilStateChanged,重新计算遮罩。

 

IsRaycastLocationValid方法

继承自ICanvasRaycastFilter,返回RectTransformUtility.RectangleContainsScreenPoint,RectTransform是否包含从相机看到的点

 

GetModifiedMaterial方法

继承自IMaterialModifier接口,MaskableGraphic也继承了这个接口,这个方法是用来修改获取的材质来实现遮罩效果

 

StencilMaterial

StencilMaterial是一个静态类,负责管理模板材质。维护了一个MatEntry类型的列表:

private static List<MatEntry> m_List = new List<MatEntry>();

外部可以调用Add、Remove和ClearAll方法来对这个List进行操作。

Add方法,会创建一个MatEntry,并将输入的baseMat以及其他参数赋值给MatEntry,并创建了赋值baseMat的customMat,并将stencilID,operation等参数赋值给customMat,实际上赋值customMat的shader参数。

 

遮罩原理剖析

Mask是利用了GPU的模板缓冲来实现的

Mask的关键代码其实只有一行,如下:

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

它的作用是为Mask对象生成一个特殊的材质,这个材质会将StencilBuffer的值置为1。同样的,在Image,Text和RawImage的基类 MaskableGraphic 中,有这样一行关键代码(为方便理解,对代码做了简化处理):

var maskMat = StencilMaterial.Add(baseMaterial, 1, StencilOp.Keep, CompareFunction.Equal, 1, 0);

它的作用是为MaskableGraphic生成一个特殊的材质,这个材质在渲染时会取出StencilBuffer的值,判断是否为1,如果是才进行渲染。

注意上述对StencilBuffer的操作是逐像素的,这样即达到了Mask的效果。同样的,我们在上篇中简单将MaskableGraphic的逻辑反转为判断StencilBuffer是否不为1,即达到了挖洞的效果。

 

模板缓存原理

StencilBuffer

简单来说,gpu为每个像素点分配一个称之为stencil buffer的1字节大小的内存区域,这个区域可以用于保存或丢弃像素的目的。我们举个简单的例子来说明这个缓冲区的本质。

如上图所示,我们的场景中有1个红色图片和1个绿色图片,黑框范围内是它们重叠部分。一帧渲染开始,首先绿色图片将它覆盖范围的每个像素颜色“画”在屏幕上,然后红色图片也将自己的颜色画在屏幕上,就是图中的效果了。这种情况下,重叠区域内红色完全覆盖了绿色。接下来,我们为绿色图片添加Mask组件。于是变成了这样:

此时一帧渲染开始,首先绿色图片将它覆盖范围都涂上绿色,同时将每个像素的stencil buffer值设置为1,此时屏幕的stencil buffer分布如下:

然后轮到红色图片“绘画”,它在涂上红色前,会先取出这个点的stencil buffer值判断,在黑框范围内,这个值是1,于是继续画红色;在黑框范围外,这个值是0,于是不再画红色,最终达到了图中的效果。

所以从本质上来讲,stencil buffer是为了实现多个“绘画者”之间互相通信而存在的。由于gpu是流水线作业,它们之间无法直接通信,所以通过这种共享数据区的方式来传递消息,从而达到一些“不可告人”的目的。

理解了stencil的原理,我们再来看下它的语法。在unity shader中定义的语法格式如下(中括号内是可以修改的值,其余都是关键字):

Stencil
{Ref [Value]Comp [CompFunction]Pass [PassOp]Fail [FailOp]ReadMask [Value]WriteMask [Value]
}

其中:

  • Ref表示要比较的值;

  • Comp表示比较方法(等于/不等于/大于/小于等);

  • Pass/Fail表示当比较通过/不通过时对stencil buffer做什么操作(保留/替换/置0/增加/减少等);

  • ReadMask/WriteMask表示取stencil buffer的值时用的mask(即可以忽略某些位);

翻译一下就是:将stencil buffer的值与ReadMask与运算,然后与Ref值进行Comp比较,结果为true时进行Pass操作,否则进行Fail操作,操作值写入stencil buffer前先与WriteMask与运算。

最后,我们来看下Unity渲染UI组件时默认使用的Shader——UI/Default(略去了一些不相关内容):

Shader "UI/Default"{Properties{……_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……}SubShader{……Stencil{Ref [_Stencil]Comp [_StencilComp]Pass [_StencilOp]ReadMask [_StencilReadMask]    WriteMask [_StencilWriteMask]}……Pass{……}
}以及我们代码中调用的StencilMaterial.Add的内部实现(略去了一些不相关内容):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相关的参数。至于Mask只能作用于它的子级的限制,则完全是代码层面的限制。那么,如果我们理解没错,事实上我们可以用更简单的方法——使用自定义材质,来完成上文提到的“挖洞”效果或者系统自带的“遮罩”效果。来验证下是不是这样。

首先创建示例场景:

如图所示,我们的场景中依次放置了blue/stencil/white/green/red/yellow等6张图片,它们都是标准的Image组件设置了颜色,没有任何特别的设置(请注意它们的顺序)。

接下来我们创建3个材质,分别命名为UIStencil/UIMask/UIHole,为它们指定材质为UI/Default,并设置Stencil相关参数:

如果你读懂了前面的介绍,那么应该能够理解这几个材质的作用了。

这里补充一些知识点:

1. 上图中Stencil Operation对应UnityEngine.Rendering命名空间下的StencilOp枚举,对应Shader——UI/Default属性中的_StencilOp,比如填入2,对应StencilOp枚举的Replace = 2,表示用reference value代替stencil buffer的值。

namespace UnityEngine.Rendering
{//// 摘要://     Specifies the operation that's performed on the stencil buffer when rendering.public enum StencilOp{//// 摘要://     Keeps the current stencil value.Keep = 0,//// 摘要://     Sets the stencil buffer value to zero.Zero = 1,//// 摘要://     Replace the stencil buffer value with reference value (specified in the shader).Replace = 2,//// 摘要://     Increments the current stencil buffer value. Clamps to the maximum representable//     unsigned value.IncrementSaturate = 3,//// 摘要://     Decrements the current stencil buffer value. Clamps to 0.DecrementSaturate = 4,//// 摘要://     Bitwise inverts the current stencil buffer value.Invert = 5,//// 摘要://     Increments the current stencil buffer value. Wraps stencil buffer value to zero//     when incrementing the maximum representable unsigned value.IncrementWrap = 6,//// 摘要://     Decrements the current stencil buffer value. Wraps stencil buffer value to the//     maximum representable unsigned value when decrementing a stencil buffer value//     of zero.DecrementWrap = 7}
}

2.上图中Stencil Comparison对应UnityEngine.Rendering命名空间下的CompareFunction枚举,对应Shader——UI/Default属性中的_StencilComp,比如填入8,对应CompareFunction枚举的Always = 8,表示无论是模版测试还是深度测试,都通过。

namespace UnityEngine.Rendering
{//// 摘要://     Depth or stencil comparison function.public enum CompareFunction{//// 摘要://     Depth or stencil test is disabled.Disabled = 0,//// 摘要://     Never pass depth or stencil test.Never = 1,//// 摘要://     Pass depth or stencil test when new value is less than old one.Less = 2,//// 摘要://     Pass depth or stencil test when values are equal.Equal = 3,//// 摘要://     Pass depth or stencil test when new value is less or equal than old one.LessEqual = 4,//// 摘要://     Pass depth or stencil test when new value is greater than old one.Greater = 5,//// 摘要://     Pass depth or stencil test when values are different.NotEqual = 6,//// 摘要://     Pass depth or stencil test when new value is greater or equal than old one.GreaterEqual = 7,//// 摘要://     Always pass depth or stencil test.Always = 8}
}

3.上图中Stencil ID就是一个数值,对应Shader——UI/Default属性中的_Stencil,也就是reference value,将要与stencil buffer比较的值。在Shader——UI/Default中,由newEnt.customMat.SetInt("_Stencil", stencilID);可以看出Stencil ID最终的应用类型是int类型,也就是说如果在上图设置1.5,最终使用的值将会是1。

       这个很容易测试,在工程中把stencil-stencil节点的shader的Stencil ID改为1.5,而white和red节点的shader的Stencil ID为1,没有发生任何变化,假设是float类型,white和red将都会渲染出来。比如,stencil-stencil节点的shader的Stencil ID为1,white和red节点的shader的Stencil ID为2,white和red将都会渲染出来。

4.上图中ColorMask对应UnityEngine.Rendering命名空间下的ColorWriteMask枚举。当ColorMask为0时,表示屏蔽颜色的输出,即不输出颜色到屏幕。当ColorMask为15时,对应ColorWriteMask枚举中的All = 15,即输出所有颜色到屏幕。

namespace UnityEngine.Rendering
{//// 摘要://     Specifies which color components will get written into the target framebuffer.[Flags]public enum ColorWriteMask{//// 摘要://     Write alpha component.Alpha = 1,//// 摘要://     Write blue component.Blue = 2,//// 摘要://     Write green component.Green = 4,//// 摘要://     Write red component.Red = 8,//// 摘要://     Write all components (R, G, B and Alpha).All = 15}
}

接下来,我们将场景中的图片改用我们自定义的材质来渲染(我在每个结点后面增加了它使用的材质名称)

可以发现,white/red图片与stencil重叠区域被挖空(white/red的Stencil Comparison为6,对应上面枚举中NotEqual = 6,也就是当reference value与stencil buffer的值不相等时,才渲染。stencil层先渲染,Stencil Operation为2,对应枚举中Replace = 2,表示用reference value代替stencil buffer的值,因此当前stencil buffer的值被设置为stencil的Stencil ID,同时与white/red图片的Stencil ID一致,因此在与stencil重叠区域不渲染white/red图片)

而green/yellow则只保留了与stencil的重叠区域(green/yellow的Stencil Comparison为3,对应上面枚举中Equal = 3,也就是当reference value与stencil buffer的值相等时,才渲染。当前stencil buffer的值被设置为stencil的Stencil ID,同时与green/yellow的Stencil ID一致,因此在与stencil重叠区域渲染white/red图片;在与stencil未重叠区域,stencil buffer的值还是默认为0,与green/yellow的Stencil ID不一致,因此在与stencil未重叠区域不渲染white/red图片)。

blue没有受到影响(因为blue层先渲染,不受影响)。同时它们也不受结点关系的影响(只要保证渲染顺序即可,因为UGUI从上往下依次渲染)。这正与我们的猜测一致。

在这个示例中,我们的stencil材质模拟了UGUI中Mask组件的作用,mask材质模拟了MaskableGraphic组件的作用,而hole模拟了HoleImage组件的作用。通过这个例子可以发现,事实上Mask组件只是标记了一处特定的区域,真正决定要“Mask”的行为是在Image的渲染中判断的。正因此,我们的例子中stencil图片可以同时起到Mask和Hole的作用。

您也可以尝试修改这几个材质的数值,观察场景的变化,可有助于更深刻理解这一模型的工作过程。

示例场景下载(使用Unity 2018.1.9f2 创建)

链接: 
提取码:u0nq 

 

理解了上述原理,再来看Mask组件,

当添加了Mask组件之后,由于它的Op设置为Replace,Comp设置为Always,那么就会将当前Mask组件所在区域的stencil buffer的值设置成它的ID为1,

而在它下面添加一个被遮罩的Image,由于它的Op设置为Keep,Comp设置为Equal,在Stencil ID与stencil buffer中的值相等时才更改当前区域内的stencil buffer的值为1,其他部分仍然为0,

这样也就解释了为什么只有在Mask组件部分重叠的地方才会渲染

 

再来一个特效的例子:

这是一个奖池的特效,由底部的底板特效,中间的波动水纹特效,上层的圆形玻璃罩特效组成。要做到中间的波动水纹根据百分比显示高度,需要做到以下三点:

一是中间的波动水纹特效要让美术做成正方形,

二是底部的底板特效,设置Stencil Operation为2,Stencil ID为1,这样就用reference value代替stencil buffer的值。

最后,中间的波动水纹特效,设置Stencil ID为1,Stencil Comparison为3,表示当前stencil buffer的值与reference value相等时,渲染与底部特效重叠区域,这时当前stencil buffer的值为1,reference value也为1,就出现了中间的波动水纹特效。

更多推荐

UGUI源码解析(RectMask2D,Mask与模板缓存原理)

本文发布于:2024-02-14 11:14:59,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1763113.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:缓存   源码   原理   模板   UGUI

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!