using System; using UnityEngine.Events; using UnityEngine.Rendering; namespace UnityEngine.UI { /// /// A Graphic that is capable of being masked out. /// public abstract class MaskableGraphic : Graphic, IClippable, IMaskable, IMaterialModifier { [NonSerialized] protected bool m_ShouldRecalculateStencil = true; [NonSerialized] protected Material m_MaskMaterial; [NonSerialized] private RectMask2D m_ParentMask; // m_Maskable is whether this graphic is allowed to be masked or not. It has the matching public property maskable. // The default for m_Maskable is true, so graphics under a mask are masked out of the box. // The maskable property can be turned off from script by the user if masking is not desired. // m_IncludeForMasking is whether we actually consider this graphic for masking or not - this is an implementation detail. // m_IncludeForMasking should only be true if m_Maskable is true AND a parent of the graphic has an IMask component. // Things would still work correctly if m_IncludeForMasking was always true when m_Maskable is, but performance would suffer. [SerializeField] private bool m_Maskable = true; private bool m_IsMaskingGraphic = false; [NonSerialized] [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] [Obsolete("Not used anymore.", true)] protected bool m_IncludeForMasking = false; [Serializable] public class CullStateChangedEvent : UnityEvent {} // Event delegates triggered on click. [SerializeField] private CullStateChangedEvent m_OnCullStateChanged = new CullStateChangedEvent(); /// /// Callback issued when culling changes. /// /// /// Called whene the culling state of this MaskableGraphic either becomes culled or visible. You can use this to control other elements of your UI as culling happens. /// public CullStateChangedEvent onCullStateChanged { get { return m_OnCullStateChanged; } set { m_OnCullStateChanged = value; } } /// /// Does this graphic allow masking. /// public bool maskable { get { return m_Maskable; } set { if (value == m_Maskable) return; m_Maskable = value; m_ShouldRecalculateStencil = true; SetMaterialDirty(); } } /// /// Is this graphic the graphic on the same object as a Mask that is enabled. /// /// /// If toggled ensure to call MaskUtilities.NotifyStencilStateChanged(this); manually as it changes how stenciles are calculated for this image. /// public bool isMaskingGraphic { get { return m_IsMaskingGraphic; } set { if (value == m_IsMaskingGraphic) return; m_IsMaskingGraphic = value; } } [NonSerialized] [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] [Obsolete("Not used anymore", true)] protected bool m_ShouldRecalculate = true; [NonSerialized] protected int m_StencilValue; /// /// See IMaterialModifier.GetModifiedMaterial /// public virtual Material GetModifiedMaterial(Material baseMaterial) { var toUse = baseMaterial; if (m_ShouldRecalculateStencil) { if (maskable) { var rootCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform); m_StencilValue = MaskUtilities.GetStencilDepth(transform, rootCanvas); } else m_StencilValue = 0; m_ShouldRecalculateStencil = false; } // if we have a enabled Mask component then it will // generate the mask material. This is an optimization // it adds some coupling between components though :( if (m_StencilValue > 0 && !isMaskingGraphic) { 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; } /// /// See IClippable.Cull /// public virtual void Cull(Rect clipRect, bool validRect) { var cull = !validRect || !clipRect.Overlaps(rootCanvasRect, true); UpdateCull(cull); } private void UpdateCull(bool cull) { if (canvasRenderer.cull != cull) { canvasRenderer.cull = cull; UISystemProfilerApi.AddMarker("MaskableGraphic.cullingChanged", this); m_OnCullStateChanged.Invoke(cull); OnCullingChanged(); } } /// /// See IClippable.SetClipRect /// public virtual void SetClipRect(Rect clipRect, bool validRect) { if (validRect) canvasRenderer.EnableRectClipping(clipRect); else canvasRenderer.DisableRectClipping(); } public virtual void SetClipSoftness(Vector2 clipSoftness) { canvasRenderer.clippingSoftness = clipSoftness; } protected override void OnEnable() { base.OnEnable(); m_ShouldRecalculateStencil = true; UpdateClipParent(); SetMaterialDirty(); if (isMaskingGraphic) { MaskUtilities.NotifyStencilStateChanged(this); } } protected override void OnDisable() { base.OnDisable(); m_ShouldRecalculateStencil = true; SetMaterialDirty(); UpdateClipParent(); StencilMaterial.Remove(m_MaskMaterial); m_MaskMaterial = null; if (isMaskingGraphic) { MaskUtilities.NotifyStencilStateChanged(this); } } #if UNITY_EDITOR protected override void OnValidate() { base.OnValidate(); m_ShouldRecalculateStencil = true; UpdateClipParent(); SetMaterialDirty(); } #endif protected override void OnTransformParentChanged() { base.OnTransformParentChanged(); if (!isActiveAndEnabled) return; m_ShouldRecalculateStencil = true; UpdateClipParent(); SetMaterialDirty(); } [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] [Obsolete("Not used anymore.", true)] public virtual void ParentMaskStateChanged() {} protected override void OnCanvasHierarchyChanged() { base.OnCanvasHierarchyChanged(); if (!isActiveAndEnabled) return; m_ShouldRecalculateStencil = true; UpdateClipParent(); SetMaterialDirty(); } readonly Vector3[] m_Corners = new Vector3[4]; private Rect rootCanvasRect { get { rectTransform.GetWorldCorners(m_Corners); if (canvas) { Matrix4x4 mat = canvas.rootCanvas.transform.worldToLocalMatrix; for (int i = 0; i < 4; ++i) m_Corners[i] = mat.MultiplyPoint(m_Corners[i]); } // bounding box is now based on the min and max of all corners (case 1013182) Vector2 min = m_Corners[0]; Vector2 max = m_Corners[0]; for (int i = 1; i < 4; i++) { min.x = Mathf.Min(m_Corners[i].x, min.x); min.y = Mathf.Min(m_Corners[i].y, min.y); max.x = Mathf.Max(m_Corners[i].x, max.x); max.y = Mathf.Max(m_Corners[i].y, max.y); } return new Rect(min, max - min); } } private void UpdateClipParent() { var newParent = (maskable && IsActive()) ? MaskUtilities.GetRectMaskForClippable(this) : null; // if the new parent is different OR is now inactive if (m_ParentMask != null && (newParent != m_ParentMask || !newParent.IsActive())) { m_ParentMask.RemoveClippable(this); UpdateCull(false); } // don't re-add it if the newparent is inactive if (newParent != null && newParent.IsActive()) newParent.AddClippable(this); m_ParentMask = newParent; } /// /// See IClippable.RecalculateClipping /// public virtual void RecalculateClipping() { UpdateClipParent(); } /// /// See IMaskable.RecalculateMasking /// public virtual void RecalculateMasking() { // Remove the material reference as either the graphic of the mask has been enable/ disabled. // This will cause the material to be repopulated from the original if need be. (case 994413) StencilMaterial.Remove(m_MaskMaterial); m_MaskMaterial = null; m_ShouldRecalculateStencil = true; SetMaterialDirty(); } } }