Project_WL/AgentScripts/WL793_Shot.cs

206 lines
10 KiB
C#

// WL793_Shot.cs — #793~#795 해상도별 실측 + 에디트 모드 렌더 캡처 (읽기 전용: 디스크 에셋 미수정)
//
// 1) unity command run_script --file AgentScripts/WL_GameViewSize.cs --entry WL_GameViewSize.Set --args '[1080,1920]'
// 2) unity command run_script --file AgentScripts/WL793_Shot.cs --entry WL793_Shot.Shot --args '["1080x1920",0]'
// mode 0 = TitleInfo.prefab (실제 타이틀 화면)
// mode 1 = Title.unity 팝업 (btn_ok 검증)
// mode 2 = TitleInfo + 마젠타 클리어 (검정 백드롭이 실제로 덮는지 진단)
//
// 왜 임시 카메라인가: 이 씬의 캔버스는 ScreenSpaceOverlay 이고 씬에 카메라가 아예 없다.
// Overlay 는 런타임 백버퍼에만 합성되므로 capture_game_view 는 에디트 모드에서 캡처가 불가능하다
// (source=screen -> "requires Play Mode", source=camera -> "No camera found").
// => 임시 카메라 + RenderTexture 로 직접 렌더한다.
//
// 안전장치: 이 스크립트는 씬을 절대 저장하지 않는다. 메모리에서만 조작하고 마지막에
// OpenScene 으로 디스크 상태를 다시 불러와 변경을 버린다. Title.unity 해시는 전후 동일해야 한다.
using System.Collections.Generic;
using System.Text;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.UI;
public static class WL793_Shot
{
const string ScenePath = "Assets/Scenes/Title.unity";
const string PrefabPath = "Assets/ResWork/UIPrefabs/Title/TitleInfo.prefab";
const string OutDir = "Screenshots_WL/title";
/// Overlay 캔버스에서 RectTransform 의 월드 코너 = 화면 픽셀. 그 상태로 재야 정확하다.
static Vector2 ScreenSize(RectTransform rt)
{
var c = new Vector3[4];
rt.GetWorldCorners(c);
return new Vector2(Vector3.Distance(c[0], c[3]), Vector3.Distance(c[0], c[1]));
}
static Vector2 ScreenCenter(RectTransform rt)
{
var c = new Vector3[4];
rt.GetWorldCorners(c);
return (c[0] + c[2]) * 0.5f;
}
static void Measure(Transform root, string tag, StringBuilder sb)
{
foreach (var b in root.GetComponentsInChildren<Button>(true))
{
var rt = (RectTransform)b.transform;
var s = ScreenSize(rt);
float min = Mathf.Min(s.x, s.y);
string verdict = min >= 120f ? "OK(권장)" : (min >= 88f ? "OK(최소)" : "미달");
sb.AppendLine(string.Format(" [버튼] {0,-28} {1,7:F1} x {2,7:F1} px 짧은변 {3,6:F1} {4}{5}",
tag + "/" + b.name, s.x, s.y, min, verdict,
b.gameObject.activeInHierarchy ? "" : " (비활성)"));
}
}
static void MeasureBg(Transform root, string tag, StringBuilder sb)
{
var t = root.Find("Title/bg_image") as RectTransform;
if (t == null) { sb.AppendLine(" [배경] " + tag + " bg_image 없음"); return; }
var raw = t.GetComponent<RawImage>();
var s = ScreenSize(t);
var c = ScreenCenter(t);
float srcRatio = raw != null && raw.texture != null ? raw.texture.width / (float)raw.texture.height : 0f;
float shownRatio = s.y > 0f ? s.x / s.y : 0f;
float distort = srcRatio > 0f ? (shownRatio / srcRatio - 1f) * 100f : 0f;
float left = c.x - s.x * 0.5f, right = c.x + s.x * 0.5f;
float bottom = c.y - s.y * 0.5f, top = c.y + s.y * 0.5f;
float marginL = Mathf.Max(0f, left), marginR = Mathf.Max(0f, Screen.width - right);
float marginB = Mathf.Max(0f, bottom), marginT = Mathf.Max(0f, Screen.height - top);
float cropL = Mathf.Max(0f, -left), cropR = Mathf.Max(0f, right - Screen.width);
sb.AppendLine(" [배경] " + tag);
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, s.x, s.y, shownRatio));
sb.AppendLine(string.Format(" 왜곡 {0:F3}% (0 = 종횡비 유지) 높이 채움 {1:F1}%",
distort, Screen.height > 0 ? s.y / Screen.height * 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 / s.x * 100f, cropL / s.x, (cropL + Screen.width) / s.x));
}
public static object Shot(string label, int mode)
{
if (EditorApplication.isPlaying) return "ABORT: Play 중";
var scene = EditorSceneManager.GetActiveScene();
if (scene.path != ScenePath) return "ABORT: 활성 씬 " + scene.path;
int w = Screen.width, h = Screen.height;
var sb = new StringBuilder();
sb.AppendLine("=== " + label + " (Screen " + w + "x" + h + ", mode " + 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 (mode == 0 || mode == 2)
{
if (mainGo != null) mainGo.SetActive(false);
var pf = AssetDatabase.LoadAssetAtPath<GameObject>(PrefabPath);
if (pf == null) return "ABORT: 프리팹 로드 실패";
inst = Object.Instantiate(pf);
inst.name = "TitleInfo_shot";
// 개발용 ID 입력 UI 는 빌드에서 꺼지므로(에디터 전용) 캡처에서도 숨긴다
var dev = inst.transform.Find("InputField (TMP)");
if (dev != null) dev.gameObject.SetActive(false);
}
else if (mode == 1)
{
if (mainGo != null) mainGo.SetActive(true);
var popup = mainGo != null ? mainGo.transform.Find("PopupUI") : null;
if (popup != null) popup.gameObject.SetActive(true);
var slider = mainGo != null ? mainGo.transform.Find("Slider_proc") : null;
if (slider != null) slider.gameObject.SetActive(true);
}
Canvas.ForceUpdateCanvases();
// --- 실측 (Overlay 상태에서: 월드 코너 = 화면 픽셀) ---
foreach (var cv in Object.FindObjectsByType<Canvas>(FindObjectsInactive.Exclude, FindObjectsSortMode.None))
{
var cs = cv.GetComponent<CanvasScaler>();
sb.AppendLine(string.Format(" [캔버스] {0} scaleFactor {1:F4} ref {2} {3} 캔버스rect {4:F0}x{5:F0}",
cv.name, cv.scaleFactor,
cs != null ? cs.referenceResolution.ToString() : "-",
cs != null ? cs.screenMatchMode.ToString() : "-",
((RectTransform)cv.transform).rect.width, ((RectTransform)cv.transform).rect.height));
}
if (inst != null) { MeasureBg(inst.transform, "TitleInfo", sb); Measure(inst.transform, "TitleInfo", sb); }
if (mode == 1 && mainGo != null) { MeasureBg(mainGo.transform, "Title.unity", sb); Measure(mainGo.transform, "Title.unity", sb); }
// --- 렌더 (Overlay 는 에디트 모드에서 캡처 불가 -> 임시 카메라로 전환) ---
var camGo = new GameObject("WL793_ShotCam");
temps.Add(camGo);
cam = camGo.AddComponent<Camera>();
cam.orthographic = true;
cam.clearFlags = CameraClearFlags.SolidColor;
cam.backgroundColor = mode == 2 ? new Color(1f, 0f, 1f, 1f) : Color.black;
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;
foreach (var cv in Object.FindObjectsByType<Canvas>(FindObjectsInactive.Exclude, FindObjectsSortMode.None))
{
if (cv.transform.parent != null && cv.transform.parent.GetComponentInParent<Canvas>() != null) continue;
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;
}
}