詳解Unity中Mask和RectMask2D組件的對(duì)比與測(cè)試
組件用法
Mask組件可以實(shí)現(xiàn)遮罩的效果,將一個(gè)圖像設(shè)為擁有mask組件圖像的子物體,最后就會(huì)隱藏掉子圖像和mask圖像不重合的部分。例如:


(藍(lán)色的圓形名為mask,數(shù)字圖片名為image)
在“mask”圖片上添加mask組件后的結(jié)果(可以選擇是否隱藏mask圖像):



RectMask2D的基本用法
RectMask2D的用法和mask大致相同,不過(guò)RectMask2D只能裁剪一個(gè)矩形區(qū)域,同時(shí)RectMask2D可以選擇邊緣虛化



原理分析
Mask的原理分析
- Mask會(huì)賦予Image一個(gè)特殊的材質(zhì),這個(gè)材質(zhì)會(huì)給Image的每個(gè)像素點(diǎn)進(jìn)行標(biāo)記,將標(biāo)記結(jié)果存放在一個(gè)緩存內(nèi)(這個(gè)緩存叫做 Stencil Buffer)
- 當(dāng)子級(jí)UI進(jìn)行渲染的時(shí)候會(huì)去檢查這個(gè) Stencil Buffer內(nèi)的標(biāo)記,如果當(dāng)前覆蓋的區(qū)域存在標(biāo)記(即該區(qū)域在Image的覆蓋范圍內(nèi)),進(jìn)行渲染,否則不渲染
那么,Stencil Buffer 究竟是什么呢?
1 StencilBuffer
簡(jiǎn)單來(lái)說(shuō),GPU為每個(gè)像素點(diǎn)分配一個(gè)稱之為StencilBuffer的1字節(jié)大小的內(nèi)存區(qū)域,這個(gè)區(qū)域可以用于保存或丟棄像素的目的。
我們舉個(gè)簡(jiǎn)單的例子來(lái)說(shuō)明這個(gè)緩沖區(qū)的本質(zhì)。

如上圖所示,我們的場(chǎng)景中有1個(gè)紅色圖片和1個(gè)綠色圖片,黑框范圍內(nèi)是它們重疊部分。一幀渲染開始,首先綠色圖片將它覆蓋范圍的每個(gè)像素顏色“畫”在屏幕上,然后紅色圖片也將自己的顏色畫在屏幕上,就是圖中的效果了。
這種情況下,重疊區(qū)域內(nèi)紅色完全覆蓋了綠色。接下來(lái),我們?yōu)榫G色圖片添加Mask組件。于是變成了這樣:

此時(shí)一幀渲染開始,首先綠色圖片將它覆蓋范圍都涂上綠色,同時(shí)將每個(gè)像素的stencil buffer值設(shè)置為1,此時(shí)屏幕的stencil buffer分布如下:

