Project_WL/Assets/WL/UI/Scripts/WLVignetteUtil.cs

246 lines
12 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// WLVignetteUtil.cs — 화면 가장자리 UI 비네트 조립 헬퍼 + Safe Area 내성 경로 탐색
// (WL-813tj · 813t/813j 공용 · #813)
//
// ■ 왜 UI 이미지인가
// 기준서 §D-1 813t·813j 가 "포스트 프로세싱 금지 → UI 이미지" 를 못 박았다.
// URP Volume(Vignette)은 모바일에서 풀스크린 패스를 하나 더 만들고, 이 프로젝트는 UI 캔버스가 주인이다.
//
// ■ 왜 풀스크린 이미지가 아니라 "가장자리 4띠" 인가 (모바일 필레이트)
// 반투명 풀스크린 쿼드 = 화면 전체 블렌드 1패스. 실제로 보이는 것은 가장자리뿐이므로
// 상·하·좌·우 4개 띠(두께 = 설정 px)만 그린다 → 덮는 면적이 두께에 비례해서만 는다.
// 부드러운 감쇠는 **런타임 생성 그라디언트 스프라이트**(32×4 / 4×32)로 낸다 — 아트 에셋 의존 0.
// 코너는 가로띠와 세로띠가 겹쳐 자연히 진해진다(비네트로서 옳은 방향).
//
// ■ 좌표 규약
// 좌 띠 = 좌측 세로 스트레치(폭 T) · 우 띠 = 같은 것을 localScale.x = 1 로 뒤집음
// 하 띠 = 하단 가로 스트레치(높이 T) · 상 띠 = localScale.y = 1
// → 스프라이트는 2장(가로 램프 · 세로 램프)만 있으면 되고, 회전이 없어 rect 가 그대로 잰다.
//
// 🔴 어셈블리 주의: Assets/WL/UI/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일).
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
using UnityEngine.UI;
namespace WL.UI
{
/// <summary>가장자리 비네트 4띠 묶음. 만들기·크기 맞추기·알파 넣기만 한다(수명 관리는 호출부).</summary>
public sealed class WLEdgeVignette
{
public RectTransform root;
public readonly Image[] strips = new Image[4]; // 0 좌 · 1 우 · 2 하 · 3 상
public bool IsBuilt { get { return root != null && strips[0] != null && strips[3] != null; } }
/// <summary>알파 0~1 을 4띠에 한 번에 넣는다. 0 이면 Image 를 꺼서 드로우콜까지 없앤다.</summary>
public void SetAlpha(Color baseColor, float t)
{
float a = baseColor.a * Mathf.Clamp01(t);
bool on = a > 0.001f;
for (int i = 0; i < strips.Length; i++)
{
var img = strips[i];
if (img == null) continue;
if (img.enabled != on) img.enabled = on;
// 꺼도 색까지 0 으로 내린다 — 덤프가 옛 알파를 남겨 오해를 부르지 않게.
img.color = new Color(baseColor.r, baseColor.g, baseColor.b, on ? a : 0f);
}
}
/// <summary>현재 첫 띠의 알파(진단용).</summary>
public float CurrentAlpha { get { return strips[0] != null && strips[0].enabled ? strips[0].color.a : 0f; } }
}
public static class WLVignetteUtil
{
public const int UILayer = 5;
// ── 런타임 생성 그라디언트 스프라이트(에셋 의존 0 · 정적 캐시) ────────
private static Sprite s_hRamp; // u=0 에서 알파 1 → u=1 에서 0
private static Sprite s_vRamp; // v=0 에서 알파 1 → v=1 에서 0
/// <summary>가로 램프(왼쪽이 진하다). 좌/우 띠가 쓴다.</summary>
public static Sprite HorizontalRamp { get { if (s_hRamp == null) s_hRamp = BuildRamp(32, 4, true); return s_hRamp; } }
/// <summary>세로 램프(아래가 진하다). 하/상 띠가 쓴다.</summary>
public static Sprite VerticalRamp { get { if (s_vRamp == null) s_vRamp = BuildRamp(4, 32, false); return s_vRamp; } }
private static Sprite BuildRamp(int w, int h, bool horizontal)
{
var tex = new Texture2D(w, h, TextureFormat.RGBA32, false);
tex.name = "WL_VignetteRamp_" + (horizontal ? "H" : "V");
tex.wrapMode = TextureWrapMode.Clamp;
tex.filterMode = FilterMode.Bilinear;
tex.hideFlags = HideFlags.DontSave;
int n = horizontal ? w : h;
var px = new Color32[w * h];
for (int y = 0; y < h; y++)
for (int x = 0; x < w; x++)
{
float t = (n <= 1) ? 0f : (float)(horizontal ? x : y) / (n - 1);
// (1-t)^2 = 바깥이 진하고 안쪽으로 빠르게 사라지는 감쇠(선형보다 덜 답답하다)
float a = (1f - t); a *= a;
px[y * w + x] = new Color32(255, 255, 255, (byte)Mathf.RoundToInt(Mathf.Clamp01(a) * 255f));
}
tex.SetPixels32(px);
tex.Apply(false, false);
var sp = Sprite.Create(tex, new Rect(0, 0, w, h), new Vector2(0.5f, 0.5f), 100f, 0, SpriteMeshType.FullRect);
sp.name = tex.name;
sp.hideFlags = HideFlags.DontSave;
return sp;
}
// ── 조립 ──────────────────────────────────────────────────────────────
/// <summary>부모 아래에 비네트 4띠를 만든다(이미 있으면 재사용). 루트는 전체 스트레치.</summary>
public static WLEdgeVignette Build(RectTransform parent, string rootName)
{
var v = new WLEdgeVignette();
if (parent == null) return v;
var rootT = parent.Find(rootName) as RectTransform;
if (rootT == null)
{
var go = new GameObject(rootName, typeof(RectTransform));
go.layer = parent.gameObject.layer;
rootT = (RectTransform)go.transform;
rootT.SetParent(parent, false);
}
Stretch(rootT);
rootT.localScale = Vector3.one;
v.root = rootT;
v.strips[0] = EnsureStrip(rootT, "Left");
v.strips[1] = EnsureStrip(rootT, "Right");
v.strips[2] = EnsureStrip(rootT, "Bottom");
v.strips[3] = EnsureStrip(rootT, "Top");
return v;
}
private static Image EnsureStrip(RectTransform parent, string childName)
{
var t = parent.Find(childName) as RectTransform;
if (t == null)
{
var go = new GameObject(childName, typeof(RectTransform));
go.layer = parent.gameObject.layer;
t = (RectTransform)go.transform;
t.SetParent(parent, false);
}
var img = t.GetComponent<Image>();
if (img == null) img = t.gameObject.AddComponent<Image>();
img.raycastTarget = false; // 비네트는 절대 입력을 먹지 않는다
img.type = Image.Type.Simple;
img.preserveAspect = false;
return img;
}
/// <summary>두께(px)·색을 4띠에 적용한다. unitsPerPx = 813c WLHudLayoutSettings.UnitsPerPx.</summary>
public static void ApplyLayout(WLEdgeVignette v, float thicknessPx, float unitsPerPx, Color color)
{
if (v == null || !v.IsBuilt) return;
float t = Mathf.Max(0f, thicknessPx) * unitsPerPx;
Place(v.strips[0].rectTransform, new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(0f, 0.5f), new Vector2(t, 0f), new Vector3(1f, 1f, 1f));
Place(v.strips[1].rectTransform, new Vector2(1f, 0f), new Vector2(1f, 1f), new Vector2(1f, 0.5f), new Vector2(t, 0f), new Vector3(-1f, 1f, 1f));
Place(v.strips[2].rectTransform, new Vector2(0f, 0f), new Vector2(1f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, t), new Vector3(1f, 1f, 1f));
Place(v.strips[3].rectTransform, new Vector2(0f, 1f), new Vector2(1f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, t), new Vector3(1f, -1f, 1f));
v.strips[0].sprite = HorizontalRamp;
v.strips[1].sprite = HorizontalRamp;
v.strips[2].sprite = VerticalRamp;
v.strips[3].sprite = VerticalRamp;
v.SetAlpha(color, 0f); // 만들자마자 보이지 않게
}
private static void Place(RectTransform rt, Vector2 aMin, Vector2 aMax, Vector2 pivot, Vector2 size, Vector3 scale)
{
rt.anchorMin = aMin; rt.anchorMax = aMax; rt.pivot = pivot;
rt.anchoredPosition = Vector2.zero;
rt.sizeDelta = size;
rt.localScale = scale;
}
public static void Stretch(RectTransform rt)
{
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
rt.pivot = new Vector2(0.5f, 0.5f);
}
/// <summary>RectTransform 자식 하나를 만들거나 찾는다(공통 조립용).</summary>
public static RectTransform NewChild(RectTransform parent, string childName)
{
var t = parent != null ? parent.Find(childName) as RectTransform : null;
if (t != null) { t.localScale = Vector3.one; return t; }
var go = new GameObject(childName, typeof(RectTransform));
go.layer = parent != null ? parent.gameObject.layer : UILayer;
t = (RectTransform)go.transform;
t.SetParent(parent, false);
t.localScale = Vector3.one;
return t;
}
// ── Safe Area 내성 경로 탐색 (WL-813tj Safe Area 재부모화 회귀 방지) ──
/// <summary>
/// "A/B/C" 경로를 찾되, 한 단계가 없으면 **이름이 safeAreaPrefix 로 시작하는 자식 아래**에서 한 번 더 본다.
/// 813tj 가 NewGameUI 캔버스 직속 5레이어를 WL_SafeArea_* 패널 아래로 옮겼기 때문에,
/// 옛 경로 문자열(예: "MessageInfo/Chat" · "Common/MyInfoUI/Slider_hp")이 그대로 살아 있어야 한다.
/// 비활성 자식도 따라간다(Transform.Find 와 달리 구분자 공백 허용).
/// </summary>
public const string SafeAreaPrefix = "WL_SafeArea";
public static Transform FindUiPath(Transform root, string path)
{
if (root == null || string.IsNullOrEmpty(path)) return null;
var parts = path.Split('/');
var cur = root;
for (int i = 0; i < parts.Length; i++)
{
var name = parts[i].Trim();
if (name.Length == 0) continue;
var next = FindDirectChild(cur, name);
if (next == null)
{
// Safe Area 패널을 한 겹 건너뛰고 다시 본다(패널은 순수 컨테이너라 경로에 없다).
for (int c = 0; c < cur.childCount && next == null; c++)
{
var panel = cur.GetChild(c);
if (panel.name.StartsWith(SafeAreaPrefix)) next = FindDirectChild(panel, name);
}
}
if (next == null) return null;
cur = next;
}
return cur;
}
private static Transform FindDirectChild(Transform parent, string name)
{
for (int c = 0; c < parent.childCount; c++)
if (parent.GetChild(c).name == name) return parent.GetChild(c);
return null;
}
/// <summary>이 UI 트리의 캔버스 루트(NewGameUI)를 찾는다.</summary>
public static Transform FindCanvasRoot(Transform any)
{
if (any == null) return null;
var canvas = any.GetComponentInParent<Canvas>(true);
if (canvas != null) return canvas.transform;
var cur = any;
while (cur.parent != null) cur = cur.parent;
return cur;
}
/// <summary>813c 와 같은 px→유닛 계수. 설정 에셋이 없으면 1(배치를 건너뛰라는 뜻).</summary>
public static float UnitsPerPx(Canvas canvas)
{
var s = WLHudLayoutSettings.Instance;
return s != null ? s.UnitsPerPx(canvas) : 1f;
}
}
}