624 lines
34 KiB
C#
624 lines
34 KiB
C#
|
|
// WL-814a 프로브 — 도트 룩 스위치 검증 + 실측 표 + 룩 캡처 4단 (게임 코드 아님 · 에디트 모드 전용)
|
|||
|
|
// run: unity command run_script --file AgentScripts/WL814a_Probe.cs --entry WL814a_Probe.RunAll
|
|||
|
|
// cap: unity command run_script --file AgentScripts/WL814a_Probe.cs --entry WL814a_Probe.Capture
|
|||
|
|
// 🔴 파이프라인 에셋에 SetDirty 를 부르지 않는다 · 임시 씬은 저장하지 않는다 · 에셋 파일 수정 0.
|
|||
|
|
|
|||
|
|
using System;
|
|||
|
|
using System.Collections.Generic;
|
|||
|
|
using System.IO;
|
|||
|
|
using System.Text;
|
|||
|
|
using UnityEditor;
|
|||
|
|
using UnityEditor.SceneManagement;
|
|||
|
|
using UnityEngine;
|
|||
|
|
using UnityEngine.Rendering;
|
|||
|
|
using UnityEngine.Rendering.Universal;
|
|||
|
|
using UnityEngine.SceneManagement;
|
|||
|
|
using UnityEngine.UI;
|
|||
|
|
using WL.Look;
|
|||
|
|
|
|||
|
|
public static class WL814a_Probe
|
|||
|
|
{
|
|||
|
|
const string kOutDir = @"E:\NerdNavis\nn_himminji\Screenshots_WL\WL814a";
|
|||
|
|
const string kTxt = "AgentScripts/WL814a_PROBE.txt";
|
|||
|
|
const string kTxtCap = "AgentScripts/WL814a_CAPTURE.txt";
|
|||
|
|
|
|||
|
|
static StringBuilder s_sb;
|
|||
|
|
static int s_pass, s_fail;
|
|||
|
|
static Camera s_uiCam;
|
|||
|
|
static RectTransform s_uiCanvas;
|
|||
|
|
|
|||
|
|
static void L(string s) { s_sb.AppendLine(s); Debug.Log("[WL814a] " + s); }
|
|||
|
|
static void Chk(string name, bool ok, string detail)
|
|||
|
|
{
|
|||
|
|
if (ok) s_pass++; else s_fail++;
|
|||
|
|
L((ok ? " PASS " : " FAIL ") + name + " — " + detail);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═════════════════════════════════════════════════════════════════════════
|
|||
|
|
// RunAll — 실측 표 + 적용/복원 + C8 + GC
|
|||
|
|
// ═════════════════════════════════════════════════════════════════════════
|
|||
|
|
public static void RunAll()
|
|||
|
|
{
|
|||
|
|
s_sb = new StringBuilder(); s_pass = 0; s_fail = 0;
|
|||
|
|
L("WL-814a 프로브 · " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " · Unity " + Application.unityVersion);
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
A_Table();
|
|||
|
|
B_ApplyRestore();
|
|||
|
|
C_C8();
|
|||
|
|
D_Gc();
|
|||
|
|
}
|
|||
|
|
catch (Exception ex) { s_fail++; L("EXCEPTION " + ex); }
|
|||
|
|
finally
|
|||
|
|
{
|
|||
|
|
// 🔴 무슨 일이 있어도 원래 값으로
|
|||
|
|
PixelLook.RestoreNow("probe-final");
|
|||
|
|
WLPixelLookSettings.RuntimeDisabled = false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
L("");
|
|||
|
|
L("RESULT " + (s_fail == 0 ? "PASS" : "FAIL") + " · " + s_pass + "/" + (s_pass + s_fail) + " · FAIL " + s_fail);
|
|||
|
|
Write();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static void Write() { Write(kTxt); }
|
|||
|
|
|
|||
|
|
static void Write(string path)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
File.WriteAllText(path, s_sb.ToString(), new UTF8Encoding(false));
|
|||
|
|
Directory.CreateDirectory(kOutDir);
|
|||
|
|
File.WriteAllText(Path.Combine(kOutDir, "probe_" + DateTime.Now.ToString("HHmmss") + ".txt"),
|
|||
|
|
s_sb.ToString(), new UTF8Encoding(false));
|
|||
|
|
}
|
|||
|
|
catch (Exception ex) { Debug.Log("[WL814a] 파일 쓰기 실패 " + ex.Message); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ───────────────────────────────────────────── ⓐ 실측 표
|
|||
|
|
static void A_Table()
|
|||
|
|
{
|
|||
|
|
L("");
|
|||
|
|
L("ⓐ 실측 표");
|
|||
|
|
|
|||
|
|
// ① Quality → URP 에셋 (ProjectSettings/QualitySettings.asset 원문 파싱 · 읽기만)
|
|||
|
|
string qsPath = "ProjectSettings/QualitySettings.asset";
|
|||
|
|
string[] qs = File.Exists(qsPath) ? File.ReadAllLines(qsPath) : new string[0];
|
|||
|
|
int perPlatCount = 0; bool perPlatEmpty = false;
|
|||
|
|
var qNames = new List<string>(); var qGuids = new List<string>();
|
|||
|
|
foreach (var raw in qs)
|
|||
|
|
{
|
|||
|
|
string t = raw.Trim();
|
|||
|
|
if (t.StartsWith("m_PerPlatformDefaultQuality:"))
|
|||
|
|
perPlatEmpty = t.EndsWith("{}");
|
|||
|
|
else if (t.StartsWith("name: ") && raw.StartsWith(" name: ")) qNames.Add(t.Substring(6));
|
|||
|
|
else if (t.StartsWith("customRenderPipeline:"))
|
|||
|
|
{
|
|||
|
|
int g = t.IndexOf("guid: ");
|
|||
|
|
qGuids.Add(g >= 0 ? t.Substring(g + 6).TrimEnd(',', ' ') : "-");
|
|||
|
|
}
|
|||
|
|
else if (perPlatEmpty == false && t.StartsWith("- first:")) perPlatCount++;
|
|||
|
|
}
|
|||
|
|
L(" ① m_PerPlatformDefaultQuality = " + (perPlatEmpty ? "{} (비어 있음 → 플랫폼별 지정 0 = 전 플랫폼이 m_CurrentQuality 레벨을 쓴다)" : perPlatCount + "개"));
|
|||
|
|
|
|||
|
|
string[] names = QualitySettings.names;
|
|||
|
|
int cur = QualitySettings.GetQualityLevel();
|
|||
|
|
for (int i = 0; i < names.Length; i++)
|
|||
|
|
{
|
|||
|
|
string g = i < qGuids.Count ? qGuids[i] : "-";
|
|||
|
|
string p = g != "-" ? AssetDatabase.GUIDToAssetPath(g) : "";
|
|||
|
|
L(" level " + i + " '" + names[i] + "'" + (i == cur ? " ← m_CurrentQuality(현재)" : "") +
|
|||
|
|
" → " + (string.IsNullOrEmpty(p) ? "(없음 → Graphics 기본)" : System.IO.Path.GetFileNameWithoutExtension(p)));
|
|||
|
|
}
|
|||
|
|
var qrp = QualitySettings.renderPipeline as UniversalRenderPipelineAsset;
|
|||
|
|
L(" QualitySettings.renderPipeline(현재 레벨 오버라이드) = " + (qrp != null ? qrp.name : "(없음)"));
|
|||
|
|
var def = GraphicsSettings.defaultRenderPipeline as UniversalRenderPipelineAsset;
|
|||
|
|
var act = GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset;
|
|||
|
|
L(" GraphicsSettings.defaultRenderPipeline = " + (def != null ? def.name : "(없음)"));
|
|||
|
|
L(" 🔴 GraphicsSettings.currentRenderPipeline(= 실제 활성) = " + (act != null ? act.name : "(없음)") +
|
|||
|
|
" path=" + (act != null ? AssetDatabase.GetAssetPath(act) : "-"));
|
|||
|
|
Chk("A1 활성 URP 에셋 존재", act != null, act != null ? act.name : "null");
|
|||
|
|
|
|||
|
|
// ② 각 URP 에셋 값
|
|||
|
|
foreach (var g in AssetDatabase.FindAssets("t:UniversalRenderPipelineAsset", new[] { "Assets/Settings" }))
|
|||
|
|
{
|
|||
|
|
string p = AssetDatabase.GUIDToAssetPath(g);
|
|||
|
|
var a = AssetDatabase.LoadAssetAtPath<UniversalRenderPipelineAsset>(p);
|
|||
|
|
if (a == null) continue;
|
|||
|
|
L(" ② " + a.name.PadRight(20) + " renderScale=" + a.renderScale +
|
|||
|
|
" · upscalingFilter=" + a.upscalingFilter +
|
|||
|
|
" · MSAA=" + a.msaaSampleCount +
|
|||
|
|
" · HDR=" + (a.supportsHDR ? 1 : 0) +
|
|||
|
|
" · mainLightShadow=" + (a.supportsMainLightShadows ? 1 : 0) +
|
|||
|
|
" · shadowDist=" + a.shadowDistance +
|
|||
|
|
(a == act ? " ← 활성" : ""));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ③ 인게임 UI 캔버스 렌더 모드
|
|||
|
|
var ui = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Res_Addr/MainUI/NewGameUI.prefab");
|
|||
|
|
if (ui != null)
|
|||
|
|
{
|
|||
|
|
var canvases = ui.GetComponentsInChildren<Canvas>(true);
|
|||
|
|
L(" ③ NewGameUI.prefab Canvas " + canvases.Length + "개");
|
|||
|
|
foreach (var c in canvases)
|
|||
|
|
L(" - " + HierPath(c.transform) + " renderMode=" + c.renderMode +
|
|||
|
|
" worldCamera=" + (c.worldCamera != null ? c.worldCamera.name : "null") +
|
|||
|
|
" planeDistance=" + c.planeDistance + " sortingOrder=" + c.sortingOrder);
|
|||
|
|
var scaler = ui.GetComponentInChildren<CanvasScaler>(true);
|
|||
|
|
if (scaler != null)
|
|||
|
|
L(" CanvasScaler ref=" + scaler.referenceResolution + " mode=" + scaler.uiScaleMode +
|
|||
|
|
" match=" + scaler.screenMatchMode + " → 1080×1920 scaleFactor=" +
|
|||
|
|
Mathf.Min(1080f / scaler.referenceResolution.x, 1920f / scaler.referenceResolution.y).ToString("0.####"));
|
|||
|
|
var uiCam = ui.GetComponentInChildren<Camera>(true);
|
|||
|
|
if (uiCam != null)
|
|||
|
|
{
|
|||
|
|
var d = uiCam.GetUniversalAdditionalCameraData();
|
|||
|
|
L(" UI 카메라 '" + uiCam.name + "' renderType=" + (d != null ? d.renderType.ToString() : "?") +
|
|||
|
|
" depth=" + uiCam.depth + " clear=" + uiCam.clearFlags + " ortho=" + uiCam.orthographic +
|
|||
|
|
" cullingMask=" + uiCam.cullingMask);
|
|||
|
|
Chk("A3 UI 카메라 = URP Overlay(= 베이스 카메라 스택에 들어간다 → 렌더 스케일 영향 받음)",
|
|||
|
|
d != null && d.renderType == CameraRenderType.Overlay, d != null ? d.renderType.ToString() : "?");
|
|||
|
|
}
|
|||
|
|
bool anyOverlayCanvas = false, anyWorld = false;
|
|||
|
|
foreach (var c in canvases)
|
|||
|
|
{
|
|||
|
|
if (c.renderMode == RenderMode.ScreenSpaceOverlay) anyOverlayCanvas = true;
|
|||
|
|
if (c.renderMode == RenderMode.WorldSpace) anyWorld = true;
|
|||
|
|
}
|
|||
|
|
Chk("A3b 인게임 루트 캔버스 = ScreenSpaceCamera(Overlay 아님)", !anyOverlayCanvas, "Overlay 캔버스 " + (anyOverlayCanvas ? "있음" : "0"));
|
|||
|
|
Chk("A4 월드 공간 캔버스 0", !anyWorld, anyWorld ? "있음" : "0");
|
|||
|
|
|
|||
|
|
// 스킬 버튼(카드) 크기 — 캡처 표의 「스킬 버튼 영역」 근거
|
|||
|
|
foreach (var mb in ui.GetComponentsInChildren<BattleUI>(true))
|
|||
|
|
{
|
|||
|
|
if (mb.skillCards == null) continue;
|
|||
|
|
for (int i = 0; i < mb.skillCards.Length && i < 2; i++)
|
|||
|
|
{
|
|||
|
|
var rt = mb.skillCards[i] != null ? mb.skillCards[i].GetComponent<RectTransform>() : null;
|
|||
|
|
if (rt != null) L(" 스킬카드[" + i + "] sizeDelta=" + rt.sizeDelta + " (유닛) → 1080×1920 화면 px = " +
|
|||
|
|
(rt.sizeDelta * 0.5625f));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
else Chk("A3 NewGameUI.prefab 로드", false, "없음");
|
|||
|
|
|
|||
|
|
// ④ 월드 UI(HP 바 · 데미지 숫자) 방식
|
|||
|
|
var hud = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Res_Addr/Ingame/HUDUI.prefab");
|
|||
|
|
var dmg = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Res_Addr/Ingame/HUDDMGUI.prefab");
|
|||
|
|
L(" ④ HUDUI.prefab Canvas=" + (hud != null ? hud.GetComponentsInChildren<Canvas>(true).Length : -1) +
|
|||
|
|
" · HUDDMGUI.prefab Canvas=" + (dmg != null ? dmg.GetComponentsInChildren<Canvas>(true).Length : -1) +
|
|||
|
|
" (0 = 자체 캔버스 없음 = 부모 캔버스[NewGameUI]를 그대로 쓴다 · HUDBase 가 WorldToViewportPoint 로 anchoredPosition 만 옮긴다)");
|
|||
|
|
Chk("A4b HP바/데미지숫자 = 스크린 공간(자체 캔버스 0)",
|
|||
|
|
hud != null && dmg != null && hud.GetComponentsInChildren<Canvas>(true).Length == 0 && dmg.GetComponentsInChildren<Canvas>(true).Length == 0,
|
|||
|
|
"HUDUI/HUDDMGUI 자체 Canvas 0");
|
|||
|
|
|
|||
|
|
// ⑤ 카메라 스택
|
|||
|
|
var camPrefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/ResWork/Prefabs/Ingame/Main Camera.prefab");
|
|||
|
|
if (camPrefab != null)
|
|||
|
|
{
|
|||
|
|
var c = camPrefab.GetComponentInChildren<Camera>(true);
|
|||
|
|
var d = c.GetUniversalAdditionalCameraData();
|
|||
|
|
L(" ⑤ Main Camera.prefab renderType=" + d.renderType + " · cameraStack(프리팹)=" + d.cameraStack.Count +
|
|||
|
|
" · postProcessing=" + d.renderPostProcessing + " · antialiasing=" + d.antialiasing +
|
|||
|
|
" · fov=" + c.fieldOfView + " · ortho=" + c.orthographic + " · rendererIndex(프리팹 직렬화 −1 = 기본)");
|
|||
|
|
L(" 🔴 런타임 스택 = NewGameUI.Start() 가 `Camera.main…cameraStack.Add(UICamera)` (Assets/Script/UI/NewGameUI.cs:38) → Base 1 + Overlay 1");
|
|||
|
|
Chk("A5 Main Camera = Base", d.renderType == CameraRenderType.Base, d.renderType.ToString());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static string HierPath(Transform t)
|
|||
|
|
{
|
|||
|
|
string s = t.name;
|
|||
|
|
while (t.parent != null) { t = t.parent; s = t.name + "/" + s; }
|
|||
|
|
return s;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ───────────────────────────────────────────── ⓑ 적용 / 복원
|
|||
|
|
static void B_ApplyRestore()
|
|||
|
|
{
|
|||
|
|
L("");
|
|||
|
|
L("ⓑ 적용 / 복원 (에디트 모드 · 가짜 경계 = ApplyNow/RestoreNow 직접 호출)");
|
|||
|
|
WLPixelLookSettings.ClearCache();
|
|||
|
|
var cfg = WLPixelLookSettings.Instance;
|
|||
|
|
Chk("B0 SO 로드", cfg != null, cfg != null ? "targetHeightPx=" + cfg.targetHeightPx + " filter=" + cfg.filter +
|
|||
|
|
" applyOnlyInBattleMap=" + cfg.applyOnlyInBattleMap + " restoreOnExit=" + cfg.restoreOnExit : "null");
|
|||
|
|
if (cfg == null) return;
|
|||
|
|
|
|||
|
|
PixelLook.ResetForProbe();
|
|||
|
|
var asset = PixelLook.CurrentAsset;
|
|||
|
|
if (asset == null) { Chk("B1 활성 에셋", false, "null"); return; }
|
|||
|
|
|
|||
|
|
float before = asset.renderScale;
|
|||
|
|
var beforeF = asset.upscalingFilter;
|
|||
|
|
L(" 전 : " + asset.name + " renderScale=" + before + " filter=" + beforeF);
|
|||
|
|
|
|||
|
|
bool applied = PixelLook.ApplyNow(cfg.targetHeightPx, 1920, "probe");
|
|||
|
|
L(" 후 : renderScale=" + asset.renderScale + " filter=" + asset.upscalingFilter);
|
|||
|
|
Chk("B1 적용됨", applied && Mathf.Approximately(asset.renderScale, 0.25f) && asset.upscalingFilter == UpscalingFilterSelection.Point,
|
|||
|
|
"renderScale=" + asset.renderScale + " (기대 0.25 = 480/1920) · filter=" + asset.upscalingFilter);
|
|||
|
|
Chk("B2 원래 값 기억", Mathf.Approximately(PixelLook.OriginalRenderScale, before) && PixelLook.OriginalFilter == beforeF,
|
|||
|
|
"orig=" + PixelLook.OriginalRenderScale + "/" + PixelLook.OriginalFilter);
|
|||
|
|
|
|||
|
|
// 4단 스케일 산식
|
|||
|
|
foreach (int px in new[] { 1920, 640, 480, 360 })
|
|||
|
|
L(" 산식: targetHeightPx " + px + " → renderScale " + PixelLook.ComputeRenderScale(px, 1920).ToString("0.####") +
|
|||
|
|
" → 실제 그려지는 세로 픽셀 " + Mathf.RoundToInt(1920f * PixelLook.ComputeRenderScale(px, 1920)));
|
|||
|
|
|
|||
|
|
// 하한 클램프
|
|||
|
|
L(" 클램프: 96px/1920 = " + (96f / 1920f).ToString("0.####") + " → " + PixelLook.ComputeRenderScale(96, 1920).ToString("0.####") + " (하한 0.1)");
|
|||
|
|
Chk("B3 하한 0.1 클램프", Mathf.Approximately(PixelLook.ComputeRenderScale(96, 1920), 0.1f), PixelLook.ComputeRenderScale(96, 1920).ToString());
|
|||
|
|
|
|||
|
|
bool restored = PixelLook.RestoreNow("probe");
|
|||
|
|
L(" 복원: renderScale=" + asset.renderScale + " filter=" + asset.upscalingFilter);
|
|||
|
|
Chk("B4 복원 프로브 — renderScale == 원래값", restored && Mathf.Approximately(asset.renderScale, before),
|
|||
|
|
asset.renderScale + " == " + before);
|
|||
|
|
Chk("B5 복원 프로브 — upscalingFilter == 원래값", asset.upscalingFilter == beforeF,
|
|||
|
|
asset.upscalingFilter + " == " + beforeF);
|
|||
|
|
Chk("B6 이중 복원 무해", !PixelLook.RestoreNow("probe-again"), "두 번째 RestoreNow = false(무동작)");
|
|||
|
|
|
|||
|
|
// 🔴 디스크 오염 검사 — 에셋이 dirty 로 표시되지 않았는가
|
|||
|
|
bool dirty = EditorUtility.IsDirty(asset);
|
|||
|
|
Chk("B7 🔴 파이프라인 에셋 dirty 아님(= Save Project 해도 디스크에 안 써진다)", !dirty, "IsDirty=" + dirty);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ───────────────────────────────────────────── ⓒ C8
|
|||
|
|
static void C_C8()
|
|||
|
|
{
|
|||
|
|
L("");
|
|||
|
|
L("ⓒ C8 롤백");
|
|||
|
|
var cfg = WLPixelLookSettings.Instance;
|
|||
|
|
var asset = PixelLook.CurrentAsset;
|
|||
|
|
if (cfg == null || asset == null) return;
|
|||
|
|
|
|||
|
|
float before = asset.renderScale;
|
|||
|
|
WLPixelLookSettings.RuntimeDisabled = true; // enabled=0 과 같은 경로(에셋 파일은 안 건드린다)
|
|||
|
|
bool applied = PixelLook.ApplyNow(480, 1920, "c8");
|
|||
|
|
Chk("C1 스위치 off → 적용 0", !applied && Mathf.Approximately(asset.renderScale, before),
|
|||
|
|
"applied=" + applied + " renderScale=" + asset.renderScale);
|
|||
|
|
Chk("C2 스위치 off → Enabled=false", !WLPixelLookSettings.Enabled, WLPixelLookSettings.Enabled.ToString());
|
|||
|
|
WLPixelLookSettings.RuntimeDisabled = false;
|
|||
|
|
|
|||
|
|
// 러너: 에디트 모드에서는 EnsureRunner 가 오브젝트를 만들지 않는다
|
|||
|
|
int before2 = CountRunners();
|
|||
|
|
PixelLook.EnsureRunnerForProbe();
|
|||
|
|
int after2 = CountRunners();
|
|||
|
|
Chk("C3 에디트 모드 러너 오브젝트 0", before2 == 0 && after2 == 0, before2 + " → " + after2);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static int CountRunners()
|
|||
|
|
{
|
|||
|
|
int n = 0;
|
|||
|
|
foreach (var go in Resources.FindObjectsOfTypeAll<GameObject>())
|
|||
|
|
if (go.name == "[WL814a] PixelLookRunner") n++;
|
|||
|
|
return n;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ───────────────────────────────────────────── ⓓ GC
|
|||
|
|
static void D_Gc()
|
|||
|
|
{
|
|||
|
|
L("");
|
|||
|
|
L("ⓓ GC 0 (Tick × 200k · 에디트 모드라 PollBoundary 는 즉시 반환하는 경로)");
|
|||
|
|
var cfg = WLPixelLookSettings.Instance;
|
|||
|
|
if (cfg == null) return;
|
|||
|
|
|
|||
|
|
PixelLook.TickForProbe(); // 워밍
|
|||
|
|
GC.Collect();
|
|||
|
|
long m0 = UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong();
|
|||
|
|
long t0 = GC.GetTotalMemory(false);
|
|||
|
|
int g0 = GC.CollectionCount(0);
|
|||
|
|
for (int i = 0; i < 200000; i++) PixelLook.TickForProbe();
|
|||
|
|
long m1 = UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong();
|
|||
|
|
long t1 = GC.GetTotalMemory(false);
|
|||
|
|
int g1 = GC.CollectionCount(0);
|
|||
|
|
Chk("D1 Tick ×200k 할당 0", (m1 - m0) == 0 && (t1 - t0) == 0 && (g1 - g0) == 0,
|
|||
|
|
"monoΔ=" + (m1 - m0) + "B · totalΔ=" + (t1 - t0) + "B · gen0=" + (g1 - g0));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═════════════════════════════════════════════════════════════════════════
|
|||
|
|
// Capture — 임시 씬(저장 0) · 1080×1920 · 4단
|
|||
|
|
// ═════════════════════════════════════════════════════════════════════════
|
|||
|
|
public static void Capture()
|
|||
|
|
{
|
|||
|
|
s_sb = new StringBuilder(); s_pass = 0; s_fail = 0;
|
|||
|
|
L("WL-814a 룩 캡처 · " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
|
|||
|
|
|
|||
|
|
Scene temp = default(Scene);
|
|||
|
|
s_uiCam = null; s_uiCanvas = null;
|
|||
|
|
Camera cam = null;
|
|||
|
|
var asset = PixelLook.CurrentAsset;
|
|||
|
|
float origScale = asset != null ? asset.renderScale : 1f;
|
|||
|
|
var origFilter = asset != null ? asset.upscalingFilter : UpscalingFilterSelection.Auto;
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
Directory.CreateDirectory(kOutDir);
|
|||
|
|
temp = EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects, NewSceneMode.Single);
|
|||
|
|
|
|||
|
|
// 기본 카메라 제거(우리 프리팹 카메라만 쓴다)
|
|||
|
|
foreach (var go in temp.GetRootGameObjects())
|
|||
|
|
if (go.GetComponent<Camera>() != null) UnityEngine.Object.DestroyImmediate(go);
|
|||
|
|
|
|||
|
|
// 맵
|
|||
|
|
var map = Inst("Assets/Res_Addr/Map/WL_Nature.prefab", Vector3.zero);
|
|||
|
|
Chk("E0 맵 배치", map != null, "WL_Nature");
|
|||
|
|
|
|||
|
|
// PC 위치 = 811o 와 같은 기준점(Portal_Start) 앞 · 지면 스냅
|
|||
|
|
Vector3 anchor = Vector3.zero;
|
|||
|
|
string anchorName = "(origin)";
|
|||
|
|
if (map != null)
|
|||
|
|
foreach (var t in map.GetComponentsInChildren<Transform>(true))
|
|||
|
|
if (t.name == "Portal_Start") { anchor = t.position; anchorName = "Portal_Start"; break; }
|
|||
|
|
|
|||
|
|
Vector3 pcPos = anchor + new Vector3(0f, 0f, 6f); // 포탈 앞 6 m(존1 방향)
|
|||
|
|
RaycastHit hit;
|
|||
|
|
if (Physics.Raycast(pcPos + Vector3.up * 200f, Vector3.down, out hit, 500f)) pcPos = hit.point;
|
|||
|
|
L(" PC 위치(" + anchorName + " " + anchor + " 앞 6 m · 지면 스냅) = " + pcPos);
|
|||
|
|
|
|||
|
|
// 카메라(811o 구도 h 76 · d 3 · up 2.4 · rotateAround 233 · m_LookAt (0,1.06,0))
|
|||
|
|
var camGo = Inst("Assets/ResWork/Prefabs/Ingame/Main Camera.prefab", Vector3.zero);
|
|||
|
|
cam = camGo != null ? camGo.GetComponentInChildren<Camera>(true) : null;
|
|||
|
|
Chk("E3 카메라 프리팹", cam != null, camGo != null ? camGo.name : "null");
|
|||
|
|
if (cam == null) return;
|
|||
|
|
|
|||
|
|
// 에디트 모드에서 원본 스크립트가 돌지 않게(Update 는 어차피 안 돈다) 카메라 스크립트만 비활성
|
|||
|
|
foreach (var mb in camGo.GetComponentsInChildren<MonoBehaviour>(true))
|
|||
|
|
if (mb != null && !(mb is UniversalAdditionalCameraData)) mb.enabled = false;
|
|||
|
|
|
|||
|
|
const float h = 76f, dAway = 3f, dUp = 2.4f, yaw = 233f;
|
|||
|
|
Vector3 rotateVector = Quaternion.Euler(h, yaw, 0f) * Vector3.one;
|
|||
|
|
Vector3 lookAt = new Vector3(0f, 1.06f, 0f);
|
|||
|
|
cam.transform.position = pcPos + Vector3.up * dUp - rotateVector * dAway;
|
|||
|
|
cam.transform.LookAt(pcPos + lookAt);
|
|||
|
|
|
|||
|
|
// 카메라가 보는 수평 방향 = 「PC 앞」
|
|||
|
|
Vector3 fwd = pcPos - cam.transform.position; fwd.y = 0f; fwd.Normalize();
|
|||
|
|
Vector3 right = Vector3.Cross(Vector3.up, fwd);
|
|||
|
|
L(" 카메라 = " + cam.transform.position + " → look " + (pcPos + lookAt) +
|
|||
|
|
" · fov " + cam.fieldOfView + " · 811o(h 76 · d 3 · up 2.4 · yaw 233) · 화면 앞 방향 " + fwd);
|
|||
|
|
|
|||
|
|
var pc = Inst("Assets/Res_Addr/PC/Ai01.prefab", pcPos);
|
|||
|
|
if (pc != null) pc.transform.rotation = Quaternion.LookRotation(fwd, Vector3.up);
|
|||
|
|
Chk("E1 PC 배치", pc != null, "Ai01 @ " + pcPos);
|
|||
|
|
|
|||
|
|
// 잡몹 3(존1 = MonsterList 101/102/103 → Mob/Batty_A·B·C · f_DefaultScale 1.5) — PC 앞(화면 안쪽)
|
|||
|
|
string[] mobs = { "Batty_A", "Batty_B", "Batty_C" };
|
|||
|
|
Vector2[] off = { new Vector2(3.2f, 1.6f), new Vector2(4.6f, -2.0f), new Vector2(6.4f, 0.6f) }; // (앞, 옆)
|
|||
|
|
var mobGos = new List<GameObject>();
|
|||
|
|
for (int i = 0; i < mobs.Length; i++)
|
|||
|
|
{
|
|||
|
|
Vector3 p = pcPos + fwd * off[i].x + right * off[i].y;
|
|||
|
|
if (Physics.Raycast(p + Vector3.up * 50f, Vector3.down, out hit, 200f)) p = hit.point;
|
|||
|
|
var m = Inst("Assets/Res_Addr/Mobs/Mob/" + mobs[i] + ".prefab", p);
|
|||
|
|
if (m != null) { m.transform.localScale = Vector3.one * 1.5f; m.transform.rotation = Quaternion.LookRotation(-fwd, Vector3.up); mobGos.Add(m); }
|
|||
|
|
}
|
|||
|
|
Chk("E2 잡몹 배치", mobGos.Count == 3, mobGos.Count + "/3 (Batty_A/B/C · scale 1.5 · PC 앞 3~6 m)");
|
|||
|
|
|
|||
|
|
// T 포즈 방지 — 컨트롤러의 Idle 클립을 0.5 s 지점으로 샘플(에디트 모드)
|
|||
|
|
Pose(pc); foreach (var m in mobGos) Pose(m);
|
|||
|
|
|
|||
|
|
// 🔴 UI 도 실제 게임과 같은 경로로 얹는다: Overlay UI 카메라 + ScreenSpaceCamera 캔버스(ref 1920×1080 Expand)
|
|||
|
|
// = 렌더 스케일이 UI 까지 픽셀화하는지 눈으로 확인하기 위한 것(실측 표 ③ 결론의 그림 증거).
|
|||
|
|
bool uiOk = BuildUiMock(cam);
|
|||
|
|
Chk("E3b UI 목업(화면 픽셀 1:1 · 스킬카드 65px × 4 + HP 바)", uiOk, uiOk ? "배치됨" : "실패");
|
|||
|
|
|
|||
|
|
// 캡처 4단
|
|||
|
|
int[] steps = { 1920, 640, 480, 360 };
|
|||
|
|
var rows = new List<string>();
|
|||
|
|
foreach (int px in steps)
|
|||
|
|
{
|
|||
|
|
float scale = px / 1920f;
|
|||
|
|
if (asset != null) { asset.renderScale = Mathf.Clamp(scale, 0.1f, 1f); asset.upscalingFilter = UpscalingFilterSelection.Point; }
|
|||
|
|
string file = System.IO.Path.Combine(kOutDir, "look_" + px + ".png");
|
|||
|
|
string row = Shot(cam, pc, mobGos, file, px, asset != null ? asset.renderScale : 1f);
|
|||
|
|
rows.Add(row);
|
|||
|
|
Chk("E4 look_" + px + ".png", File.Exists(file), new FileInfo(file).Length + " B");
|
|||
|
|
}
|
|||
|
|
L("");
|
|||
|
|
L(" | targetHeightPx | renderScale | 실제 그려진 해상도 | PC 키(px) | 잡몹 키(px) | 스킬버튼 한 변(px) |");
|
|||
|
|
L(" |---|---|---|---|---|---|");
|
|||
|
|
foreach (var r in rows) L(" " + r);
|
|||
|
|
}
|
|||
|
|
catch (Exception ex) { s_fail++; L("EXCEPTION " + ex); }
|
|||
|
|
finally
|
|||
|
|
{
|
|||
|
|
if (asset != null) { asset.renderScale = origScale; asset.upscalingFilter = origFilter; } // 🔴 복원 · SetDirty 0
|
|||
|
|
L("");
|
|||
|
|
L(" 복원: renderScale=" + (asset != null ? asset.renderScale.ToString() : "-") +
|
|||
|
|
" filter=" + (asset != null ? asset.upscalingFilter.ToString() : "-") +
|
|||
|
|
" · dirty=" + (asset != null ? EditorUtility.IsDirty(asset).ToString() : "-"));
|
|||
|
|
// 임시 씬은 저장하지 않는다(커밋 0)
|
|||
|
|
try { EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); } catch { }
|
|||
|
|
}
|
|||
|
|
L("");
|
|||
|
|
L("RESULT " + (s_fail == 0 ? "PASS" : "FAIL") + " · " + s_pass + "/" + (s_pass + s_fail) + " · FAIL " + s_fail);
|
|||
|
|
Write(kTxtCap);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>Animator 컨트롤러의 Idle 계열 클립을 0.5 s 로 샘플해서 T 포즈를 없앤다(에디트 모드 · 애니메이터 상태 변경 0).</summary>
|
|||
|
|
static void Pose(GameObject go)
|
|||
|
|
{
|
|||
|
|
if (go == null) return;
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
var an = go.GetComponentInChildren<Animator>(true);
|
|||
|
|
if (an == null || an.runtimeAnimatorController == null) return;
|
|||
|
|
var clips = an.runtimeAnimatorController.animationClips;
|
|||
|
|
AnimationClip pick = null;
|
|||
|
|
foreach (var c in clips)
|
|||
|
|
{
|
|||
|
|
if (c == null) continue;
|
|||
|
|
if (c.name.ToLower().Contains("idle")) { pick = c; break; }
|
|||
|
|
if (pick == null) pick = c;
|
|||
|
|
}
|
|||
|
|
if (pick != null) pick.SampleAnimation(an.gameObject, 0.5f);
|
|||
|
|
}
|
|||
|
|
catch { }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// UI 목업 — 화면 픽셀 1:1 로 배치한 캔버스.
|
|||
|
|
/// 🔴 실제 게임 경로(URP Overlay 카메라 + ScreenSpaceCamera 캔버스)는 **에디트 모드 Camera.Render 에서 스택이 그려지지 않아**
|
|||
|
|
/// 캡처에 안 나온다(이 함수 안에서 실측 · 로그로 남긴다). 그래서 캡처용으로만 캔버스를 베이스 카메라 앞
|
|||
|
|
/// 월드 공간에 두고 화면 픽셀과 1:1 이 되게 스케일한다 — 같은 렌더 타겟(= 같은 renderScale)에 그려지므로
|
|||
|
|
/// 「렌더 스케일이 UI 를 어떻게 만드는가」는 실제와 같다.
|
|||
|
|
/// </summary>
|
|||
|
|
static bool BuildUiMock(Camera baseCam)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
int realUiLayer = LayerMask.NameToLayer("UI"); if (realUiLayer < 0) realUiLayer = 5;
|
|||
|
|
bool baseSeesUi = (baseCam.cullingMask & (1 << realUiLayer)) != 0;
|
|||
|
|
L(" 🔴 인게임 베이스 카메라 cullingMask = " + baseCam.cullingMask + " · UI 레이어(" + realUiLayer + ") 포함? " +
|
|||
|
|
(baseSeesUi ? "예" : "아니오 → UI 는 반드시 Overlay UI 카메라가 그린다 = 같은 스택 = 같은 렌더 스케일"));
|
|||
|
|
L(" UI 경로(실기) = ScreenSpaceCamera 캔버스 + Overlay 카메라 스택 · 캡처 목업은 베이스 카메라가 보는 레이어 0 에 화면 픽셀 1:1 로 둔다");
|
|||
|
|
int uiLayer = 0; // 목업 전용(베이스 카메라 cullingMask 안)
|
|||
|
|
|
|||
|
|
// 화면 픽셀 1:1 — 카메라 앞 dist 에서 세로 1920 px 이 정확히 프러스텀 높이가 되게 스케일
|
|||
|
|
const float dist = 2.0f;
|
|||
|
|
float worldH = 2f * dist * Mathf.Tan(baseCam.fieldOfView * 0.5f * Mathf.Deg2Rad);
|
|||
|
|
float unit = worldH / 1920f;
|
|||
|
|
|
|||
|
|
var canvasGo = new GameObject("UI_Mock(1080x1920)", typeof(RectTransform), typeof(Canvas));
|
|||
|
|
canvasGo.layer = uiLayer;
|
|||
|
|
var canvas = canvasGo.GetComponent<Canvas>();
|
|||
|
|
canvas.renderMode = RenderMode.WorldSpace;
|
|||
|
|
canvas.worldCamera = baseCam;
|
|||
|
|
var crt = canvasGo.GetComponent<RectTransform>();
|
|||
|
|
crt.SetParent(baseCam.transform, false);
|
|||
|
|
crt.sizeDelta = new Vector2(1080f, 1920f);
|
|||
|
|
crt.localPosition = new Vector3(0f, 0f, dist);
|
|||
|
|
crt.localRotation = Quaternion.identity;
|
|||
|
|
crt.localScale = Vector3.one * unit;
|
|||
|
|
|
|||
|
|
// 스킬 카드 4개 — 실측 115 유닛 × NewGameUI Expand 계수 0.5625 = 화면 64.7 px
|
|||
|
|
float card = 115f * 0.5625f;
|
|||
|
|
for (int i = 0; i < 4; i++)
|
|||
|
|
{
|
|||
|
|
var c = Quad(canvasGo.transform, uiLayer, new Vector2(card, card),
|
|||
|
|
new Vector2(1f, 0f), new Vector2(-40f - i * (card + 14f), 90f), new Color(0.12f, 0.14f, 0.20f, 0.95f));
|
|||
|
|
Quad(c.transform, uiLayer, new Vector2(card - 9f, card - 9f), new Vector2(0.5f, 0.5f), Vector2.zero,
|
|||
|
|
new Color(0.85f, 0.72f, 0.28f, 1f));
|
|||
|
|
}
|
|||
|
|
// 상단 HP 바(760×34 유닛 → 428×19 px)
|
|||
|
|
var hp = Quad(canvasGo.transform, uiLayer, new Vector2(760f * 0.5625f, 34f * 0.5625f), new Vector2(0.5f, 1f),
|
|||
|
|
new Vector2(0f, -60f), new Color(0.10f, 0.10f, 0.12f, 0.9f));
|
|||
|
|
Quad(hp.transform, uiLayer, new Vector2(520f * 0.5625f, 22f * 0.5625f), new Vector2(0f, 0.5f),
|
|||
|
|
new Vector2(70f, 0f), new Color(0.90f, 0.21f, 0.16f, 1f));
|
|||
|
|
|
|||
|
|
s_uiCanvas = crt;
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
catch (Exception ex) { L(" UI 목업 실패 " + ex.Message); return false; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static GameObject Quad(Transform parent, int layer, Vector2 size, Vector2 anchor, Vector2 pos, Color col)
|
|||
|
|
{
|
|||
|
|
var go = new GameObject("q", typeof(RectTransform), typeof(Image));
|
|||
|
|
go.layer = layer;
|
|||
|
|
var rt = go.GetComponent<RectTransform>();
|
|||
|
|
rt.SetParent(parent, false);
|
|||
|
|
rt.anchorMin = rt.anchorMax = anchor;
|
|||
|
|
rt.pivot = anchor;
|
|||
|
|
rt.sizeDelta = size;
|
|||
|
|
rt.anchoredPosition = pos;
|
|||
|
|
var img = go.GetComponent<Image>();
|
|||
|
|
img.sprite = null; img.color = col; img.raycastTarget = false;
|
|||
|
|
return go;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static GameObject Inst(string path, Vector3 pos)
|
|||
|
|
{
|
|||
|
|
var src = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
|||
|
|
if (src == null) { L(" 프리팹 없음 " + path); return null; }
|
|||
|
|
var go = (GameObject)PrefabUtility.InstantiatePrefab(src);
|
|||
|
|
go.transform.position = pos;
|
|||
|
|
return go;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static string Shot(Camera cam, GameObject pc, List<GameObject> mobs, string file, int targetPx, float scale)
|
|||
|
|
{
|
|||
|
|
var rt = new RenderTexture(1080, 1920, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
|
|||
|
|
rt.antiAliasing = 1;
|
|||
|
|
rt.Create();
|
|||
|
|
var prev = cam.targetTexture;
|
|||
|
|
cam.targetTexture = rt;
|
|||
|
|
// 🔴 ScreenSpaceCamera 캔버스는 worldCamera.pixelRect 로 크기를 정한다 → 오버레이 카메라에도 같은 RT 를 물려
|
|||
|
|
// 1080×1920 기준으로 배치시킨다(URP 는 오버레이의 targetTexture 를 무시하고 베이스의 타겟에 그린다).
|
|||
|
|
if (s_uiCam != null) s_uiCam.targetTexture = rt;
|
|||
|
|
Canvas.ForceUpdateCanvases();
|
|||
|
|
cam.Render();
|
|||
|
|
|
|||
|
|
var active = RenderTexture.active;
|
|||
|
|
RenderTexture.active = rt;
|
|||
|
|
var tex = new Texture2D(1080, 1920, TextureFormat.RGB24, false);
|
|||
|
|
tex.ReadPixels(new Rect(0, 0, 1080, 1920), 0, 0);
|
|||
|
|
tex.Apply();
|
|||
|
|
RenderTexture.active = active;
|
|||
|
|
|
|||
|
|
File.WriteAllBytes(file, tex.EncodeToPNG());
|
|||
|
|
|
|||
|
|
// 화면 픽셀 측정(전체 해상도 기준) → × renderScale = 실제 그려진 픽셀
|
|||
|
|
float pcPx = ScreenHeightPx(cam, pc);
|
|||
|
|
float mobPx = mobs.Count > 0 ? ScreenHeightPx(cam, mobs[0]) : 0f;
|
|||
|
|
float skillPx = SkillButtonPx(); // 스킬 카드 실측 유닛 × 0.5625(NewGameUI Expand 계수)
|
|||
|
|
|
|||
|
|
// 색 분산(빈 화면 방지)
|
|||
|
|
var px32 = tex.GetPixels32();
|
|||
|
|
var seen = new HashSet<int>();
|
|||
|
|
for (int i = 0; i < px32.Length; i += 977) seen.Add((px32[i].r << 16) | (px32[i].g << 8) | px32[i].b);
|
|||
|
|
int distinct = seen.Count;
|
|||
|
|
|
|||
|
|
cam.targetTexture = prev;
|
|||
|
|
if (s_uiCam != null) s_uiCam.targetTexture = null;
|
|||
|
|
foreach (var c in UnityEngine.Object.FindObjectsByType<Camera>(FindObjectsInactive.Include, FindObjectsSortMode.None))
|
|||
|
|
if (c != null && c.targetTexture == rt) c.targetTexture = null;
|
|||
|
|
RenderTexture.active = null;
|
|||
|
|
UnityEngine.Object.DestroyImmediate(tex);
|
|||
|
|
rt.Release(); UnityEngine.Object.DestroyImmediate(rt);
|
|||
|
|
|
|||
|
|
L(" 캡처 " + System.IO.Path.GetFileName(file) + " · renderScale=" + scale.ToString("0.####") +
|
|||
|
|
" · 색 " + distinct + "종(1 이면 빈 화면) · PC " + pcPx.ToString("0") + "px(전체해상도) · 스킬버튼 " + skillPx.ToString("0") + "px" +
|
|||
|
|
" · Screen " + Screen.width + "×" + Screen.height +
|
|||
|
|
" · uiCam px " + (s_uiCam != null ? s_uiCam.pixelWidth + "×" + s_uiCam.pixelHeight : "-") +
|
|||
|
|
" · 캔버스 rect " + (s_uiCanvas != null ? s_uiCanvas.rect.size.ToString() : "-"));
|
|||
|
|
|
|||
|
|
return "| " + targetPx + " | " + scale.ToString("0.####") + " | " +
|
|||
|
|
Mathf.RoundToInt(1080f * scale) + "×" + Mathf.RoundToInt(1920f * scale) + " | " +
|
|||
|
|
Mathf.RoundToInt(pcPx * scale) + " | " + Mathf.RoundToInt(mobPx * scale) + " | " +
|
|||
|
|
Mathf.RoundToInt(skillPx * scale) + " |";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>렌더러 바운드를 화면에 투영한 세로 픽셀(1080×1920 전체 해상도 기준).</summary>
|
|||
|
|
static float ScreenHeightPx(Camera cam, GameObject go)
|
|||
|
|
{
|
|||
|
|
if (go == null) return 0f;
|
|||
|
|
var rs = go.GetComponentsInChildren<Renderer>();
|
|||
|
|
if (rs.Length == 0) return 0f;
|
|||
|
|
var b = rs[0].bounds;
|
|||
|
|
foreach (var r in rs) b.Encapsulate(r.bounds);
|
|||
|
|
float minY = float.MaxValue, maxY = float.MinValue;
|
|||
|
|
for (int i = 0; i < 8; i++)
|
|||
|
|
{
|
|||
|
|
var c = new Vector3(((i & 1) == 0 ? b.min : b.max).x, ((i & 2) == 0 ? b.min : b.max).y, ((i & 4) == 0 ? b.min : b.max).z);
|
|||
|
|
var s = cam.WorldToScreenPoint(c);
|
|||
|
|
if (s.z <= 0f) continue;
|
|||
|
|
minY = Mathf.Min(minY, s.y); maxY = Mathf.Max(maxY, s.y);
|
|||
|
|
}
|
|||
|
|
return maxY > minY ? maxY - minY : 0f;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>스킬 카드 한 변의 1080×1920 화면 픽셀. NewGameUI(ref 1920×1080 · Expand) scaleFactor 0.5625 적용.</summary>
|
|||
|
|
static float SkillButtonPx()
|
|||
|
|
{
|
|||
|
|
var ui = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Res_Addr/MainUI/NewGameUI.prefab");
|
|||
|
|
if (ui == null) return 0f;
|
|||
|
|
foreach (var b in ui.GetComponentsInChildren<BattleUI>(true))
|
|||
|
|
{
|
|||
|
|
if (b.skillCards == null || b.skillCards.Length == 0 || b.skillCards[0] == null) continue;
|
|||
|
|
var rt = b.skillCards[0].GetComponent<RectTransform>();
|
|||
|
|
if (rt != null) return Mathf.Max(rt.sizeDelta.x, rt.sizeDelta.y) * 0.5625f;
|
|||
|
|
}
|
|||
|
|
return 0f;
|
|||
|
|
}
|
|||
|
|
}
|