然后輪到紅色圖片“繪畫”,它在涂上紅色前,會(huì)先取出這個(gè)點(diǎn)的stencil buffer值判斷,在黑框范圍內(nèi),這個(gè)值是1,于是繼續(xù)畫紅色;在黑框范圍外,這個(gè)值是0,于是不再畫紅色,最終達(dá)到了圖中的效果。
所以從本質(zhì)上來(lái)講,stencil buffer是為了實(shí)現(xiàn)多個(gè)“繪畫者”之間互相通信而存在的。由于gpu是流水線作業(yè),它們之間無(wú)法直接通信,所以通過(guò)這種共享數(shù)據(jù)區(qū)的方式來(lái)傳遞消息。
理解了stencil的原理,我們?cè)賮?lái)看下它的語(yǔ)法。在unity shader中定義的語(yǔ)法格式如下
(中括號(hào)內(nèi)是可以修改的值,其余都是關(guān)鍵字):
Stencil
{
Ref [_Stencil]//Ref表示要比較的值;0-255
Comp [_StencilComp]//Comp表示比較方法(等于/不等于/大于/小于等);
Pass [_StencilOp]// Pass/Fail表示當(dāng)比較通過(guò)/不通過(guò)時(shí)對(duì)stencil buffer做什么操作
// Keep(保留)
// Replace(替換)
// Zero(置0)
// IncrementSaturate(增加)
// DecrementSaturate(減少)
ReadMask [_StencilReadMask]//ReadMask/WriteMask表示取stencil buffer的值時(shí)用的mask(即可以忽略某些位);
WriteMask [_StencilWriteMask]
}
翻譯一下就是:將stencil buffer的值與ReadMask與運(yùn)算,然后與Ref值進(jìn)行Comp比較,結(jié)果為true時(shí)進(jìn)行Pass操作,否則進(jìn)行Fail操作,操作值寫入stencil buffer前先與WriteMask與運(yùn)算。
2 mask的源碼實(shí)現(xiàn)
了解了stencil,我們?cè)賮?lái)看mask的源碼實(shí)現(xiàn)
由于裁切需要同時(shí)裁切圖片和文本,所以Image和Text都會(huì)派生自MaskableGraphic。
如果要讓Mask節(jié)點(diǎn)下的元素裁切,那么它需要占一個(gè)DrawCall,因?yàn)檫@些元素需要一個(gè)新的Shader參數(shù)來(lái)渲染。
如下代碼所示,MaskableGraphic實(shí)現(xiàn)了IMaterialModifier接口, 而StencilMaterial.Add()就是設(shè)置Shader中的裁切參數(shù)。
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,它會(huì)生成一個(gè)mask材質(zhì),
Mask maskComponent = GetComponent<Mask>();
if (m_StencilValue > 0 && (maskComponent == null || !maskComponent.IsActive()))
{
//設(shè)置模板緩沖值,并且設(shè)置在該區(qū)域內(nèi)的顯示,不在的裁切掉
var maskMat = StencilMaterial.Add(toUse, // Material baseMat
(1 << m_StencilValue) - 1, // 參考值
StencilOp.Keep, // 不修改模板緩存
CompareFunction.Equal, // 相等通過(guò)測(cè)試
ColorWriteMask.All, // ColorMask
(1 << m_StencilValue) - 1, // Readmask
0); // WriteMas
StencilMaterial.Remove(m_MaskMaterial);
//并且更換新的材質(zhì)
m_MaskMaterial = maskMat;
toUse = m_MaskMaterial;
}
return toUse;
}
Image對(duì)象在進(jìn)行Rebuild()時(shí),UpdateMaterial()方法中會(huì)獲取需要渲染的材質(zhì),并且判斷當(dāng)前對(duì)象的組件是否有繼承IMaterialModifier接口,如果有那么它就是綁定了Mask腳本,接著調(diào)用GetModifiedMaterial方法修改材質(zhì)上Shader的參數(shù)。
Image.cs
protected virtual void UpdateMaterial()
{
if (!IsActive())
return;
//更新剛剛替換的新的模板緩沖的材質(zhì)
canvasRenderer.materialCount = 1;
canvasRenderer.SetMaterial(materialForRendering, 0);
canvasRenderer.SetTexture(mainTexture);
}
public virtual Material materialForRendering
{
get
{
//遍歷UI中的每個(gè)Mask組件
var components = ListPool<Component>.Get();
GetComponents(typeof(IMaterialModifier), components);
//并且更新每個(gè)Mask組件的模板緩沖材質(zhì)
var currentMat = material;
for (var i = 0; i < components.Count; i++)
currentMat = (components[i] as IMaterialModifier).GetModifiedMaterial(currentMat);
ListPool<Component>.Release(components);
//返回新的材質(zhì),用于裁切
return currentMat;
}
}
因?yàn)槟0寰彌_可以提供模板的區(qū)域,也就是前面設(shè)置的圓形圖片,所以最終會(huì)將元素裁切到這個(gè)圓心圖片中。
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 組件調(diào)用了模板材質(zhì)球構(gòu)建了一個(gè)自己的材質(zhì)球,因此它使用了實(shí)時(shí)渲染中的模板方法來(lái)裁切不需要顯示的部分,所有在 Mask 組件的子節(jié)點(diǎn)都會(huì)進(jìn)行裁切。
我們可以說(shuō) Mask 是在 GPU 中做的裁切,使用的方法是著色器中的模板方法。
RectMask2D的原理分析
RectMask2D的工作流大致如下:
①C#層:找出父物體中所有RectMask2D覆蓋區(qū)域的交集(FindCullAndClipWorldRect)
②C#層:所有繼承MaskGraphic的子物體組件調(diào)用方法設(shè)置剪裁區(qū)域(SetClipRect)傳遞給Shader
③Shader層:接收到矩形區(qū)域_ClipRect,片元著色器中判斷像素是否在矩形區(qū)域內(nèi),不在則透明度設(shè)置為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會(huì)在OnEnable()方法中,將當(dāng)前組件注冊(cè)ClipperRegistry.Register(this);
這樣在上面ClipperRegistry.instance.Cull();方法時(shí)就可以遍歷所有Mask2D組件并且調(diào)用它們的PerformClipping()方法了。
PerformClipping()方法,需要找到所有需要裁切的UI元素,因?yàn)镮mage和Text都繼承了IClippable接口,最終將調(diào)用Cull()進(jìn)行裁切。
RectMask2D.cs
protected override void OnEnable()
{
//注冊(cè)當(dāng)前RectMask2D裁切對(duì)象,保證下次Rebuild時(shí)可進(jìn)行裁切。
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)
//把裁切區(qū)域傳到每個(gè)UI元素的Shader中[劃重點(diǎn)?。?!]
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;
// 調(diào)用所有繼承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();
}
}
性能區(qū)分
Mask組件需要依賴一個(gè)Image組件,裁剪區(qū)域就是Image的大小。
Mask會(huì)在首尾(首=Mask節(jié)點(diǎn),尾=Mask節(jié)點(diǎn)下的孩子遍歷完后)drawcall,多個(gè)Mask間如果符合合批條件這兩個(gè)drawcall可以對(duì)應(yīng)合批(mask1 的首 和 mask2 的首合;mask1 的尾 和 mask2 的尾合。首尾不能合)
Mask內(nèi)的UI節(jié)點(diǎn)和非Mask外的UI節(jié)點(diǎn)不能合批,但多個(gè)Mask內(nèi)的UI節(jié)點(diǎn)間如果符合合批條件,可以合批。
具體來(lái)說(shuō):
新建一個(gè)場(chǎng)景,默認(rèn)drawcall是2個(gè);
現(xiàn)在添加一個(gè)mask,

