From c3b3a16aa05635c9dea6e699acd67930cd7f118a Mon Sep 17 00:00:00 2001 From: swrring Date: Thu, 10 Sep 2026 23:00:22 +0900 Subject: [PATCH] =?UTF-8?q?[WL-814a]=20=EB=8F=84=ED=8A=B8=20=EB=A3=A9=201?= =?UTF-8?q?=EC=B0=A8=20=EC=8B=9C=EC=95=88:=20URP=20=EB=A0=8C=EB=8D=94=20?= =?UTF-8?q?=EC=8A=A4=EC=BC=80=EC=9D=BC=C2=B7=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?= =?UTF-8?q?=EC=97=85=EC=8A=A4=EC=BC=80=EC=9D=BC=20=EB=9F=B0=ED=83=80?= =?UTF-8?q?=EC=9E=84=20=EC=8A=A4=EC=9C=84=EC=B9=98(=EC=9D=B8=EA=B2=8C?= =?UTF-8?q?=EC=9E=84=20=ED=95=9C=EC=A0=95)=20=C2=B7=20=EB=A3=A9=20?= =?UTF-8?q?=EC=BA=A1=EC=B2=98=204=EB=8B=A8=20(#814)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AgentScripts/WL814a_CAPTURE.txt | 29 + AgentScripts/WL814a_PROBE.txt | 60 ++ AgentScripts/WL814a_Probe.cs | 623 ++++++++++++++++++ .../Resources/WL/WLPixelLookSettings.asset | 24 + .../WL/WLPixelLookSettings.asset.meta | 8 + Assets/WL/Look.meta | 8 + Assets/WL/Look/PixelLook.cs | 357 ++++++++++ Assets/WL/Look/PixelLook.cs.meta | 11 + Assets/WL/Look/WLPixelLookSettings.cs | 126 ++++ Assets/WL/Look/WLPixelLookSettings.cs.meta | 11 + 10 files changed, 1257 insertions(+) create mode 100644 AgentScripts/WL814a_CAPTURE.txt create mode 100644 AgentScripts/WL814a_PROBE.txt create mode 100644 AgentScripts/WL814a_Probe.cs create mode 100644 Assets/WL/Combat/Settings/Resources/WL/WLPixelLookSettings.asset create mode 100644 Assets/WL/Combat/Settings/Resources/WL/WLPixelLookSettings.asset.meta create mode 100644 Assets/WL/Look.meta create mode 100644 Assets/WL/Look/PixelLook.cs create mode 100644 Assets/WL/Look/PixelLook.cs.meta create mode 100644 Assets/WL/Look/WLPixelLookSettings.cs create mode 100644 Assets/WL/Look/WLPixelLookSettings.cs.meta diff --git a/AgentScripts/WL814a_CAPTURE.txt b/AgentScripts/WL814a_CAPTURE.txt new file mode 100644 index 000000000..87ac3db41 --- /dev/null +++ b/AgentScripts/WL814a_CAPTURE.txt @@ -0,0 +1,29 @@ +WL-814a 룩 캡처 · 2026-09-10 22:59:13 + PASS E0 맵 배치 — WL_Nature + PC 위치(Portal_Start (115.00, 0.00, 9.98) 앞 6 m · 지면 스냅) = (115.00, 0.02, 15.98) + PASS E3 카메라 프리팹 — Main Camera + 카메라 = (119.71, 4.60, 15.77) → look (115.00, 1.08, 15.98) · fov 60 · 811o(h 76 · d 3 · up 2.4 · yaw 233) · 화면 앞 방향 (-1.00, 0.00, 0.04) + PASS E1 PC 배치 — Ai01 @ (115.00, 0.02, 15.98) + PASS E2 잡몹 배치 — 3/3 (Batty_A/B/C · scale 1.5 · PC 앞 3~6 m) + 🔴 인게임 베이스 카메라 cullingMask = 95 · UI 레이어(5) 포함? 아니오 → UI 는 반드시 Overlay UI 카메라가 그린다 = 같은 스택 = 같은 렌더 스케일 + UI 경로(실기) = ScreenSpaceCamera 캔버스 + Overlay 카메라 스택 · 캡처 목업은 베이스 카메라가 보는 레이어 0 에 화면 픽셀 1:1 로 둔다 + PASS E3b UI 목업(화면 픽셀 1:1 · 스킬카드 65px × 4 + HP 바) — 배치됨 + 캡처 look_1920.png · renderScale=1 · 색 238종(1 이면 빈 화면) · PC 661px(전체해상도) · 스킬버튼 65px · Screen 640×480 · uiCam px - · 캔버스 rect (1080.00, 1920.00) + PASS E4 look_1920.png — 332084 B + 캡처 look_640.png · renderScale=0.3333 · 색 256종(1 이면 빈 화면) · PC 661px(전체해상도) · 스킬버튼 65px · Screen 640×480 · uiCam px - · 캔버스 rect (1080.00, 1920.00) + PASS E4 look_640.png — 108395 B + 캡처 look_480.png · renderScale=0.25 · 색 245종(1 이면 빈 화면) · PC 661px(전체해상도) · 스킬버튼 65px · Screen 640×480 · uiCam px - · 캔버스 rect (1080.00, 1920.00) + PASS E4 look_480.png — 83676 B + 캡처 look_360.png · renderScale=0.1875 · 색 241종(1 이면 빈 화면) · PC 661px(전체해상도) · 스킬버튼 65px · Screen 640×480 · uiCam px - · 캔버스 rect (1080.00, 1920.00) + PASS E4 look_360.png — 68957 B + + | targetHeightPx | renderScale | 실제 그려진 해상도 | PC 키(px) | 잡몹 키(px) | 스킬버튼 한 변(px) | + |---|---|---|---|---|---| + | 1920 | 1 | 1080×1920 | 661 | 130 | 65 | + | 640 | 0.3333 | 360×640 | 220 | 43 | 22 | + | 480 | 0.25 | 270×480 | 165 | 32 | 16 | + | 360 | 0.1875 | 202×360 | 124 | 24 | 12 | + + 복원: renderScale=1 filter=Auto · dirty=False + +RESULT PASS · 9/9 · FAIL 0 diff --git a/AgentScripts/WL814a_PROBE.txt b/AgentScripts/WL814a_PROBE.txt new file mode 100644 index 000000000..6991dff09 --- /dev/null +++ b/AgentScripts/WL814a_PROBE.txt @@ -0,0 +1,60 @@ +WL-814a 프로브 · 2026-09-10 22:58:23 · Unity 6000.3.23f1 + +ⓐ 실측 표 + ① m_PerPlatformDefaultQuality = {} (비어 있음 → 플랫폼별 지정 0 = 전 플랫폼이 m_CurrentQuality 레벨을 쓴다) + level 0 'High Fidelity' → URP-HighFidelity + level 1 'Balanced' ← m_CurrentQuality(현재) → URP-Balanced + level 2 'Performant' → URP-Performant + QualitySettings.renderPipeline(현재 레벨 오버라이드) = URP-Balanced + GraphicsSettings.defaultRenderPipeline = URP-Performant + 🔴 GraphicsSettings.currentRenderPipeline(= 실제 활성) = URP-Balanced path=Assets/Settings/URP-Balanced.asset + PASS A1 활성 URP 에셋 존재 — URP-Balanced + ② URP-Balanced renderScale=1 · upscalingFilter=Auto · MSAA=1 · HDR=1 · mainLightShadow=1 · shadowDist=50 ← 활성 + ② URP-HighFidelity renderScale=1 · upscalingFilter=Auto · MSAA=4 · HDR=1 · mainLightShadow=1 · shadowDist=150 + ② URP-Performant renderScale=1 · upscalingFilter=Auto · MSAA=1 · HDR=0 · mainLightShadow=0 · shadowDist=50 + ③ NewGameUI.prefab Canvas 1개 + - NewGameUI renderMode=ScreenSpaceCamera worldCamera=Camera planeDistance=100 sortingOrder=0 + CanvasScaler ref=(1920.00, 1080.00) mode=ScaleWithScreenSize match=Expand → 1080×1920 scaleFactor=0.5625 + UI 카메라 'Camera' renderType=Overlay depth=6 clear=Nothing ortho=True cullingMask=32 + PASS A3 UI 카메라 = URP Overlay(= 베이스 카메라 스택에 들어간다 → 렌더 스케일 영향 받음) — Overlay + PASS A3b 인게임 루트 캔버스 = ScreenSpaceCamera(Overlay 아님) — Overlay 캔버스 0 + PASS A4 월드 공간 캔버스 0 — 0 + 스킬카드[0] sizeDelta=(115.00, 115.00) (유닛) → 1080×1920 화면 px = (64.69, 64.69) + 스킬카드[1] sizeDelta=(115.00, 115.00) (유닛) → 1080×1920 화면 px = (64.69, 64.69) + 스킬카드[0] sizeDelta=(115.00, 115.00) (유닛) → 1080×1920 화면 px = (64.69, 64.69) + 스킬카드[1] sizeDelta=(115.00, 115.00) (유닛) → 1080×1920 화면 px = (64.69, 64.69) + 스킬카드[0] sizeDelta=(115.00, 115.00) (유닛) → 1080×1920 화면 px = (64.69, 64.69) + 스킬카드[1] sizeDelta=(115.00, 115.00) (유닛) → 1080×1920 화면 px = (64.69, 64.69) + ④ HUDUI.prefab Canvas=0 · HUDDMGUI.prefab Canvas=0 (0 = 자체 캔버스 없음 = 부모 캔버스[NewGameUI]를 그대로 쓴다 · HUDBase 가 WorldToViewportPoint 로 anchoredPosition 만 옮긴다) + PASS A4b HP바/데미지숫자 = 스크린 공간(자체 캔버스 0) — HUDUI/HUDDMGUI 자체 Canvas 0 + ⑤ Main Camera.prefab renderType=Base · cameraStack(프리팹)=0 · postProcessing=True · antialiasing=None · fov=60 · ortho=False · rendererIndex(프리팹 직렬화 −1 = 기본) + 🔴 런타임 스택 = NewGameUI.Start() 가 `Camera.main…cameraStack.Add(UICamera)` (Assets/Script/UI/NewGameUI.cs:38) → Base 1 + Overlay 1 + PASS A5 Main Camera = Base — Base + +ⓑ 적용 / 복원 (에디트 모드 · 가짜 경계 = ApplyNow/RestoreNow 직접 호출) + PASS B0 SO 로드 — targetHeightPx=480 filter=Point applyOnlyInBattleMap=True restoreOnExit=True + 전 : URP-Balanced renderScale=1 filter=Auto + 후 : renderScale=0.25 filter=Point + PASS B1 적용됨 — renderScale=0.25 (기대 0.25 = 480/1920) · filter=Point + PASS B2 원래 값 기억 — orig=1/Auto + 산식: targetHeightPx 1920 → renderScale 1 → 실제 그려지는 세로 픽셀 1920 + 산식: targetHeightPx 640 → renderScale 0.3333 → 실제 그려지는 세로 픽셀 640 + 산식: targetHeightPx 480 → renderScale 0.25 → 실제 그려지는 세로 픽셀 480 + 산식: targetHeightPx 360 → renderScale 0.1875 → 실제 그려지는 세로 픽셀 360 + 클램프: 96px/1920 = 0.05 → 0.1 (하한 0.1) + PASS B3 하한 0.1 클램프 — 0.1 + 복원: renderScale=1 filter=Auto + PASS B4 복원 프로브 — renderScale == 원래값 — 1 == 1 + PASS B5 복원 프로브 — upscalingFilter == 원래값 — Auto == Auto + PASS B6 이중 복원 무해 — 두 번째 RestoreNow = false(무동작) + PASS B7 🔴 파이프라인 에셋 dirty 아님(= Save Project 해도 디스크에 안 써진다) — IsDirty=False + +ⓒ C8 롤백 + PASS C1 스위치 off → 적용 0 — applied=False renderScale=1 + PASS C2 스위치 off → Enabled=false — False + PASS C3 에디트 모드 러너 오브젝트 0 — 0 → 0 + +ⓓ GC 0 (Tick × 200k · 에디트 모드라 PollBoundary 는 즉시 반환하는 경로) + PASS D1 Tick ×200k 할당 0 — monoΔ=0B · totalΔ=0B · gen0=0 + +RESULT PASS · 18/18 · FAIL 0 diff --git a/AgentScripts/WL814a_Probe.cs b/AgentScripts/WL814a_Probe.cs new file mode 100644 index 000000000..92c8c0096 --- /dev/null +++ b/AgentScripts/WL814a_Probe.cs @@ -0,0 +1,623 @@ +// 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(); var qGuids = new List(); + 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(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("Assets/Res_Addr/MainUI/NewGameUI.prefab"); + if (ui != null) + { + var canvases = ui.GetComponentsInChildren(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(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(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(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() : 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("Assets/Res_Addr/Ingame/HUDUI.prefab"); + var dmg = AssetDatabase.LoadAssetAtPath("Assets/Res_Addr/Ingame/HUDDMGUI.prefab"); + L(" ④ HUDUI.prefab Canvas=" + (hud != null ? hud.GetComponentsInChildren(true).Length : -1) + + " · HUDDMGUI.prefab Canvas=" + (dmg != null ? dmg.GetComponentsInChildren(true).Length : -1) + + " (0 = 자체 캔버스 없음 = 부모 캔버스[NewGameUI]를 그대로 쓴다 · HUDBase 가 WorldToViewportPoint 로 anchoredPosition 만 옮긴다)"); + Chk("A4b HP바/데미지숫자 = 스크린 공간(자체 캔버스 0)", + hud != null && dmg != null && hud.GetComponentsInChildren(true).Length == 0 && dmg.GetComponentsInChildren(true).Length == 0, + "HUDUI/HUDDMGUI 자체 Canvas 0"); + + // ⑤ 카메라 스택 + var camPrefab = AssetDatabase.LoadAssetAtPath("Assets/ResWork/Prefabs/Ingame/Main Camera.prefab"); + if (camPrefab != null) + { + var c = camPrefab.GetComponentInChildren(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()) + 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() != 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(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(true) : null; + Chk("E3 카메라 프리팹", cam != null, camGo != null ? camGo.name : "null"); + if (cam == null) return; + + // 에디트 모드에서 원본 스크립트가 돌지 않게(Update 는 어차피 안 돈다) 카메라 스크립트만 비활성 + foreach (var mb in camGo.GetComponentsInChildren(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(); + 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(); + 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); + } + + /// Animator 컨트롤러의 Idle 계열 클립을 0.5 s 로 샘플해서 T 포즈를 없앤다(에디트 모드 · 애니메이터 상태 변경 0). + static void Pose(GameObject go) + { + if (go == null) return; + try + { + var an = go.GetComponentInChildren(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 { } + } + + /// + /// UI 목업 — 화면 픽셀 1:1 로 배치한 캔버스. + /// 🔴 실제 게임 경로(URP Overlay 카메라 + ScreenSpaceCamera 캔버스)는 **에디트 모드 Camera.Render 에서 스택이 그려지지 않아** + /// 캡처에 안 나온다(이 함수 안에서 실측 · 로그로 남긴다). 그래서 캡처용으로만 캔버스를 베이스 카메라 앞 + /// 월드 공간에 두고 화면 픽셀과 1:1 이 되게 스케일한다 — 같은 렌더 타겟(= 같은 renderScale)에 그려지므로 + /// 「렌더 스케일이 UI 를 어떻게 만드는가」는 실제와 같다. + /// + 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.renderMode = RenderMode.WorldSpace; + canvas.worldCamera = baseCam; + var crt = canvasGo.GetComponent(); + 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(); + rt.SetParent(parent, false); + rt.anchorMin = rt.anchorMax = anchor; + rt.pivot = anchor; + rt.sizeDelta = size; + rt.anchoredPosition = pos; + var img = go.GetComponent(); + img.sprite = null; img.color = col; img.raycastTarget = false; + return go; + } + + static GameObject Inst(string path, Vector3 pos) + { + var src = AssetDatabase.LoadAssetAtPath(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 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(); + 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(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) + " |"; + } + + /// 렌더러 바운드를 화면에 투영한 세로 픽셀(1080×1920 전체 해상도 기준). + static float ScreenHeightPx(Camera cam, GameObject go) + { + if (go == null) return 0f; + var rs = go.GetComponentsInChildren(); + 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; + } + + /// 스킬 카드 한 변의 1080×1920 화면 픽셀. NewGameUI(ref 1920×1080 · Expand) scaleFactor 0.5625 적용. + static float SkillButtonPx() + { + var ui = AssetDatabase.LoadAssetAtPath("Assets/Res_Addr/MainUI/NewGameUI.prefab"); + if (ui == null) return 0f; + foreach (var b in ui.GetComponentsInChildren(true)) + { + if (b.skillCards == null || b.skillCards.Length == 0 || b.skillCards[0] == null) continue; + var rt = b.skillCards[0].GetComponent(); + if (rt != null) return Mathf.Max(rt.sizeDelta.x, rt.sizeDelta.y) * 0.5625f; + } + return 0f; + } +} diff --git a/Assets/WL/Combat/Settings/Resources/WL/WLPixelLookSettings.asset b/Assets/WL/Combat/Settings/Resources/WL/WLPixelLookSettings.asset new file mode 100644 index 000000000..47293e480 --- /dev/null +++ b/Assets/WL/Combat/Settings/Resources/WL/WLPixelLookSettings.asset @@ -0,0 +1,24 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 814a00029c7b4d1e8f2a6c5b3d47e902, type: 3} + m_Name: WLPixelLookSettings + m_EditorClassIdentifier: Assembly-CSharp::WL.Look.WLPixelLookSettings + enabled: 1 + verboseLog: 0 + targetHeightPx: 480 + filter: 0 + minRenderScale: 0.1 + maxRenderScale: 1 + applyOnlyInBattleMap: 1 + restoreOnExit: 1 + pollSeconds: 0.25 + trackScreenSize: 1 diff --git a/Assets/WL/Combat/Settings/Resources/WL/WLPixelLookSettings.asset.meta b/Assets/WL/Combat/Settings/Resources/WL/WLPixelLookSettings.asset.meta new file mode 100644 index 000000000..ca02fd8d5 --- /dev/null +++ b/Assets/WL/Combat/Settings/Resources/WL/WLPixelLookSettings.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 814a00049c7b4d1e8f2a6c5b3d47e904 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/Look.meta b/Assets/WL/Look.meta new file mode 100644 index 000000000..8e12bb489 --- /dev/null +++ b/Assets/WL/Look.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 814a00019c7b4d1e8f2a6c5b3d47e901 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/Look/PixelLook.cs b/Assets/WL/Look/PixelLook.cs new file mode 100644 index 000000000..e4b21851a --- /dev/null +++ b/Assets/WL/Look/PixelLook.cs @@ -0,0 +1,357 @@ +// ───────────────────────────────────────────────────────────────────────────── +// PixelLook.cs — 도트(픽셀아트) 룩 런타임 스위치 (인게임 맵 한정 · 원본 훅 0 · 에셋 파일 수정 0) +// +// PD 지시 #814 · 발주서 WL-814a §1-2 (2026-09-10) +// +// ■ 무엇을 하나 +// 맵 진입 경계(= AutoCombatOnEnter / EffectWarmup 과 **같은 판정**: 메인 PC 교체 또는 맵 ID 변경)에서 +// **활성 파이프라인 에셋**(GraphicsSettings.currentRenderPipeline → UniversalRenderPipelineAsset)의 +// renderScale = targetHeightPx ÷ Screen.height (1080×1920 · 480px → 0.25) +// upscalingFilter = Point (Nearest-Neighbor) +// 로 바꾼다. 맵을 벗어나면(타이틀/로비 복귀) · 앱 종료 · 에디터 플레이모드 종료에서 **원래 값으로 되돌린다**. +// +// ■ 🔴 왜 「되돌리기」가 이 파일의 절반인가 +// 파이프라인 에셋은 디스크 위의 프로젝트 에셋이다. 에디터 Play 중에 그 오브젝트의 프로퍼티를 바꾸면 +// Play 를 멈춰도 **메모리에 그 값이 남고**, PD 가 「Save Project」(Ctrl+S) 를 누르는 순간 +// `Assets/Settings/URP-*.asset` 에 renderScale 0.25 가 그대로 기록된다 = 프로젝트 오염. +// 그래서 +// · EditorUtility.SetDirty 호출 **0** +// · 4중 복원: 맵 이탈 · 러너 OnDisable · OnApplicationQuit · EditorApplication.playModeStateChanged +// · 복원할 값을 에디터 SessionState 에도 적어 둔다 → 도메인 리로드로 static 이 날아가도 +// 다음 에디터 로드에서 EditorSafetyNet 이 마저 되돌린다. +// +// ■ 성능 +// Tick 은 「지금이 폴링 시각인가」 float 비교 1회뿐이다(경계에서만 실제 작업). 프레임당 할당 0. +// renderScale 자체의 비용은 URP 가 내부에서 처리한다(렌더 타겟이 작아지므로 오히려 GPU 가 가벼워진다). +// +// ■ 이 방식의 한계 (에셋 도착 전 1차 시안 · 보고서 §4) +// 카메라가 픽셀 격자에 스냅되지 않으므로 카메라가 움직일 때 가장자리에서 **픽셀 크롤/지터**가 보인다. +// 외곽선·팔레트 제한도 없다. 그 셋이 Critter 3D Pixel Camera 가 대체할 지점이다. +// +// ■ C8 롤백 +// WLPixelLookSettings.enabled = 0 → 러너 오브젝트 0 · 파이프라인 에셋 접촉 0. +// +// 🔴 어셈블리 주의: Assets/WL/Look/ 에 .asmdef 를 만들지 말 것. +// 🔴 이 파일은 Debug.LogError / Debug.LogException 을 쓰지 않는다(813z 규칙). +// ───────────────────────────────────────────────────────────────────────────── + +using UnityEngine; +using UnityEngine.Rendering; +using UnityEngine.Rendering.Universal; + +namespace WL.Look +{ + /// 도트 룩 스위치. 정적 · 새 Manager/Singleton 0(러너는 숨김 GameObject 1개). + public static class PixelLook + { + // ───────────────────────────────────────── 설정 + static WLPixelLookSettings Cfg { get { return WLPixelLookSettings.Instance; } } + + /// 기능이 살아 있는가 = 에셋 존재 · enabled · RuntimeDisabled off. + public static bool Active { get { return WLPixelLookSettings.Enabled; } } + + // ───────────────────────────────────────── 상태(적용 중일 때만 의미가 있다) + static UniversalRenderPipelineAsset s_asset; // 값을 바꾼 그 에셋(복원 대상) + static float s_origRenderScale; + static UpscalingFilterSelection s_origFilter; + static bool s_applied; + static int s_appliedScreenHeight; + static int s_appliedTargetPx; + + // ───────────────────────────────────────── 경계 감시 + static PCActor s_lastPc; + static int s_lastMapId = int.MinValue; + static float s_nextPoll; + static bool s_faulted; + + // ───────────────────────────────────────── 진단(프로브가 읽는다) + /// 맵 경계를 본 횟수 · 적용 횟수 · 복원 횟수 · 재계산(해상도 변화) 횟수. + public static int Boundaries, Applies, Restores, Rescales; + /// 로비라서 건너뛴 횟수 · 활성 URP 에셋을 못 찾아 건너뛴 횟수. + public static int SkippedLobby, SkippedNoAsset; + /// 지금 적용돼 있는가. + public static bool IsApplied { get { return s_applied; } } + /// 적용 전 원래 renderScale(적용 중일 때만 의미 있음). + public static float OriginalRenderScale { get { return s_origRenderScale; } } + /// 적용 전 원래 upscalingFilter(적용 중일 때만 의미 있음). + public static UpscalingFilterSelection OriginalFilter { get { return s_origFilter; } } + /// 값을 바꾼 파이프라인 에셋 이름(없으면 빈 문자열). + public static string TargetAssetName { get { return s_asset != null ? s_asset.name : string.Empty; } } + public static string LastLog = ""; + + // ───────────────────────────────────────────────────────────────────── + // 현재 활성 파이프라인 에셋 + // ───────────────────────────────────────────────────────────────────── + + /// 지금 실제로 쓰이는 URP 에셋(Quality 별 오버라이드가 있으면 그것 · 없으면 Graphics 기본). + public static UniversalRenderPipelineAsset CurrentAsset + { + get { return GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset; } + } + + /// 목표 픽셀 높이와 화면 높이로 renderScale 을 구한다(SO 상·하한으로 클램프). + public static float ComputeRenderScale(int targetHeightPx, int screenHeight) + { + var c = Cfg; + float lo = c != null ? c.minRenderScale : 0.1f; + float hi = c != null ? c.maxRenderScale : 1f; + if (screenHeight <= 0) return hi; + return Mathf.Clamp((float)targetHeightPx / screenHeight, lo, hi); + } + + static UpscalingFilterSelection ToUrp(PixelUpscaleFilter f) + { + switch (f) + { + case PixelUpscaleFilter.Linear: return UpscalingFilterSelection.Linear; + case PixelUpscaleFilter.Auto: return UpscalingFilterSelection.Auto; + case PixelUpscaleFilter.FSR: return UpscalingFilterSelection.FSR; + default: return UpscalingFilterSelection.Point; + } + } + + // ───────────────────────────────────────────────────────────────────── + // 적용 / 복원 (프로브·캡처도 이 두 개만 쓴다) + // ───────────────────────────────────────────────────────────────────── + + /// SO 값 그대로 적용. 이미 적용 중이면 해상도만 다시 맞춘다. + public static bool ApplyNow(string reason) + { + var c = Cfg; + if (c == null) return false; + return ApplyNow(c.targetHeightPx, Screen.height, reason); + } + + /// 목표 픽셀 높이·화면 높이를 지정해서 적용(캡처 4단이 이 경로를 쓴다). + public static bool ApplyNow(int targetHeightPx, int screenHeight, string reason) + { + var c = Cfg; + if (c == null || !WLPixelLookSettings.Enabled) return false; + + var asset = CurrentAsset; + if (asset == null) { SkippedNoAsset++; LastLog = "활성 URP 에셋 없음 — 건너뜀"; return false; } + + // 🔴 다른 에셋으로 바뀌었으면(Quality 변경) 이전 에셋부터 되돌린다. + if (s_applied && !ReferenceEquals(asset, s_asset)) RestoreNow("quality-changed"); + + if (!s_applied) + { + s_asset = asset; + s_origRenderScale = asset.renderScale; + s_origFilter = asset.upscalingFilter; + EditorSafetyNet.Remember(asset, s_origRenderScale, s_origFilter); // 에디터 전용 · 빌드에서는 빈 함수 + } + + float scale = ComputeRenderScale(targetHeightPx, screenHeight); + var filter = ToUrp(c.filter); + + // 🔴 SetDirty 호출 0 — 프로퍼티 대입만 한다. + asset.renderScale = scale; + asset.upscalingFilter = filter; + + s_appliedScreenHeight = screenHeight; + s_appliedTargetPx = targetHeightPx; + if (!s_applied) { s_applied = true; Applies++; } + else Rescales++; + + if (c.verboseLog) + Debug.Log("[WL814a PixelLook] 적용(" + reason + ") " + asset.name + + " renderScale " + s_origRenderScale.ToString("0.####") + " → " + scale.ToString("0.####") + + " · filter " + s_origFilter + " → " + filter + + " · " + targetHeightPx + "px / " + screenHeight + "px"); + LastLog = "적용 " + reason; + return true; + } + + /// 원래 값으로 되돌린다. 적용 중이 아니면 아무것도 하지 않는다(true = 실제로 되돌렸다). + public static bool RestoreNow(string reason) + { + if (!s_applied) return false; + var asset = s_asset; + s_applied = false; + s_asset = null; + + if (asset != null) + { + asset.renderScale = s_origRenderScale; // 🔴 SetDirty 0 + asset.upscalingFilter = s_origFilter; + } + EditorSafetyNet.Forget(); + Restores++; + + var c = Cfg; + if (c != null && c.verboseLog) + Debug.Log("[WL814a PixelLook] 복원(" + reason + ") " + (asset != null ? asset.name : "?") + + " renderScale → " + s_origRenderScale.ToString("0.####") + " · filter → " + s_origFilter); + LastLog = "복원 " + reason; + return true; + } + + /// 프로브·A/B 용: 경계 감시 상태를 처음으로 되돌린다(적용 중이면 먼저 복원한다). + public static void ResetForProbe() + { + RestoreNow("probe-reset"); + s_lastPc = null; s_lastMapId = int.MinValue; s_nextPoll = 0f; s_faulted = false; + Boundaries = Applies = Restores = Rescales = SkippedLobby = SkippedNoAsset = 0; + s_appliedScreenHeight = 0; s_appliedTargetPx = 0; + LastLog = ""; + } + + // ───────────────────────────────────────────────────────────────────── + // 틱 (러너가 매 프레임 부른다) — 프레임당 할당 0 + // ───────────────────────────────────────────────────────────────────── + + /// 프로브 전용 공개 진입점(AgentScripts 는 별도 어셈블리라 internal 을 못 본다). + public static void TickForProbe() { Tick(); } + + /// 프로브 전용 공개 진입점 — 에디트 모드에서는 러너를 만들지 않는 것을 확인하는 데 쓴다. + public static void EnsureRunnerForProbe() { EnsureRunner(); } + + internal static void Tick() + { + if (s_faulted || !WLPixelLookSettings.Enabled) return; + var c = Cfg; + float now = Time.unscaledTime; + if (now < s_nextPoll) return; + s_nextPoll = now + Mathf.Max(0.05f, c.pollSeconds); + + try { PollBoundary(c); } + catch (System.Exception ex) { s_faulted = true; LastLog = "중단(예외) — " + ex.Message; RestoreNow("fault"); } + } + + /// 맵 진입/이탈 경계 — AutoCombatOnEnter 와 같은 규칙(메인 PC 교체 또는 맵 ID 변경). + static void PollBoundary(WLPixelLookSettings c) + { + if (!Application.isPlaying) return; // 에디트 모드 오염 0(프로브는 ApplyNow/RestoreNow 로 직접 들어온다) + + var pc = MyValue.MyPC; + if (DSUtil.CheckNull(pc)) + { + // PC 가 없다 = 타이틀·로딩·로비 복귀 = 맵 이탈 + s_lastPc = null; + s_lastMapId = int.MinValue; + if (s_applied && c.restoreOnExit) RestoreNow("map-exit(no-pc)"); + return; + } + + var info = InGameInfo.Ins; + if (DSUtil.CheckNull(info)) return; // 아직 준비 안 됨 — 경계를 소비하지 않는다 + + int map = MyValue.MyChoiceMapData != null ? MyValue.MyChoiceMapData.n_MapID : int.MinValue; + bool pcChanged = !ReferenceEquals(pc, s_lastPc); + bool mapChanged = map != s_lastMapId; + + if (!pcChanged && !mapChanged) + { + // 경계가 아니면: 해상도 변화만 따라간다(회전·창 크기). + if (s_applied && c.trackScreenSize && Screen.height != s_appliedScreenHeight) + ApplyNow(s_appliedTargetPx > 0 ? s_appliedTargetPx : c.targetHeightPx, Screen.height, "screen-resize"); + return; + } + + s_lastPc = pc; s_lastMapId = map; + Boundaries++; + + bool lobby = info.Get_GameMode() == eGameMode.Lobby; + if (c.applyOnlyInBattleMap && lobby) + { + SkippedLobby++; + if (s_applied && c.restoreOnExit) RestoreNow("map-exit(lobby)"); + return; + } + + ApplyNow(c.targetHeightPx, Screen.height, "map-enter"); + } + + // ───────────────────────────────────────────────────────────────────── + // 러너 (숨김 GameObject 1개 · SO off 면 아예 만들지 않는다 = C8) + // ───────────────────────────────────────────────────────────────────── + + static PixelLookRunner s_runner; + + internal static void EnsureRunner() + { + if (!Application.isPlaying || !DSUtil.CheckNull(s_runner)) return; + var go = new GameObject("[WL814a] PixelLookRunner"); + go.hideFlags = HideFlags.HideAndDontSave; + s_runner = go.AddComponent(); + } + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] + static void Boot() + { + if (WLPixelLookSettings.Enabled) EnsureRunner(); // 꺼져 있으면 오브젝트 0(C8) + } + } + + /// 도트 룩 스위치의 시간 축. 숨김 GameObject 1개 · 코루틴 0. + internal sealed class PixelLookRunner : MonoBehaviour + { + void Update() { PixelLook.Tick(); } + void OnDisable() { PixelLook.RestoreNow("runner-disable"); } + void OnApplicationQuit() { PixelLook.RestoreNow("app-quit"); } + } + + /// + /// 🔴 에디터 전용 안전망 — 도메인 리로드로 static 이 날아가도 파이프라인 에셋을 되돌린다. + /// 빌드에서는 세 함수 모두 빈 몸통이라 비용 0. + /// + internal static class EditorSafetyNet + { +#if UNITY_EDITOR + const string kKey = "WL814a.PixelLook.Pending"; // "||" + + /// 플레이모드 상태 훅 등록 + 지난 세션에 남은 값이 있으면 지금 복원한다. + [UnityEditor.InitializeOnLoadMethod] + static void Hook() + { + UnityEditor.EditorApplication.playModeStateChanged -= OnPlayMode; + UnityEditor.EditorApplication.playModeStateChanged += OnPlayMode; + RestorePending(); + } + + static void OnPlayMode(UnityEditor.PlayModeStateChange s) + { + // 🔴 ExitingPlayMode 는 도메인 리로드 **전**이라 여기서 되돌리면 그대로 끝난다. + if (s == UnityEditor.PlayModeStateChange.ExitingPlayMode) PixelLook.RestoreNow("exit-playmode"); + else if (s == UnityEditor.PlayModeStateChange.EnteredEditMode) RestorePending(); // 혹시 놓친 경우 + } + + internal static void Remember(UniversalRenderPipelineAsset asset, float scale, UpscalingFilterSelection filter) + { + if (asset == null) return; + string path = UnityEditor.AssetDatabase.GetAssetPath(asset); + if (string.IsNullOrEmpty(path)) return; + UnityEditor.SessionState.SetString(kKey, path + "|" + scale.ToString("R") + "|" + ((int)filter)); + } + + internal static void Forget() { UnityEditor.SessionState.EraseString(kKey); } + + static void RestorePending() + { + string v = UnityEditor.SessionState.GetString(kKey, string.Empty); + if (string.IsNullOrEmpty(v)) return; + UnityEditor.SessionState.EraseString(kKey); + + int b = v.LastIndexOf('|'); + if (b <= 0) return; + int a = v.LastIndexOf('|', b - 1); + if (a <= 0) return; + + string path = v.Substring(0, a); + float scale; + int filter; + if (!float.TryParse(v.Substring(a + 1, b - a - 1), out scale)) return; + if (!int.TryParse(v.Substring(b + 1), out filter)) return; + + var asset = UnityEditor.AssetDatabase.LoadAssetAtPath(path); + if (asset == null) return; + asset.renderScale = scale; // 🔴 SetDirty 0 + asset.upscalingFilter = (UpscalingFilterSelection)filter; + } +#else + internal static void Remember(UniversalRenderPipelineAsset asset, float scale, UpscalingFilterSelection filter) { } + internal static void Forget() { } +#endif + } +} diff --git a/Assets/WL/Look/PixelLook.cs.meta b/Assets/WL/Look/PixelLook.cs.meta new file mode 100644 index 000000000..ed1ca0fe5 --- /dev/null +++ b/Assets/WL/Look/PixelLook.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 814a00039c7b4d1e8f2a6c5b3d47e903 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/Look/WLPixelLookSettings.cs b/Assets/WL/Look/WLPixelLookSettings.cs new file mode 100644 index 000000000..0f2ebced2 --- /dev/null +++ b/Assets/WL/Look/WLPixelLookSettings.cs @@ -0,0 +1,126 @@ +// ───────────────────────────────────────────────────────────────────────────── +// WLPixelLookSettings.cs — 도트(픽셀아트) 룩 스위치 값의 단일 출처 (SOT · C45) +// +// PD 지시 #814(「Critter 3D Pixel Art Toolkit 으로 우리 게임을 도트 게임처럼 보이게」) +// 발주서 WL-814a §1-2 (2026-09-10) +// +// ■ 왜 URP 기본 기능인가 (실측) +// 임포트된 것은 `Assets/Critter 3D Pixel Art Toolkit/README.pdf` **1개뿐**이다 +// (번들 3종 = Critter 3D Pixel Camera · Environment · Volumetric Lighting 은 Asset Store 에서 별도 다운로드). +// → 에셋이 도착하기 전까지 URP 17.3 이 이미 가진 두 값만으로 「도트 룩」 1차 시안을 만든다: +// UniversalRenderPipelineAsset.renderScale … 낮은 해상도로 그린다(= 픽셀이 커진다) +// UniversalRenderPipelineAsset.upscalingFilter … Point(Nearest-Neighbor) 로 확대한다(= 뭉개지지 않는다) +// 이 둘은 **파이프라인 에셋의 런타임 프로퍼티**라 코드에서 켜고 끌 수 있다(셰이더·머티리얼·포스트 0). +// +// ■ 🔴 절대 규칙 — 디스크에 쓰지 않는다 +// 파이프라인 에셋은 프로젝트 에셋이다. 에디터에서 그 오브젝트의 값을 바꾸면 **메모리에 남고**, +// PD 가 「Save Project」 를 누르면 `Assets/Settings/URP-*.asset` 에 그대로 기록된다. +// 그래서 이 기능은 +// · `EditorUtility.SetDirty` 를 **부르지 않는다**(호출 0) +// · 맵 이탈 · `OnApplicationQuit` · 에디터 플레이모드 종료에서 **원래 값으로 되돌린다** +// · 되돌릴 값(에셋 경로 + renderScale + filter)을 에디터 `SessionState` 에도 적어 둬서 +// 도메인 리로드로 static 이 날아가도 다음 로드에서 복원한다(§PixelLook.EditorSafetyNet) +// +// ■ C8 롤백 +// `enabled = 0` → 러너 오브젝트가 아예 뜨지 않고 파이프라인 에셋을 한 번도 건드리지 않는다(원본 100 %). +// +// 에셋 경로(고정): Assets/WL/Combat/Settings/Resources/WL/WLPixelLookSettings.asset +// → Resources.Load("WL/WLPixelLookSettings") 1회 로드 후 캐시(기존 WL 설정 SO 관례). +// +// 🔴 어셈블리 주의: Assets/WL/Look/ 에 .asmdef 를 만들지 말 것(MyValue·InGameInfo 가 Assembly-CSharp 에 있다). +// 🔴 이 파일은 Debug.LogError / Debug.LogException 을 쓰지 않는다(813z 규칙). +// ───────────────────────────────────────────────────────────────────────────── + +using UnityEngine; + +namespace WL.Look +{ + /// 낮은 해상도를 화면 크기로 확대할 때 쓰는 필터. URP UpscalingFilterSelection 으로 매핑된다. + public enum PixelUpscaleFilter + { + /// Nearest-Neighbor — 픽셀이 네모로 남는다(도트 룩의 핵심). + Point = 0, + /// Bilinear — 뭉개진다(비교용). + Linear = 1, + /// URP 자동 선택(렌더 스케일·해상도에 따라 Point 또는 Linear). + Auto = 2, + /// AMD FSR 1.0(선명하게 복원 = 도트 룩과 반대 방향 · 비교용). + FSR = 3, + } + + [CreateAssetMenu(fileName = "WLPixelLookSettings", menuName = "WL/Pixel Look Settings", order = 324)] + public sealed class WLPixelLookSettings : ScriptableObject + { + public const string ResourcesPath = "WL/WLPixelLookSettings"; + + static WLPixelLookSettings s_cached; + static bool s_lookupDone; + + /// 설정 에셋(1회 로드 후 캐시). 없으면 null = 기능 전체 무동작. + public static WLPixelLookSettings Instance + { + get + { + if (!s_lookupDone) + { + s_cached = Resources.Load(ResourcesPath); + s_lookupDone = true; + } + return s_cached; + } + } + + /// 진단·A/B 전용 런타임 스위치(WLCameraFramingSettings.RuntimeDisabled 선례). + public static bool RuntimeDisabled; + + /// 기능이 살아 있는가 = 에셋 존재 · enabled · 런타임 스위치 off. + public static bool Enabled + { + get { var i = Instance; return i != null && i.enabled && !RuntimeDisabled; } + } + + /// 테스트용: 캐시를 비워 다음 접근 때 다시 Resources.Load 한다. + public static void ClearCache() { s_cached = null; s_lookupDone = false; } + + // ─────────────────────────────────────────── 전체 스위치 + [Header("전체 스위치 (C8)")] + [Tooltip("0 이면 러너 오브젝트도 뜨지 않고 파이프라인 에셋을 한 번도 건드리지 않는다 = 원본 100 %.")] + public bool enabled = true; + + [Tooltip("적용/복원 1줄 로그. 프레임당 로그는 없다(경계에서만).")] + public bool verboseLog = false; + + // ─────────────────────────────────────────── 픽셀 크기 + [Header("픽셀 크기")] + [Tooltip("세로로 몇 픽셀을 그릴 것인가. renderScale = targetHeightPx ÷ Screen.height.\n" + + "1080×1920 기준: 1920=원본 · 640≈0.333 · 480=0.25 · 360≈0.1875")] + [Range(64, 4096)] + public int targetHeightPx = 480; + + [Tooltip("확대 필터. 도트 룩은 Point(Nearest-Neighbor).")] + public PixelUpscaleFilter filter = PixelUpscaleFilter.Point; + + [Tooltip("renderScale 하한(URP 자체 하한도 0.1). 0.1 미만은 URP 가 어차피 0.1 로 잘라낸다.")] + [Range(0.05f, 1f)] + public float minRenderScale = 0.1f; + + [Tooltip("renderScale 상한. 1 이면 「원본보다 크게 그리는 것」을 막는다(=슈퍼샘플링 방지).")] + [Range(0.5f, 3f)] + public float maxRenderScale = 1f; + + // ─────────────────────────────────────────── 적용 범위 + [Header("적용 범위 (인게임 한정)")] + [Tooltip("1 이면 전투 맵(Stage/Dungeon)에서만 켠다. 로비·타이틀·농장은 선명하게 둔다.")] + public bool applyOnlyInBattleMap = true; + + [Tooltip("1 이면 맵 이탈(타이틀/로비 복귀) · 종료 · 플레이모드 종료에서 원래 값으로 되돌린다. 🔴 0 으로 두지 말 것.")] + public bool restoreOnExit = true; + + [Tooltip("맵 진입/이탈 경계를 보는 주기(초). 프레임당 비용 0(시간 비교 1회).")] + [Range(0.05f, 2f)] + public float pollSeconds = 0.25f; + + [Tooltip("1 이면 해상도가 바뀌었을 때(회전·창 크기) renderScale 을 다시 계산한다.")] + public bool trackScreenSize = true; + } +} diff --git a/Assets/WL/Look/WLPixelLookSettings.cs.meta b/Assets/WL/Look/WLPixelLookSettings.cs.meta new file mode 100644 index 000000000..2bb98ffbe --- /dev/null +++ b/Assets/WL/Look/WLPixelLookSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 814a00029c7b4d1e8f2a6c5b3d47e902 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: