【UGUI源码分析】六、MaskableGraphic、遮罩、裁剪和剔除
类: MaskableGraphic, Mask, RectMask2D, ClipperRegistry, CanvasRenderer 辅助类:Clipping, MaskUtilities, StencilMaterial 接口: IMaskable, IClippable, IClipper |
本文总结UGUI的遮罩(mask)、裁剪(clipping)和剔除(culling),对应的组件分别是Mask和RectMask2D,原理不一样,前者是用模版测试,后者是用裁剪和剔除。
遮罩通过设置材质实现,剔除和裁剪通过设置canvasRenderer实现。
1. 剔除和裁剪
之前Canvas的文章里写了CanvasUpdateRegistry.PerformUpdate()时,会先进行布局,然后会进行剔除和裁剪,最后更新网格和材质数据。通过调用 ClipperRegistry.instance.Cull(); 来进行剔除和裁剪。
ClipperRegistry是一个注册表,保存了场景内所有的IClipper对象(具体就是RectMask2D),在Cull方法里会遍历所有的IClipper, 并调用IClipper的PerformClipping方法
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// ClipperRegistry public void Cull() { for (var i = 0; i < m_Clippers.Count; ++i) { m_Clippers[i].PerformClipping(); } } // public interface IClipper { void PerformClipping(); } |
RectMask2D实现了IClipper接口,会处理剔除和裁剪。RectMask2D的数据主要包括
1 2 3 4 |
// 可裁剪的Graphics子节点 private HashSet<IClippable> m_ClipTargets = new HashSet<IClippable>(); // 这个RectMask2D的父节点的所有RectMask2D,用来算交集 private List<RectMask2D> m_Clippers = new List<RectMask2D>(); |
MaskableGraphic在合适的时机注册到 m_ClipTargets 里,而且只会注册到最近的包含RectMask2D祖先节点里。做裁剪和剔除前会收集 m_Clippers
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
public virtual void PerformClipping() { //TODO See if an IsActive() test would work well here or whether it might cause unexpected side effects (re case 776771) // 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); bool clipRectChanged = clipRect != m_LastClipRectCanvasSpace; if (clipRectChanged || m_ForceClip) { foreach (IClippable clipTarget in m_ClipTargets) clipTarget.SetClipRect(clipRect, validRect); m_LastClipRectCanvasSpace = clipRect; m_LastValidClipRect = validRect; } // 剔除 foreach (IClippable clipTarget in m_ClipTargets) { var maskable = clipTarget as MaskableGraphic; if (maskable != null && !maskable.canvasRenderer.hasMoved && !clipRectChanged) continue; clipTarget.Cull(m_LastClipRectCanvasSpace, m_LastValidClipRect); } } |
PerformClipping()方法里面主要做的内容是
- 调用MaskUtilities.GetRectMasksForClip, 收集这个RectMask2D以及父节点中满足一定条件(rectMask2D最近的没有OverrideSorting的父canvas节点的RectMask2D子节点)的RectMask2D
- 计算这些RectMask2D矩形的交集,得到裁剪和剔除的矩形 clipRect
- 调用IClippable的SetClipRect方法裁剪
- 调用IClippable的Cull方法剔除
IClippable即MaskableGraphic具体的裁剪和剔除方法是通过设置canvasRenderer状态。
剔除通过设置canvasRenderer.cull = true ,剔除后的Graphic将没有网格,不会进入渲染管线,而遮罩还是有网格,会进行渲染,只是FS之后、写入FB之前进行一次模板测试。
裁剪是通过设置canvasRenderer.EnableRectClipping(clipRect) ,这个矩形最后会传到shader里作为参数_ClipRect,矩形外面的部分透明度是0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// UI-Default.shader fixed4 frag(v2f IN) : SV_Target { half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color; #ifdef UNITY_UI_CLIP_RECT color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect); #endif #ifdef UNITY_UI_ALPHACLIP clip (color.a - 0.001); #endif return color; } |
如果开启了UNITY_UI_ALPHACLIP关键字,会做一次alpha测试。
所以说UGUI的裁剪(clipping)并不是生成新的网格,而是做alpha test也就是调用clip()函数。
2. 遮罩
遮罩(Mask)的实现和裁剪剔除不同,是通过模板测试实现。
遮罩的思路是
- 先渲染Mask,写入模板值
- 渲染Graphic,设置合适的参数做模板测试
- 再渲染一次Mask,重置模板值
例子
1) Mask
Mask的主要成员和方法
1 2 3 4 5 6 7 8 |
// Mask用的Graphic private Graphic m_Graphic; // 写入Mask值的材质 private Material m_MaskMaterial; // 重置Mask值的材质 private Material m_UnmaskMaterial; // IMaterialModifier接口 public virtual Material GetModifiedMaterial(Material baseMaterial); |
在GetModifiedMaterial方法中,会构建m_MaskMaterial和m_UnMaskMaterial(设置模板测试参数),其中m_MaskMaterial会作为返回值用来设置canvasRenderer.SetMaterail,而m_UnMaskMaterail会通过canvasRenderer.SetPopMaterial设置进mask graphic的canvasRenderer。
Unity在渲染canvasRenderer的时候会维护一个渲染栈(render stack)的概念,因为canvas下graphic的渲染是按照hierachy顺序,父节点压入栈渲染,如果有子节点将子节点压入栈渲染,否则弹出栈,子节点同样。SetPopMaterail就是在弹出栈的时候进行一次(可以多次)额外的渲染,在Mask这里是为了重置stencil buffer。
When rendering using the hierarchy the renderer can insert a 'pop'. The pop instruction is executed after all children have been rendered. The canvas renderer is rerendered using the configured pop materials.
一个渲染栈的例子
与设置渲染栈相关的方法有
1 2 3 4 5 6 |
// 开启Pop material graphic.canvasRenderer.hasPopInstruction = true; // 设置Pop materail个数 graphic.canvasRenderer.popMaterialCount = 1; // 设置Pop materail graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0); |
在Mask的GetModifiedMaterial方法里,会先计算出一个stencilDepth,这个mask graphic在同一个canvas的mask中的层级深度,最上层的mask stencilDepth是0,最多可以嵌套到7,即8层,当超过8层时会报一个错。对应模板的8个位(每个像素的模板缓冲值是1个字节存储),UGUI用模板值的每个位标记每一层。
模板测试原理:
if Func(Ref & ReadMask, STENCIL_BUFFER & ReadMask) then Op(Ref & WriteMask) |
Mask push时材质设置的模板参数
Mask pop时材质的模板参数,
其中desiredStencilBit = 1 << stencilDepth,desiredStencilBit-1即这一位之前所有位为1。在渲染mask的时候会依次从低到高将模板值置为1,pop时会从高到低依次置为0。具体的算法参见Mask.GetModifierMaterail()方法。
渲染Mask Graphic时UI-Default Shader会开启UNITY_UI_ALPHACLIP做一次alpha测试,丢弃透明像素,不透明的像素会更新模板值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// UI-Default.shader fixed4 frag(v2f IN) : SV_Target { half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color; #ifdef UNITY_UI_CLIP_RECT color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect); #endif #ifdef UNITY_UI_ALPHACLIP clip (color.a - 0.001); #endif return color; } |
2) MaskableGraphic
上面是Mask遮罩的渲染,MaskableGraphic本身也实现了IMaterailModifier接口,用来针对遮罩做材质替换,如果检测到父节点中有Mask,就替换为包含模板测试的材质。MaskableGraphic渲染时的模板参数设置是
其中stencilValue是这个Graphic在masks下的深度,(1<<stencilValue)-1就是它的所有父mask标志位都为1,只有这些mask的交集最终在模板缓冲里的位才会全部是1。这样就实现了嵌套,graphic的每个父mask都是一次模板位写入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// MaskableGraphic.cs public virtual Material GetModifiedMaterial(Material baseMaterial) { var toUse = baseMaterial; if (m_ShouldRecalculateStencil) { var rootCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform); // Graphic在masks下的深度,如果不是0且没有mask组件意味着是普通的非mask的graphic(如果是0一定是mask用的graphic) 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())) { // Ref = (1 << stencilValue) - 1 // Op = Keep // Func = Equal // ReadMask = (1 << stencilValue) - 1 // WriteMask = 0 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; } |
3) StencilMaterial
UGUI使用一个StencilMaterail静态类作为Mask材质的缓存池。因为并不是每次IMaterailModifier调用的时候都意味着要更新模板值,回想Graphic的SetXXXDirty()触发时机。所以需要对材质做一个缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// StencilMaterail.cs private class MatEntry { public Material baseMat; public Material customMat; public int count; // 引用计数,这个材质被Graphic(包括mask graphic和MaskableGraphic)引用的次数 public int stencilId; public StencilOp operation = StencilOp.Keep; public CompareFunction compareFunction = CompareFunction.Always; public int readMask; public int writeMask; public bool useAlphaClip; public ColorWriteMask colorMask; } private static List<MatEntry> m_List = new List<MatEntry>(); |
如果缓存池中有符合模板参数的材质,直接从池子里拿,用一个引用计数维护材质的销毁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// StencilMaterial.cs /// <summary> /// Remove an existing material, automatically cleaning it up if it's no longer in use. /// </summary> public static void Remove(Material customMat) { if (customMat == null) return; for (int i = 0; i < m_List.Count; ++i) { MatEntry ent = m_List[i]; if (ent.customMat != customMat) continue; if (--ent.count == 0) // 这个材质没有被其他graphic引用时销毁它 { Misc.DestroyImmediate(ent.customMat); ent.baseMat = null; m_List.RemoveAt(i); } return; } } |
3. 自定义和扩展
1) 派生MaskableGraphic
继承MaskableGraphic能让graphic支持剔除、裁剪和遮罩。Image、RawImage、Text都是派生自MaskableGraphic。
2) 实现IMaterailModifier接口
MaskableGraphic实现了IMaterailModifier接口,这是一个graphic本身去实现IMaterailModifier接口的粒子,而不一定要用一个单独的UIEffect组件去实现这个接口。
3) 自定义Pop Materail
Mask提供了一个设置pop materail的范例,通过实现IMaterailModifier接口,在GetModifiedMaterail方法内调用canvasRenderer.SetPopMaterial方法设置pop materail。Pop materail的本质是父节点在子节点渲染完后增加一次额外渲染,可以用来实现一种UI层面的“后处理”效果,而不局限于Mask这里的应用。比如在子节点渲染完后做一次color shift、tone mapping等等,非常有用。