drawcall+3,Mask導(dǎo)致2個(gè)drawcall(第1個(gè)和第3個(gè),一頭一尾),Mask下的子節(jié)點(diǎn)Image導(dǎo)致1個(gè)drawcall(中間的)
再看下RectMask2D的情況

只有新增1個(gè)子節(jié)點(diǎn)Image的drawcall, 而RectMask2D不會(huì)導(dǎo)致drawcall.
而這時(shí)增加一個(gè)mask,不要重疊:

還是5個(gè)drawcall, 沒(méi)有變化.
Unity把2個(gè)Mask進(jìn)行了網(wǎng)格合并, 3個(gè)drawcall, 分別為[2個(gè)Mask頭]、[2個(gè)Image]、[2個(gè)Mask尾].
這里可以看出, Mask之間是可以進(jìn)行合并的, 從而不額外增加drawcall
而如果放到一起,

**這是因?yàn)閁nity的合批需要同渲染層級(jí)(depth), 同材質(zhì), 同圖集, 如果重疊了, depth就不同了, 6個(gè)drawcall分別為Mask頭、Mask的Image、Mask尾、Mask(1)頭、Mask(1)的Image、Mask(1)尾.
Mask小結(jié):
1.多個(gè)Mask之間可以進(jìn)行合批(頭和頭合批, 子對(duì)象和子對(duì)象合批, 尾和尾合批),需要同渲染層級(jí)(depth), 同材質(zhì), 同圖集.
2.Mask內(nèi)外不能進(jìn)行合批.
再試試RectMask2D
把RectMask2D復(fù)制一個(gè)出來(lái), 然后把位置擺開.**

drawcall為4, 因?yàn)镽ectMask2D本身不會(huì)導(dǎo)致drawcall, 所以RectMask2D之間不能進(jìn)行合批.
RectMask2D小結(jié):
1.RectMask2D本身不產(chǎn)生drawcall.
2.不同RectMask2D的子對(duì)象不能合批.
對(duì)比測(cè)試
下面放上我在手機(jī)端做的一個(gè)簡(jiǎn)單的對(duì)比測(cè)試:

可以大致看出,在圖像很大且cpu任務(wù)較重的的情況下,mask會(huì)對(duì)性能有明顯的影響,而在圖像數(shù)量較多時(shí)mask略好于RectMask2D
項(xiàng)目鏈接:https://git.woa.com/jnjnjnzhang/MaskVsRectmask2d
注:測(cè)試場(chǎng)景中自帶約60個(gè)batches。每個(gè)mask測(cè)試加入同樣的20個(gè)mask。圖像數(shù)量少的場(chǎng)景每個(gè)mask下掛一個(gè)圖像,面積大情況下mask大小不變圖像邊長(zhǎng)放大1000倍,數(shù)量多情況下每個(gè)mask下掛同樣的100個(gè)圖像。瓶頸為drawcall時(shí),每個(gè)物體僅有簡(jiǎn)單的渲染,在物體上掛載了需要進(jìn)行復(fù)雜運(yùn)算的腳本。瓶頸為gpu時(shí),去掉腳本,在場(chǎng)景中掛載了后處理渲染提高gpu負(fù)載。
參考文章
https://zhuanlan.zhihu.com/p/136505882
以上就是Unity中Mask和RectMask2D組件的對(duì)比與測(cè)試的詳細(xì)內(nèi)容,更多關(guān)于Unity中Mask和RectMask2D的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
用Linq從一個(gè)集合選取幾列得到一個(gè)新的集合(可改列名)
這篇文章主要介紹了用Linq從一個(gè)集合選取幾列得到一個(gè)新的集合(可改列名),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12
C#基于Socket的網(wǎng)絡(luò)通信類你了解嗎
這篇文章主要為大家詳細(xì)介紹了C#基于Socket的網(wǎng)絡(luò)通信類,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來(lái)幫助2022-03-03
C# .net core HttpClientFactory用法及說(shuō)明
C#串口編程System.IO.Ports.SerialPort類
C#實(shí)現(xiàn)計(jì)算一個(gè)點(diǎn)圍繞另一個(gè)點(diǎn)旋轉(zhuǎn)指定弧度后坐標(biāo)值的方法

