1. 程式人生 > 實用技巧 >UGUI 原始碼筆記

UGUI 原始碼筆記

目錄

一、UGUI

1. Define

通過 3D 網格系統構建的 UI 系統。

1.1 實現

使用 UGUI 製作一個圖元(圖片、按鈕...),會先構建一個方形網格,然後繫結材質球,材質球裡存放了要顯示的圖片。

1.2 問題

材質球和網格的渲染,效能開銷過大,drawcell 過高。

1.3 解決

  1. 減少材質球:一部分型別相同的圖片,進行合圖。多個圖元可以只是用一個材質球,改變模型頂點上的 UV 就能顯示不同的圖片了。
  2. 合併網格:將有相同材質球(圖片、shader)和相同層級的網格,進行合併。

2 . 原始碼檔案結構(v2019.1)

  • EventSystem 輸入事件
    • EventData 事件資料
    • InputModules 輸入事件捕捉模組
    • Raycasters 射線碰撞檢測
    • EventHandle 事件處理和回撥
  • Animation 動畫
    • CoroutineTween 補間動畫
  • Core 渲染核心
    • Culling 裁剪
    • Layout 佈局
    • MaterialModifies 材質球修改器
    • SpecializedCollections 特殊集合
    • Utility 工具集
    • VertexModifies 頂點修改器

二、部分元件

1. Canvas

類似於畫畫的畫布。Canvas 包含了上述解決步驟中的合併網格的功能。

1.1 Render Mode

  • Screen Space - Overlay:不與空間上的排序有任何關係,常用在純 UI。Sort Order 值越大,顯示越前面
  • Screen Space - Camera:依賴 Camera 的平面透視,能夠加入非 UGUI的元素到 UI 中。【常用】
  • World Space: UI 物體會放出現在世界空間中,常在世界空間中與普通3D物體一同展示,列如場景中物件的血條。

2. Canvas Scale

縮放比例元件,用來指定 Canvas 中元素的比例大小。

2.1 UI Scale Mode

  • Constant Pixel Size:指定比例大小。
  • Scale With Screen Size:以螢幕基準自動縮放,能設定以寬還是以高。【手遊專案常用】
  • Constant Physical Size:以物理大小為基準。

2.2 ScreenMatchMode.MatchWidthOrHeight

根據螢幕高寬匹配。使用對數空間轉換能夠有更好的表現。

float logWidth = Mathf.Log(screenSize.x / m_ReferenceResolution.x, 2);
float logHeight = Mathf.Log(screenSize.y / m_ReferenceResolution.y, 2);
float logWeightedAverage = Mathf.Lerp(logWidth, logHeight, m_MatchWidthOrHeight);
scaleFactor = Mathf.Pow(2, logWeightedAverage);

e.g. 裝置和遊戲螢幕比例,寬是 10/5 = 2,高是 5/10 = 0.5,匹配值設定為 0.5。

正常空間平均值計算:(2+0.5)* 0.5 = 1.25,會放大 Canvas。

對數空間平均值計算:2^((log2(2)+log2(0.5))*0.5) =2^((1+-1)*0.5) = 1 ,Canvas 比例不變。

3. Image 、RawImage

對於圖片、圖集展示的元件。

3.1 區別

  • Image:展示圖集中的圖元,能夠參與合併。
  • RawImage:能展示單張圖片,不能合併。

3.2 選擇

圖片尺寸小,能夠打成圖集 -> Image

圖片尺寸大,合圖效率低 -> RawIamge

圖片相同型別數量多,合併圖集太大,實際展示圖片又很少 -> RawIamge

4. RectTransForm

雖然 RectTransform 是 UnityEngine 下的類,但是在 UGUI 中大量使用,也需要有所瞭解。

簡單來說,UGUI 通過 RectTransform 來定義 UI 的位置、大小、旋轉。

4.1 Anchors

錨點:子節點相對於父節點的對齊依據。數值為相對於父節點的比例。

100X100的圖片,全展 Anchors,父節點 120X120 效果如下。

Anchors Presets 工具,列出了常用的 Anchor 佈局。按住 Shift 可以連同 Pivot 一同改變,按住 Alt 可以連同位置一同改變,在子節點鋪滿父節點的時候非常好用。

新增圖片註釋,不超過 140 字(可選)

4.2 Pivot

物體自身的支點,影響物體旋轉、縮放、位置。

三、輸入事件

1. EventData 事件資料

  • BaseEventData 基礎事件資料
  • AxisEventData 滾輪事件資料
  • PointerEventData 點位事件資料

1.1 BaseEventData 基礎事件資料

基礎事件資料。包含了 EventSystem,能夠獲取 currentInputModule(當前輸入模式)和 selectObject(選擇物件)。

1.2 AxisEventData 滾輪事件資料

軸向事件資料。包含了moveDirection (移動方向)和 moveVector(移動向量)。

1.3 PointerEventData 點位事件資料

點位資料,包含了點選和觸發相關的資料,有著大量事件系統邏輯需要的資料。比如按下的位置,鬆開與按下的時間差,拖動的位移差等等。

public class PointerEventData : BaseEventData
{
    //...
    public GameObject pointerEnter { get; set; }

    // 接收OnPointerDown事件的物體
    private GameObject m_PointerPress;
    // 上一下接收OnPointerDown事件的物體
    public GameObject lastPress { get; private set; }
    // 接收按下事件的無法響應處理的物體
    public GameObject rawPointerPress { get; set; }
    // 接收OnDrag事件的物體
    public GameObject pointerDrag { get; set; }

    public RaycastResult pointerCurrentRaycast { get; set; }
    public RaycastResult pointerPressRaycast { get; set; }

    public List<GameObject> hovered = new List<GameObject>();

    public bool eligibleForClick { get; set; }

    public int pointerId { get; set; }

    // 滑鼠或觸控時的點位
    public Vector2 position { get; set; }
    // 滾輪的移速
    public Vector2 delta { get; set; }
    // 按下時的點位
    public Vector2 pressPosition { get; set; }

    // 為雙擊服務的上次點選時間
    public float clickTime { get; set; }
    // 為雙擊服務的點選次數
    public int clickCount { get; set; }

    public Vector2 scrollDelta { get; set; }
    public bool useDragThreshold { get; set; }
    public bool dragging { get; set; }

    public InputButton button { get; set; }
     //...
}

2. InputModules 輸入事件捕捉模組

  • BaseInputModule:抽象基類,提供基本屬性和介面。
  • PointerInputModule:指標輸入模組,擴充套件了點位的輸入邏輯,增加了輸入型別和狀態。
  • StandaloneInputModule:獨立輸入模組,擴充套件了滑鼠、鍵盤、控制器輸入。(觸控也支援了)
  • TouchInputModule:觸控輸入模組,擴充套件了觸控輸入。(已經過時,觸控輸入在 StandaloneInputModule 中處理)

