前幾天同事做了一個效果,希望在原本使用了遮罩組件 Mask 的技能圖標(讓技能圖標變成圓形)上在添加一個置灰的功能,但問題來了:因為是動態根據游戲中玩家的條件才動態置灰,以修改 Mask 下子節點 Image 組件的材質來實現的,但是實際上怎么修改也不起作用,呈現出的效果都只停留在第一次運行時的樣子。
一開始我也以為是 shader 的問題,修改的 property 沒有生效,但是通過各種測試發現 shader 是已經修改成功的了。就是沒有應用上,在查閱了各方資料無效果的情況下只能翻閱 unity 托管在 BitBucket 上的 UI 源碼了(贊一個)。
先看看 Mask.cs,通過 StencilBuffer 實現遮罩,繼承自 IMaterialModifier,需要實現接口:Material GetModifiedMaterial(Material baseMaterial); 這個接口是用來修改獲取的材質來實現遮罩效果。在各種 MonoBehaviour 改變時都會通過 MaskUtilities 這個靜態類來向所有的子節點發送 Stencil 狀態改變的消息。
所以想知道為什么材質效果總是維持在第一次啟動執行時,就看看 Mask.OnEnable 里調用的 MaskUtilities.NotifyStencilStateChanged(this); 做了什么。
1 public static void NotifyStencilStateChanged(Component mask) 2 { 3 var components = ListPool<Component>.Get(); 4 mask.GetComponentsInChildren(components); 5 for (var i = 0; i < components.Count; i++) 6 { 7 if (components[i] == null || components[i].gameObject == mask.gameObject) 8 continue; 9 10 var toNotify = components[i] as IMaskable; 11 if (toNotify != null) 12 toNotify.RecalculateMasking(); 13 } 14 ListPool<Component>.Release(components); 15 }
看以上代碼可以得知,Mask 會調用所有子節點中繼承自 IMaskable 組件(Image 繼承 MaskableGraphic, MaskableGraphic 繼承自此)的 RecalculateMasking() 函數。該函數將
MaskableGraphic 中的 m_ShouldRecalculateStencil 修改為 true,這樣當開始渲染時,每個組件都會被調用 GetModifiedMaterial 以返回一個適用於當前渲染的 材質(有可能會返回一個修改過的拷貝),當 Imange.m_ShouldRecalculateStencil = true 時,會在 GetModifiedMaterial 中返回一個支持 Stencil 的修改過的材質,用於實現 Mask 遮罩效果,所以問題也很明了了,修改 Mask 下的 Image 組件原始的材質是不起作用的,因為實際渲染使用的不是它。
那么如何修改?只需要自己繼承一個比如 Image 組件,並重載 GetModifiedMaterial 方法,將基類返回材質保存即可,這就是實際渲染使用的材質,當你想修改置灰時,使用這個材質即可。代碼如下:
1 public class CustomImage : Image 2 { 3 public override Material GetModifiedMaterial(Material baseMaterial) 4 { 5 Material cModifiedMat = base.GetModifiedMaterial(baseMaterial); 6 // Do whatever you want with this "cModifiedMat"... 7 // You can also hold this and process it in your grayscale code. 8 // ... 9 return cModifiedMat; 10 } 11 }
也可以去看看我在 Unity Answer 上對於該問題的回答:http://answers.unity3d.com/questions/1130203/ui-mask-override-my-shaders-custom-property.html