246 lines
12 KiB
C#
246 lines
12 KiB
C#
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|||
|
|
// 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;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|