2.1 StandaloneInputModule 獨立輸入模組

輸入檢測的邏輯,是通過 EventSystem 在 Update 中每幀呼叫當前輸入模組(StandaloneInputModule)的 Progress 方法實現不斷檢測的。

關鍵方法就是 Progress。在 Progress 方法中,因為滑鼠模擬層的原因,觸控需要先進行判斷,然後根據判斷是否有滑鼠(input.mousePresent),進行滑鼠事件處理。

2.2 ProcessTouchEvent 處理觸控事件

private bool ProcessTouchEvents()
{
    for (int i = 0; i < input.touchCount; ++i)
    {
        Touch touch = input.GetTouch(i);

        if (touch.type == TouchType.Indirect)
            continue;

        bool released;
        bool pressed;
        var pointer = GetTouchPointerEventData(touch, out pressed, out released);
        // 處理觸控按壓 or 釋放
        ProcessTouchPress(pointer, pressed, released);

        if (!released)
        {
            // 觸控沒有釋放,需要處理移動和拖拽
            ProcessMove(pointer);
            ProcessDrag(pointer);
        }
        else
            RemovePointerData(pointer);
    }
    return input.touchCount > 0;
}
protected void ProcessTouchPress(PointerEventData pointerEvent, bool pressed, bool released)
{
    var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject;

    // 處理按壓
    // PointerDown notification
    if (pressed)
    {
        // 初始化 pointerEvent
        pointerEvent.eligibleForClick = true;
        pointerEvent.delta = Vector2.zero;
        pointerEvent.dragging = false;
        pointerEvent.useDragThreshold = true;
        pointerEvent.pressPosition = pointerEvent.position;
        pointerEvent.pointerPressRaycast = pointerEvent.pointerCurrentRaycast;

        DeselectIfSelectionChanged(currentOverGo, pointerEvent);

        if (pointerEvent.pointerEnter != currentOverGo)
        {
            // 當前按壓物件與上一進入物件不同,觸發 exit 和 enter
            // send a pointer enter to the touched element if it isn't the one to select...
            HandlePointerExitAndEnter(pointerEvent, currentOverGo);
            // 更新進入物件
            pointerEvent.pointerEnter = currentOverGo;
        }

        // 依照當前物件所在樹狀結構(Hierarchy)自下而上尋找能夠執行 PointerDown 的物件
        // search for the control that will receive the press
        // if we can't find a press handler set the press
        // handler to be what would receive a click.
        var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.pointerDownHandler);

        // 如果沒有找到執行 PointerDown 事件的物件,嘗試 Click 事件,同樣自下而上查詢
        // didnt find a press handler... search for a click handler
        if (newPressed == null)
            newPressed = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);

        // Debug.Log("Pressed: " + newPressed);

        float time = Time.unscaledTime;

        // 響應的是同一個物件,與上一次點選間隔小於0.3,點選次數才會增加
        if (newPressed == pointerEvent.lastPress)
        {
            var diffTime = time - pointerEvent.clickTime;
            if (diffTime < 0.3f)
                ++pointerEvent.clickCount;
            else
                pointerEvent.clickCount = 1;

            pointerEvent.clickTime = time;
        }
        else
        {
            pointerEvent.clickCount = 1;
        }
        // 更新資料
        pointerEvent.pointerPress = newPressed;
        pointerEvent.rawPointerPress = currentOverGo;

        pointerEvent.clickTime = time;

        // 初始化一個潛在的拖拽 drop 事件
        // Save the drag handler as well
        pointerEvent.pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>(currentOverGo);

        if (pointerEvent.pointerDrag != null)
            ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.initializePotentialDrag);

        m_InputPointerEvent = pointerEvent;
    }

    // 處理釋放
    // PointerUp notification
    if (released)
    {
        // 處理 PointerUp 事件
        // Debug.Log("Executing pressup on: " + pointer.pointerPress);
        ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);

        // Debug.Log("KeyCode: " + pointer.eventData.keyCode);

        // see if we mouse up on the same element that we clicked on...
        var pointerUpHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);

        // 如果 PointerPress 和 PointerUp 的物件相同,執行 PointerClick 事件
        // PointerClick and Drop events
        if (pointerEvent.pointerPress == pointerUpHandler && pointerEvent.eligibleForClick)
        {
            ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerClickHandler);
        }
        else if (pointerEvent.pointerDrag != null && pointerEvent.dragging) // 判斷是否有拖拽,執行拖拽 drop 事件
        {
            ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.dropHandler);
        }
        // 更新資料,清除點選物件
        pointerEvent.eligibleForClick = false;
        pointerEvent.pointerPress = null;
        pointerEvent.rawPointerPress = null;

        // 執行 EndDrog 事件
        if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
            ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.endDragHandler);

        pointerEvent.dragging = false;
        pointerEvent.pointerDrag = null;

        // 執行 Exit 事件
        // send exit events as we need to simulate this on touch up on touch device
        ExecuteEvents.ExecuteHierarchy(pointerEvent.pointerEnter, pointerEvent, ExecuteEvents.pointerExitHandler);
        pointerEvent.pointerEnter = null;

        m_InputPointerEvent = pointerEvent;
    }
}

2.3 ProcessMouseEvent 處理滑鼠事件

protected void ProcessMouseEvent(int id)
{
    var mouseData = GetMousePointerEventData(id);
    var leftButtonData = mouseData.GetButtonState(PointerEventData.InputButton.Left).eventData;

    m_CurrentFocusedGameObject = leftButtonData.buttonData.pointerCurrentRaycast.gameObject;

    // 處理左鍵
    // Process the first mouse button fully
    ProcessMousePress(leftButtonData);
    ProcessMove(leftButtonData.buttonData);
    ProcessDrag(leftButtonData.buttonData);

    // 處理右鍵和中鍵
    // Now process right / middle clicks
    ProcessMousePress(mouseData.GetButtonState(PointerEventData.InputButton.Right).eventData);
    ProcessDrag(mouseData.GetButtonState(PointerEventData.InputButton.Right).eventData.buttonData);
    ProcessMousePress(mouseData.GetButtonState(PointerEventData.InputButton.Middle).eventData);
    ProcessDrag(mouseData.GetButtonState(PointerEventData.InputButton.Middle).eventData.buttonData);

    // 處理滾輪事件   
    if (!Mathf.Approximately(leftButtonData.buttonData.scrollDelta.sqrMagnitude, 0.0f))
    {
        var scrollHandler = ExecuteEvents.GetEventHandler<IScrollHandler>(leftButtonData.buttonData.pointerCurrentRaycast.gameObject);
        ExecuteEvents.ExecuteHierarchy(scrollHandler, leftButtonData.buttonData, ExecuteEvents.scrollHandler);
    }
}

ProcessMousePress 與 ProcessTouchPress 極其相似,不過多敘述。

