using System.Collections; using System.Collections.Generic; using System.Reflection; using System.Text; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.UI; namespace Coffee.UIExtensions { /// /// Soft mask. /// Use instead of Mask for smooth masking. /// public class SoftMask : Mask, IMeshModifier { //################################ // Constant or Static Members. //################################ /// /// Desampling rate. /// public enum DesamplingRate { None = 0, x1 = 1, x2 = 2, x4 = 4, x8 = 8, } static readonly List[] s_TmpSoftMasks = new List[] { new List(), new List(), new List(), new List(), }; static readonly Color[] s_ClearColors = new Color[] { new Color(0, 0, 0, 0), new Color(1, 0, 0, 0), new Color(1, 1, 0, 0), new Color(1, 1, 1, 0), }; //################################ // Serialize Members. //################################ [Tooltip("The desampling rate for soft mask buffer.")] [SerializeField] DesamplingRate m_DesamplingRate = DesamplingRate.None; [Tooltip("The value used by the soft mask to select the area of influence defined over the soft mask's graphic.")] [SerializeField][Range(0.01f, 1)] float m_Softness = 1; [Tooltip("Should the soft mask ignore parent soft masks?")] [SerializeField] bool m_IgnoreParent = false; [Tooltip("Is the soft mask a part of parent soft mask?")] [SerializeField] bool m_PartOfParent = false; //################################ // Public Members. //################################ /// /// The desampling rate for soft mask buffer. /// public DesamplingRate desamplingRate { get { return m_DesamplingRate; } set { if (m_DesamplingRate != value) { m_DesamplingRate = value; hasChanged = true; } } } /// /// The value used by the soft mask to select the area of influence defined over the soft mask's graphic. /// public float softness { get { return m_Softness; } set { value = Mathf.Clamp01(value); if (m_Softness != value) { m_Softness = value; hasChanged = true; } } } /// /// Should the soft mask ignore parent soft masks? /// /// If set to true the soft mask will ignore any parent soft mask settings. public bool ignoreParent { get { return m_IgnoreParent; } set { if (m_IgnoreParent != value) { m_IgnoreParent = value; hasChanged = true; OnTransformParentChanged(); } } } /// /// Is the soft mask a part of parent soft mask? /// public bool partOfParent { get { return m_PartOfParent; } set { if (m_PartOfParent != value) { m_PartOfParent = value; hasChanged = true; OnTransformParentChanged(); } } } /// /// The soft mask buffer. /// public RenderTexture softMaskBuffer { get { if (_parent) { ReleaseRT(ref _softMaskBuffer); return _parent.softMaskBuffer; } // Check the size of soft mask buffer. int w, h; GetDesamplingSize(m_DesamplingRate, out w, out h); if (_softMaskBuffer && (_softMaskBuffer.width != w || _softMaskBuffer.height != h)) { ReleaseRT(ref _softMaskBuffer); } if (!_softMaskBuffer) { _softMaskBuffer = RenderTexture.GetTemporary(w, h, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default); hasChanged = true; _hasStencilStateChanged = true; } return _softMaskBuffer; } } public bool hasChanged { get { return _parent ? _parent.hasChanged : _hasChanged; } private set { if (_parent) { _parent.hasChanged = value; } _hasChanged = value; } } public SoftMask parent { get { return _parent; } } /// /// Perform material modification in this function. /// /// Modified material. /// Configured Material. public override Material GetModifiedMaterial(Material baseMaterial) { hasChanged = true; var result = base.GetModifiedMaterial(baseMaterial); if (m_IgnoreParent && result != baseMaterial) { result.SetInt(s_StencilCompId, (int)CompareFunction.Always); } return result; } /// /// Call used to modify mesh. /// void IMeshModifier.ModifyMesh(Mesh mesh) { hasChanged = true; _mesh = mesh; } /// /// Call used to modify mesh. /// void IMeshModifier.ModifyMesh(VertexHelper verts) { if (isActiveAndEnabled) { verts.FillMesh(mesh); } hasChanged = true; } /// /// Given a point and a camera is the raycast valid. /// /// Valid. /// Screen position. /// Raycast camera. /// Target graphic. public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera, Graphic g, int[] interactions) { if (!isActiveAndEnabled || (g == graphic && !g.raycastTarget)) { return true; } int x = (int)((softMaskBuffer.width - 1) * Mathf.Clamp01(sp.x / Screen.width)); int y = (int)((softMaskBuffer.height - 1) * Mathf.Clamp01(sp.y / Screen.height)); return 0.5f < GetPixelValue(x, y, interactions); } public override bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera) { return true; } //################################ // Protected Members. //################################ /// /// This function is called when the object becomes enabled and active. /// protected override void OnEnable() { hasChanged = true; // Register. if (s_ActiveSoftMasks.Count == 0) { Canvas.willRenderCanvases += UpdateMaskTextures; if (s_StencilCompId == 0) { s_StencilCompId = Shader.PropertyToID("_StencilComp"); s_ColorMaskId = Shader.PropertyToID("_ColorMask"); s_MainTexId = Shader.PropertyToID("_MainTex"); s_SoftnessId = Shader.PropertyToID("_Softness"); } } s_ActiveSoftMasks.Add(this); // Reset the parent-child relation. GetComponentsInChildren(false, s_TempRelatables); for (int i = s_TempRelatables.Count - 1; 0 <= i; i--) { s_TempRelatables[i].OnTransformParentChanged(); } s_TempRelatables.Clear(); // Create objects. _mpb = new MaterialPropertyBlock(); _cb = new CommandBuffer(); graphic.SetVerticesDirty(); base.OnEnable(); _hasStencilStateChanged = false; } /// /// This function is called when the behaviour becomes disabled. /// protected override void OnDisable() { // Unregister. s_ActiveSoftMasks.Remove(this); if (s_ActiveSoftMasks.Count == 0) { Canvas.willRenderCanvases -= UpdateMaskTextures; } // Reset the parent-child relation. for (int i = _children.Count - 1; 0 <= i; i--) { _children[i].SetParent(_parent); } _children.Clear(); SetParent(null); // Destroy objects. _mpb.Clear(); _mpb = null; _cb.Release(); _cb = null; ReleaseObject(_mesh); _mesh = null; ReleaseObject(_material); _material = null; ReleaseRT(ref _softMaskBuffer); base.OnDisable(); _hasStencilStateChanged = false; } /// /// This function is called when the parent property of the transform of the GameObject has changed. /// protected override void OnTransformParentChanged() { hasChanged = true; SoftMask newParent = null; if (isActiveAndEnabled && !m_IgnoreParent) { var parentTransform = transform.parent; while (parentTransform && (!newParent || !newParent.enabled)) { newParent = parentTransform.GetComponent(); parentTransform = parentTransform.parent; } } SetParent(newParent); hasChanged = true; } protected override void OnRectTransformDimensionsChange() { hasChanged = true; } #if UNITY_EDITOR /// /// This function is called when the script is loaded or a value is changed in the inspector (Called in the editor only). /// protected override void OnValidate() { graphic.SetMaterialDirty(); OnTransformParentChanged(); base.OnValidate(); _hasStencilStateChanged = false; } #endif //################################ // Private Members. //################################ static Shader s_SoftMaskShader; static Texture2D s_ReadTexture; static List s_ActiveSoftMasks = new List(); static List s_TempRelatables = new List(); static int s_StencilCompId; static int s_ColorMaskId; static int s_MainTexId; static int s_SoftnessId; MaterialPropertyBlock _mpb; CommandBuffer _cb; Material _material; RenderTexture _softMaskBuffer; int _stencilDepth; Mesh _mesh; SoftMask _parent; List _children = new List(); bool _hasChanged = false; bool _hasStencilStateChanged = false; Material material { get { return _material ? _material : _material = new Material(s_SoftMaskShader ? s_SoftMaskShader : s_SoftMaskShader = Resources.Load("SoftMask")){ hideFlags = HideFlags.HideAndDontSave }; } } Mesh mesh { get { return _mesh ? _mesh : _mesh = new Mesh(){ hideFlags = HideFlags.HideAndDontSave }; } } /// /// Update all soft mask textures. /// static void UpdateMaskTextures() { foreach (var sm in s_ActiveSoftMasks) { if (!sm || sm._hasChanged) continue; var rt = sm.rectTransform; if (rt.hasChanged) { rt.hasChanged = false; sm.hasChanged = true; } #if UNITY_EDITOR if (!Application.isPlaying) { sm.hasChanged = true; } #endif } foreach (var sm in s_ActiveSoftMasks) { if (!sm || !sm._hasChanged) continue; sm._hasChanged = false; if (!sm._parent) { sm.UpdateMaskTexture(); if (sm._hasStencilStateChanged) { sm._hasStencilStateChanged = false; MaskUtilities.NotifyStencilStateChanged (sm); } } } } /// /// Update the mask texture. /// void UpdateMaskTexture() { if (!graphic || !graphic.canvas) { return; } _stencilDepth = MaskUtilities.GetStencilDepth(transform, MaskUtilities.FindRootSortOverrideCanvas(transform)); // Collect children soft masks. int depth = 0; s_TmpSoftMasks[0].Add(this); while (_stencilDepth + depth < 3) { int count = s_TmpSoftMasks[depth].Count; for (int i = 0; i < count; i++) { List children = s_TmpSoftMasks[depth][i]._children; int childCount = children.Count; for (int j = 0; j < childCount; j++) { var child = children[j]; var childDepth = child.m_PartOfParent ? depth : depth + 1; s_TmpSoftMasks[childDepth].Add(child); } } depth++; } // Clear. _cb.Clear(); _cb.SetRenderTarget(softMaskBuffer); _cb.ClearRenderTarget(false, true, s_ClearColors[_stencilDepth]); // Set view and projection matrices. var c = graphic.canvas.rootCanvas; var cam = c.worldCamera ?? Camera.main; if (c && c.renderMode != RenderMode.ScreenSpaceOverlay && cam) { _cb.SetViewProjectionMatrices(cam.worldToCameraMatrix, GL.GetGPUProjectionMatrix(cam.projectionMatrix, false)); } else { var pos = c.transform.localPosition; var vm = Matrix4x4.TRS(new Vector3(-pos.x, -pos.y, -1000), Quaternion.identity, new Vector3(1, 1, -1f)); var pm = Matrix4x4.TRS(new Vector3(0, 0, -1), Quaternion.identity, new Vector3(1 / pos.x, 1 / pos.y, -2 / 10000f)); _cb.SetViewProjectionMatrices(vm, pm); } // Draw soft masks. for (int i = 0; i < s_TmpSoftMasks.Length; i++) { int count = s_TmpSoftMasks[i].Count; for (int j = 0; j < count; j++) { var sm = s_TmpSoftMasks[i][j]; if (i != 0) { sm._stencilDepth = MaskUtilities.GetStencilDepth(sm.transform, MaskUtilities.FindRootSortOverrideCanvas(sm.transform)); } // Set material property. sm.material.SetInt(s_ColorMaskId, (int)1 << (3 - _stencilDepth - i)); sm._mpb.SetTexture(s_MainTexId, sm.graphic.mainTexture); sm._mpb.SetFloat(s_SoftnessId, sm.m_Softness); // Draw mesh. _cb.DrawMesh(sm.mesh, sm.transform.localToWorldMatrix, sm.material, 0, 0, sm._mpb); } s_TmpSoftMasks[i].Clear(); } Graphics.ExecuteCommandBuffer(_cb); } /// /// Gets the size of the desampling. /// void GetDesamplingSize(DesamplingRate rate, out int w, out int h) { #if UNITY_EDITOR var res = UnityEditor.UnityStats.screenRes.Split('x'); w = int.Parse(res[0]); h = int.Parse(res[1]); #else w = Screen.width; h = Screen.height; #endif if (rate == DesamplingRate.None) return; float aspect = (float)w / h; if (w < h) { h = Mathf.ClosestPowerOfTwo(h / (int)rate); w = Mathf.CeilToInt(h * aspect); } else { w = Mathf.ClosestPowerOfTwo(w / (int)rate); h = Mathf.CeilToInt(w / aspect); } } /// /// Release the specified obj. /// /// Object. void ReleaseRT(ref RenderTexture tmpRT) { if (tmpRT) { tmpRT.Release(); RenderTexture.ReleaseTemporary(tmpRT); tmpRT = null; } } /// /// Release the specified obj. /// /// Object. void ReleaseObject(Object obj) { if (obj) { #if UNITY_EDITOR if (!Application.isPlaying) DestroyImmediate(obj); else #endif Destroy(obj); obj = null; } } /// /// Set the parent of the soft mask. /// /// The parent soft mask to use. void SetParent(SoftMask newParent) { if (_parent != newParent && this != newParent) { if (_parent && _parent._children.Contains(this)) { _parent._children.Remove(this); _parent._children.RemoveAll(x => x == null); } _parent = newParent; } if (_parent && !_parent._children.Contains(this)) { _parent._children.Add(this); } } /// /// Gets the pixel value. /// float GetPixelValue(int x, int y, int[] interactions) { if (!s_ReadTexture) { s_ReadTexture = new Texture2D(1, 1, TextureFormat.ARGB32, false); } var currentRT = RenderTexture.active; RenderTexture.active = softMaskBuffer; s_ReadTexture.ReadPixels(new Rect(x, y, 1, 1), 0, 0); s_ReadTexture.Apply(false, false); RenderTexture.active = currentRT; var colors = s_ReadTexture.GetRawTextureData(); for (int i = 0; i < 4; i++) { switch (interactions[(i + 3)%4]) { case 0: colors[i] = 255; break; case 2: colors[i] = (byte)(255 - colors[i]); break; } } switch (_stencilDepth) { case 0: return (colors[1] / 255f); case 1: return (colors[1] / 255f) * (colors[2] / 255f); case 2: return (colors[1] / 255f) * (colors[2] / 255f) * (colors[3] / 255f); case 3: return (colors[1] / 255f) * (colors[2] / 255f) * (colors[3] / 255f) * (colors[0] / 255f); default: return 0; } } } }