原图放在我的 Processon:UGUI Mask+MaskableGraphic 工作原理| ProcessOn免费在线作图,在线流程图,在线思维导图
1、Mask 是通过模板测试进行遮罩的。
ShaderLab 命令:模板 - Unity 手册
需要知道:模板测试发生在光栅化阶段/逐片元操作时。如图:
2、Mask 通过创建遮罩材质使UI系统完成模板测试。
⑴、Mask 组件在 OnEnable() 时将同级关联的 CanvasRenderer 的 hasPopInstruction 字段置为 true。使这个节点的所有子节点绘制完之后多一次绘制,用于遮罩。
⑵、Mask 组件会在 GetModifiedMaterial 方法(实现自 IMaterialModifier)中为同级关联的 CanvasRenderer 生成一个 unmaskMaterial。并设置为 popMaterial,用于遮罩。
⑶、Mask 组件会在 GetModifiedMaterial 方法(实现自 IMaterialModifier)中为同级的 Graphic 组件生成一个 maskMaterial。(最终赋给 CanvasRenderer)(见 Graphic类中的 canvasRenderer.SetMaterial(materialForRendering, 0);)
⑷、Mask 的子 MaskableGraphic 会在GetModifiedMaterial 方法(实现自 IMaterialModifier)中为自身生成一个maskMaterial。(最终赋给 CanvasRenderer)(见 Graphic类中的 canvasRenderer.SetMaterial(materialForRendering, 0);)
⑸、从 StencilMaterial 类中可以看到,生成的材质都是基于 UI-Default.shader 创建的,然后修改了其模板测试相关的参数。可以继续查看 UI-Default.shader 了解详情。(Unity 官网下载 Built-in-Shaders:https://unity.cn/releases/lts。)
⑹、为使多层 Mask 可以嵌套工作(和 RectMask2D 一样取交集),在生成这些材质时会根据自身深度(自身所在的、嵌套Mask下的位置)提供不同的模板测试参数。
例如一个三层嵌套的例子。
原图在我的 processon: UGUI Mask嵌套原理(3层示例)| ProcessOn免费在线作图,在线流程图,在线思维导图
3、模板测试参数
创建一个材质(如上图),Shader选择为UI/Default,关注这些参数:
⑴、StencilComp(_StencilComp):这个参数定义了在绘制UI元素时如何与模板缓冲进行比较。它控制着当前像素与模板缓冲中相应像素值的比较方式。其值对应枚举:
⑵、StencilID(_Stencil): 这个参数定义了在绘制UI元素时将要写入模板缓冲的值。它通常用于给UI元素设置一个唯一的标识值,以便在后续绘制中进行识别和控制。
⑶、StencilOp(_StencilOp):这个参数定义了在绘制UI元素时如何操作模板缓冲。它控制着模板缓冲中像素值的更新方式。其值对应枚举:
⑷、StencilWriteMask(_StencilWriteMask):这个参数定义了在绘制UI元素时哪些位可以被写入模板缓冲。它使用一个位掩码来指定允许写入的位。
⑸、StencilReadMask(_StencilReadMask):这个参数定义了在绘制UI元素时哪些位可以被读取模板缓冲。它使用一个位掩码来指定允许读取的位。
⑹、ColorMask(_ColorMask):这个参数定义了在绘制UI元素时哪些颜色通道会被写入帧缓冲(Frame Buffer)。它使用一个位掩码来指定允许写入的通道,包括红色(R)、绿色(G)、蓝色(B)、透明度(A)。其值对应枚举:
⑺、UseAlphaClip(_UseAlphaClip):这个参数用于启用或禁用Alpha裁剪(Alpha Clipping)效果。当启用Alpha裁剪时,UI元素会根据_AlphaClipThreshold参数的阈值来进行裁剪,使得UI元素的透明区域不会显示。
可再参考Unity文档中对参数的描述:ShaderLab 命令:模板 - Unity 手册
4、从性能上简单比较 Mask 和 RectMask2D。
⑴、Mask 的模板测试发生在光栅化阶段/逐片元操作时,而 RectMask2D 的粗裁剪发生在应用阶段/设置渲染状态时。可以看出 RectMask2D 的粗裁剪执行得很早,可以大幅略过被裁剪部分的渲染过程。
⑵、Mask 会将 CanvasRenderer的 hasPopInstruction 设为 true,这会增加一次 drawcall。
⑶、Mask 基于 UI-Default.shader 为自身及子Graphic 创建了新材质,所以会打断与其他UI的合批。
---------------------- NRatel 割 -------------------------------
更多 UGUI 注释已放入 https://github.com/NRatel/uGUI。
---------------------- NRatel 割 -------------------------------
1、IMaterialModifier
namespace UnityEngine.UI
{
// Interface which allows for the modification of the Material used to render a Graphic before they are passed to the CanvasRenderer.
// When a Graphic sets a material is is passed (in order) to any components on the GameObject that implement IMaterialModifier.
// This component can modify the material to be used for rendering.
// 这个接口允许渲染一个图形的材质在传递到 CanvasRenderer 之前被修改。
public interface IMaterialModifier
{
// Perform material modification in this function.
// 在此方法中执行材质的修改。
// "baseMaterial":The material that is to be modified. //将被修改的材质
// 返回值:The modified material. //被修改后的材质
Material GetModifiedMaterial(Material baseMaterial);
}
}
2、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]
// A component for masking children elements.
// By using this element any children elements that have masking enabled will mask where a sibling Graphic would write 0 to the stencil buffer.
// 用于遮罩子元素的组件。
// 通过使用这个元素,任何启用了 Mask(MaskableGraphic 的 maskable) 的子元素都会被遮罩。与本组件同级关联的 Graphic 将在模板测试缓冲区上写入0。
public class Mask : UIBehaviour, ICanvasRaycastFilter, IMaterialModifier
{
[NonSerialized]
private RectTransform m_RectTransform; //与本组件关联的 RectTransform。
public RectTransform rectTransform
{
get { return m_RectTransform ?? (m_RectTransform = GetComponent<RectTransform>()); }
}
[SerializeField]
private bool m_ShowMaskGraphic = true;
// Show the graphic that is associated with the Mask render area.
// 是否显示与 Mask 关联的图形的渲染区域。
// 设置时,若关联的 Graphic 不为 null,则标记 Graphic 的材质脏标记 为脏。
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;
// The graphic associated with the Mask.
// 与 Mask 关联的 graphic。
public Graphic graphic
{
get { return m_Graphic ?? (m_Graphic = GetComponent<Graphic>()); }
}
[NonSerialized]
private Material m_MaskMaterial; //Mask材质
[NonSerialized]
private Material m_UnmaskMaterial;
protected Mask()
{}
//Mask是否启用(生效):激活且关联的 Graphic 不为 null。
public virtual bool MaskEnabled() { return IsActive() && graphic != null; }
[Obsolete("Not used anymore.")]
public virtual void OnSiblingGraphicEnabledDisabled() {}
// 1、调用父类 OnEnable。
// 2、若关联的 Graphic 不为null
// ⑴、启用与 Graphic 关联的 CanvasRenderer 组件的 hasPopInstruction。
// ⑵、标记 Graphic 的材质脏标记 为脏。
// 3、通知 StencilStateChanged。(通知所有实现 IMaskable 接口的子物体重新计算遮罩。
protected override void OnEnable()
{
base.OnEnable();
if (graphic != null)
{
// hasPopInstruction:
// Enable“render stack”pop draw call。
// 当使用 hierarchy 渲染时,canvasRenderer 可以插入一个"pop"指令。
// 这个"pop"指令将在所有子元素被渲染后执行。
// CanvasRenderer 组件将使用配置的 pop 材质渲染。
graphic.canvasRenderer.hasPopInstruction = true;
graphic.SetMaterialDirty();
}
MaskUtilities.NotifyStencilStateChanged(this);
}
// 1、调用父类 OnDisable。
// 2、若关联的 Graphic 不为null
// ⑴、标记 Graphic 的材质脏标记 为脏。
// ⑵、关闭与 Graphic 关联的 CanvasRenderer 组件的 hasPopInstruction。
// ⑶、设置与 Graphic 关联的 CanvasRenderer 组件的 popMaterialCount 为 0。
// 3、将 m_MaskMaterial 从 StencilMaterial 中移除,并设置 m_MaskMaterial 为 null。
// 4、将 m_UnmaskMaterial 从 StencilMaterial 中移除,并设置 m_UnmaskMaterial 为 null。
// 5、通知 StencilStateChanged。(通知所有实现 IMaskable 接口的子物体重新计算遮罩。
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,因为我们需要 在通知子物体Mask状态改变时,让 IsActive 返回正确的值。
// 疑问 ??? 未理解,为什么调 base.OnDisable 会影响到 IsActive。
// 实际测试,某 UIBehaviour 的子类的 OnDisable 中,调用 base.OnDisable 前后,其 activeInHierarchy 和 activeSelf 均为 false。
base.OnDisable();
if (graphic != null)
{
graphic.SetMaterialDirty();
graphic.canvasRenderer.hasPopInstruction = false;
graphic.canvasRenderer.popMaterialCount = 0; // popMaterialCount:CanvasRenderer组件可用的材质数量,用于内部遮罩。
}
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)
graphic.SetMaterialDirty();
MaskUtilities.NotifyStencilStateChanged(this);
}
#endif
// 实现 ICanvasRaycastFilter 的接口
// 射线投射位置是否有效
public virtual bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
{
if (!isActiveAndEnabled) //若未激活或未启用,则有效(不过滤)
return true;
// 若激活且启用,则检查投射点是否在本 rectTransform 的矩形内。 在则有效。
return RectTransformUtility.RectangleContainsScreenPoint(rectTransform, sp, eventCamera);
}
// Stencil calculation time!
// 实际的模板测试在这里进行!
// 实现 IMaterialModifier 的接口。
// 1、检查 Mask 是否启用,若未启用,直接返回 baseMaterial。
public virtual Material GetModifiedMaterial(Material baseMaterial)
{
if (!MaskEnabled())
return baseMaterial;
var rootSortCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform); // 获取最深根的 Canvas,或第一个“使用独立绘制顺序”的 Canvas。
var stencilDepth = MaskUtilities.GetStencilDepth(transform, rootSortCanvas); // 计算模板测试深度。
if (stencilDepth >= 8)
{
Debug.LogWarning("Attempting to use a stencil mask with depth > 8", gameObject);
return baseMaterial; //如果深度>=8,抛出警告,直接返回 baseMaterial。
}
int desiredStencilBit = 1 << stencilDepth; //预期的模板测试深度Bit。
// if we are at the first level... we want to destroy what is there
// 如果是嵌套 Mask 的第一层(最上面的一层)
if (desiredStencilBit == 1) // ( 即 stencilDepth == 0)。
{
// 创建/获取第一层 Mask 关联的 Gaphic 使用的材质:m_MaskMaterial
var maskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Replace, CompareFunction.Always, m_ShowMaskGraphic ? ColorWriteMask.All : 0);
StencilMaterial.Remove(m_MaskMaterial);
m_MaskMaterial = maskMaterial;
// 创建/获取第一层 Mask 关联的 Gaphic 使用的 pop材质:unmaskMaterial
var unmaskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Zero, CompareFunction.Always, 0);
StencilMaterial.Remove(m_UnmaskMaterial);
m_UnmaskMaterial = unmaskMaterial;
graphic.canvasRenderer.popMaterialCount = 1; // popMaterialCount:CanvasRenderer 组件可用的材质数量,用于内部遮罩。
graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0); //SetPopMaterial:设置 canvasRenderer 的材质,用于内部遮罩。
return m_MaskMaterial; //返回修改后的材质
}
//otherwise we need to be a bit smarter and set some read / write masks
// 否则。需要设置一些 read / write 的遮罩。
// 创建/获取第N层 Mask 关联的 Gaphic 使用的 pop材质:m_MaskMaterial
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;
// 创建/获取第N层 Mask 关联的 Gaphic 使用的 pop材质:unmaskMaterial
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; // popMaterialCount:CanvasRenderer组件可用的材质数量,用于内部遮罩。
graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0); //SetPopMaterial:设置 canvasRenderer 的材质,用于内部遮罩。
return m_MaskMaterial; //返回修改后的材质
}
}
}
3、IMaskable
在 Mask 状态改变时,MaskUtilities 利用这个接口配合对遮罩情况进行更新。
using System;
namespace UnityEngine.UI
{
// This element is capable of being masked out.
// 这个元素可以被遮罩。(目前只有MaskableGraphic实现它)
public interface IMaskable
{
// Recalculate masking for this element and all children elements.
// Use this to update the internal state (recreate materials etc).
// 重新计算此元素和所有子元素的遮罩。
// 更新内部状态(重新创建材质等)。
void RecalculateMasking();
}
}