3. Raycasters 射線碰撞檢測

  • BaseRaycaster:抽象類
  • PhysicsRaycaster:3D 射線碰撞檢測,用射線的方式做碰撞檢測,碰撞結果根據距離遠近排序。
  • Physics2DRaycaster:2D 射線碰撞檢測,與 3D 的區別是預留了 2D 的層級順序進行排序。
  • GraphicRaycaster:圖形射線碰撞檢測,通過遍歷可點選 UGUI 元素,根據點位判斷。【常用】

3.1 GraphicRaycaster 圖形射線碰撞檢測

[NonSerialized] static readonly List<Graphic> s_SortedGraphics = new List<Graphic>();
private static void Raycast(Canvas canvas, Camera eventCamera, Vector2 pointerPosition, IList<Graphic> foundGraphics, List<Graphic> results)
{
    // Necessary for the event system
    int totalCount = foundGraphics.Count;
    for (int i = 0; i < totalCount; ++i)
    {
        Graphic graphic = foundGraphics[i];

        // 依次判斷所有圖形,depth 不為 -1,是射線目標,沒有被渲染剔除(可點選)
        // depth 深度為 -1 代表未被 Canvas 處理,還沒有被繪製出來
        if (graphic.depth == -1 || !graphic.raycastTarget || graphic.canvasRenderer.cull)
            continue;

        if (!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, pointerPosition, eventCamera))
            continue;

        if (eventCamera != null && eventCamera.WorldToScreenPoint(graphic.rectTransform.position).z > eventCamera.farClipPlane)
            continue;
        // 判斷點位是否落在圖形上
        if (graphic.Raycast(pointerPosition, eventCamera))
        {
            s_SortedGraphics.Add(graphic);
        }
    }
    // 根據 depth 排序
    s_SortedGraphics.Sort((g1, g2) => g2.depth.CompareTo(g1.depth));
    totalCount = s_SortedGraphics.Count;
    for (int i = 0; i < totalCount; ++i)
        results.Add(s_SortedGraphics[i]);

    s_SortedGraphics.Clear();
}

4. EventHandle 事件處理和回撥

主要邏輯集中在 EventSystem 中,是整個事件模組的入口,繼承了 MonoBehavior,在 Update 中幀迴圈做輪詢。

protected virtual void Update()
{
    if (current != this)
        return;
    // tick 模組(輸入模組執行 UpdateModule)
    TickModules();

    bool changedModule = false;
    for (var i = 0; i < m_SystemInputModules.Count; i++)
    {
        var module = m_SystemInputModules[i];
        if (module.IsModuleSupported() && module.ShouldActivateModule())
        {
            if (m_CurrentInputModule != module)
            {
                ChangeEventModule(module);
                changedModule = true;
            }
            break;
        }
    }

    // 沒有設定輸入模組,設定第一個
    if (m_CurrentInputModule == null)
    {
        for (var i = 0; i < m_SystemInputModules.Count; i++)
        {
            var module = m_SystemInputModules[i];
            if (module.IsModuleSupported())
            {
                ChangeEventModule(module);
                changedModule = true;
                break;
            }
        }
    }
    // 呼叫輸入模組 Progress
    if (!changedModule && m_CurrentInputModule != null)
        m_CurrentInputModule.Process();
}

四、Core

1. Culling 裁剪

Culling 下都是裁剪的工具類,大都用在了 Mask 遮罩上。Clipping 中包含了 RectMask2D 的裁剪方法。

/// <summary>
/// Find the Rect to use for clipping.
/// Given the input RectMask2ds find a rectangle that is the overlap of all the inputs.
/// </summary>
/// <param name="rectMaskParents">RectMasks to build the overlap rect from.</param>
/// <param name="validRect">Was there a valid Rect found.</param>
/// <returns>The final compounded overlapping rect</returns>
public static Rect FindCullAndClipWorldRect(List<RectMask2D> rectMaskParents, out bool validRect)
{
    if (rectMaskParents.Count == 0)
    {
        validRect = false;
        return new Rect();
    }

    // 比較出 rectMask CanvasRect 交集部分,min取大值,max取小值
    Rect current = rectMaskParents[0].canvasRect;
    float xMin = current.xMin;
    float xMax = current.xMax;
    float yMin = current.yMin;
    float yMax = current.yMax;
    for (var i = 1; i < rectMaskParents.Count; ++i)
    {
        current = rectMaskParents[i].canvasRect;
        if (xMin < current.xMin)
            xMin = current.xMin;
        if (yMin < current.yMin)
            yMin = current.yMin;
        if (xMax > current.xMax)
            xMax = current.xMax;
        if (yMax > current.yMax)
            yMax = current.yMax;
    }
	
    validRect = xMax > xMin && yMax > yMin;
    if (validRect)
        return new Rect(xMin, yMin, xMax - xMin, yMax - yMin);
    else
        return new Rect();
}

2. Layout 佈局

主要是佈局類和自動適配類,以及部分介面。

佈局類如下:

  • LayoutGroup:佈局抽象類,實現 ILayoutElement 和 ILayoutGroup 介面
  • GridLayoutGroup:網格佈局
  • HorizontalOrVerticalLayoutGroup:橫向或縱向佈局
  • HorizontalLayoutGroup:橫向佈局
  • VerticalLayoutGroup:縱向佈局

2.1 LayoutGroup

主要關注 SetDirt 方法,該方法會在LayoutGroup 以下幾處觸發,導致更新佈局。

// LayoutGroup 被啟用
protected override void OnEnable()
{
    base.OnEnable();
    SetDirty();
}

// RectTransform 傳送變化
protected override void OnRectTransformDimensionsChange()
{
    base.OnRectTransformDimensionsChange();
    if (isRootLayoutGroup)
        SetDirty();
}
// 有 Transform 的子物件數量傳送變化
protected virtual void OnTransformChildrenChanged()
{
    SetDirty();
}
// 動畫改編屬性
protected override void OnDidApplyAnimationProperties()
{
    SetDirty();
}

// 設定不同屬性
protected void SetProperty<T>(ref T currentValue, T newValue)
{
    if ((currentValue == null && newValue == null) || (currentValue != null && currentValue.Equals(newValue)))
        return;
    currentValue = newValue;
    SetDirty();
}

2.2 CalculateLayoutInputXxx

ILayoutElement 定義了 CalculateLayoutInputXxx 相關方法,可以計算水平(CalculateLayoutInputHorizontal)和垂直(CalculateLayoutInputVertical)的值(minWidth,preferredWidth,flexibleWidth)。

  • minWidth:需要為此物件分配的最小寬度
  • preferredWidth:如果空間充足的話,應當為此物件分配的寬度
  • flexibleWidth:如果有多餘的空間的話,可以為此物件額外分配的相對寬度

在Rebuild 的時候,會先調 CalculateLayoutInputXxx ,再 SetLayoutXxx。

