using TMPro; using UnityEngine; namespace AibisDream.MiniGame.Language { /// /// Gives one TMP object its own hard rectangular clip material. The screen rect is /// converted from world corners into the TMP object's local coordinate system every /// frame, so clipping remains correct while the text moves, scales or rotates. /// [DisallowMultipleComponent] public sealed class TMPRectClipper : MonoBehaviour { private static readonly int ClipRectId = Shader.PropertyToID("_ClipRect"); private TMP_Text target; private RectTransform screenRect; private Material sourceMaterial; private Material runtimeMaterial; public void Initialize(TMP_Text text, RectTransform rect) { target = text; screenRect = rect; RebuildMaterial(); UpdateClipRect(); } public void SetScreenRect(RectTransform rect) { screenRect = rect; UpdateClipRect(); } private void LateUpdate() { UpdateClipRect(); } public void UpdateClipRect() { if (target == null || screenRect == null) return; if (runtimeMaterial == null) RebuildMaterial(); if (runtimeMaterial == null) return; runtimeMaterial.SetVector(ClipRectId, CalculateLocalClipRect(target.transform, screenRect)); } public static Vector4 CalculateLocalClipRect(Transform textTransform, RectTransform rect) { if (textTransform == null || rect == null) return new Vector4(-32767f, -32767f, 32767f, 32767f); Vector3[] corners = new Vector3[4]; rect.GetWorldCorners(corners); float minX = float.PositiveInfinity; float minY = float.PositiveInfinity; float maxX = float.NegativeInfinity; float maxY = float.NegativeInfinity; for (int i = 0; i < corners.Length; i++) { Vector3 local = textTransform.InverseTransformPoint(corners[i]); minX = Mathf.Min(minX, local.x); minY = Mathf.Min(minY, local.y); maxX = Mathf.Max(maxX, local.x); maxY = Mathf.Max(maxY, local.y); } return new Vector4(minX, minY, maxX, maxY); } public void ReleaseMaterial() { if (target != null && sourceMaterial != null) target.fontSharedMaterial = sourceMaterial; if (runtimeMaterial != null) { if (Application.isPlaying) Destroy(runtimeMaterial); else DestroyImmediate(runtimeMaterial); } runtimeMaterial = null; sourceMaterial = null; } private void RebuildMaterial() { ReleaseMaterial(); if (target == null || target.fontSharedMaterial == null) return; sourceMaterial = target.fontSharedMaterial; runtimeMaterial = new Material(sourceMaterial) { name = $"{sourceMaterial.name} (Screen Clip)", hideFlags = HideFlags.DontSave }; runtimeMaterial.EnableKeyword("MASK_HARD"); target.fontSharedMaterial = runtimeMaterial; } private void OnDestroy() { ReleaseMaterial(); } } }