UnityGUI擴展實例:圖片挖洞效果 Mask的反向實現


轉載自 https://www.taidous.com/forum.php?mod=viewthread&fid=211&tid=55259

我想大家在用uGUI做界面時,可能經常會碰到一種需求,就是在圖片上“挖洞”。

說起來我們可以有幾種實現方案,比如最簡單的方式,直接導入帶有“洞”的圖片。這種方式簡單,但不適合需要動態變化的場合。考慮有這種需求:當我們上線一個新功能時,可能希望在玩家第一次打開游戲時,將界面其它地方變暗,突出新增的功能,即所謂的“新手引導”功能。

如果用黑色含透明區域圖片來展示這種效果,也不是不可以,但是會有幾個問題。首先需要處理UI遮擋問題,因為Image的透明區域依然會阻擋下層的點擊事件;其次如果新手引導分若干步,每步要展示的區域大小和形狀都不同,那可能需要針對每步都做圖,這個過程會變得非常復雜。

反過來考慮,如果我們可以實現將圖片上任意形狀區域“剔除”的功能,不就剛好可以滿足這種需求嗎?這就是本文所討論的重點:圖片“挖洞”的一種實現手段。

 

首先明確下我們的需求:

    • 我們需要能將圖片中某個形狀區域隱藏顯示;
    • 最好能夠讓點擊事件穿過此區域;
          • 此時熟悉uGUI的同學可能已經發現了,uGUI內置了一種組件叫做Mask,恰好實現了這兩種需求(的大部分)。我們先來分析下Mask。

            Mask的設計思路是這樣的:它與Image組件配合工作,根據Image的覆蓋區域來定位顯示范圍,所有此Image的子級UI元素,超出此區域的部分都會被隱藏(包括UI交互事件)。
            於是我們發現,我們想要實現的功能與Mask組件似乎恰好相反:我們是想要此Image覆蓋區域的子級UI元素不顯示,而超出區域的部分照常顯示,這樣即可(初步)滿足需求。

            那么我們看下Mask的實現原理吧,看看是不是可以借鑒思路呢?Unity官方文檔關於Mask的實現原理說明如下:
          • 可以簡單理解為:Mask會將Image的渲染區域像素進行特別標記,稍后子級UI進行像素渲染時,判斷如果存在此標記(說明渲染像素位於Mask區域內)就進行渲染,否則不渲染。可以發現,此功能的實現除了Mask組件,還需要子級UI元素的配合。實際上,Unity的內置UI組件都繼承自MaskableGraphic,此類型正是Mask的配合實現者,它的相關代碼實現如下:
            • 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; } // if we have a enabled Mask component then it will // generate the mask material. This is an optimisation // it adds some coupling between components though :( Mask maskComponent = GetComponent<Mask>(); if (m_StencilValue > 0 && (maskComponent == null || !maskComponent.IsActive())) { var maskMat = StencilMaterial.Add(toUse, (1 << m_StencilValue) - 1, StencilOp.Keep, CompareFunction.Equal, ColorWriteMask.All, (1 << m_StencilValue) - 1, 0); StencilMaterial.Remove(m_MaskMaterial); m_MaskMaterial = maskMat; toUse = m_MaskMaterial; } return toUse; }
              • 知道了Mask的原理,那么我們就會想到一種可能的方案,如果重寫MaskableGraphic的GetModifiedMaterial方法,將它的判斷邏輯逆轉,是否就可以了呢?來試一下吧!新建腳本HoleImage,內容如下:
              • public class HoleImage : Image {
                    public override 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;
                        }
                
                        // if we have a enabled Mask component then it will
                        // generate the mask material. This is an optimisation
                        // it adds some coupling between components though :(
                        Mask maskComponent = GetComponent<Mask>();
                        if (m_StencilValue > 0 && (maskComponent == null || !maskComponent.IsActive()))
                        {
                            var maskMat = StencilMaterial.Add(toUse, (1 << m_StencilValue) - 1, StencilOp.Keep, CompareFunction.NotEqual, ColorWriteMask.All, (1 << m_StencilValue) - 1, 0);
                            StencilMaterial.Remove(m_MaskMaterial);
                            m_MaskMaterial = maskMat;
                            toUse = m_MaskMaterial;
                        }
                        return toUse;
                    }
                }
              • 本帖最后由 younglee 於 2017-3-22 21:33 編輯

                我想大家在用uGUI做界面時,可能經常會碰到一種需求,就是在圖片上“挖洞”。





                說起來我們可以有幾種實現方案,比如最簡單的方式,直接導入帶有“洞”的圖片。這種方式簡單,但不適合需要動態變化的場合。考慮有這種需求:當我們上線一個新功能時,可能希望在玩家第一次打開游戲時,將界面其它地方變暗,突出新增的功能,即所謂的“新手引導”功能。



                如果用黑色含透明區域圖片來展示這種效果,也不是不可以,但是會有幾個問題。首先需要處理UI遮擋問題,因為Image的透明區域依然會阻擋下層的點擊事件;其次如果新手引導分若干步,每步要展示的區域大小和形狀都不同,那可能需要針對每步都做圖,這個過程會變得非常復雜。

                反過來考慮,如果我們可以實現將圖片上任意形狀區域“剔除”的功能,不就剛好可以滿足這種需求嗎?這就是本文所討論的重點:圖片“挖洞”的一種實現手段。

                首先明確下我們的需求:

                • 我們需要能將圖片中某個形狀區域隱藏顯示;
                • 最好能夠讓點擊事件穿過此區域;

                此時熟悉uGUI的同學可能已經發現了,uGUI內置了一種組件叫做Mask,恰好實現了這兩種需求(的大部分)。我們先來分析下Mask。

                Mask的設計思路是這樣的:它與Image組件配合工作,根據Image的覆蓋區域來定位顯示范圍,所有此Image的子級UI元素,超出此區域的部分都會被隱藏(包括UI交互事件)。
                於是我們發現,我們想要實現的功能與Mask組件似乎恰好相反:我們是想要此Image覆蓋區域的子級UI元素不顯示,而超出區域的部分照常顯示,這樣即可(初步)滿足需求。

                那么我們看下Mask的實現原理吧,看看是不是可以借鑒思路呢?Unity官方文檔關於Mask的實現原理說明如下:

                Implementation
                  Masking is implemented using the stencil buffer of the GPU.
                  The first Mask element writes a 1 to the stencil buffer All elements below the mask check when rendering, and only render to areas where there is a 1 in the stencil buffer *Nested Masks will write incremental bit masks into the buffer, this means that renderable children need to have the logical & of the stencil values to be rendered.
                可以簡單理解為:Mask會將Image的渲染區域像素進行特別標記,稍后子級UI進行像素渲染時,判斷如果存在此標記(說明渲染像素位於Mask區域內)就進行渲染,否則不渲染。可以發現,此功能的實現除了Mask組件,還需要子級UI元素的配合。實際上,Unity的內置UI組件都繼承自MaskableGraphic,此類型正是Mask的配合實現者,它的相關代碼實現如下:

                [AppleScript]  純文本查看 復制代碼
                   
                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;
                        }
                
                        // if we have a enabled Mask component then it will
                        // generate the mask material. This is an optimisation
                        // it adds some coupling between components though :(
                        Mask maskComponent = GetComponent<Mask>();
                        if (m_StencilValue > 0 && (maskComponent == null || !maskComponent.IsActive()))
                        {
                            var maskMat = StencilMaterial.Add(toUse, (1 << m_StencilValue) - 1, StencilOp.Keep, CompareFunction.Equal, ColorWriteMask.All, (1 << m_StencilValue) - 1, 0);
                            StencilMaterial.Remove(m_MaskMaterial);
                            m_MaskMaterial = maskMat;
                            toUse = m_MaskMaterial;
                        }
                        return toUse;
                    }


                知道了Mask的原理,那么我們就會想到一種可能的方案,如果重寫MaskableGraphic的GetModifiedMaterial方法,將它的判斷邏輯逆轉,是否就可以了呢?來試一下吧!新建腳本HoleImage,內容如下:

                [AppleScript]  純文本查看 復制代碼
                public class HoleImage : Image {
                    public override 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;
                        }
                
                        // if we have a enabled Mask component then it will
                        // generate the mask material. This is an optimisation
                        // it adds some coupling between components though :(
                        Mask maskComponent = GetComponent<Mask>();
                        if (m_StencilValue > 0 && (maskComponent == null || !maskComponent.IsActive()))
                        {
                            var maskMat = StencilMaterial.Add(toUse, (1 << m_StencilValue) - 1, StencilOp.Keep, CompareFunction.NotEqual, ColorWriteMask.All, (1 << m_StencilValue) - 1, 0);
                            StencilMaterial.Remove(m_MaskMaterial);
                            m_MaskMaterial = maskMat;
                            toUse = m_MaskMaterial;
                        }
                        return toUse;
                    }
                }


                注意,我們唯一改動的地方在19行,將CompareFunction.Equal改為了CompareFunction.NotEqual,即只有沒有被Mask標記的區域才進行渲染。回到Unity,在Canvas下新建一個較小的Image,添加Mask組件,取消勾選“Show Mask Graphic”,並添加一個較大的子級Image,可以發現子級Image已經正確地被Mask組件給挖出了一個洞。

                至此,本文的核心問題已經被解決,這個簡陋的東西已經可以解決一些問題。接下來我們對它進行進一步處理完善。第一個小問題很容易就暴露了,你會發現游戲運行中當你點擊圖片空洞時,UI事件並不會傳遞到下層,反而點擊Mask外部區域卻傳遞了UI事件,實際上這正是Mask期望的結果,但卻不是我們期望的結果~

                這個問題不難解決,我們來分析下uGUI的UI事件傳遞機制。uGUI通過ICanvasRaycastFilter接口來處理UI捕獲,相關方法如下:
bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera);

UI對象需要實現此接口來自定義焦點捕獲判斷邏輯。當某個區域坐標被點擊時,系統會對當前區域所有UI元素(從頂層到底層)調用此方法,第一個返回true的元素即認定為捕獲點擊。稍微復雜一點的是嵌套的UI結構。對於某個UI元素,如果它是被嵌套的,那么這個接口調用會從它自身開始,逐級向上調用它的父級UI元素,此過程中任意層級返回false系統都會立刻終止判斷,認為此UI不能捕獲點擊。簡單理解的話,就是某個UI元素想要響應某個點擊,除了看它自身的意願,還要看 歷史的進程 它爹的意願,而它爹同意后還要看它爺爺的意見……

所以我們直接從源頭做起,干掉它爹——也就是Mask的判斷邏輯即可。Mask原本是這樣處理的:

[AppleScript]  純文本查看 復制代碼
public virtual bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
{
    if (!isActiveAndEnabled)
        return true;

    return RectTransformUtility.RectangleContainsScreenPoint(rectTransform, sp, eventCamera);
}


嗯,跟我們預期的一樣簡單粗暴:不在我自身“勢力范圍”內的統統返回false。那么同樣的,我們只需要反轉此邏輯即可。新建腳本Hole,內容如下:

[AppleScript]  純文本查看 復制代碼
public class Hole : Mask
{
    public override bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
    {
        if (!isActiveAndEnabled)
            return true;

        return !RectTransformUtility.RectangleContainsScreenPoint(rectTransform, sp, eventCamera);
    }
}


將場景中的Mask替換為Hole,運行測試,會發現UI事件已經按照新的邏輯執行了。

至此,通過Hole替代Mask,HoleImage替代Image,我們開頭提到的需求已經能夠完整解決了。其實還有一個小問題,我們的Hole完整的繼承了Mask的邏輯,只是反轉了UI事件檢測,這也就意味着……對,它的其它子級UI元素依然會表現出Mask的作用。這在一些情形下可能並不是你想要的結果。那么,你能想到用什么方式來解決此問題嗎?

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM