詳解Unity中Mask和RectMask2D組件的對比與測試
組件用法
Mask組件可以實現遮罩的效果,將一個圖像設為擁有mask組件圖像的子物體,最後就會隱藏掉子圖像和mask圖像不重合的部分。例如:
(藍色的圓形名為mask,數字圖片名為image)
在“mask”圖片上添加mask組件後的結果(可以選擇是否隱藏mask圖像):
RectMask2D的基本用法
RectMask2D的用法和mask大致相同,不過RectMask2D隻能裁剪一個矩形區域,同時RectMask2D可以選擇邊緣虛化
原理分析
Mask的原理分析
- Mask會賦予Image一個特殊的材質,這個材質會給Image的每個像素點進行標記,將標記結果存放在一個緩存內(這個緩存叫做 Stencil Buffer)
- 當子級UI進行渲染的時候會去檢查這個 Stencil Buffer內的標記,如果當前覆蓋的區域存在標記(即該區域在Image的覆蓋范圍內),進行渲染,否則不渲染
那麼,Stencil Buffer 究竟是什麼呢?
1 StencilBuffer
簡單來說,GPU為每個像素點分配一個稱之為StencilBuffer的1字節大小的內存區域,這個區域可以用於保存或丟棄像素的目的。
我們舉個簡單的例子來說明這個緩沖區的本質。
如上圖所示,我們的場景中有1個紅色圖片和1個綠色圖片,黑框范圍內是它們重疊部分。一幀渲染開始,首先綠色圖片將它覆蓋范圍的每個像素顏色“畫”在屏幕上,然後紅色圖片也將自己的顏色畫在屏幕上,就是圖中的效果瞭。
這種情況下,重疊區域內紅色完全覆蓋瞭綠色。接下來,我們為綠色圖片添加Mask組件。於是變成瞭這樣:
此時一幀渲染開始,首先綠色圖片將它覆蓋范圍都塗上綠色,同時將每個像素的stencil buffer值設置為1,此時屏幕的stencil buffer分佈如下:
然後輪到紅色圖片“繪畫”,它在塗上紅色前,會先取出這個點的stencil buffer值判斷,在黑框范圍內,這個值是1,於是繼續畫紅色;在黑框范圍外,這個值是0,於是不再畫紅色,最終達到瞭圖中的效果。
所以從本質上來講,stencil buffer是為瞭實現多個“繪畫者”之間互相通信而存在的。由於gpu是流水線作業,它們之間無法直接通信,所以通過這種共享數據區的方式來傳遞消息。
理解瞭stencil的原理,我們再來看下它的語法。在unity shader中定義的語法格式如下
(中括號內是可以修改的值,其餘都是關鍵字):
Stencil { Ref [_Stencil]//Ref表示要比較的值;0-255 Comp [_StencilComp]//Comp表示比較方法(等於/不等於/大於/小於等); Pass [_StencilOp]// Pass/Fail表示當比較通過/不通過時對stencil buffer做什麼操作 // Keep(保留) // Replace(替換) // Zero(置0) // IncrementSaturate(增加) // DecrementSaturate(減少) ReadMask [_StencilReadMask]//ReadMask/WriteMask表示取stencil buffer的值時用的mask(即可以忽略某些位); WriteMask [_StencilWriteMask] }
翻譯一下就是:將stencil buffer的值與ReadMask與運算,然後與Ref值進行Comp比較,結果為true時進行Pass操作,否則進行Fail操作,操作值寫入stencil buffer前先與WriteMask與運算。
2 mask的源碼實現
瞭解瞭stencil,我們再來看mask的源碼實現
由於裁切需要同時裁切圖片和文本,所以Image和Text都會派生自MaskableGraphic。
如果要讓Mask節點下的元素裁切,那麼它需要占一個DrawCall,因為這些元素需要一個新的Shader參數來渲染。
如下代碼所示,MaskableGraphic實現瞭IMaterialModifier接口, 而StencilMaterial.Add()就是設置Shader中的裁切參數。
MaskableGraphic.cs 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; } // 如果我們用瞭Mask,它會生成一個mask材質, 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); // WriteMas StencilMaterial.Remove(m_MaskMaterial); //並且更換新的材質 m_MaskMaterial = maskMat; toUse = m_MaskMaterial; } return toUse; }
Image對象在進行Rebuild()時,UpdateMaterial()方法中會獲取需要渲染的材質,並且判斷當前對象的組件是否有繼承IMaterialModifier接口,如果有那麼它就是綁定瞭Mask腳本,接著調用GetModifiedMaterial方法修改材質上Shader的參數。
Image.cs protected virtual void UpdateMaterial() { if (!IsActive()) return; //更新剛剛替換的新的模板緩沖的材質 canvasRenderer.materialCount = 1; canvasRenderer.SetMaterial(materialForRendering, 0); canvasRenderer.SetTexture(mainTexture); } public virtual Material materialForRendering { get { //遍歷UI中的每個Mask組件 var components = ListPool<Component>.Get(); GetComponents(typeof(IMaterialModifier), components); //並且更新每個Mask組件的模板緩沖材質 var currentMat = material; for (var i = 0; i < components.Count; i++) currentMat = (components[i] as IMaterialModifier).GetModifiedMaterial(currentMat); ListPool<Component>.Release(components); //返回新的材質,用於裁切 return currentMat; } }
因為模板緩沖可以提供模板的區域,也就是前面設置的圓形圖片,所以最終會將元素裁切到這個圓心圖片中。
Mask.cs /// Stencil calculation time! public virtual Material GetModifiedMaterial(Material baseMaterial) { if (!MaskEnabled()) return baseMaterial; var rootSortCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform); var stencilDepth = MaskUtilities.GetStencilDepth(transform, rootSortCanvas); // stencil隻支持最大深度為8的遮罩 if (stencilDepth >= 8) { Debug.LogError("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 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; }
Mask 組件調用瞭模板材質球構建瞭一個自己的材質球,因此它使用瞭實時渲染中的模板方法來裁切不需要顯示的部分,所有在 Mask 組件的子節點都會進行裁切。
我們可以說 Mask 是在 GPU 中做的裁切,使用的方法是著色器中的模板方法。
RectMask2D的原理分析
RectMask2D的工作流大致如下:
①C#層:找出父物體中所有RectMask2D覆蓋區域的交集(FindCullAndClipWorldRect)
②C#層:所有繼承MaskGraphic的子物體組件調用方法設置剪裁區域(SetClipRect)傳遞給Shader
③Shader層:接收到矩形區域_ClipRect,片元著色器中判斷像素是否在矩形區域內,不在則透明度設置為0(UnityGet2DClipping )
④Shader層:丟棄掉alpha小於0.001的元素(clip (color.a – 0.001))
CanvasUpdateRegistry.cs protected CanvasUpdateRegistry() { Canvas.willRenderCanvases += PerformUpdate; } private void PerformUpdate() { //...略 // 開始裁切Mask2D ClipperRegistry.instance.Cull(); //...略 } ClipperRegistry.cs public void Cull() { for (var i = 0; i < m_Clippers.Count; ++i) { m_Clippers[i].PerformClipping(); } }
RectMask2D會在OnEnable()方法中,將當前組件註冊ClipperRegistry.Register(this);
這樣在上面ClipperRegistry.instance.Cull();方法時就可以遍歷所有Mask2D組件並且調用它們的PerformClipping()方法瞭。
PerformClipping()方法,需要找到所有需要裁切的UI元素,因為Image和Text都繼承瞭IClippable接口,最終將調用Cull()進行裁切。
RectMask2D.cs protected override void OnEnable() { //註冊當前RectMask2D裁切對象,保證下次Rebuild時可進行裁切。 base.OnEnable(); m_ShouldRecalculateClipRects = true; ClipperRegistry.Register(this); MaskUtilities.Notify2DMaskStateChanged(this); } public virtual void PerformClipping() { //...略 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) //把裁切區域傳到每個UI元素的Shader中[劃重點!!!] 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; // 調用所有繼承IClippable的Cull方法 clipTarget.Cull(m_LastClipRectCanvasSpace, m_LastValidClipRect); } } MaskableGraphic.cs 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(); } }
性能區分
Mask組件需要依賴一個Image組件,裁剪區域就是Image的大小。
Mask會在首尾(首=Mask節點,尾=Mask節點下的孩子遍歷完後)drawcall,多個Mask間如果符合合批條件這兩個drawcall可以對應合批(mask1 的首 和 mask2 的首合;mask1 的尾 和 mask2 的尾合。首尾不能合)
Mask內的UI節點和非Mask外的UI節點不能合批,但多個Mask內的UI節點間如果符合合批條件,可以合批。
具體來說:
新建一個場景,默認drawcall是2個;
現在添加一個mask,
drawcall+3,Mask導致2個drawcall(第1個和第3個,一頭一尾),Mask下的子節點Image導致1個drawcall(中間的)
再看下RectMask2D的情況
隻有新增1個子節點Image的drawcall, 而RectMask2D不會導致drawcall.
而這時增加一個mask,不要重疊:
還是5個drawcall, 沒有變化.
Unity把2個Mask進行瞭網格合並, 3個drawcall, 分別為[2個Mask頭]、[2個Image]、[2個Mask尾].
這裡可以看出, Mask之間是可以進行合並的, 從而不額外增加drawcall
而如果放到一起,
**這是因為Unity的合批需要同渲染層級(depth), 同材質, 同圖集, 如果重疊瞭, depth就不同瞭, 6個drawcall分別為Mask頭、Mask的Image、Mask尾、Mask(1)頭、Mask(1)的Image、Mask(1)尾.
Mask小結:
1.多個Mask之間可以進行合批(頭和頭合批, 子對象和子對象合批, 尾和尾合批),需要同渲染層級(depth), 同材質, 同圖集.
2.Mask內外不能進行合批.
再試試RectMask2D
把RectMask2D復制一個出來, 然後把位置擺開.**
drawcall為4, 因為RectMask2D本身不會導致drawcall, 所以RectMask2D之間不能進行合批.
RectMask2D小結:
1.RectMask2D本身不產生drawcall.
2.不同RectMask2D的子對象不能合批.
對比測試
下面放上我在手機端做的一個簡單的對比測試:
可以大致看出,在圖像很大且cpu任務較重的的情況下,mask會對性能有明顯的影響,而在圖像數量較多時mask略好於RectMask2D
項目鏈接:https://git.woa.com/jnjnjnzhang/MaskVsRectmask2d
註:測試場景中自帶約60個batches。每個mask測試加入同樣的20個mask。圖像數量少的場景每個mask下掛一個圖像,面積大情況下mask大小不變圖像邊長放大1000倍,數量多情況下每個mask下掛同樣的100個圖像。瓶頸為drawcall時,每個物體僅有簡單的渲染,在物體上掛載瞭需要進行復雜運算的腳本。瓶頸為gpu時,去掉腳本,在場景中掛載瞭後處理渲染提高gpu負載。
參考文章
https://zhuanlan.zhihu.com/p/136505882
以上就是Unity中Mask和RectMask2D組件的對比與測試的詳細內容,更多關於Unity中Mask和RectMask2D的資料請關註WalkonNet其它相關文章!