360 lines
12 KiB
C#
360 lines
12 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using System.Reflection;
|
|
using TMPro;
|
|
|
|
|
|
#if UNITY_2021_1_OR_NEWER
|
|
using UnityEngine.Pool;
|
|
#endif
|
|
|
|
namespace ACRoundedRectMask
|
|
{
|
|
/// <summary>
|
|
/// Overrides the RectMask2D.PerformClipping method to add extra checks before doing exhaustive culling on
|
|
/// each maskable target.
|
|
/// </summary>
|
|
public class RoundedRectMask2D : RectMask2D
|
|
{
|
|
public static readonly string RadiiPropertyName = "_ClipRectRadii";
|
|
|
|
|
|
[SerializeField]
|
|
private bool independantRadii;
|
|
[Tooltip("The four corner radii of the rounded rect. (x: top left, y: top right, z: bottom left, w: bottom right)")]
|
|
[SerializeField]
|
|
private Vector4 radii = Vector4.one * 10.0f;
|
|
public Vector4 Radii
|
|
{
|
|
get => radii;
|
|
set
|
|
{
|
|
radii = value;
|
|
MaskUtilities.Notify2DMaskStateChanged(this);
|
|
ForceClip = true;
|
|
}
|
|
}
|
|
|
|
[Tooltip("If not set to true, you will need to handle that all masked UI elements have their own material instances")]
|
|
[SerializeField]
|
|
private bool cloneMaskableMaterialsOnStart = true;
|
|
|
|
private static int clipRectRadiiID = 0;
|
|
|
|
|
|
private HashSet<IClippable> clipTargets = null;
|
|
private HashSet<MaskableGraphic> maskableTargets = null;
|
|
private int lastclipTargetsCount = 0;
|
|
private int lastmaskableTargetsCount = 0;
|
|
private bool shouldRecalculateClipRects = false;
|
|
|
|
private Canvas cachedCanvas = null;
|
|
private Vector3[] cachedCorners = new Vector3[4];
|
|
private Rect lastClipRectCanvasSpace = new Rect();
|
|
private Vector2Int lastSoftness = new Vector2Int();
|
|
private List<RectMask2D> clippers = new List<RectMask2D>();
|
|
|
|
#region MonoBehaviour Implementation
|
|
/// <inheritdoc />
|
|
protected override void OnEnable()
|
|
{
|
|
base.OnEnable();
|
|
shouldRecalculateClipRects = true;
|
|
ForceClip = true;
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
/// <inheritdoc />
|
|
protected override void OnValidate()
|
|
{
|
|
base.OnValidate();
|
|
shouldRecalculateClipRects = true;
|
|
ForceClip = true;
|
|
}
|
|
#endif
|
|
/// <inheritdoc />
|
|
protected override void OnDidApplyAnimationProperties()
|
|
{
|
|
base.OnDidApplyAnimationProperties();
|
|
|
|
shouldRecalculateClipRects = true;
|
|
ForceClip = true;
|
|
}
|
|
|
|
#endregion MonoBehaviour Implementation
|
|
|
|
#region RectMask2D Implementation
|
|
|
|
/// <inheritdoc />
|
|
protected override void OnTransformParentChanged()
|
|
{
|
|
base.OnTransformParentChanged();
|
|
shouldRecalculateClipRects = true;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void OnCanvasHierarchyChanged()
|
|
{
|
|
cachedCanvas = null;
|
|
base.OnCanvasHierarchyChanged();
|
|
shouldRecalculateClipRects = true;
|
|
}
|
|
|
|
|
|
protected override void Start()
|
|
{
|
|
base.Start();
|
|
|
|
shouldRecalculateClipRects = true;
|
|
PerformClipping();
|
|
|
|
if (cloneMaskableMaterialsOnStart && maskableTargets != null)
|
|
{
|
|
foreach (MaskableGraphic mg in maskableTargets)
|
|
{
|
|
if (mg.materialForRendering.Equals(mg.material))
|
|
{
|
|
Material m = new Material(mg.material);
|
|
mg.material = m;
|
|
}
|
|
else if (mg is TMP_Text tmpText)
|
|
{
|
|
Material m = new Material(tmpText.fontMaterial);
|
|
tmpText.fontMaterial = m;
|
|
}
|
|
else
|
|
{
|
|
Debug.Log("[RoundedRectMask2d] Can't clone material for " + mg.name + ". This will result in same rounded corners for all assets sharing its materiel " + mg.materialForRendering);
|
|
continue;
|
|
}
|
|
|
|
OnSetClipRect(mg);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Improves the base class method by:
|
|
/// - Checks if the canvas renderer has moved before exhaustive culling.
|
|
/// - Interleaves UpdateClipSoftness so objects are not iterated over twice.
|
|
/// - Adds a OnSetClipRect callback for derived classes to use.
|
|
/// </summary>
|
|
public override void PerformClipping()
|
|
{
|
|
// Not calling the base class method intentionally to provide a more optimal version.
|
|
//base.PerformClipping();
|
|
|
|
if (clipRectRadiiID == 0)
|
|
{
|
|
clipRectRadiiID = Shader.PropertyToID(RadiiPropertyName);
|
|
}
|
|
|
|
|
|
Initialize();
|
|
|
|
if (ReferenceEquals(Canvas, null))
|
|
{
|
|
return;
|
|
}
|
|
|
|
//TODO See if an IsActive() test would work well here or whether it might cause unexpected side effects (re case 776771)
|
|
|
|
// if the parents are changed
|
|
// or something similar we
|
|
// do a recalculate here
|
|
if (shouldRecalculateClipRects || ForceClip)
|
|
{
|
|
MaskUtilities.GetRectMasksForClip(this, clippers);
|
|
shouldRecalculateClipRects = false;
|
|
}
|
|
|
|
// get the compound rects from
|
|
// the clippers that are valid
|
|
bool validRect = true;
|
|
Rect clipRect = Clipping.FindCullAndClipWorldRect(clippers, out validRect);
|
|
|
|
// If the mask is in ScreenSpaceOverlay/Camera render mode, its content is only rendered when its rect
|
|
// overlaps that of the root canvas.
|
|
RenderMode renderMode = Canvas.rootCanvas.renderMode;
|
|
bool maskIsCulled =
|
|
(renderMode == RenderMode.ScreenSpaceCamera || renderMode == RenderMode.ScreenSpaceOverlay) &&
|
|
!clipRect.Overlaps(RootCanvasRect, true);
|
|
|
|
if (maskIsCulled)
|
|
{
|
|
// Children are only displayed when inside the mask. If the mask is culled, then the children
|
|
// inside the mask are also culled. In that situation, we pass an invalid rect to allow callees
|
|
// to avoid some processing.
|
|
clipRect = Rect.zero;
|
|
validRect = false;
|
|
}
|
|
|
|
if (clipRect != lastClipRectCanvasSpace || softness != lastSoftness)
|
|
{
|
|
foreach (IClippable clipTarget in clipTargets)
|
|
{
|
|
clipTarget.SetClipRect(clipRect, validRect);
|
|
clipTarget.SetClipSoftness(softness);
|
|
}
|
|
|
|
foreach (MaskableGraphic maskableTarget in maskableTargets)
|
|
{
|
|
maskableTarget.SetClipRect(clipRect, validRect);
|
|
maskableTarget.SetClipSoftness(softness);
|
|
OnSetClipRect(maskableTarget);
|
|
|
|
maskableTarget.Cull(clipRect, validRect);
|
|
}
|
|
}
|
|
else if (ForceClip)
|
|
{
|
|
foreach (IClippable clipTarget in clipTargets)
|
|
{
|
|
clipTarget.SetClipRect(clipRect, validRect);
|
|
clipTarget.SetClipSoftness(softness);
|
|
}
|
|
|
|
foreach (MaskableGraphic maskableTarget in maskableTargets)
|
|
{
|
|
maskableTarget.SetClipRect(clipRect, validRect);
|
|
maskableTarget.SetClipSoftness(softness);
|
|
OnSetClipRect(maskableTarget);
|
|
|
|
if (maskableTarget.canvasRenderer.hasMoved)
|
|
{
|
|
maskableTarget.Cull(clipRect, validRect);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
foreach (MaskableGraphic maskableTarget in maskableTargets)
|
|
{
|
|
if (!maskableTarget.canvasRenderer.hasMoved)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
maskableTarget.Cull(clipRect, validRect);
|
|
}
|
|
}
|
|
|
|
ForceClip = false;
|
|
lastClipRectCanvasSpace = clipRect;
|
|
lastSoftness = softness;
|
|
}
|
|
|
|
#endregion RectMask2D Implementation
|
|
|
|
public bool ForceClip
|
|
{
|
|
get
|
|
{
|
|
// This is an imprecise check if a clip or mask target gets added then removed on the same frame.
|
|
// But... the alternative is we reflect into m_ForceClip base member which would be a per frame allocation due to it being a value type.
|
|
// If this check is return false negatives in your scenario, then set ForceClip to true.
|
|
return clipTargets.Count != lastclipTargetsCount ||
|
|
maskableTargets.Count != lastmaskableTargetsCount;
|
|
}
|
|
set
|
|
{
|
|
if (value == true)
|
|
{
|
|
lastclipTargetsCount = 0;
|
|
lastmaskableTargetsCount = 0;
|
|
}
|
|
else
|
|
{
|
|
Initialize();
|
|
|
|
lastclipTargetsCount = clipTargets.Count;
|
|
lastmaskableTargetsCount = maskableTargets.Count;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Callback whenever the clip rect is mutated.
|
|
/// </summary>
|
|
protected virtual void OnSetClipRect(IClippable clippable)
|
|
{
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Callback whenever the clip rect is mutated.
|
|
/// </summary>
|
|
protected virtual void OnSetClipRect(MaskableGraphic maskableTarget)
|
|
{
|
|
Material targetMaterial = maskableTarget.materialForRendering;
|
|
|
|
if (targetMaterial != null)
|
|
{
|
|
targetMaterial.SetVector(clipRectRadiiID, Radii);
|
|
}
|
|
|
|
Debug.Log("Setting clip rect for " + maskableTarget.name);
|
|
}
|
|
|
|
private void Initialize()
|
|
{
|
|
// Check if we have already initialized.
|
|
if (clipTargets != null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Many of the properties we need access to for clipping are not exposed. So, we have to do reflection to get access to them.
|
|
BindingFlags bindFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
|
|
clipTargets = (HashSet<IClippable>)typeof(RectMask2D).GetField("m_ClipTargets", bindFlags).GetValue(this);
|
|
maskableTargets = (HashSet<MaskableGraphic>)typeof(RectMask2D).GetField("m_MaskableTargets", bindFlags).GetValue(this);
|
|
}
|
|
|
|
private Canvas Canvas
|
|
{
|
|
get
|
|
{
|
|
if (cachedCanvas == null)
|
|
{
|
|
#if UNITY_2021_1_OR_NEWER
|
|
var list = ListPool<Canvas>.Get();
|
|
gameObject.GetComponentsInParent(false, list);
|
|
if (list.Count > 0)
|
|
cachedCanvas = list[list.Count - 1];
|
|
else
|
|
cachedCanvas = null;
|
|
ListPool<Canvas>.Release(list);
|
|
#else
|
|
var list = gameObject.GetComponentsInParent<Canvas>(false);
|
|
if (list.Length > 0)
|
|
cachedCanvas = list[list.Length - 1];
|
|
else
|
|
cachedCanvas = null;
|
|
#endif
|
|
}
|
|
|
|
return cachedCanvas;
|
|
}
|
|
}
|
|
|
|
private Rect RootCanvasRect
|
|
{
|
|
get
|
|
{
|
|
rectTransform.GetWorldCorners(cachedCorners);
|
|
|
|
if (!ReferenceEquals(Canvas, null))
|
|
{
|
|
Canvas rootCanvas = Canvas.rootCanvas;
|
|
for (int i = 0; i < 4; ++i)
|
|
cachedCorners[i] = rootCanvas.transform.InverseTransformPoint(cachedCorners[i]);
|
|
}
|
|
|
|
return new Rect(cachedCorners[0].x, cachedCorners[0].y, cachedCorners[2].x - cachedCorners[0].x, cachedCorners[2].y - cachedCorners[0].y);
|
|
}
|
|
}
|
|
}
|
|
}
|