Unity遮罩之Mask、RectMask2D與Sprite Mask適用場景分析

iwiniwin 發表於 2021-08-27
Unity

遮罩,顧名思義是一種可以掩蓋其它元素的控制元件。常用於修改其它元素的外觀,或限制元素的形狀。比如ScrollView或者圓頭像效果都有用到遮罩功能。本系列文章希望通過閱讀UGUI原始碼的方式,來探究遮罩的實現原理,以及通過Unity不同遮罩之間實現方式的對比,找到每一種遮罩的最佳使用場合。

本文是UGUI遮罩系列的第三篇,也是最後一篇。前兩篇分別是對Mask和RectMask2D的原始碼分析,詳細解讀了它們的原理與實現細節。這次的側重點是對Mask和RectMask2D做一個對比分析,同時總結一下在Mask和RectMask2D不起作用的場景下如何實現遮罩效果。本文大部分內容建立在讀者已瞭解Mask與RectMask2D原理的基礎之上,所以在閱讀本文前建議先看下前兩篇文章。

  1. 【UGUI原始碼分析】Unity遮罩之Mask詳細解讀
  2. 【UGUI原始碼分析】Unity遮罩之RectMask2D詳細解讀

本文所做的一些測試與驗證均基於Unity2019.4版本

Mask與RectMask2D對比

1. Mask遮罩的大小與形狀依賴於Graphic,而RectMask2D只需要依賴RectTransform

Mask是利用Graphic渲染時修改對應片元的模板值來確定遮罩的大小與形狀的,Graphic的形狀決定了Mask遮罩的形狀。因此缺少Graphic元件,Mask遮罩將會失效。當禁用了物件的Graphic元件,比如Image元件,Unity會有以下警告提示

Unity遮罩之Mask、RectMask2D與Sprite Mask適用場景分析

RectMask2D是利用自己的RectTransform計算出裁剪矩形,然後降低不在矩形內的片元透明度來實現遮罩效果,因此不需要依賴Graphic元件

2. Mask支援圓形或其他形狀遮罩, 而RectMask2D只支援矩形

Mask遮罩形狀可以更加多樣,由於Mask遮罩的形狀由Graphic決定,所以利用不同的Graphic可以實現不同形狀的遮罩

而RectMask2D通過RectTransform計算裁剪矩形的機制導致它只能支援矩形遮罩,僅在 2D 空間中有效,不能正確掩蓋不共面的元素

3. Mask會增加drawcall

除了繪製元素本身所需的1個drawcall以外,Mask還會額外增加2個drawcall。一個用來在繪製元素前修改模板緩衝的值,另一個用來在所有UI繪製完後將模板緩衝的值恢復原樣

舉個例子,如下所示的一個UI場景,畫布下一個帶有Mask元件的panel父節點,其下有一個子節點Image

Unity遮罩之Mask、RectMask2D與Sprite Mask適用場景分析

通過Unity的幀偵錯程式檢視渲染過程,共有3次drawcall

Unity遮罩之Mask、RectMask2D與Sprite Mask適用場景分析

3次drawcall的區別主要在於模板引數的不同。第一次是總是通過(Stencil Comp:Always)模板測試,並將模板值替換(Stencil Pass:Replace)為1(Stencil Ref:1)。第二次是用於繪製Image的。第三次是總是通過(Stencil Comp:Always)模板測試,並將模板值設定為0(Stencil Pass:Zero),即起到擦除模板值的作用。

Unity遮罩之Mask、RectMask2D與Sprite Mask適用場景分析

4. RectMask2D可能會破壞合批

有如下所示的一個測試場景,Panel1和Panel2都是隻掛有RectTransform元件的單純父節點,其下都有一個Image子節點,正常情況下應該可以合批,drawcall應為1

Unity遮罩之Mask、RectMask2D與Sprite Mask適用場景分析

通過幀偵錯程式檢視,確實如此,成功合批,drawcall是1

Unity遮罩之Mask、RectMask2D與Sprite Mask適用場景分析

此時給Panel1新增一個RectMask2D元件,實現遮罩效果。Panel2保持不變

Unity遮罩之Mask、RectMask2D與Sprite Mask適用場景分析

通過幀偵錯程式檢視,drawcall數量是2,原本的合批被破壞了。

Unity遮罩之Mask、RectMask2D與Sprite Mask適用場景分析

由此,網上查到的一些資料會得出“RectMask2D節點下的所有孩子都不能與外界UI節點合批且多個RectMask2D之間不能合批”的結論,實際上這是一種不嚴謹的說法,甚至是錯誤的。要搞清楚這個問題,需要先弄明白為什麼RectMask2D會破壞合批?

通過幀偵錯程式可以發現,是RectMask2D傳遞裁剪矩形時,修改了Shader的引數,導致不能合批。從下圖可以看到2次drawcall的區別就在於_ClipRect不同

Unity遮罩之Mask、RectMask2D與Sprite Mask適用場景分析

Unity遮罩之Mask、RectMask2D與Sprite Mask適用場景分析

既然是裁剪矩形引數不同導致不能合批,那如果將兩個裁剪矩形引數設定為一致是不是就能合批了呢?動手驗證一下,給Panel1和Panel2都新增上RectMask2D元件,同時將它們的RectTransform引數設定為完全一致(這樣可以保證裁剪矩形引數相同),然後把Panel1的子節點Image往左移,Panel2的子節點Image往右移,讓它們都能顯示出來。最終效果如下圖所示

Unity遮罩之Mask、RectMask2D與Sprite Mask適用場景分析

再次測試後可以看到drawcall只有1次了,_ClipRect是相同的值。因此可以得出結論,RectMask2D確實由於裁剪矩形引數的設定會破壞合批,但不是一定的。在滿足條件時,RectMask2D節點下的孩子也能與外界UI節點合批,多個RectMask2D之間也是能合批的。

Unity遮罩之Mask、RectMask2D與Sprite Mask適用場景分析

5. Mask與RectMask2D用哪個?

Mask的實現利用了模板緩衝區,會增加2個drawcall,效能會受到一定影響。簡單的UGUI介面,還是建議使用RectMask2D,相對來說效能更強,也無需額外的繪製呼叫。但由於RectMask2D也有可能破壞合批,在複雜的情況下,並沒有確切的結論來判斷哪個更優,只能利用工具實際測試找到最優者,具體問題具體分析才是正確做法。當然,諸如圓形遮罩等一些RectMask2D無法勝任的場景,還是要使用Mask

粒子系統實現遮罩效果

遊戲的UI介面也經常會新增粒子效果,有時也會需要對粒子新增遮罩。Mask和RectMask2D只適用於UGUI,對粒子系統無法生效。此時可以使用SpriteMask。SpriteMask的原理與Mask相同,都是基於模板測試實現。

Unity遮罩之Mask、RectMask2D與Sprite Mask適用場景分析

粒子系統的Renderer模組有對應的Mask屬性設定,可以調整粒子在精靈遮罩外部和內部的可見性

Unity遮罩之Mask、RectMask2D與Sprite Mask適用場景分析

MeshRenderer實現遮罩效果

UI介面新增的一些特效也有可能是MeshRenderer實現的,例如利用Shader製作的頂點動畫。但MeshRenderer沒有提供Mask相關設定,無法使用遮罩。好在基於模板測試實現遮罩的原理都是相同的,可以自己動手修改MeshRenderer使用的材質,在Shader中新增ShaderLab模板配置來使用模板測試

需要新增到Shader中的程式碼如下所示

Properties
{
    _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
}

Stencil
{
    Ref [_Stencil]
    Comp [_StencilComp]
    Pass [_StencilOp]
    ReadMask [_StencilReadMask]
    WriteMask [_StencilWriteMask]
}

新增完成後,材質介面會多出模板相關的配置,如下所示

Unity遮罩之Mask、RectMask2D與Sprite Mask適用場景分析

再配合SpriteMask,修改對應的模板引數,就可以模擬遮罩效果了。例如

  • Stencil Comparison設定為3,就相當於"Visible Inside Mask"
  • Stencil Comparison設定為6,就相當於"Visible Outside Mask"
  • Stencil Comparison設定為8,就相當於"No Masking"

參考