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

246 lines
12 KiB
C#
Raw Normal View History

// ─────────────────────────────────────────────────────────────────────────────
// 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;
}
}
}