在上一篇博文"扔掉遮罩群凶,更好的圓形Image組件"中,筆者改變Image的頂點(diǎn)數(shù)據(jù)哄辣,使得Image呈圓形顯示请梢,避免了Mask的使用,從而節(jié)省Drawcall消耗力穗,提高渲染效率了毅弧。這也啟發(fā)了筆者,有沒有可能通過同樣原理實(shí)現(xiàn)Mask当窗,做到在某些需要顯示特定形狀I(lǐng)con的場(chǎng)景下够坐,替代Unity原生Mask,且能保有節(jié)省Drawcall崖面,減少渲染像素點(diǎn)咆霜,實(shí)現(xiàn)精確點(diǎn)擊等優(yōu)點(diǎn)?經(jīng)過一番折騰嘶朱,就有了MeshMask組件蛾坯。
組件效果##
可以看到無(wú)論Mask形狀是凸邊形還是復(fù)雜的凹邊形,都能準(zhǔn)確地將Mask形狀數(shù)據(jù)序列化成頂點(diǎn)疏遏,面片數(shù)據(jù)脉课,
提供給需要Mask的圖片修改渲染頂點(diǎn),達(dá)到遮罩效果财异。組件用法類似于Unity Mask倘零,且效率優(yōu)于Unity Mask。插件已上傳至Github[點(diǎn)擊下載]戳寸, 歡迎試用~
效率對(duì)比##
從上面三張圖可以看到MeshMask相比Unity的Mask袖瞻,在減少Drawcall消耗拆吆、Overdraw消耗等兩方面都是完勝的聋迎。
Drawcall消耗#####
這10個(gè)icon都打包在同一圖集的,使用Unity Mask枣耀,沒辦法享受圖層合并霉晕,消耗了15個(gè)Drawcall;使用MeshMask的情況下,看截圖里Batches為2牺堰,除去攝像機(jī)占用的1個(gè)Batch拄轻,10個(gè)icon僅占用1個(gè)Batch,即1個(gè)Drawcall伟葫。在Drawcall資源如此昂貴的情況下(一般機(jī)器都會(huì)要求Drawcall在200以下)恨搓,這種性能節(jié)省效果非常顯著。
Overdraw消耗#####
而看圖三的Overdraw扒俯,使用Unity Mask的紅框部分奶卓,被Mask的圖片全部繪制一次,Unity Mask再做像素剔除撼玄,被Mask的部分又繪制了一次夺姑,總共需要繪制兩次,且有一次是繪制了完全用不到的區(qū)域掌猛。使用MeshMask的藍(lán)框部分盏浙,因?yàn)槭强扛淖冺旤c(diǎn)繪制出來(lái)的icon,因此僅有被Mask部分被繪制了一次荔茬。
面片消耗#####
當(dāng)然废膘,使用MeshMask的Image需要消耗比普通Image多一些的頂點(diǎn)和面片,觀察Stats面板慕蔚,使用MeshMsk的10個(gè)icon多占用1.3K的頂點(diǎn)和面片丐黄,即1個(gè)icon占用130個(gè)頂點(diǎn),面片孔飒。然而GPU渲染頂點(diǎn)灌闺,面片的效率非常高(市面手機(jī)GPU渲染多邊形數(shù)基本上2000-10000+萬(wàn)多邊形/每秒以上),這點(diǎn)消耗跟Drawcall比起來(lái)就微不足道了坏瞄。
小結(jié)#####
在渲染上桂对,GPU、CPU兩者的性能瓶頸往往是CPU鸠匀;GPU的性能瓶頸往往是像素點(diǎn)填充率(Overdraw導(dǎo)致)蕉斜,CPU的性能瓶頸往往是Drawcall。所以缀棍,渲染性能排查宅此,幾項(xiàng)指標(biāo)關(guān)注優(yōu)先級(jí)應(yīng)該是:Drawcall > Overdraw > 面片
組件使用##
插件里有MeshMask、MeshImage睦柴、MeshButton三個(gè)UI組件
MeshMask組件作用類似Unity Mask诽凌,依賴了Image及PolygonCollider2D組件,帶有[根據(jù)Image組件生成Mask]坦敌、[根據(jù)Collider組件生成Mask]兩個(gè)菜單項(xiàng),支持兩種方式生成Mask數(shù)據(jù)。
MeshImage狱窘、MeshButton組件掛在需要被遮罩的GameObject上杜顺,設(shè)置好MeshMask對(duì)象,就能獲得數(shù)據(jù)蘸炸,實(shí)現(xiàn)遮罩或者精確點(diǎn)擊躬络。
組件實(shí)現(xiàn)##
不同于CircleImage,只需要簡(jiǎn)單的對(duì)圓形進(jìn)行頂點(diǎn)搭儒,面片計(jì)算穷当;MeshMask要考慮幾個(gè)點(diǎn):
- 需要能對(duì)所有可能的圖形進(jìn)行頂點(diǎn),面片計(jì)算淹禾。
- 考慮頂點(diǎn)馁菜,面片計(jì)算需要讀取Image,且有一定性能開銷铃岔,所以不能在Run-time中實(shí)時(shí)計(jì)算數(shù)據(jù)蒋困,需要預(yù)先計(jì)算好vertices,triangle數(shù)據(jù)葱峡,并序列化存放在GameObject中,運(yùn)行時(shí)讀取。
- 保證MeshMask靈活性打洼,除了根據(jù)Image進(jìn)行頂點(diǎn),面片計(jì)算难述,希望像PS一樣妻往,提供路徑工具,讓開發(fā)可以可視化地新增载碌、修改Mask形狀猜嘱。
- 對(duì)所有圖形支持像素級(jí)點(diǎn)擊判斷
其中做頂點(diǎn),面片計(jì)算這一步比較麻煩恐仑,涉及以下幾個(gè)技術(shù)點(diǎn):
邊緣檢測(cè)####
邊緣檢測(cè)算法算是圖形學(xué)應(yīng)用最廣泛最基礎(chǔ)的算法了泉坐,主要原理是濾波器對(duì)圖形進(jìn)行濾波從而得到梯度圖像,通過判斷梯度圖像的某像素點(diǎn)灰度值是否超過閾值裳仆,就能判斷該點(diǎn)是否為邊緣點(diǎn)腕让。筆者采用了簡(jiǎn)單的Sobel算子邊緣檢測(cè)算法。
這里拿米老鼠圖來(lái)做示例圖,看看Sobel邊緣檢測(cè)的效果静袖。
可以看到算法效果不錯(cuò)觉鼻,但我們并不需要這么多邊緣“信息”,只需要最外圍的邊緣“信息”队橙。因此將非透明區(qū)域都填充成統(tǒng)一的顏色坠陈,再做邊緣檢測(cè)萨惑。
離散化####
獲得了外圍邊緣信息后,下一步需要做離散化:剔除冗余信息仇矾,并將邊緣信息以有序集合的形式表示庸蔼。這個(gè)有序集合,就是渲染底層所需要的頂點(diǎn)數(shù)據(jù)贮匕。
冗余頂點(diǎn):對(duì)于邊緣的直線姐仅,除直線首尾兩點(diǎn)外,其他點(diǎn)都是冗余可剔除的刻盐。
有序集合:集合點(diǎn)依次連接起來(lái)掏膏,就如同用筆按逆時(shí)針/順時(shí)針方向畫出來(lái)的邊緣圖形。
筆者挑選了邊緣點(diǎn)集中x最小的點(diǎn)作為起始點(diǎn)敦锌,以順時(shí)針順序查找鄰接點(diǎn)的方法來(lái)計(jì)算有序頂點(diǎn)集馒疹。
算法步驟:
- 選擇邊緣點(diǎn)集x最小的點(diǎn)為起始點(diǎn),當(dāng)前點(diǎn)
- 查找當(dāng)前點(diǎn)周邊8個(gè)像素點(diǎn)是否有邊緣點(diǎn)供屉,如都沒有就繼續(xù)向外圍一圈行冰,直到找到邊緣點(diǎn)。
- 當(dāng)找到多個(gè)邊緣點(diǎn)情況下伶丐,比較當(dāng)前點(diǎn)與各邊緣點(diǎn)所呈夾角悼做,選夾角最小的邊緣點(diǎn)作為鄰接點(diǎn)。
- 若鄰接點(diǎn)即為起始點(diǎn)哗魂,則算法結(jié)束肛走,否則繼續(xù)
- 判斷鄰接點(diǎn)與有序頂點(diǎn)集最后一個(gè)點(diǎn)是否共邊,若共邊則刪除最后一個(gè)點(diǎn)
- 將鄰接點(diǎn)加入有序頂點(diǎn)集
- 設(shè)置鄰接點(diǎn)為當(dāng)前點(diǎn)录别,重復(fù)步驟2
三角化####
三角化(Triangulation)也是圖形學(xué)應(yīng)用較多的算法了组题,特別是在3D建模葫男、游戲領(lǐng)域。三角化是指從一組已知點(diǎn)集中崔列,構(gòu)建出三角形網(wǎng)格梢褐。隨著構(gòu)建條件不同,三角化算法也不同赵讯。像最近LowPoly繪畫風(fēng)格比較熱門盈咳,一些濾鏡軟件會(huì)支持LowPoly轉(zhuǎn)換。軟件在將一張普通圖像轉(zhuǎn)換位LowPoly圖像的過程中边翼,除了一樣要做邊緣檢測(cè)鱼响,離散化外,在三角化這一步组底,需要生成顯示質(zhì)量較高的三角形丈积,不能有過于狹長(zhǎng)的三角形筐骇,就需要用Delaunay算法。在我們這個(gè)場(chǎng)景下桶癣,對(duì)生成的三角形并沒有特殊要求拥褂,不需要用上復(fù)雜的Delaunay算法娘锁,Unity3d wiki社區(qū)上提供了一個(gè)簡(jiǎn)單的三角化算法牙寞,剛好適用。
算法原理
從點(diǎn)集中隨機(jī)挑選三點(diǎn)組成三角形莫秆,然后遍歷其他點(diǎn)间雀,看是否有點(diǎn)落在三角形內(nèi),如果三角形內(nèi)無(wú)點(diǎn)則為合格三角形镊屎。循環(huán)此過程直到所有點(diǎn)都被處理惹挟。
可視化編輯####
經(jīng)過前面處理,我們已經(jīng)拿到了頂點(diǎn)數(shù)據(jù)缝驳、面片數(shù)據(jù)连锯。筆者希望組件能將這些頂點(diǎn)數(shù)據(jù)可視化,以便讓使用者直觀了解處理結(jié)果用狱。Unity自帶的PolygonCollider2D組件运怖,正好適用。
public sealed class PolygonCollider2D : Collider2D
{
....
public void SetPath(int index, Vector2[] points);
}
通過SetPath接口將頂點(diǎn)數(shù)據(jù)傳入PolygonCollider2D 組件夏伊,PolygonCollider2D完美地生成米老鼠的路徑摇展。在一開始實(shí)驗(yàn)中,筆者驚奇地發(fā)現(xiàn)組件竟然也對(duì)頂點(diǎn)做了三角化處理溺忧。遺憾地是咏连,組件并沒有提供接口獲取三角化結(jié)果,Unity社區(qū)的技術(shù)人員也承認(rèn)此點(diǎn)鲁森,說(shuō)Unity的未來(lái)版本可能會(huì)考慮暴露此接口祟滴,并建議自己做三角化處理,就是前面所說(shuō)的算法(汗.. = . = ||)歌溉。通過下圖比較垄懂,可以看到組件跟算法的三角化結(jié)果還是有所不同的。
利用PolygonCollider2D組件除了讓我們可以看到頂點(diǎn)結(jié)果研底,還可以通過Inspector上的[Edit Collider]按鈕微調(diào)埠偿,頂點(diǎn)的位置,做出更理想的Mask效果榜晦。
甚至冠蒋,我們可以直接利用PolygonCollider2D組件,從無(wú)到有地編輯Mask形狀后乾胶,再三角化處理獲得面片數(shù)據(jù)抖剿。
渲染####
已經(jīng)有了頂點(diǎn)數(shù)據(jù)朽寞,面片數(shù)據(jù),終于到了最后的渲染步驟斩郎。筆者利用MeshMask組件存放這些數(shù)據(jù)脑融,并不直接渲染MeshMask,而是在MeshMask子節(jié)點(diǎn)下添加MeshImage組件缩宜,進(jìn)行修改頂點(diǎn)渲染肘迎。
在5.3版本里,Unity提供了BaseMeshEffect類锻煌,是Unity提供給開發(fā)者用于給Graphic進(jìn)行二次修改繪制的類妓布,我們可以在ModifyMesh方法中修改VertexHelper攜帶的頂點(diǎn),面片宋梧,uv等數(shù)據(jù)來(lái)改變渲染匣沼。(在5.3之前的版本,對(duì)應(yīng)的類和接口是BaseVertexEffect捂龄、ModifyVertices)
MeshImage繼承BaseMeshEffect释涛,在ModifyMesh里先將VertexHelper的原有數(shù)據(jù)清空,獲取MeshMask的頂點(diǎn)倦沧、面片數(shù)據(jù)唇撬,經(jīng)過坐標(biāo)轉(zhuǎn)換后將再傳給VertexHelper。
public abstract class BaseMeshEffect : UIBehaviour, IMeshModifier
{
public abstract void ModifyMesh(VertexHelper vh);
}
public class MeshImage : BaseMeshEffect{
...
public override void ModifyMesh(VertexHelper vh)
{
if (this.enabled)
{
vh.Clear();
_uiVertices.Clear();
if (mask)
{
if (mask.vertices != null && mask.triangles != null)
{
float tw = image.rectTransform.rect.width;
float th = image.rectTransform.rect.height;
Vector4 uv = image.overrideSprite != null ? DataUtility.GetOuterUV(image.overrideSprite) : Vector4.zero;
float uvCenterX = (uv.x + uv.z) * image.rectTransform.pivot.x;
float uvCenterY = (uv.y + uv.w) * image.rectTransform.pivot.y;
float uvScaleX = (uv.z - uv.x) / tw;
float uvScaleY = (uv.w - uv.y) / th;
List<Vector3> vertices = this.mask.vertices.Select(
x => { return this.transform.InverseTransformPoint(this.mask.transform.TransformPoint(x)); }).ToList();
for (int i = 0; i < mask.vertices.Count; i++)
{
UIVertex v = new UIVertex();
v.color = image.color;
v.position = vertices[i];
v.uv0 = new Vector2(v.position.x * uvScaleX + uvCenterX, v.position.y * uvScaleY + uvCenterY);
_uiVertices.Add(v);
}
vh.AddUIVertexStream(_uiVertices, mask.triangles);
}
}
}
}
}
像素級(jí)精確點(diǎn)擊####
如上篇博文所講局荚,為了實(shí)現(xiàn)精確點(diǎn)擊,Unity提供了eventAlphaThreshold字段愈污,但有著Sprite占用雙倍內(nèi)存耀态,無(wú)法合入圖集等缺陷,而MeshButton組件正好解決了痛點(diǎn)暂雹。MeshButton實(shí)現(xiàn)ICanvasRaycastFilter接口類首装,實(shí)現(xiàn)IsRaycastLocationValid方法,在方法內(nèi)獲取MeshMask的頂點(diǎn)數(shù)據(jù)杭跪,通過Ray-Crossing算法就可以判斷點(diǎn)擊點(diǎn)是否在區(qū)域內(nèi)仙逻。
public class MeshButton : UIBehaviour, ICanvasRaycastFilter
{
public virtual bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera){
//Stopwatch sw = new Stopwatch();
//sw.Start();
Sprite sprite = image.overrideSprite;
if (sprite == null)
return true;
bool ret = true;
if (this.mask != null && this.mask.vertices != null)
{
Vector2 local;
RectTransformUtility.ScreenPointToLocalPointInRectangle(image.rectTransform, screenPoint, eventCamera, out local);
List<Vector2> vertices = this.mask.vertices.Select(
x =>
{
Vector3 p = this.transform.InverseTransformPoint(this.mask.transform.TransformPoint(x));
return new Vector2(p.x, p.y);
}).ToList();
ret = ImageUtil.Contains(local, vertices);
}
//sw.Stop();
//UnityEngine.Debug.Log("點(diǎn)擊檢測(cè)耗時(shí):" + sw.ElapsedTicks + " tick");
return ret;
}
}
關(guān)于MeshMask##
- MeshMask組件適合用來(lái)顯示特殊形狀的Icon。MeshMask并不能完全取代Unity Mask涧尿,在需要顯示特殊形狀I(lǐng)con時(shí)作為Unity Mask的替代方案系奉,能達(dá)到提高渲染效率的目的,減少Unity Mask的不必要使用姑廉。
- 被Mask的圖片如果被移出Mask范圍外缺亮,會(huì)因?yàn)镾prite Wrap mode而出現(xiàn)邊緣像素拉伸,或者貼圖重復(fù)的問題桥言,這個(gè)問題暫時(shí)不能很好解決萌踱,因?yàn)镾prite Wrap mode必須設(shè)置為clamp或者repeat葵礼,就會(huì)出現(xiàn)這種問題。只能設(shè)置為clamp后并鸵,人為為貼圖邊緣留1px的透明邊解決鸳粉。好在,做特殊形狀I(lǐng)con的使用場(chǎng)景下园担,基本無(wú)須擔(dān)心這個(gè)問題届谈。