// 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(); 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(); var arf = t.GetComponent(); var fit = t.GetComponent(); 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(); var restore = new List>(); Camera cam = null; RenderTexture rt = null; Texture2D png = null; try { if (mainGo != null) mainGo.SetActive(false); // 아래 깔린 타이틀 씬 캔버스를 잠시 끈다 var pf = AssetDatabase.LoadAssetAtPath(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(); var tex = AssetDatabase.LoadAssetAtPath(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(); var fit = raw != null ? raw.GetComponent() : null; var sync = raw != null ? raw.GetComponent() : 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(); if (fit2 != null) { fit2.Apply(); Canvas.ForceUpdateCanvases(); } } // --- 실측 --- var cv = inst.GetComponent(); var cs = inst.GetComponent(); 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(); 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(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; } }