using UnityEngine; using UnityEngine.Rendering; using UnityEngine.UI; /// /// Inverted stencil mask (see materialForRendering). Raycast options live on /// on the same GameObject (Unity's Image /// inspector does not show fields on Image subclasses). /// /// Parent also implements and only /// allows points inside its RectTransform. A fullscreen child under a small mask would /// otherwise fail the raycast outside that rect (dimmed area lets clicks through) while /// still blocking inside the mask bounds. We skip only Mask/RectMask2D in the raycast /// walk ( with /// ignoreMasks) so hit testing matches the fullscreen Image; rendering still uses stencil /// because stays true. /// [RequireComponent(typeof(CutoutMaskUISettings))] public class CutoutMaskUI : Image { CutoutMaskUISettings settings; Mask cachedParentMask; Graphic cachedMaskGraphic; bool storedMaskGraphicRaycast; bool storedMaskGraphicRaycastValid; public override bool Raycast(Vector2 sp, Camera eventCamera) { return Raycast(sp, eventCamera, ignoreMasks: true); } public override Material materialForRendering { get { Material material = new Material(base.materialForRendering); material.SetInt("_StencilComp", (int)CompareFunction.NotEqual); return material; } } protected override void OnEnable() { settings = GetComponent(); base.OnEnable(); if (settings == null || !settings.disableRaycastOnParentMaskGraphic) return; cachedParentMask = GetComponentInParent(); if (cachedParentMask == null) return; cachedMaskGraphic = cachedParentMask.graphic; if (cachedMaskGraphic == null || cachedMaskGraphic == this) return; storedMaskGraphicRaycastValid = true; storedMaskGraphicRaycast = cachedMaskGraphic.raycastTarget; cachedMaskGraphic.raycastTarget = false; } protected override void OnDisable() { if (storedMaskGraphicRaycastValid && cachedMaskGraphic != null) { cachedMaskGraphic.raycastTarget = storedMaskGraphicRaycast; storedMaskGraphicRaycastValid = false; } cachedMaskGraphic = null; cachedParentMask = null; base.OnDisable(); } public override bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera) { if (IsInsidePassThroughHole(screenPoint, eventCamera)) return settings != null && settings.blockRaycastThroughCutout; return base.IsRaycastLocationValid(screenPoint, eventCamera); } bool IsInsidePassThroughHole(Vector2 screenPoint, Camera eventCamera) { RectTransform[] passThroughRects = settings != null ? settings.raycastPassThroughRects : null; if (passThroughRects != null) { for (int i = 0; i < passThroughRects.Length; i++) { RectTransform rect = passThroughRects[i]; if (rect != null && RectTransformUtility.RectangleContainsScreenPoint(rect, screenPoint, eventCamera)) return true; } } Mask mask = cachedParentMask != null ? cachedParentMask : GetComponentInParent(); if (mask != null && mask.rectTransform != null && RectTransformUtility.RectangleContainsScreenPoint(mask.rectTransform, screenPoint, eventCamera)) return true; return false; } }