在上一篇博文"扔掉遮罩,更好的圓形Image組件"中,筆者改變Image的頂點數據,使得Image呈圓形顯示,避免了Mask的使用,從而節省Drawcall消耗,提升渲染效率了。這也啓發了筆者,有沒有可能經過一樣原理實現Mask,作到在某些須要顯示特定形狀Icon的場景下,替代Unity原生Mask,且能保有節省Drawcall,減小渲染像素點,實現精確點擊等優勢?通過一番折騰,就有了MeshMask組件。php
MeshMask遮罩效果圖git
能夠看到不管Mask形狀是凸邊形仍是複雜的凹邊形,都能準確地將Mask形狀數據序列化成頂點,面片數據,
提供給須要Mask的圖片修改渲染頂點,達到遮罩效果。組件用法相似於Unity Mask,且效率優於Unity Mask。插件已上傳至Github[點擊下載], 歡迎試用~github
使用原生Mask,10個Icon佔用了15個Drawcall算法
使用MeshMask,10個Icon僅佔用1個Drawcallide
Scene切換到Overdraw模式:紅框爲Mask的Overdraw;藍框爲MeshMask的Overdraw工具
從上面三張圖能夠看到MeshMask相比Unity的Mask,在減小Drawcall消耗、Overdraw消耗等兩方面都是完勝的。性能
這10個icon都打包在同一圖集的,使用Unity Mask,沒辦法享受圖層合併,消耗了15個Drawcall;使用MeshMask的狀況下,看截圖裏Batches爲2,除去攝像機佔用的1個Batch,10個icon僅佔用1個Batch,即1個Drawcall。在Drawcall資源如此昂貴的狀況下(通常機器都會要求Drawcall在200如下),這種性能節省效果很是顯著。ui
而看圖三的Overdraw,使用Unity Mask的紅框部分,被Mask的圖片所有繪製一次,Unity Mask再作像素剔除,被Mask的部分又繪製了一次,總共須要繪製兩次,且有一次是繪製了徹底用不到的區域。使用MeshMask的藍框部分,由於是靠改變頂點繪製出來的icon,所以僅有被Mask部分被繪製了一次。this
固然,使用MeshMask的Image須要消耗比普通Image多一些的頂點和麪片,觀察Stats面板,使用MeshMsk的10個icon多佔用1.3K的頂點和麪片,即1個icon佔用130個頂點,面片。然而GPU渲染頂點,面片的效率很是高(市面手機GPU渲染多邊形數基本上2000-10000+萬多邊形/每秒以上),這點消耗跟Drawcall比起來就微不足道了。pwa
在渲染上,GPU、CPU二者的性能瓶頸每每是CPU;GPU的性能瓶頸每每是像素點填充率(Overdraw致使),CPU的性能瓶頸每每是Drawcall。因此,渲染性能排查,幾項指標關注優先級應該是:Drawcall > Overdraw > 面片
MeshMask插件目錄結構
插件裏有MeshMask、MeshImage、MeshButton三個UI組件
MeshMask組件Inspector面板
MeshMask組件做用相似Unity Mask,依賴了Image及PolygonCollider2D組件,帶有[根據Image組件生成Mask]、[根據Collider組件生成Mask]兩個菜單項,支持兩種方式生成Mask數據。
被遮罩GameOjecct的Inspector面板
MeshImage、MeshButton組件掛在須要被遮罩的GameObject上,設置好MeshMask對象,就能得到數據,實現遮罩或者精確點擊。
不一樣於CircleImage,只須要簡單的對圓形進行頂點,面片計算;MeshMask要考慮幾個點:
其中作頂點,面片計算這一步比較麻煩,涉及如下幾個技術點:
圖片處理流程
邊緣檢測算法算是圖形學應用最普遍最基礎的算法了,主要原理是濾波器對圖形進行濾波從而獲得梯度圖像,經過判斷梯度圖像的某像素點灰度值是否超過閾值,就能判斷該點是否爲邊緣點。筆者採用了簡單的Sobel算子邊緣檢測算法。
Sobel算子:3x3的矩形濾波器
A表明原始圖像,Gx及Gy分別表明經橫向及縱向邊緣檢測的圖像灰度值
圖像某像素點灰度值
一般,爲了提升效率 使用不開平方的近似值
這裏拿米老鼠圖來作示例圖,看看Sobel邊緣檢測的效果。
原圖
sobel邊緣檢測後的灰度圖
能夠看到算法效果不錯,但咱們並不須要這麼多邊緣「信息」,只須要最外圍的邊緣「信息」。所以將非透明區域都填充成統一的顏色,再作邊緣檢測。
最終效果:理想的外圍邊緣
得到了外圍邊緣信息後,下一步須要作離散化:剔除冗餘信息,並將邊緣信息以有序集合的形式表示。這個有序集合,就是渲染底層所須要的頂點數據。
冗餘頂點:對於邊緣的直線,除直線首尾兩點外,其餘點都是冗餘可剔除的。
有序集合:集合點依次鏈接起來,就如同用筆按逆時針/順時針方向畫出來的邊緣圖形。
筆者挑選了邊緣點集中x最小的點做爲起始點,以順時針順序查找鄰接點的方法來計算有序頂點集。
算法步驟:
- 選擇邊緣點集x最小的點爲起始點,當前點
- 查找當前點周邊8個像素點是否有邊緣點,如都沒有就繼續向外圍一圈,直到找到邊緣點。
- 當找到多個邊緣點狀況下,比較當前點與各邊緣點所呈夾角,選夾角最小的邊緣點做爲鄰接點。
- 若鄰接點即爲起始點,則算法結束,不然繼續
- 判斷鄰接點與有序頂點集最後一個點是否共邊,若共邊則刪除最後一個點
- 將鄰接點加入有序頂點集
- 設置鄰接點爲當前點,重複步驟2
刪除共邊頂點圖示:當C即將加入頂點集中,發現ABC三點共邊的狀況,刪除中間點B
三角化(Triangulation)也是圖形學應用較多的算法了,特別是在3D建模、遊戲領域。三角化是指從一組已知點集中,構建出三角形網格。隨着構建條件不一樣,三角化算法也不一樣。像最近LowPoly繪畫風格比較熱門,一些濾鏡軟件會支持LowPoly轉換。軟件在將一張普通圖像轉換位LowPoly圖像的過程當中,除了同樣要作邊緣檢測,離散化外,在三角化這一步,須要生成顯示質量較高的三角形,不能有過於狹長的三角形,就須要用Delaunay算法。在咱們這個場景下,對生成的三角形並無特殊要求,不須要用上覆雜的Delaunay算法,Unity3d wiki社區上提供了一個簡單的三角化算法,恰好適用。
算法原理
從點集中隨機挑選三點組成三角形,而後遍歷其餘點,看是否有點落在三角形內,若是三角形內無點則爲合格三角形。循環此過程直到全部點都被處理。
通過前面處理,咱們已經拿到了頂點數據、面片數據。筆者但願組件能將這些頂點數據可視化,以便讓使用者直觀瞭解處理結果。Unity自帶的PolygonCollider2D組件,正好適用。
public sealed class PolygonCollider2D : Collider2D { .... public void SetPath(int index, Vector2[] points); }
經過SetPath接口將頂點數據傳入PolygonCollider2D 組件,PolygonCollider2D完美地生成米老鼠的路徑。在一開始實驗中,筆者驚奇地發現組件居然也對頂點作了三角化處理。遺憾地是,組件並無提供接口獲取三角化結果,Unity社區的技術人員也認可此點,說Unity的將來版本可能會考慮暴露此接口,並建議本身作三角化處理,就是前面所說的算法(汗.. = . = ||)。經過下圖比較,能夠看到組件跟算法的三角化結果仍是有所不一樣的。
頂點數據傳入PolygonCollider2D後的效果
算法處理後的三角化效果
利用PolygonCollider2D組件除了讓咱們能夠看到頂點結果,還能夠經過Inspector上的[Edit Collider]按鈕微調,頂點的位置,作出更理想的Mask效果。
甚至,咱們能夠直接利用PolygonCollider2D組件,從無到有地編輯Mask形狀後,再三角化處理得到面片數據。
直接用PolygonCollider2D編輯出來的「愛心」
已經有了頂點數據,面片數據,終於到了最後的渲染步驟。筆者利用MeshMask組件存放這些數據,並不直接渲染MeshMask,而是在MeshMask子節點下添加MeshImage組件,進行修改頂點渲染。
在5.3版本里,Unity提供了BaseMeshEffect類,是Unity提供給開發者用於給Graphic進行二次修改繪製的類,咱們能夠在ModifyMesh方法中修改VertexHelper攜帶的頂點,面片,uv等數據來改變渲染。(在5.3以前的版本,對應的類和接口是BaseVertexEffect、ModifyVertices)
MeshImage繼承BaseMeshEffect,在ModifyMesh裏先將VertexHelper的原有數據清空,獲取MeshMask的頂點、面片數據,通過座標轉換後將再傳給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); } } } } }
拖動MeshImage的位置,圖片外顯區域始終限定在米老鼠Mask內
如上篇博文所講,爲了實現精確點擊,Unity提供了eventAlphaThreshold字段,但有着Sprite佔用雙倍內存,沒法合入圖集等缺陷。而MeshButton組件正好解決了痛點。MeshButton實現ICanvasRaycastFilter接口類,實現IsRaycastLocationValid方法,在方法內獲取MeshMask的頂點數據,經過Ray-Crossing算法就能夠判斷點擊點是否在區域內。
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("點擊檢測耗時:" + sw.ElapsedTicks + " tick"); return ret; } }