0. 版本
- 源码版本:2017.3.0
- 着色器版本:2017.3.0
1. PerformClipping调用顺序
- CanvasUpdateRegistry.PerformUpdate()
public class CanvasUpdateRegistry
{
protected CanvasUpdateRegistry()
{
// 每一帧都会调用
Canvas.willRenderCanvases += PerformUpdate;
}
private void PerformUpdate()
{
// 更新Layout
......
// now layout is complete do culling...
ClipperRegistry.instance.Cull();
// 更新Graphic
......
}
}
public class ClipperRegistry
{
// RectMask2D实现了IClipper接口
readonly IndexedSet m_Clippers = new IndexedSet();
public void Cull()
{
for (var i = 0; i < m_Clippers.Count; ++i)
{
m_Clippers[i].PerformClipping();
}
}
}
- RectMask2D.PerformClipping()
public class RectMask2D : UIBehaviour, IClipper, ICanvasRaycastFilter
{
private List m_Clippers = new List();
// MaskableGraphic实现了IClippable接口
private List m_ClipTargets = new List();
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)
{
// m_Clippers = this的Parent路径上的,所有的RectMask2D组件。
MaskUtilities.GetRectMasksForClip(this, m_Clippers);
m_ShouldRecalculateClipRects = false;
}
// get the compound rects from
// the clippers that are valid
bool validRect = true;
// clipRect = m_Clippers中所有的RectMask2D的区域的交集。
// vaildRect = 区域是否有交集。
Rect clipRect = Clipping.FindCullAndClipWorldRect(m_Clippers, out validRect);
bool clipRectChanged = clipRect != m_LastClipRectCanvasSpace;
if (clipRectChanged || m_ForceClip)
{
// 重点1:向MaskGraphic设置剪裁区域。
// m_ClipTargets有哪些后面会分析。
foreach (IClippable clipTarget in m_ClipTargets)
clipTarget.SetClipRect(clipRect, validRect);
m_LastClipRectCanvasSpace = clipRect;
m_LastValidClipRect = validRect;
}
foreach (IClippable clipTarget in m_ClipTargets)
{
// hasMoved : True if any change has occured that would invalidate the positions of generated geometry.
var maskable = clipTarget as MaskableGraphic;
if (maskable != null && !maskable.canvasRenderer.hasMoved && !clipRectChanged)
continue;
// 重点2:MaskGraphic进行剪裁。
clipTarget.Cull(m_LastClipRectCanvasSpace, m_LastValidClipRect);
}
}
- MaskGraphic.SetClipRect
- MaskGraphic.Cull
public abstract class MaskableGraphic : Graphic, IClippable, IMaskable, IMaterialModifier
{
public virtual void SetClipRect(Rect clipRect, bool validRect)
{
if (validRect) // Mask有交接,设置区域。修改材质的事被CanvasRenderer做了,黑盒看不到。
canvasRenderer.EnableRectClipping(clipRect);
else
canvasRenderer.DisableRectClipping(); // Mask没有交集。
}
public virtual void Cull(Rect clipRect, bool validRect)
{
var cull = !validRect || !clipRect.Overlaps(rootCanvasRect, true);
UpdateCull(cull);
}
private void UpdateCull(bool cull)
{
var cullingChanged = canvasRenderer.cull != cull;
canvasRenderer.cull = cull;
if (cullingChanged)
{
UISystemProfilerApi.AddMarker("MaskableGraphic.cullingChanged", this);
m_OnCullStateChanged.Invoke(cull);
SetVerticesDirty(); // 疑问:为什么cull变了,要重建网格?猜测是CanvasRender根据cull的情况,做了网格的优化,减少了一些Overdraw。可以实验看看。
}
}
}
2. 剪裁区域
internal class RectangularVertexClipper
{
readonly Vector3[] m_WorldCorners = new Vector3[4];
readonly Vector3[] m_CanvasCorners = new Vector3[4];
public Rect GetCanvasRect(RectTransform t, Canvas c)
{
if (c == null)
return new Rect();
t.GetWorldCorners(m_WorldCorners);
var canvasTransform = c.GetComponent();
for (int i = 0; i < 4; ++i)
m_CanvasCorners[i] = canvasTransform.InverseTransformPoint(m_WorldCorners[i]);
return new Rect(m_CanvasCorners[0].x, m_CanvasCorners[0].y, m_CanvasCorners[2].x - m_CanvasCorners[0].x, m_CanvasCorners[2].y - m_CanvasCorners[0].y);
}
}
- UI/Default
疑问:OUT.worldPosition = v.vertex; 读取的是Local坐标,怎么是World坐标呢?
猜测:CanvasBuildBatch生成的Mesh,这个Mesh直接放到世界坐标系下了,Local坐标是World坐标是一样的。
// 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
{
Tags
{
"Queue"="Transparent"
"IgnoreProjector"="True"
"RenderType"="Transparent"
"PreviewType"="Plane"
"CanUseSpriteAtlas"="True"
}
Stencil
{
Ref [_Stencil]
Comp [_StencilComp]
Pass [_StencilOp]
ReadMask [_StencilReadMask]
WriteMask [_StencilWriteMask]
}
Cull Off
Lighting Off
ZWrite Off
ZTest [unity_GUIZTestMode]
Blend SrcAlpha OneMinusSrcAlpha
ColorMask [_ColorMask]
Pass
{
Name "Default"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 2.0
#include "UnityCG.cginc"
#include "UnityUI.cginc" // 2D Mask 剪裁。
#pragma multi_compile __ UNITY_UI_CLIP_RECT
#pragma multi_compile __ UNITY_UI_ALPHACLIP
struct appdata_t
{
float4 vertex : POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
float2 texcoord : TEXCOORD0;
float4 worldPosition : TEXCOORD1; // 2D Mask 剪裁。
UNITY_VERTEX_OUTPUT_STEREO
};
fixed4 _Color;
fixed4 _TextureSampleAdd;
float4 _ClipRect; // 2D Mask 剪裁。
v2f vert(appdata_t v)
{
v2f OUT;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
OUT.worldPosition = v.vertex; // 2D Mask 剪裁。
OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);
OUT.texcoord = v.texcoord;
OUT.color = v.color * _Color;
return OUT;
}
sampler2D _MainTex;
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); // 2D Mask 剪裁。
#endif
#ifdef UNITY_UI_ALPHACLIP
clip (color.a - 0.001);
#endif
return color;
}
ENDCG
}
}
}
3. m_ClipTargets
// MaskGraphic.cs
protected override void OnEnable()
{
......
UpdateClipParent();
......
}
protected override void OnDisable()
{
......
UpdateClipParent();
......
}
protected override void OnTransformParentChanged()
{
......
UpdateClipParent();
......
}
protected override void OnCanvasHierarchyChanged()
{
......
UpdateClipParent();
......
}
public virtual void RecalculateClipping()
{
UpdateClipParent();
}
// MaskGraphic.cs
private void UpdateClipParent()
{
// 返回最近的ParentMask
var newParent = (maskable && IsActive()) ? MaskUtilities.GetRectMaskForClippable(this) : null;
// if the new parent is different OR is now inactive
if (m_ParentMask != null && (newParent != m_ParentMask || !newParent.IsActive()))
{
m_ParentMask.RemoveClippable(this);
UpdateCull(false);
}
// don't re-add it if the newparent is inactive
if (newParent != null && newParent.IsActive())
newParent.AddClippable(this);
m_ParentMask = newParent;
}
// RectMask2D.cs
public void AddClippable(IClippable clippable)
{
if (clippable == null)
return;
m_ShouldRecalculateClipRects = true;
if (!m_ClipTargets.Contains(clippable))
m_ClipTargets.Add(clippable);
m_ForceClip = true;
}
4. 总结
- RectMask2D在每帧都会检查剪裁区域是否变化了。PerformUpdate中执行。
- 剪裁区域 = 当前RectMask2D到Parent路径上的所有有效的RectMask2D的剪裁区域的交集。
- 如果剪裁区域变化了,会通过CanvasRenderer来间接修改MaskGraphic的材质参数,把剪裁区域传进去。
- 如果剪裁区域变化了,还会为MaskGraphic调用SetVerticesDirty,重新生成网格。猜测是CanvasRenderer对剪裁的网格进行了优化,避免了OverDraw。代价就是剪裁区域变化的时候,要重新生成网格。
- 每个MaskGraphic,会把自己注册到,最近的一个ParentMask上面。
- OUT.worldPosition = v.vertex; 读取的是Local坐标。猜测CanvasBuildBatch生成的Mesh,这个Mesh直接放到世界坐标系下了,Local坐标是World坐标是一样的。
- 放到CanvasRenderer里面的是Local坐标,怀疑CanvasRenderer,把这个Local坐标又转成World坐标,再放到_ClipRect里的。根据一些测试,直接跳过CanvasRenderer,向_ClipRect直接传入World坐标是对的,传入Local坐标是不对的。