[WL-814a] 도트 룩 1차 시안: URP 렌더 스케일·포인트 업스케일 런타임 스위치(인게임 한정) · 룩 캡처 4단 (#814)
This commit is contained in:
parent
2a0625f11d
commit
c3b3a16aa0
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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<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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 814a00049c7b4d1e8f2a6c5b3d47e904
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 814a00019c7b4d1e8f2a6c5b3d47e901
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>도트 룩 스위치. 정적 · 새 Manager/Singleton 0(러너는 숨김 GameObject 1개).</summary>
|
||||
public static class PixelLook
|
||||
{
|
||||
// ───────────────────────────────────────── 설정
|
||||
static WLPixelLookSettings Cfg { get { return WLPixelLookSettings.Instance; } }
|
||||
|
||||
/// <summary>기능이 살아 있는가 = 에셋 존재 · enabled · RuntimeDisabled off.</summary>
|
||||
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;
|
||||
|
||||
// ───────────────────────────────────────── 진단(프로브가 읽는다)
|
||||
/// <summary>맵 경계를 본 횟수 · 적용 횟수 · 복원 횟수 · 재계산(해상도 변화) 횟수.</summary>
|
||||
public static int Boundaries, Applies, Restores, Rescales;
|
||||
/// <summary>로비라서 건너뛴 횟수 · 활성 URP 에셋을 못 찾아 건너뛴 횟수.</summary>
|
||||
public static int SkippedLobby, SkippedNoAsset;
|
||||
/// <summary>지금 적용돼 있는가.</summary>
|
||||
public static bool IsApplied { get { return s_applied; } }
|
||||
/// <summary>적용 전 원래 renderScale(적용 중일 때만 의미 있음).</summary>
|
||||
public static float OriginalRenderScale { get { return s_origRenderScale; } }
|
||||
/// <summary>적용 전 원래 upscalingFilter(적용 중일 때만 의미 있음).</summary>
|
||||
public static UpscalingFilterSelection OriginalFilter { get { return s_origFilter; } }
|
||||
/// <summary>값을 바꾼 파이프라인 에셋 이름(없으면 빈 문자열).</summary>
|
||||
public static string TargetAssetName { get { return s_asset != null ? s_asset.name : string.Empty; } }
|
||||
public static string LastLog = "";
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// 현재 활성 파이프라인 에셋
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>지금 실제로 쓰이는 URP 에셋(Quality 별 오버라이드가 있으면 그것 · 없으면 Graphics 기본).</summary>
|
||||
public static UniversalRenderPipelineAsset CurrentAsset
|
||||
{
|
||||
get { return GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset; }
|
||||
}
|
||||
|
||||
/// <summary>목표 픽셀 높이와 화면 높이로 renderScale 을 구한다(SO 상·하한으로 클램프).</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// 적용 / 복원 (프로브·캡처도 이 두 개만 쓴다)
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>SO 값 그대로 적용. 이미 적용 중이면 해상도만 다시 맞춘다.</summary>
|
||||
public static bool ApplyNow(string reason)
|
||||
{
|
||||
var c = Cfg;
|
||||
if (c == null) return false;
|
||||
return ApplyNow(c.targetHeightPx, Screen.height, reason);
|
||||
}
|
||||
|
||||
/// <summary>목표 픽셀 높이·화면 높이를 지정해서 적용(캡처 4단이 이 경로를 쓴다).</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>원래 값으로 되돌린다. 적용 중이 아니면 아무것도 하지 않는다(true = 실제로 되돌렸다).</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>프로브·A/B 용: 경계 감시 상태를 처음으로 되돌린다(적용 중이면 먼저 복원한다).</summary>
|
||||
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
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>프로브 전용 공개 진입점(AgentScripts 는 별도 어셈블리라 internal 을 못 본다).</summary>
|
||||
public static void TickForProbe() { Tick(); }
|
||||
|
||||
/// <summary>프로브 전용 공개 진입점 — 에디트 모드에서는 러너를 만들지 않는 것을 확인하는 데 쓴다.</summary>
|
||||
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"); }
|
||||
}
|
||||
|
||||
/// <summary>맵 진입/이탈 경계 — AutoCombatOnEnter 와 같은 규칙(메인 PC 교체 또는 맵 ID 변경).</summary>
|
||||
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<PixelLookRunner>();
|
||||
}
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||||
static void Boot()
|
||||
{
|
||||
if (WLPixelLookSettings.Enabled) EnsureRunner(); // 꺼져 있으면 오브젝트 0(C8)
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>도트 룩 스위치의 시간 축. 숨김 GameObject 1개 · 코루틴 0.</summary>
|
||||
internal sealed class PixelLookRunner : MonoBehaviour
|
||||
{
|
||||
void Update() { PixelLook.Tick(); }
|
||||
void OnDisable() { PixelLook.RestoreNow("runner-disable"); }
|
||||
void OnApplicationQuit() { PixelLook.RestoreNow("app-quit"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 🔴 에디터 전용 안전망 — 도메인 리로드로 static 이 날아가도 파이프라인 에셋을 되돌린다.
|
||||
/// 빌드에서는 세 함수 모두 빈 몸통이라 비용 0.
|
||||
/// </summary>
|
||||
internal static class EditorSafetyNet
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
const string kKey = "WL814a.PixelLook.Pending"; // "<assetPath>|<renderScale>|<filter>"
|
||||
|
||||
/// <summary>플레이모드 상태 훅 등록 + 지난 세션에 남은 값이 있으면 지금 복원한다.</summary>
|
||||
[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<UniversalRenderPipelineAsset>(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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 814a00039c7b4d1e8f2a6c5b3d47e903
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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<WLPixelLookSettings>("WL/WLPixelLookSettings") 1회 로드 후 캐시(기존 WL 설정 SO 관례).
|
||||
//
|
||||
// 🔴 어셈블리 주의: Assets/WL/Look/ 에 .asmdef 를 만들지 말 것(MyValue·InGameInfo 가 Assembly-CSharp 에 있다).
|
||||
// 🔴 이 파일은 Debug.LogError / Debug.LogException 을 쓰지 않는다(813z 규칙).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace WL.Look
|
||||
{
|
||||
/// <summary>낮은 해상도를 화면 크기로 확대할 때 쓰는 필터. URP UpscalingFilterSelection 으로 매핑된다.</summary>
|
||||
public enum PixelUpscaleFilter
|
||||
{
|
||||
/// <summary>Nearest-Neighbor — 픽셀이 네모로 남는다(도트 룩의 핵심).</summary>
|
||||
Point = 0,
|
||||
/// <summary>Bilinear — 뭉개진다(비교용).</summary>
|
||||
Linear = 1,
|
||||
/// <summary>URP 자동 선택(렌더 스케일·해상도에 따라 Point 또는 Linear).</summary>
|
||||
Auto = 2,
|
||||
/// <summary>AMD FSR 1.0(선명하게 복원 = 도트 룩과 반대 방향 · 비교용).</summary>
|
||||
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;
|
||||
|
||||
/// <summary>설정 에셋(1회 로드 후 캐시). 없으면 null = 기능 전체 무동작.</summary>
|
||||
public static WLPixelLookSettings Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!s_lookupDone)
|
||||
{
|
||||
s_cached = Resources.Load<WLPixelLookSettings>(ResourcesPath);
|
||||
s_lookupDone = true;
|
||||
}
|
||||
return s_cached;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>진단·A/B 전용 런타임 스위치(WLCameraFramingSettings.RuntimeDisabled 선례).</summary>
|
||||
public static bool RuntimeDisabled;
|
||||
|
||||
/// <summary>기능이 살아 있는가 = 에셋 존재 · enabled · 런타임 스위치 off.</summary>
|
||||
public static bool Enabled
|
||||
{
|
||||
get { var i = Instance; return i != null && i.enabled && !RuntimeDisabled; }
|
||||
}
|
||||
|
||||
/// <summary>테스트용: 캐시를 비워 다음 접근 때 다시 Resources.Load 한다.</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 814a00029c7b4d1e8f2a6c5b3d47e902
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Loading…
Reference in New Issue