Mask使用了Gpu的模板测试功能,具体参阅 模板测试Unity官方文档
模板测试公式为:
(ref & readMask) comparisonFunction (stencilBufferValue & readMask)
对应Shader渲染设置:
Stencil
{
Ref [_Stencil]
Comp [_StencilComp]
Pass [_StencilOp]
ReadMask [_StencilReadMask]
WriteMask [_StencilWriteMask]
}
为实现遮罩效果,需要Image组件 + Mask组件;
Mask组件主要用来修改同gameObject上Image组件使用的材质球,使Image组件向模板缓冲写入缓冲值:
/// Stencil calculation time!
/// 用作Mask的Graphic,修改其材质球,支持写入模板测试值
public virtual Material GetModifiedMaterial(Material baseMaterial)
{
if (!MaskEnabled())
return baseMaterial;
var rootSortCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
//stencilDepth表示父节点有多少个Mask,不包括自身
var stencilDepth = MaskUtilities.GetStencilDepth(transform, rootSortCanvas);
if (stencilDepth >= 8)
{
Debug.LogWarning("Attempting to use a stencil mask with depth > 8", gameObject);
return baseMaterial;
}
//每个mask对应一个bit位
int desiredStencilBit = 1 << stencilDepth;
// if we are at the first level...
// we want to destroy what is there
if (desiredStencilBit == 1)
{
//渲染当前Graphic,模板值1,替换模板值,模板测试通过
var maskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Replace, CompareFunction.Always, m_ShowMaskGraphic ? ColorWriteMask.All : 0);
StencilMaterial.Remove(m_MaskMaterial);
m_MaskMaterial = maskMaterial;
//Mask子节点的元素渲染完后,清空模板值,渲染时不写入颜色
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
//当前渲染的Graphic,受父节点Mask影响时
//模板值 = 当前bit位对应值 + 所有低位bit对应的值
//测试通过直接替换
//比较函数为Equal,读取掩码为“所有低位对应的值”,以实现父节点Mask功能
//写入掩码为当前模板值
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;
//Mask里面的元素渲染完后,复位到先前的模板值,不渲染颜色
//模板值 = 所有低位对应的值
//由于要清空当前Mask对应的bit位,写入掩码为desiredStencilBit | (desiredStencilBit - 1)
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 类:
public static Material Add(Material baseMat, int stencilID, StencilOp operation, CompareFunction compareFunction, ColorWriteMask colorWriteMask, int readMask, int writeMask)
{
...
var listCount = m_List.Count;
for (int i = 0; i < listCount; ++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;
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;
//为了避免透明区域写入模板值,需要AlphaClip
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.SetFloat("_Stencil", (float)stencilID);
newEnt.customMat.SetFloat("_StencilOp", (float)operation);
newEnt.customMat.SetFloat("_StencilComp", (float)compareFunction);
newEnt.customMat.SetFloat("_StencilReadMask", (float)readMask);
newEnt.customMat.SetFloat("_StencilWriteMask", (float)writeMask);
newEnt.customMat.SetFloat("_ColorMask", (float)colorWriteMask);
newEnt.customMat.SetFloat("_UseUIAlphaClip", newEnt.useAlphaClip ? 1.0f : 0.0f);
//应该多次一举了,设置了_UseUIAlphaClip会自动启用UNITY_UI_ALPHACLIP
if (newEnt.useAlphaClip)
newEnt.customMat.EnableKeyword("UNITY_UI_ALPHACLIP");
else
newEnt.customMat.DisableKeyword("UNITY_UI_ALPHACLIP");
m_List.Add(newEnt);
return newEnt.customMat;
}
对于Mask子节点的Image组件,其渲染需要进行模板测试,实现在MaskableGraphic中:
public abstract class MaskableGraphic : Graphic, IClippable, IMaskable, IMaterialModifier
{
...
//该组件是否支持被遮罩
[SerializeField]
private bool m_Maskable = true;
//该组件是用于遮罩的Graphic
private bool m_IsMaskingGraphic = false;
[NonSerialized]
protected int m_StencilValue;
/// <summary>
/// See IMaterialModifier.GetModifiedMaterial
/// </summary>
public virtual Material GetModifiedMaterial(Material baseMaterial)
{
var toUse = baseMaterial;
if (m_ShouldRecalculateStencil)
{
if (maskable)
{
var rootCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
//受多少个Mask影响
m_StencilValue = MaskUtilities.GetStencilDepth(transform, rootCanvas);
}
else
m_StencilValue = 0;
m_ShouldRecalculateStencil = false;
}
// if we have a enabled Mask component then it will
// generate the mask material. This is an optimization
// it adds some coupling between components though :(
if (m_StencilValue > 0 && !isMaskingGraphic) //如果Graphic用于Mask,则在Mask组件里面修改材质球
{
//模板测试设置
//Ref值 = (1 << m_StencilValue) - 1,每个Mask占一个bit位
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;
}
...
/// <summary>
/// See IMaskable.RecalculateMasking
/// 父节点Mask变化时,重新获取渲染的材质球
/// </summary>
public virtual void RecalculateMasking()
{
// Remove the material reference as either the graphic of the mask has been enable/ disabled.
// This will cause the material to be repopulated from the original if need be. (case 994413)
StencilMaterial.Remove(m_MaskMaterial);
m_MaskMaterial = null;
m_ShouldRecalculateStencil = true;
SetMaterialDirty();
}
}