后续补充:
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.忍了!
网友评论