LayoutGroup 中只重寫了 CalculateLayoutInputHorizontal,用來查詢和統計排除掉不參與佈局(ignoreLayout = true)的子節點。

VerticalLayoutGroup 和 HorizontalLayoutGroup 都會呼叫父類 HorizontalOrVerticalLayoutGroup 的 CalcAlongAxis 方法。

/// <summary>
/// 通過給定的軸,計運算元元素位置和大小
/// </summary>
/// <param name="axis">軸,0是水平,1是垂直</param>
/// <param name="isVertical">是否是垂直佈局</param>
protected void CalcAlongAxis(int axis, bool isVertical)
{
    float combinedPadding = (axis == 0 ? padding.horizontal : padding.vertical);
    bool controlSize = (axis == 0 ? m_ChildControlWidth : m_ChildControlHeight);
    bool useScale = (axis == 0 ? m_ChildScaleWidth : m_ChildScaleHeight);
    bool childForceExpandSize = (axis == 0 ? m_ChildForceExpandWidth : m_ChildForceExpandHeight);

    float totalMin = combinedPadding;
    float totalPreferred = combinedPadding;
    float totalFlexible = 0;
	
    // 軸與當前佈局相反
    bool alongOtherAxis = (isVertical ^ (axis == 1));	
    for (int i = 0; i < rectChildren.Count; i++)
    {
        RectTransform child = rectChildren[i];
        float min, preferred, flexible;
        GetChildSizes(child, axis, controlSize, childForceExpandSize, out min, out preferred, out flexible);

        if (useScale)
        {
            float scaleFactor = child.localScale[axis];
            min *= scaleFactor;
            preferred *= scaleFactor;
            flexible *= scaleFactor;
        }

        if (alongOtherAxis)
        {
            totalMin = Mathf.Max(min + combinedPadding, totalMin);
            totalPreferred = Mathf.Max(preferred + combinedPadding, totalPreferred);
            totalFlexible = Mathf.Max(flexible, totalFlexible);
        }
        else
        {
            // 軸與佈局相同的時候,需要加上 spacing
            totalMin += min + spacing;
            totalPreferred += preferred + spacing;

            // Increment flexible size with element's flexible size.
            totalFlexible += flexible;
        }
    }

    if (!alongOtherAxis && rectChildren.Count > 0)
    {
        // 相同軸,刪除最後一個 spacing
        totalMin -= spacing;
        totalPreferred -= spacing;
    }
    totalPreferred = Mathf.Max(totalMin, totalPreferred);
    // 儲存計算後的結果
    SetLayoutInputForAxis(totalMin, totalPreferred, totalFlexible, axis);
}

GridLayoutGroup 是通過網格行列數來計算 width 值的。

public override void CalculateLayoutInputHorizontal()
{
    base.CalculateLayoutInputHorizontal();

    int minColumns = 0;
    int preferredColumns = 0;
    if (m_Constraint == Constraint.FixedColumnCount)
    {
        minColumns = preferredColumns = m_ConstraintCount;
    }
    else if (m_Constraint == Constraint.FixedRowCount)
    {
        minColumns = preferredColumns = Mathf.CeilToInt(rectChildren.Count / (float)m_ConstraintCount - 0.001f);
    }
    else
    {
        minColumns = 1;
        preferredColumns = Mathf.CeilToInt(Mathf.Sqrt(rectChildren.Count));
    }

    SetLayoutInputForAxis(
        padding.horizontal + (cellSize.x + spacing.x) * minColumns - spacing.x,
        padding.horizontal + (cellSize.x + spacing.x) * preferredColumns - spacing.x,
        -1, 0);
}

public override void CalculateLayoutInputVertical()
{
    int minRows = 0;
    if (m_Constraint == Constraint.FixedColumnCount)
    {
        minRows = Mathf.CeilToInt(rectChildren.Count / (float)m_ConstraintCount - 0.001f);
    }
    else if (m_Constraint == Constraint.FixedRowCount)
    {
        minRows = m_ConstraintCount;
    }
    else
    {
        float width = rectTransform.rect.width;
        int cellCountX = Mathf.Max(1, Mathf.FloorToInt((width - padding.horizontal + spacing.x + 0.001f) / (cellSize.x + spacing.x)));
        minRows = Mathf.CeilToInt(rectChildren.Count / (float)cellCountX);
    }

    float minSpace = padding.vertical + (cellSize.y + spacing.y) * minRows - spacing.y;
    SetLayoutInputForAxis(minSpace, minSpace, -1, 1);
}

2.3 SetLayoutXxx

在 CalculateLayoutInputXxx 後,ILayoutElement 各元素都能取到正確值,接下來就是根據這些值來控制子節點的了。

