257 lines
14 KiB
C#
257 lines
14 KiB
C#
|
|
// WL800_Shot.cs — #800 로딩 화면 해상도별 실측 + 에디트 모드 렌더 캡처 (읽기 전용: 디스크 에셋 미수정)
|
||
|
|
//
|
||
|
|
// 1) unity command run_script --file AgentScripts/WL_GameViewSize.cs --entry WL_GameViewSize.Set --args '[1080,1920]'
|
||
|
|
// 2) unity command run_script --file AgentScripts/WL800_Shot.cs --entry WL800_Shot.Shot --args '["before_1080x1920",0,1]'
|
||
|
|
// args = [라벨, mode, 로딩이미지 인덱스(1~8)]
|
||
|
|
// mode 0 = LoadingUI (배경/슬라이더/텍스트) 1 = PopupUI 2 = OptionInfo 3 = NotiInfo 4 = NetWait
|
||
|
|
// 3) unity command run_script --file AgentScripts/WL800_Shot.cs --entry WL800_Shot.ResetScene
|
||
|
|
//
|
||
|
|
// 왜 임시 카메라인가: SortOrder_5 캔버스는 ScreenSpaceOverlay 이고 Title.unity 에는 카메라가 없다.
|
||
|
|
// Overlay 는 런타임 백버퍼에만 합성되므로 에디트 모드에서 capture_game_view 로는 찍을 수 없다.
|
||
|
|
// => 프리팹을 씬에 임시 인스턴스화하고 캔버스를 ScreenSpaceCamera 로 잠깐 바꿔 RenderTexture 에 직접 렌더한다.
|
||
|
|
//
|
||
|
|
// 안전장치: 씬을 절대 저장하지 않는다. 프리팹도 인스턴스만 조작하고 DestroyImmediate 로 지운다.
|
||
|
|
// (프리팹 인스턴스에 가한 변경은 ApplyPrefabInstance 를 호출하지 않는 한 원본에 반영되지 않는다.)
|
||
|
|
using System.Collections.Generic;
|
||
|
|
using System.Text;
|
||
|
|
using UnityEditor;
|
||
|
|
using UnityEditor.SceneManagement;
|
||
|
|
using UnityEngine;
|
||
|
|
using UnityEngine.UI;
|
||
|
|
|
||
|
|
public static class WL800_Shot
|
||
|
|
{
|
||
|
|
const string ScenePath = "Assets/Scenes/Title.unity";
|
||
|
|
const string PrefabPath = "Assets/ResWork/UIPrefabs/Title/SortOrder_5.prefab";
|
||
|
|
const string TexDir = "Assets/Res_Addr/Loading";
|
||
|
|
const string OutDir = "Screenshots_WL/loading";
|
||
|
|
// run_script 는 파일 단위로 독립 컴파일되므로 WL800_Apply 의 상수를 참조할 수 없다. 값이 같아야 한다.
|
||
|
|
const string BackdropPath = "LoadingUI/child";
|
||
|
|
const string BgPath = "LoadingUI/child/RawImage";
|
||
|
|
|
||
|
|
/// Overlay/ScreenSpace 캔버스에서 RectTransform 의 월드 코너 = 화면 픽셀.
|
||
|
|
static Rect ScreenRect(RectTransform rt)
|
||
|
|
{
|
||
|
|
var c = new Vector3[4];
|
||
|
|
rt.GetWorldCorners(c);
|
||
|
|
float w = Vector3.Distance(c[0], c[3]);
|
||
|
|
float h = Vector3.Distance(c[0], c[1]);
|
||
|
|
var center = (c[0] + c[2]) * 0.5f;
|
||
|
|
return new Rect(center.x - w * 0.5f, center.y - h * 0.5f, w, h);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void MeasureRect(Transform root, string path, StringBuilder sb)
|
||
|
|
{
|
||
|
|
var t = root.Find(path) as RectTransform;
|
||
|
|
if (t == null) { sb.AppendLine(" [" + path + "] 없음"); return; }
|
||
|
|
var r = ScreenRect(t);
|
||
|
|
bool outL = r.xMin < -0.5f, outR = r.xMax > Screen.width + 0.5f;
|
||
|
|
bool outB = r.yMin < -0.5f, outT = r.yMax > Screen.height + 0.5f;
|
||
|
|
string clip = (outL || outR || outB || outT)
|
||
|
|
? (" !! 화면밖" + (outL ? " 좌" + (-r.xMin).ToString("F0") : "") + (outR ? " 우" + (r.xMax - Screen.width).ToString("F0") : "")
|
||
|
|
+ (outB ? " 하" + (-r.yMin).ToString("F0") : "") + (outT ? " 상" + (r.yMax - Screen.height).ToString("F0") : ""))
|
||
|
|
: "";
|
||
|
|
var tmp = t.GetComponent<TMPro.TextMeshProUGUI>();
|
||
|
|
string font = tmp != null ? (" 글자 " + (tmp.fontSize * t.lossyScale.y).ToString("F1") + "px") : "";
|
||
|
|
sb.AppendLine(string.Format(" [{0,-22}] {1,7:F1} x {2,7:F1} px 좌하({3,7:F1},{4,7:F1}) 하단여백 {5,6:F1}px{6}{7}",
|
||
|
|
path, r.width, r.height, r.xMin, r.yMin, r.yMin, font, clip));
|
||
|
|
}
|
||
|
|
|
||
|
|
static void MeasureBg(Transform root, StringBuilder sb)
|
||
|
|
{
|
||
|
|
var t = root.Find(BgPath) as RectTransform;
|
||
|
|
if (t == null) { sb.AppendLine(" [배경] 경로 없음"); return; }
|
||
|
|
var raw = t.GetComponent<RawImage>();
|
||
|
|
var arf = t.GetComponent<AspectRatioFitter>();
|
||
|
|
var fit = t.GetComponent<WLBackgroundFit>();
|
||
|
|
var r = ScreenRect(t);
|
||
|
|
|
||
|
|
float srcRatio = raw != null && raw.texture != null ? raw.texture.width / (float)raw.texture.height : 0f;
|
||
|
|
float shownRatio = r.height > 0f ? r.width / r.height : 0f;
|
||
|
|
float distort = srcRatio > 0f ? (shownRatio / srcRatio - 1f) * 100f : 0f;
|
||
|
|
|
||
|
|
float marginL = Mathf.Max(0f, r.xMin), marginR = Mathf.Max(0f, Screen.width - r.xMax);
|
||
|
|
float marginB = Mathf.Max(0f, r.yMin), marginT = Mathf.Max(0f, Screen.height - r.yMax);
|
||
|
|
float cropL = Mathf.Max(0f, -r.xMin), cropR = Mathf.Max(0f, r.xMax - Screen.width);
|
||
|
|
|
||
|
|
sb.AppendLine(" [배경] AspectRatioFitter=" + (arf != null ? (arf.aspectMode + "/" + arf.aspectRatio.ToString("F4")) : "없음")
|
||
|
|
+ " WLBackgroundFit=" + (fit != null ? ("focusX " + fit.focusX.ToString("F2")) : "없음"));
|
||
|
|
sb.AppendLine(string.Format(" 원본 {0}x{1} (종횡비 {2:F4}) -> 화면 출력 {3:F1} x {4:F1} px (종횡비 {5:F4})",
|
||
|
|
raw != null && raw.texture != null ? raw.texture.width : 0,
|
||
|
|
raw != null && raw.texture != null ? raw.texture.height : 0,
|
||
|
|
srcRatio, r.width, r.height, shownRatio));
|
||
|
|
sb.AppendLine(string.Format(" 왜곡 {0:F3}% (0 = 종횡비 유지) 높이 채움 {1:F1}% 폭 채움 {2:F1}%",
|
||
|
|
distort, Screen.height > 0 ? r.height / Screen.height * 100f : 0f,
|
||
|
|
Screen.width > 0 ? Mathf.Min(r.width, Screen.width) / Screen.width * 100f : 0f));
|
||
|
|
sb.AppendLine(string.Format(" 검정 여백 좌 {0:F0} 우 {1:F0} 상 {2:F0} 하 {3:F0} px | 화면밖 크롭 좌 {4:F0} 우 {5:F0} px",
|
||
|
|
marginL, marginR, marginT, marginB, cropL, cropR));
|
||
|
|
if (cropL + cropR > 0f)
|
||
|
|
sb.AppendLine(string.Format(" -> 원본 가로의 {0:F1}% 만 보인다 (보이는 구간 x = {1:F3} ~ {2:F3})",
|
||
|
|
Screen.width / r.width * 100f, cropL / r.width, (cropL + Screen.width) / r.width));
|
||
|
|
}
|
||
|
|
|
||
|
|
static readonly string[] Modes = { "LoadingUI", "PopupUI", "OptionInfo", "NotiInfo", "NetWait" };
|
||
|
|
|
||
|
|
public static object Shot(string label, int mode, int imgIndex)
|
||
|
|
{
|
||
|
|
if (EditorApplication.isPlaying) return "ABORT: Play 중";
|
||
|
|
var scene = EditorSceneManager.GetActiveScene();
|
||
|
|
if (scene.path != ScenePath) return "ABORT: 활성 씬 " + scene.path + " (기대 " + ScenePath + ")";
|
||
|
|
if (mode < 0 || mode >= Modes.Length) return "ABORT: mode 범위 0~" + (Modes.Length - 1);
|
||
|
|
|
||
|
|
int w = Screen.width, h = Screen.height;
|
||
|
|
var sb = new StringBuilder();
|
||
|
|
sb.AppendLine("=== " + label + " (Screen " + w + "x" + h + ", " + Modes[mode] + ") ===");
|
||
|
|
|
||
|
|
var mainGo = GameObject.Find("MainToTitle");
|
||
|
|
GameObject inst = null;
|
||
|
|
var temps = new List<Object>();
|
||
|
|
var restore = new List<KeyValuePair<Canvas, Camera>>();
|
||
|
|
Camera cam = null; RenderTexture rt = null; Texture2D png = null;
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
if (mainGo != null) mainGo.SetActive(false); // 아래 깔린 타이틀 씬 캔버스를 잠시 끈다
|
||
|
|
|
||
|
|
var pf = AssetDatabase.LoadAssetAtPath<GameObject>(PrefabPath);
|
||
|
|
if (pf == null) return "ABORT: 프리팹 로드 실패 " + PrefabPath;
|
||
|
|
inst = Object.Instantiate(pf);
|
||
|
|
inst.name = "SortOrder_5_shot";
|
||
|
|
|
||
|
|
// 대상 서브트리만 켠다
|
||
|
|
var target = inst.transform.Find(Modes[mode]);
|
||
|
|
if (target == null) return "ABORT: " + Modes[mode] + " 없음";
|
||
|
|
target.gameObject.SetActive(true);
|
||
|
|
|
||
|
|
if (mode == 0)
|
||
|
|
{
|
||
|
|
var child = inst.transform.Find(BackdropPath);
|
||
|
|
if (child == null) return "ABORT: " + BackdropPath + " 없음";
|
||
|
|
child.gameObject.SetActive(true); // LoadingUI.Start_Loading() 과 동일 상태
|
||
|
|
|
||
|
|
// 런타임엔 Addressables 가 넣는 텍스처를 에디트 모드에서 수동 주입(인스턴스 한정)
|
||
|
|
int idx = Mathf.Clamp(imgIndex, 1, 8);
|
||
|
|
var raw = inst.transform.Find(BgPath)?.GetComponent<RawImage>();
|
||
|
|
var tex = AssetDatabase.LoadAssetAtPath<Texture2D>(TexDir + "/Loading" + idx + ".jpg");
|
||
|
|
if (raw != null && tex != null) raw.texture = tex;
|
||
|
|
sb.AppendLine(" 미리보기 텍스처 = Loading" + idx + ".jpg");
|
||
|
|
|
||
|
|
// 런타임에 LoadingUI 가 하는 일(장별 초점 적용)을 에디트 모드에서도 재현한다
|
||
|
|
var lui = inst.transform.Find("LoadingUI")?.GetComponent<LoadingUI>();
|
||
|
|
var fit = raw != null ? raw.GetComponent<WLBackgroundFit>() : null;
|
||
|
|
var sync = raw != null ? raw.GetComponent<WLRawImageAspectSync>() : null;
|
||
|
|
if (sync != null) sync.Refresh(); // 종횡비를 새 텍스처에서 다시 읽는다
|
||
|
|
if (lui != null && fit != null && lui.focusX_perImage != null && idx - 1 < lui.focusX_perImage.Length)
|
||
|
|
{
|
||
|
|
fit.focusX = lui.focusX_perImage[idx - 1];
|
||
|
|
sb.AppendLine(" 장별 초점 적용 = " + fit.focusX.ToString("F2"));
|
||
|
|
}
|
||
|
|
if (fit != null) fit.Apply();
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
// 팝업류는 내용이 비활성 "child" 아래에 있다
|
||
|
|
var body = target.Find("child");
|
||
|
|
if (body != null) body.gameObject.SetActive(true);
|
||
|
|
}
|
||
|
|
|
||
|
|
Canvas.ForceUpdateCanvases();
|
||
|
|
LayoutRebuilder.ForceRebuildLayoutImmediate((RectTransform)inst.transform);
|
||
|
|
Canvas.ForceUpdateCanvases();
|
||
|
|
|
||
|
|
// WLBackgroundFit 은 rect 가 확정된 뒤라야 오프셋을 제대로 clamp 한다 -> 레이아웃 후 한 번 더
|
||
|
|
if (mode == 0)
|
||
|
|
{
|
||
|
|
var fit2 = inst.transform.Find(BgPath)?.GetComponent<WLBackgroundFit>();
|
||
|
|
if (fit2 != null) { fit2.Apply(); Canvas.ForceUpdateCanvases(); }
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- 실측 ---
|
||
|
|
var cv = inst.GetComponent<Canvas>();
|
||
|
|
var cs = inst.GetComponent<CanvasScaler>();
|
||
|
|
sb.AppendLine(string.Format(" [캔버스] scaleFactor {0:F4} ref {1} {2} 캔버스rect {3:F0}x{4:F0}",
|
||
|
|
cv.scaleFactor, cs != null ? cs.referenceResolution.ToString() : "-",
|
||
|
|
cs != null ? cs.screenMatchMode.ToString() : "-",
|
||
|
|
((RectTransform)inst.transform).rect.width, ((RectTransform)inst.transform).rect.height));
|
||
|
|
|
||
|
|
if (mode == 0)
|
||
|
|
{
|
||
|
|
MeasureBg(inst.transform, sb);
|
||
|
|
sb.AppendLine(" [로딩 UI 요소]");
|
||
|
|
MeasureRect(inst.transform, "LoadingUI/child/botimg", sb);
|
||
|
|
MeasureRect(inst.transform, "LoadingUI/child/mark", sb);
|
||
|
|
MeasureRect(inst.transform, "LoadingUI/child/Slider_", sb);
|
||
|
|
MeasureRect(inst.transform, "LoadingUI/child/Slider_/t_proc", sb);
|
||
|
|
MeasureRect(inst.transform, "LoadingUI/child/Slider_/handle", sb);
|
||
|
|
MeasureRect(inst.transform, "LoadingUI/child/tip", sb);
|
||
|
|
MeasureRect(inst.transform, "LoadingUI/child/tip/tip", sb);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
sb.AppendLine(" [" + Modes[mode] + " 요소]");
|
||
|
|
var body = target.Find("child") ?? target;
|
||
|
|
string bp = Modes[mode] + (body == target ? "" : "/child");
|
||
|
|
if (body.childCount == 0) sb.AppendLine(" (자식 없음 — 런타임 생성 UI 로 보임)");
|
||
|
|
foreach (Transform c in body)
|
||
|
|
MeasureRect(inst.transform, bp + "/" + c.name, sb);
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- 렌더 (Overlay 는 에디트 모드 캡처 불가 -> 임시 카메라) ---
|
||
|
|
var camGo = new GameObject("WL800_ShotCam");
|
||
|
|
temps.Add(camGo);
|
||
|
|
cam = camGo.AddComponent<Camera>();
|
||
|
|
cam.orthographic = true;
|
||
|
|
cam.clearFlags = CameraClearFlags.SolidColor;
|
||
|
|
cam.backgroundColor = new Color(1f, 0f, 1f, 1f); // 마젠타 = "아무것도 안 그린 영역" 진단색
|
||
|
|
cam.nearClipPlane = 0.1f; cam.farClipPlane = 1000f;
|
||
|
|
cam.transform.position = new Vector3(0f, 0f, -500f);
|
||
|
|
|
||
|
|
rt = new RenderTexture(w, h, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
|
||
|
|
rt.Create();
|
||
|
|
cam.targetTexture = rt;
|
||
|
|
|
||
|
|
restore.Add(new KeyValuePair<Canvas, Camera>(cv, cv.worldCamera));
|
||
|
|
cv.renderMode = RenderMode.ScreenSpaceCamera;
|
||
|
|
cv.worldCamera = cam;
|
||
|
|
cv.planeDistance = 100f;
|
||
|
|
Canvas.ForceUpdateCanvases();
|
||
|
|
|
||
|
|
cam.Render();
|
||
|
|
|
||
|
|
var prev = RenderTexture.active;
|
||
|
|
RenderTexture.active = rt;
|
||
|
|
png = new Texture2D(w, h, TextureFormat.RGBA32, false);
|
||
|
|
png.ReadPixels(new Rect(0, 0, w, h), 0, 0);
|
||
|
|
png.Apply();
|
||
|
|
RenderTexture.active = prev;
|
||
|
|
|
||
|
|
System.IO.Directory.CreateDirectory(OutDir);
|
||
|
|
var outPath = OutDir + "/" + label + ".png";
|
||
|
|
System.IO.File.WriteAllBytes(outPath, png.EncodeToPNG());
|
||
|
|
sb.AppendLine(" 캡처 -> " + outPath);
|
||
|
|
}
|
||
|
|
finally
|
||
|
|
{
|
||
|
|
foreach (var kv in restore)
|
||
|
|
if (kv.Key != null) { kv.Key.renderMode = RenderMode.ScreenSpaceOverlay; kv.Key.worldCamera = kv.Value; }
|
||
|
|
if (cam != null) cam.targetTexture = null;
|
||
|
|
if (rt != null) { rt.Release(); Object.DestroyImmediate(rt); }
|
||
|
|
if (png != null) Object.DestroyImmediate(png);
|
||
|
|
if (inst != null) Object.DestroyImmediate(inst);
|
||
|
|
foreach (var o in temps) if (o != null) Object.DestroyImmediate(o);
|
||
|
|
if (mainGo != null) mainGo.SetActive(true);
|
||
|
|
}
|
||
|
|
|
||
|
|
return sb.ToString();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// 캡처 패스가 끝난 뒤 씬을 디스크 상태로 되돌린다(저장하지 않았으므로 변경 폐기).
|
||
|
|
public static object ResetScene()
|
||
|
|
{
|
||
|
|
if (EditorApplication.isPlaying) return "ABORT: Play 중";
|
||
|
|
EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single);
|
||
|
|
var s = EditorSceneManager.GetActiveScene();
|
||
|
|
return "reload " + s.path + " isDirty=" + s.isDirty + " rootCount=" + s.rootCount;
|
||
|
|
}
|
||
|
|
}
|