VerticalLayoutGroup 和 HorizontalLayoutGroup 依舊都會呼叫父類 HorizontalOrVerticalLayoutGroup 的 SetChildrenAlongAxis 方法。`

protected void SetChildrenAlongAxis(int axis, bool isVertical)
{
    // 初始化引數
    float size = rectTransform.rect.size[axis];
    bool controlSize = (axis == 0 ? m_ChildControlWidth : m_ChildControlHeight);
    bool useScale = (axis == 0 ? m_ChildScaleWidth : m_ChildScaleHeight);
    bool childForceExpandSize = (axis == 0 ? m_ChildForceExpandWidth : m_ChildForceExpandHeight);
    // 根據對齊方式,返回浮點數
    // axis=0 水平,返回 0(左),0.5(中),1(右)
    // axis=1 垂直,返回 0(上),0.5(中),1(下)
    float alignmentOnAxis = GetAlignmentOnAxis(axis);

    bool alongOtherAxis = (isVertical ^ (axis == 1));
    if (alongOtherAxis)
    {
        float innerSize = size - (axis == 0 ? padding.horizontal : padding.vertical);
        for (int i = 0; i < rectChildren.Count; i++)
        {
            RectTransform child = rectChildren[i];
            float min, preferred, flexible;
            GetChildSizes(child, axis, controlSize, childForceExpandSize, out min, out preferred, out flexible);
            float scaleFactor = useScale ? child.localScale[axis] : 1f;

            float requiredSpace = Mathf.Clamp(innerSize, min, flexible > 0 ? size : preferred);
            float startOffset = GetStartOffset(axis, requiredSpace * scaleFactor);
            if (controlSize)
            {
                // 如果控制子節點的 Size,就不需要設定偏移,
                SetChildAlongAxisWithScale(child, axis, startOffset, requiredSpace, scaleFactor);
            }
            else
            {
                float offsetInCell = (requiredSpace - child.sizeDelta[axis]) * alignmentOnAxis;
                SetChildAlongAxisWithScale(child, axis, startOffset + offsetInCell, scaleFactor);
            }
        }
    }
    else
    {
        float pos = (axis == 0 ? padding.left : padding.top);
        float itemFlexibleMultiplier = 0;
        float surplusSpace = size - GetTotalPreferredSize(axis);
		
        // LayoutGroup 的尺寸比 preferred 大
        if (surplusSpace > 0)
        {
            if (GetTotalFlexibleSize(axis) == 0)
                pos = GetStartOffset(axis, GetTotalPreferredSize(axis) - (axis == 0 ? padding.horizontal : padding.vertical));
            else if (GetTotalFlexibleSize(axis) > 0)
                itemFlexibleMultiplier = surplusSpace / GetTotalFlexibleSize(axis);
        }
		
        // Preferred 不等於 min 的時候,計算出 min 和 Preferred 之間插值的係數
        float minMaxLerp = 0;
        if (GetTotalMinSize(axis) != GetTotalPreferredSize(axis))
            minMaxLerp = Mathf.Clamp01((size - GetTotalMinSize(axis)) / (GetTotalPreferredSize(axis) - GetTotalMinSize(axis)));

        for (int i = 0; i < rectChildren.Count; i++)
        {
            RectTransform child = rectChildren[i];
            float min, preferred, flexible;
            GetChildSizes(child, axis, controlSize, childForceExpandSize, out min, out preferred, out flexible);
            float scaleFactor = useScale ? child.localScale[axis] : 1f;

            float childSize = Mathf.Lerp(min, preferred, minMaxLerp);
            childSize += flexible * itemFlexibleMultiplier;
            if (controlSize)
            {
                SetChildAlongAxisWithScale(child, axis, pos, childSize, scaleFactor);
            }
            else
            {
                float offsetInCell = (childSize - child.sizeDelta[axis]) * alignmentOnAxis;
                SetChildAlongAxisWithScale(child, axis, pos + offsetInCell, scaleFactor);
            }
            pos += childSize * scaleFactor + spacing;
        }
    }
}
protected void SetChildAlongAxisWithScale(RectTransform rect, int axis, float pos, float size, float scaleFactor)
{
    if (rect == null)
        return;

    m_Tracker.Add(this, rect,
                  DrivenTransformProperties.Anchors |
                  (axis == 0 ?
                   (DrivenTransformProperties.AnchoredPositionX | DrivenTransformProperties.SizeDeltaX) :
                   (DrivenTransformProperties.AnchoredPositionY | DrivenTransformProperties.SizeDeltaY)
                  )
                 );

    // Inlined rect.SetInsetAndSizeFromParentEdge(...) and refactored code in order to multiply desired size by scaleFactor.
    // sizeDelta must stay the same but the size used in the calculation of the position must be scaled by the scaleFactor.

    // 設定 (0,1) 錨點
    rect.anchorMin = Vector2.up;
    rect.anchorMax = Vector2.up;
	
    // 賦值 尺寸
    Vector2 sizeDelta = rect.sizeDelta;
    sizeDelta[axis] = size;
    rect.sizeDelta = sizeDelta;
	
    // 賦值 位置
    Vector2 anchoredPosition = rect.anchoredPosition;
    anchoredPosition[axis] = (axis == 0) ? (pos + size * rect.pivot[axis] * scaleFactor) : (-pos - size * (1f - rect.pivot[axis]) * scaleFactor);
    rect.anchoredPosition = anchoredPosition;
}

2.4 自適應

  • AspectRatioFitter:朝向自適應
  • CanvasScaler:畫布大小自適應,具體內容可以看 二、2 CanvasScale
  • ContentSizeFitter:內容自適應

AspectRatioFitter 主要定義了一個列舉型別 AspectMode。

public enum AspectMode 
{ 	                  
	//不使用適合的縱橫比
	None,
	      
	//讓Height隨著Width自動調節
	WidthControlsHeight,
	
	//讓Width隨著Height自動調節
	HeightControlsWidth,
	         
	//寬度、高度、位置和錨點都會被自動調整,以使得該矩形擬合父物體的矩形內,同時保持寬高比
    FitInParent,
    
	//寬度、高度、位置和錨點都會被自動調整,以使得該矩形覆蓋父物體的整個區域,同時保持寬高比
	EnvelopeParent
}

核心方法 UpdateRect,在 OnEnable、OnRectTransformDimensionsChange 和 SetDirty 的時候都會呼叫。UpdateRect 根據 AspectMode 調整自身的 RectTransform 值。

private void UpdateRect()
{
    if (!IsActive())
        return;

    m_Tracker.Clear();

    switch (m_AspectMode)
    {
            #if UNITY_EDITOR
                case AspectMode.None:
            {
                if (!Application.isPlaying)
                    m_AspectRatio = Mathf.Clamp(rectTransform.rect.width / rectTransform.rect.height, 0.001f, 1000f);

                break;
            }
            #endif
                case AspectMode.HeightControlsWidth:
            {
                m_Tracker.Add(this, rectTransform, DrivenTransformProperties.SizeDeltaX);
                rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, rectTransform.rect.height * m_AspectRatio);
                break;
            }
        case AspectMode.WidthControlsHeight:
            {
                m_Tracker.Add(this, rectTransform, DrivenTransformProperties.SizeDeltaY);
                rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, rectTransform.rect.width / m_AspectRatio);
                break;
            }
        case AspectMode.FitInParent:
        case AspectMode.EnvelopeParent:
            {
                m_Tracker.Add(this, rectTransform,
                              DrivenTransformProperties.Anchors |
                              DrivenTransformProperties.AnchoredPosition |
                              DrivenTransformProperties.SizeDeltaX |
                              DrivenTransformProperties.SizeDeltaY);

                rectTransform.anchorMin = Vector2.zero;
                rectTransform.anchorMax = Vector2.one;
                rectTransform.anchoredPosition = Vector2.zero;

                Vector2 sizeDelta = Vector2.zero;
                Vector2 parentSize = GetParentSize();
                // 按照寬高比計算父節點大小
                if ((parentSize.y * aspectRatio < parentSize.x) ^ (m_AspectMode == AspectMode.FitInParent))
                {
                    sizeDelta.y = GetSizeDeltaToProduceSize(parentSize.x / aspectRatio, 1);
                }
                else
                {
                    sizeDelta.x = GetSizeDeltaToProduceSize(parentSize.y * aspectRatio, 0);
                }
                rectTransform.sizeDelta = sizeDelta;

                break;
            }
    }
}

ContentSizeFitter是通過 SetDirty 呼叫的,同樣是在 OnEnable、OnRectTransformDimensionsChange、改變引數的時候,會設定 Dirty。

protected void SetDirty()
{
    if (!IsActive())
        return;
	// layout reduild
    LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
}

重構佈局,會呼叫 SetLayoutHorizontal、SetLayoutVertical,最終呼叫到了核心方法 HandleSelfFittingAlongAxis。

private void HandleSelfFittingAlongAxis(int axis)
{
    FitMode fitting = (axis == 0 ? horizontalFit : verticalFit);
    if (fitting == FitMode.Unconstrained)
    {
        // Keep a reference to the tracked transform, but don't control its properties:
        m_Tracker.Add(this, rectTransform, DrivenTransformProperties.None);
        return;
    }

    m_Tracker.Add(this, rectTransform, (axis == 0 ? DrivenTransformProperties.SizeDeltaX : DrivenTransformProperties.SizeDeltaY));

    // Set size to min or preferred size
    if (fitting == FitMode.MinSize)
        // getMinSize 是獲取 CalculateLayoutInputXxx 的值
        rectTransform.SetSizeWithCurrentAnchors((RectTransform.Axis)axis, LayoutUtility.GetMinSize(m_Rect, axis));
    else
        // GetPreferredSize 同上
        rectTransform.SetSizeWithCurrentAnchors((RectTransform.Axis)axis, LayoutUtility.GetPreferredSize(m_Rect, axis));
}

3. Utility 工具

3.1ObjectPool

物件池,由一個棧實現。

internal class ObjectPool<T> where T : new()
{
    private readonly Stack<T> m_Stack = new Stack<T>();
    private readonly UnityAction<T> m_ActionOnGet;
    private readonly UnityAction<T> m_ActionOnRelease;

    public int countAll { get; private set; }
    public int countActive { get { return countAll - countInactive; } }
    public int countInactive { get { return m_Stack.Count; } }

    public ObjectPool(UnityAction<T> actionOnGet, UnityAction<T> actionOnRelease)
    {
        m_ActionOnGet = actionOnGet;
        m_ActionOnRelease = actionOnRelease;
    }

    public T Get()
    {
        T element;
        // 池子為空
        if (m_Stack.Count == 0)
        {
            element = new T();
            countAll++;
        }
        else
        {
            // 從池子取出物件
            element = m_Stack.Pop();
        }
        if (m_ActionOnGet != null)
            m_ActionOnGet(element);
        return element;
    }

    public void Release(T element)
    {
        if (m_Stack.Count > 0 && ReferenceEquals(m_Stack.Peek(), element))
            Debug.LogError("Internal error. Trying to destroy object that is already released to pool.");
        if (m_ActionOnRelease != null)
            m_ActionOnRelease(element);
        // 放入池子
        m_Stack.Push(element);
    }
}

3.2 ListPool

在物件池 ObjectPooL 基礎上實現的一個靜態列表物件池。

internal static class ListPool<T>
{
    // Object pool to avoid allocations.
    private static readonly ObjectPool<List<T>> s_ListPool = new ObjectPool<List<T>>(null, Clear);
    static void Clear(List<T> l) { l.Clear(); }

    public static List<T> Get()
    {
        return s_ListPool.Get();
    }

    public static void Release(List<T> toRelease)
    {
        s_ListPool.Release(toRelease);
    }
}

3.3 VertexHelper

該類十分重要,是儲存用來生成 Mesh 網格需要的所有資料。該類利用了上面兩個類 ObjectPool 和 ListPool 來使得資料高效利用,本身不負責計算和生成 Mesh,僅僅是資料的儲存集合。

public class VertexHelper : IDisposable
{
    private List<Vector3> m_Positions;
    private List<Color32> m_Colors;
    private List<Vector2> m_Uv0S;
    private List<Vector2> m_Uv1S;
    private List<Vector2> m_Uv2S;
    private List<Vector2> m_Uv3S;
    private List<Vector3> m_Normals;
    private List<Vector4> m_Tangents;
    private List<int> m_Indices;
    
    private static readonly Vector4 s_DefaultTangent = new Vector4(1.0f, 0.0f, 0.0f, -1.0f);
    private static readonly Vector3 s_DefaultNormal = Vector3.back;

    private bool m_ListsInitalized = false;
    
    private void InitializeListIfRequired()
    {
        if (!m_ListsInitalized)
        {
            m_Positions = ListPool<Vector3>.Get();
            m_Colors = ListPool<Color32>.Get();
            m_Uv0S = ListPool<Vector2>.Get();
            m_Uv1S = ListPool<Vector2>.Get();
            m_Uv2S = ListPool<Vector2>.Get();
            m_Uv3S = ListPool<Vector2>.Get();
            m_Normals = ListPool<Vector3>.Get();
            m_Tangents = ListPool<Vector4>.Get();
            m_Indices = ListPool<int>.Get();
            m_ListsInitalized = true;
        }
    }
}

4. SpecializedCollections 特殊集合

4.1IndexedSet

由一個 List 和 Dictionary<T,int> 構成的一個索引集合。特點是用 Dictionary 加速 List 查詢,也能快速判重。

public bool AddUnique(T item)
{
    // 判斷新增重複
    if (m_Dictionary.ContainsKey(item))
        return false;

    m_List.Add(item);
    m_Dictionary.Add(item, m_List.Count - 1);

    return true;
}
public bool Contains(T item)
{
    return m_Dictionary.ContainsKey(item);
}
public bool Remove(T item)
{
    int index = -1;
    if (!m_Dictionary.TryGetValue(item, out index))
        return false;

    RemoveAt(index);
    return true;
}
public void RemoveAt(int index)
{
    T item = m_List[index];
    m_Dictionary.Remove(item);
    if (index == m_List.Count - 1)
        m_List.RemoveAt(index);
    else
    {
        // List 交換刪除位置和末位,然後刪除末位
        int replaceItemIndex = m_List.Count - 1;
        T replaceItem = m_List[replaceItemIndex];
        m_List[index] = replaceItem;
        m_Dictionary[replaceItem] = index;	// 修改 Dictionary 末位的 Index 值
        m_List.RemoveAt(replaceItemIndex);
    }
}

5. VertexModifies 頂點修改器

  • BaseMeshEffect:抽象基類,提供修改UI元素網格需要的變數和介面,關鍵介面 ModifyMesh,整合類通過該介面實現效果
  • PositionAsUV1:修改位置 UV
  • Shadow:增加陰影
  • Outline:增加包邊

5.1 PositionAsUV1

public override void ModifyMesh(VertexHelper vh)
{
    UIVertex vert = new UIVertex();
    for (int i = 0; i < vh.currentVertCount; i++)
    {
        vh.PopulateUIVertex(ref vert, i);
        // 根據座標點設定 uv1 座標
        vert.uv1 =  new Vector2(vert.position.x, vert.position.y);
        vh.SetUIVertex(vert, i);
    }
}

5.2 Shadow

public override void ModifyMesh(VertexHelper vh)
{
    if (!IsActive())
        return;

    var output = ListPool<UIVertex>.Get();
    vh.GetUIVertexStream(output);
	// 新增陰影
    ApplyShadow(output, effectColor, 0, output.Count, effectDistance.x, effectDistance.y);
    vh.Clear();
    vh.AddUIVertexTriangleStream(output);
    ListPool<UIVertex>.Release(output);
}
protected void ApplyShadow(List<UIVertex> verts, Color32 color, int start, int end, float x, float y)
{
    ApplyShadowZeroAlloc(verts, color, start, end, x, y);
}
protected void ApplyShadowZeroAlloc(List<UIVertex> verts, Color32 color, int start, int end, float x, float y)
{
    UIVertex vt;
	
    // 增加頂點容量
    var neededCapacity = verts.Count + end - start;
    if (verts.Capacity < neededCapacity)
        verts.Capacity = neededCapacity;

    for (int i = start; i < end; ++i)
    {
        vt = verts[i];
        verts.Add(vt);

        Vector3 v = vt.position;
        // 設定頂點偏移
        v.x += x;
        v.y += y;
        vt.position = v;
        var newColor = color;
        if (m_UseGraphicAlpha)
            newColor.a = (byte)((newColor.a * verts[i].color.a) / 255);
        vt.color = newColor;
        verts[i] = vt;
    }
}

5.3 Outline

public override void ModifyMesh(VertexHelper vh)
{
    if (!IsActive())
        return;

    var verts = ListPool<UIVertex>.Get();
    vh.GetUIVertexStream(verts);

    var neededCpacity = verts.Count * 5; // 增加四個頂點
    if (verts.Capacity < neededCpacity)
        verts.Capacity = neededCpacity;

    var start = 0;
    var end = verts.Count;
    // 對應圖 (x,y)
    ApplyShadowZeroAlloc(verts, effectColor, start, verts.Count, effectDistance.x, effectDistance.y);

    start = end;
    end = verts.Count;
    // 對應圖 (x,-y)
    ApplyShadowZeroAlloc(verts, effectColor, start, verts.Count, effectDistance.x, -effectDistance.y);

    start = end;
    end = verts.Count;
    // 對應圖 (-x,y)
    ApplyShadowZeroAlloc(verts, effectColor, start, verts.Count, -effectDistance.x, effectDistance.y);

    start = end;
    end = verts.Count;
    // 對應圖 (-x,-y)
    ApplyShadowZeroAlloc(verts, effectColor, start, verts.Count, -effectDistance.x, -effectDistance.y);

    vh.Clear();
    vh.AddUIVertexTriangleStream(verts);
    ListPool<UIVertex>.Release(verts);
}

6 .核心渲染

  • Graphic:圖形的基類,能夠通知元素重新佈局,重新構建材質球,重新構建網格。
  • MaskableGraphic:增加了被遮罩能力。
  • Image:UI 層級上的一個 Texture 元素。
  • RawImage:在 UI 上展示一個 Texture2D 圖片。會增加格外的 draw call,因此最好只用於背景或者臨時課件圖片。
  • Text:展示文字的圖形。

6.1 SetDirty 重構流程

關鍵在於 SetDirty 進行重新整理的流程。

public virtual void SetAllDirty()
{
    // Optimization: Graphic layout doesn't need recalculation if
    // the underlying Sprite is the same size with the same texture.
    // (e.g. Sprite sheet texture animation)

    if (m_SkipLayoutUpdate)
    {
        m_SkipLayoutUpdate = false;
    }
    else
    {
        SetLayoutDirty();
    }

    if (m_SkipMaterialUpdate)
    {
        m_SkipMaterialUpdate = false;
    }
    else
    {
        SetMaterialDirty();
    }

    SetVerticesDirty();
}

public virtual void SetLayoutDirty()
{
    if (!IsActive())
        return;
    
    LayoutRebuilder.MarkLayoutForRebuild(rectTransform);

    if (m_OnDirtyLayoutCallback != null)
        m_OnDirtyLayoutCallback();
}

public virtual void SetVerticesDirty()
{
    if (!IsActive())
        return;

    m_VertsDirty = true;
    CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);

    if (m_OnDirtyVertsCallback != null)
        m_OnDirtyVertsCallback();
}


public virtual void SetMaterialDirty()
{
    if (!IsActive())
        return;

    m_MaterialDirty = true;
    CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);

    if (m_OnDirtyMaterialCallback != null)
        m_OnDirtyMaterialCallback();
}

SetLayoutDirty 會通知 LayoutRebuilder 佈局管理類進行重新佈局, LayoutRebuilder.MarkLayoutForRebuild 最後會呼叫CanvasUpdateRegistry.TryRegisterCanvasElementForLayoutRebuild 加入重構隊伍。

CanvasUpdateRegistry 接到通知後不會立即重構,而是將需要重構元素(ICanvasElement)新增到佇列(IndexSet)中,等待下次重構。

因此最後三個 SetDirty 都會通知 CanvasUpdateRegistry,新增到對應的重構佇列中。SetLayoutDirty 會新增到佈局重構佇列 m_LayoutRebuildQueue,SetVerticesDirty 和 SetMaterialDirty 都會新增到圖形重構佇列 m_GraphicRebuildQueue 中。

CanvasUpdateRegistry 在 PerformUpdate 中處理新增到重構佇列中的元素。

private void PerformUpdate()
{
    UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout);
    CleanInvalidItems();

    m_PerformingLayoutUpdate = true;
	
    // 重構佈局
    m_LayoutRebuildQueue.Sort(s_SortLayoutFunction);
    for (int i = 0; i <= (int)CanvasUpdate.PostLayout; i++)
    {
        for (int j = 0; j < m_LayoutRebuildQueue.Count; j++)
        {
            var rebuild = instance.m_LayoutRebuildQueue[j];	// 實現 ICanvasElement 介面的 LayoutBuilder 元素
            try
            {
                if (ObjectValidForUpdate(rebuild))
                    rebuild.Rebuild((CanvasUpdate)i);		
            }
            catch (Exception e)
            {
                Debug.LogException(e, rebuild.transform);
            }
        }
    }
	// 通知佈局重構完成
    for (int i = 0; i < m_LayoutRebuildQueue.Count; ++i)
        m_LayoutRebuildQueue[i].LayoutComplete();

    instance.m_LayoutRebuildQueue.Clear();
    m_PerformingLayoutUpdate = false;
	
    // 剪裁
    // now layout is complete do culling...
    ClipperRegistry.instance.Cull();
	
    // 重構圖形
    m_PerformingGraphicUpdate = true;
    for (var i = (int)CanvasUpdate.PreRender; i < (int)CanvasUpdate.MaxUpdateValue; i++)
    {
        for (var k = 0; k < instance.m_GraphicRebuildQueue.Count; k++)
        {
            try
            {
                var element = instance.m_GraphicRebuildQueue[k]; // 實現 ICanvasElement 的 Graphic 元素
                if (ObjectValidForUpdate(element))
                    element.Rebuild((CanvasUpdate)i);	
            }
            catch (Exception e)
            {
                Debug.LogException(e, instance.m_GraphicRebuildQueue[k].transform);
            }
        }
    }
	
    // 通知圖形重構完成
    for (int i = 0; i < m_GraphicRebuildQueue.Count; ++i)
        m_GraphicRebuildQueue[i].GraphicUpdateComplete();

    instance.m_GraphicRebuildQueue.Clear();
    m_PerformingGraphicUpdate = false;
    UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Layout);
}

6.2 DoMeshGeneration 網格初始化

重構圖形,會呼叫到 Graphic 另一個重要方法 DoMeshGeneration。進行網格的重建,注意的是 Graphic 只負責重建網格,不負責渲染和合並。

private void DoMeshGeneration()
{
    if (rectTransform != null && rectTransform.rect.width >= 0 && rectTransform.rect.height >= 0)
		// 初始化網格頂點資訊(四個頂點構成兩個三角形)
        OnPopulateMesh(s_VertexHelper);
    else
        s_VertexHelper.Clear(); // clear the vertex helper so invalid graphics dont draw.
	
    // 查詢會修改頂點的元件,e.g.  上文的 Shadow、OutLine
    var components = ListPool<Component>.Get();
    GetComponents(typeof(IMeshModifier), components);

    for (var i = 0; i < components.Count; i++)
        ((IMeshModifier)components[i]).ModifyMesh(s_VertexHelper);

    ListPool<Component>.Release(components);
	
    // 填充網格, 並將網格提交到 CanvasRenderer
    s_VertexHelper.FillMesh(workerMesh);
    canvasRenderer.SetMesh(workerMesh);
}

元件中 Image、RawImage、Text 都重寫了 OnPopulateMesh 的方法。這些元件自己定義了不同網格樣式。可以結合 Wireframe 線框圖模式,檢視生成的 Mesh。

6.3 Mask

Mask 是通過著色器中模板緩衝(stencil buffer)實現遮罩的,UI 元件繼承了 MaskableGraphic,提供了被遮罩的能力(進行模板測試)。

stencil buffer 簡單來說就是 GPU 為每一個畫素提供了 1位元組(8bit) 的記憶體區域,多個 draw call 可以通過這個共享記憶體,來傳遞訊息,實現效果。

MaskableGraphic 實現了 IMaterialModifier 介面的 GetModifiedMaterial 方法,能夠修改材質。

public virtual Material GetModifiedMaterial(Material baseMaterial)
{
    var toUse = baseMaterial;

    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,  // Material baseMat
                    (1 << m_StencilValue) - 1, // 參考值
                    StencilOp.Keep, // 保持模板值不做修改
                    CompareFunction.Equal,  // 判斷相等
                    ColorWriteMask.All, // ColorMask
                    (1 << m_StencilValue) - 1,// Readmask
                    0);//  WriteMask
        StencilMaterial.Remove(m_MaskMaterial);
        m_MaskMaterial = maskMat;
        toUse = m_MaskMaterial;
    }
    return toUse;
}

Mask 也實現了相同介面。

public virtual Material GetModifiedMaterial(Material baseMaterial)
{
    if (!MaskEnabled())
        return baseMaterial;

    var rootSortCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
    // 獲取模板深度值 (小於8 因為 stencil buffer 就 8bit)
    var stencilDepth = MaskUtilities.GetStencilDepth(transform, rootSortCanvas);
    if (stencilDepth >= 8)
    {
        Debug.LogWarning("Attempting to use a stencil mask with depth > 8", gameObject);
        return baseMaterial;
    }

    int desiredStencilBit = 1 << stencilDepth;

    // if we are at the first level...
    // we want to destroy what is there
    // 第一層 比較方法就是 Always,總是指向
    if (desiredStencilBit == 1)
    {
        // Mask 自身使用 maskMaterial
        var maskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Replace, CompareFunction.Always, m_ShowMaskGraphic ? ColorWriteMask.All : 0);
        StencilMaterial.Remove(m_MaskMaterial);
        m_MaskMaterial = maskMaterial;
		
        // 非遮罩材質 unmaskMaterial 交給 CanvasRenderer
        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;
}

為了方便理解,參照該部落格,列出瞭如下表格。最後一層是 MaskableGraphic 進行的操作。

首層,則都寫入緩衝值,非首層,通過 ReadMask 讀取比當前層小的 快取值,找到相同,填入新的緩衝值(bit|bit-1),e.g. bit =3 填入值就是 1000|0111 =1111

MaskGraph 會進行相等測試,只有模板測試通過的,圖形才會顯示出來。

6.4 RectMask2D

RectMask2D 與 Mask 不一樣,是通過關聯物件的 RectTransform,直接計算出不需要裁剪的部分。

public virtual void PerformClipping()
{
    if (ReferenceEquals(Canvas, null))
    {
        return;
    }

    // if the parents are changed
    // or something similar we
    // do a recalculate here
    if (m_ShouldRecalculateClipRects)
    {
        // 獲取所有 RectMesh2D 遮罩範圍
        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);

    // If the mask is in ScreenSpaceOverlay/Camera render mode, its content is only rendered when its rect
    // overlaps that of the root canvas.
    RenderMode renderMode = Canvas.rootCanvas.renderMode;
    bool maskIsCulled =
        (renderMode == RenderMode.ScreenSpaceCamera || renderMode == RenderMode.ScreenSpaceOverlay) &&
        !clipRect.Overlaps(rootCanvasRect, true);

    if (maskIsCulled)
    {
        // Children are only displayed when inside the mask. If the mask is culled, then the children
        // inside the mask are also culled. In that situation, we pass an invalid rect to allow callees
        // to avoid some processing.
        clipRect = Rect.zero;
        validRect = false;
    }

    if (clipRect != m_LastClipRectCanvasSpace)
    {
        foreach (IClippable clipTarget in m_ClipTargets)
        {
            clipTarget.SetClipRect(clipRect, validRect);
        }

        foreach (MaskableGraphic maskableTarget in m_MaskableTargets)
        {
            maskableTarget.SetClipRect(clipRect, validRect);
            maskableTarget.Cull(clipRect, validRect);	// 對UI元素進行裁切
        }
    }
    else if (m_ForceClip)
    {
        foreach (IClippable clipTarget in m_ClipTargets)
        {
            clipTarget.SetClipRect(clipRect, validRect);
        }

        foreach (MaskableGraphic maskableTarget in m_MaskableTargets)
        {
            maskableTarget.SetClipRect(clipRect, validRect);

            if (maskableTarget.canvasRenderer.hasMoved)
                maskableTarget.Cull(clipRect, validRect);// 對UI元素進行裁切
        }
    }
    else
    {
        foreach (MaskableGraphic maskableTarget in m_MaskableTargets)
        {
            if (maskableTarget.canvasRenderer.hasMoved)
                maskableTarget.Cull(clipRect, validRect);// 對UI元素進行裁切
        }
    }

    m_LastClipRectCanvasSpace = clipRect;
    m_ForceClip = false;
}

五、參考

  1. 《Unity3D高階程式設計之進階主程》第四章,UI(四) - UGUI核心原始碼剖析
  2. Unity3D UGUI 原始碼學習