749 lines
38 KiB
C#
749 lines
38 KiB
C#
// WL-814j 프로브 — 팔레트 양자화 + Bayer 디더(저해상도 게임 카메라 한정) 실측 (에디트 모드 · Play 0 · 로그인 0)
|
||
// probe: unity command run_script --file AgentScripts/WL814j_Probe.cs --entry WL814j_Probe.RunAll
|
||
// cap: unity command run_script --file AgentScripts/WL814j_Probe.cs --entry WL814j_Probe.Capture
|
||
// 산출물: AgentScripts/WL814j_PROBE.txt · WL814j_CAPTURE.txt · 캡처 PNG 4장
|
||
// 🔴 씬 저장 0 · 에셋 쓰기 0(AssetDatabase.SaveAssets 호출 0 = URP 에셋 재저장 방지) · 임시 오브젝트는 DontSave + DestroyImmediate.
|
||
|
||
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 WL.Look.Env;
|
||
using WL.Look.Palette;
|
||
using WL.Look.Toggle;
|
||
using WL.PixelArt;
|
||
|
||
public static class WL814j_Probe
|
||
{
|
||
const string kOutDir = @"E:\NerdNavis\nn_himminji\Screenshots_WL\WL814j";
|
||
const string kTxt = "AgentScripts/WL814j_PROBE.txt";
|
||
const string kTxtCap = "AgentScripts/WL814j_CAPTURE.txt";
|
||
const string kMapPrefab = "Assets/Res_Addr/Map/WL_Nature.prefab";
|
||
const string kCamPrefab = "Assets/ResWork/Prefabs/Ingame/Main Camera.prefab";
|
||
const string kRendererPath = "Assets/Settings/URP-Balanced-Renderer.asset";
|
||
|
||
// 도트A 프리셋 = ortho 3.4 · pixelHeight 240 → 9:16 → 135×240 · 화면 1080×1920 = 정수 8배
|
||
const int kLowW = 135, kLowH = 240, kScale = 8;
|
||
const int kHiW = kLowW * kScale, kHiH = kLowH * kScale;
|
||
|
||
static StringBuilder s_sb;
|
||
static int s_pass, s_fail;
|
||
static void L(string s) { s_sb.AppendLine(s); }
|
||
static void H(string s) { s_sb.AppendLine(); s_sb.AppendLine("── " + s); }
|
||
static void Chk(string n, bool ok, string d) { if (ok) s_pass++; else s_fail++; L((ok ? " PASS " : " FAIL ") + n + " — " + d); }
|
||
static void Write(string p) { File.WriteAllText(p, s_sb.ToString(), new UTF8Encoding(false)); Debug.Log("[WL814j] → " + p + " · " + (s_fail == 0 ? "PASS" : "FAIL") + " " + s_pass + "/" + (s_pass + s_fail)); }
|
||
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
// RunAll
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
public static void RunAll()
|
||
{
|
||
s_sb = new StringBuilder(); s_pass = 0; s_fail = 0;
|
||
L("# WL-814j 프로브 " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " (edit mode · Play 0)");
|
||
L("playMode=" + Application.isPlaying + " · colorSpace=" + QualitySettings.activeColorSpace);
|
||
|
||
bool savedRtDisable = WLPaletteSettings.RuntimeDisabled;
|
||
try
|
||
{
|
||
A_Setup();
|
||
B_TargetOnly();
|
||
C_DitherGrid();
|
||
D_Levels();
|
||
E_Gc();
|
||
F_C8();
|
||
G_ModeCycle();
|
||
}
|
||
catch (Exception ex) { s_fail++; L("EXCEPTION " + ex); }
|
||
finally
|
||
{
|
||
WLPaletteSettings.RuntimeDisabled = savedRtDisable;
|
||
Z_Cleanup();
|
||
}
|
||
|
||
L("");
|
||
L("RESULT " + (s_fail == 0 ? "PASS" : "FAIL") + " · " + s_pass + "/" + (s_pass + s_fail) + " · FAIL " + s_fail);
|
||
Write(kTxt);
|
||
}
|
||
|
||
// ─────────────────────────────────────────── A. 전제
|
||
static void A_Setup()
|
||
{
|
||
H("A. 전제 — SO · 셰이더 · 렌더러 피처");
|
||
WLPaletteSettings.ClearCache();
|
||
var c = WLPaletteSettings.Instance;
|
||
Chk("A1 SO", c != null, c != null ? "Resources/WL/WLPaletteSettings · enabled " + c.enabled_ + " · levels " + c.levels + " · dither " + c.ditherStrength : "없음");
|
||
|
||
var sh = Shader.Find(PaletteDitherFeature.ShaderName);
|
||
Chk("A2 셰이더", sh != null && sh.isSupported, sh != null ? PaletteDitherFeature.ShaderName + " · isSupported " + sh.isSupported + " · passCount " + sh.passCount : "없음");
|
||
|
||
var rd = AssetDatabase.LoadAssetAtPath<ScriptableRendererData>(kRendererPath);
|
||
Chk("A3 렌더러 에셋", rd != null, kRendererPath);
|
||
if (rd != null)
|
||
{
|
||
int pal = 0;
|
||
L(" 피처 " + rd.rendererFeatures.Count + "개(순서대로):");
|
||
for (int i = 0; i < rd.rendererFeatures.Count; i++)
|
||
{
|
||
var f = rd.rendererFeatures[i];
|
||
L(" [" + i + "] " + (f == null ? "(null)" : f.name + " [" + f.GetType().Name + "] active=" + f.isActive));
|
||
if (f is PaletteDitherFeature) pal++;
|
||
}
|
||
Chk("A4 팔레트 피처 1건만", pal == 1, "WLPaletteDither " + pal + "개");
|
||
}
|
||
|
||
PaletteDither.ClearFeatureCache();
|
||
var feat = PaletteDither.SetFeature(true);
|
||
Chk("A5 피처 찾기·켜기", feat && PaletteDither.FeatureActiveNow, "isActive=" + PaletteDither.FeatureActiveNow);
|
||
var pf = PaletteDither.Feature as PaletteDitherFeature;
|
||
Chk("A6 머티리얼 생성", pf != null && pf.ProbeMaterial != null, pf != null && pf.ProbeMaterial != null ? pf.ProbeMaterial.shader.name : "없음");
|
||
}
|
||
|
||
// ─────────────────────────────────────────── B. 대상 카메라에서만 패스
|
||
static Camera _camA, _camB, _camPrev;
|
||
static GameObject _goA, _goB, _goPrev;
|
||
|
||
static void B_TargetOnly()
|
||
{
|
||
H("B. 🔴 대상 카메라에서만 패스 삽입 (뷰·UI·프리뷰 0)");
|
||
_goA = MakeCam("__WL814j_Target", out _camA);
|
||
_goB = MakeCam("__WL814j_Other", out _camB);
|
||
_goPrev = MakeCam("__WL814j_Preview", out _camPrev);
|
||
_camPrev.cameraType = CameraType.Preview;
|
||
|
||
PaletteDitherFeature.TargetCamera = _camA;
|
||
PaletteDither.SetFeature(true);
|
||
|
||
PaletteDitherFeature.ResetCounters();
|
||
RenderToLow(_camA, null);
|
||
int a = PaletteDitherFeature.PassesAdded;
|
||
Chk("B1 대상 카메라 = 패스 1건", a == 1, "PassesAdded " + a + " · 대상 '" + PaletteDitherFeature.LastTargetName + "' · 이벤트 " + PaletteDitherFeature.LastEvent);
|
||
|
||
PaletteDitherFeature.ResetCounters();
|
||
RenderToLow(_camB, null);
|
||
Chk("B2 다른 카메라 = 패스 0", PaletteDitherFeature.PassesAdded == 0,
|
||
"PassesAdded " + PaletteDitherFeature.PassesAdded + " · skip=" + PaletteDitherFeature.LastSkipReason);
|
||
|
||
PaletteDitherFeature.ResetCounters();
|
||
RenderToLow(_camPrev, null);
|
||
Chk("B3 프리뷰 카메라 = 패스 0", PaletteDitherFeature.PassesAdded == 0,
|
||
"PassesAdded " + PaletteDitherFeature.PassesAdded + " · skip=" + PaletteDitherFeature.LastSkipReason);
|
||
|
||
// 대상 미지정 + 리그 미조립 → 어느 카메라에서도 0
|
||
PaletteDitherFeature.TargetCamera = null;
|
||
PaletteDitherFeature.ResetCounters();
|
||
RenderToLow(_camA, null);
|
||
Chk("B4 대상 없음 = 패스 0", PaletteDitherFeature.PassesAdded == 0,
|
||
"리그조립 " + PixelCameraRig.IsAssembled + " · skip=" + PaletteDitherFeature.LastSkipReason);
|
||
PaletteDitherFeature.TargetCamera = _camA;
|
||
|
||
// 렌더 순서 실측 — 814c 외곽선 피처와의 상대 위치
|
||
var rd = AssetDatabase.LoadAssetAtPath<ScriptableRendererData>(kRendererPath);
|
||
string order = "";
|
||
if (rd != null)
|
||
foreach (var f in rd.rendererFeatures)
|
||
if (f != null) order += (order.Length > 0 ? " → " : "") + f.name;
|
||
var c = WLPaletteSettings.Instance;
|
||
L(" 렌더러 피처 배열 순서 = " + order);
|
||
L(" 팔레트 패스 이벤트 = " + PaletteDitherFeature.LastEvent + "(" + (int)PaletteDitherFeature.LastEvent + ")" +
|
||
" · SO afterPostProcessing=" + (c != null && c.afterPostProcessing));
|
||
L(" 814c PixelOutlineSetup 이벤트 = AfterRenderingOpaques(" + (int)RenderPassEvent.AfterRenderingOpaques + ") — 빈 패스로 깊이·노멀·색 텍스처만 켠다.");
|
||
L(" → 외곽선 자체는 Critter Toon 머티리얼 안에서 **불투명 지오메트리와 함께** 그려지므로 항상 이 패스보다 앞이다 = 외곽선까지 양자화된다.");
|
||
Chk("B5 팔레트가 외곽선보다 뒤", (int)PaletteDitherFeature.LastEvent > (int)RenderPassEvent.AfterRenderingOpaques,
|
||
(int)PaletteDitherFeature.LastEvent + " > " + (int)RenderPassEvent.AfterRenderingOpaques);
|
||
}
|
||
|
||
// ─────────────────────────────────────────── C. 디더 격자 = RT 픽셀 1:1
|
||
static void C_DitherGrid()
|
||
{
|
||
H("C. 🔴 Bayer 디더 격자가 RT 픽셀과 1:1 (단색 화면 · 4×4 주기 실측)");
|
||
var c = WLPaletteSettings.Instance;
|
||
if (c == null || _camA == null) { Chk("C0", false, "전제 없음"); return; }
|
||
|
||
float savedD = c.ditherStrength; bool saved8 = c.dither8x8; int savedL = c.levels;
|
||
try
|
||
{
|
||
_camA.cullingMask = 0;
|
||
_camA.clearFlags = CameraClearFlags.SolidColor;
|
||
_camA.backgroundColor = new Color(0.52f, 0.31f, 0.66f, 1f); // 세 채널 모두 양자화 경계 근처
|
||
PaletteDitherFeature.TargetCamera = _camA;
|
||
PaletteDither.SetFeature(true);
|
||
|
||
// ① 디더 off = 완전 단색
|
||
c.levels = 6; c.ditherStrength = 0f; c.dither8x8 = false;
|
||
var off = ReadLow(_camA);
|
||
int offColors = Distinct(off);
|
||
Chk("C1 디더 0 → 단색 1종", offColors == 1, "고유 색 " + offColors + "종 · " + Rgb(off[0]));
|
||
|
||
// ② 디더 4×4
|
||
c.ditherStrength = 0.6f;
|
||
var d4 = ReadLow(_camA);
|
||
int d4Colors = Distinct(d4);
|
||
int bad4x = Period(d4, 4, 0), bad4y = Period(d4, 0, 4);
|
||
int bad2x = Period(d4, 2, 0), bad2y = Period(d4, 0, 2);
|
||
Chk("C2 디더 4×4 → 색 2종 이상", d4Colors >= 2, "고유 색 " + d4Colors + "종");
|
||
Chk("C3 x 주기 4 정확", bad4x == 0, "p(x,y) != p(x+4,y) 인 픽셀 " + bad4x + "개 / " + ((kLowW - 4) * kLowH));
|
||
Chk("C4 y 주기 4 정확", bad4y == 0, "p(x,y) != p(x,y+4) 인 픽셀 " + bad4y + "개 / " + (kLowW * (kLowH - 4)));
|
||
Chk("C5 주기 2 아님(= 진짜 4×4)", bad2x > 0 || bad2y > 0, "x " + bad2x + " · y " + bad2y + " 개 불일치");
|
||
|
||
// ③ 디더 8×8
|
||
c.dither8x8 = true;
|
||
var d8 = ReadLow(_camA);
|
||
int bad8x = Period(d8, 8, 0), bad8y = Period(d8, 0, 8);
|
||
int bad84x = Period(d8, 4, 0);
|
||
Chk("C6 8×8 x 주기 8 정확", bad8x == 0, "불일치 " + bad8x + "개");
|
||
Chk("C7 8×8 y 주기 8 정확", bad8y == 0, "불일치 " + bad8y + "개");
|
||
Chk("C8g 8×8 는 주기 4 아님", bad84x > 0, "주기 4 불일치 " + bad84x + "개");
|
||
|
||
L(" → 디더 점 1개 = 저해상도 RT 1픽셀(135×240). 화면(1080×1920)에서는 Point 확대로 8×8 화면픽셀 블록이 된다.");
|
||
}
|
||
finally
|
||
{
|
||
c.ditherStrength = savedD; c.dither8x8 = saved8; c.levels = savedL;
|
||
_camA.cullingMask = -1; _camA.clearFlags = CameraClearFlags.Skybox;
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────── D. 레벨별 색 수 상한
|
||
static void D_Levels()
|
||
{
|
||
H("D. 채널 레벨별 색 수 (그라데이션 화면 · levels^3 상한 실측)");
|
||
var c = WLPaletteSettings.Instance;
|
||
if (c == null || _camA == null) { Chk("D0", false, "전제 없음"); return; }
|
||
|
||
int savedL = c.levels; float savedD = c.ditherStrength;
|
||
GameObject quad = null;
|
||
try
|
||
{
|
||
// 화면을 꽉 채우는 그라데이션 = 스카이박스(하늘 그라데이션)로 대신한다 — 색이 충분히 다양하다.
|
||
_camA.cullingMask = 0;
|
||
_camA.clearFlags = CameraClearFlags.Skybox;
|
||
PaletteDitherFeature.TargetCamera = _camA;
|
||
|
||
PaletteDither.SetFeature(false);
|
||
var baseline = ReadLow(_camA);
|
||
int n0 = Distinct(baseline);
|
||
|
||
PaletteDither.SetFeature(true);
|
||
L("");
|
||
L(" | levels | dither | 고유 색 수 | 상한 levels^3 |");
|
||
L(" |---|---|---|---|");
|
||
L(" | (패스 없음) | — | " + n0 + " | — |");
|
||
int[] ls = { 4, 6, 8 };
|
||
for (int i = 0; i < ls.Length; i++)
|
||
{
|
||
c.levels = ls[i]; c.ditherStrength = 0.6f;
|
||
var px = ReadLow(_camA);
|
||
int n = Distinct(px);
|
||
int cap = ls[i] * ls[i] * ls[i];
|
||
L(" | " + ls[i] + " | 0.6 | " + n + " | " + cap + " |");
|
||
Chk("D" + (i + 1) + " levels " + ls[i] + " 상한 준수", n <= cap, n + " ≤ " + cap);
|
||
}
|
||
c.levels = 6;
|
||
var q6 = ReadLow(_camA);
|
||
Chk("D4 색 수 감소", Distinct(q6) < n0, "패스 없음 " + n0 + "종 → levels 6 " + Distinct(q6) + "종");
|
||
}
|
||
finally
|
||
{
|
||
c.levels = savedL; c.ditherStrength = savedD;
|
||
if (quad != null) UnityEngine.Object.DestroyImmediate(quad);
|
||
_camA.cullingMask = -1;
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────── E. GC
|
||
static void E_Gc()
|
||
{
|
||
H("E. GC — 프레임당 할당 0");
|
||
var pf = PaletteDither.Feature as PaletteDitherFeature;
|
||
PaletteDither.Tick(); // 안정화
|
||
|
||
Gc("PaletteDither.Tick ×200k", delegate { PaletteDither.Tick(); }, 200000);
|
||
Gc("PaletteDither.Want ×200k", delegate { PaletteDither.Want(); }, 200000);
|
||
if (pf != null) Gc("피처 값 푸시(SO→머티리얼) ×200k", delegate { pf.ProbePushValues(); }, 200000);
|
||
|
||
// 대조군 — 계기 감도
|
||
long b2 = GC.GetTotalMemory(false);
|
||
var junk = new byte[16 * 1024 * 1024]; junk[0] = 1;
|
||
long a2 = GC.GetTotalMemory(false);
|
||
Chk("E9 대조군 16 MB 감지", a2 - b2 > 15000000, "Δ" + (a2 - b2) + " B = 계기 감도 확인");
|
||
junk = null;
|
||
}
|
||
|
||
static void Gc(string name, Action a, int n)
|
||
{
|
||
a();
|
||
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
||
long mono0 = GC.GetTotalMemory(false);
|
||
long total0 = UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong();
|
||
int g0 = GC.CollectionCount(0);
|
||
for (int i = 0; i < n; i++) a();
|
||
long mono1 = GC.GetTotalMemory(false);
|
||
long total1 = UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong();
|
||
Chk("E " + name, mono1 - mono0 == 0 && GC.CollectionCount(0) == g0,
|
||
"mono Δ" + (mono1 - mono0) + " B · profiler Δ" + (total1 - total0) + " B · gen0 " + g0 + "→" + GC.CollectionCount(0));
|
||
}
|
||
|
||
// ─────────────────────────────────────────── F. C8
|
||
static void F_C8()
|
||
{
|
||
H("F. C8 — SO off / 피처 off 면 저해상도 RT 가 원본과 픽셀 단위로 같다");
|
||
var c = WLPaletteSettings.Instance;
|
||
if (c == null || _camA == null) { Chk("F0", false, "전제 없음"); return; }
|
||
|
||
_camA.cullingMask = 0;
|
||
_camA.clearFlags = CameraClearFlags.Skybox;
|
||
PaletteDitherFeature.TargetCamera = _camA;
|
||
|
||
PaletteDither.SetFeature(false);
|
||
var orig = ReadLow(_camA);
|
||
long hOrig = Hash(orig);
|
||
|
||
// ① 피처 on + SO off
|
||
PaletteDither.SetFeature(true);
|
||
WLPaletteSettings.RuntimeDisabled = true;
|
||
PaletteDitherFeature.ResetCounters();
|
||
var so_off = ReadLow(_camA);
|
||
Chk("F1 SO off → 패스 0", PaletteDitherFeature.PassesAdded == 0, "PassesAdded " + PaletteDitherFeature.PassesAdded + " · skip=" + PaletteDitherFeature.LastSkipReason);
|
||
Chk("F2 SO off → 픽셀 동일", Hash(so_off) == hOrig, "해시 " + Hash(so_off) + " vs " + hOrig);
|
||
WLPaletteSettings.RuntimeDisabled = false;
|
||
|
||
// ② 피처 off
|
||
PaletteDither.SetFeature(false);
|
||
PaletteDitherFeature.ResetCounters();
|
||
var f_off = ReadLow(_camA);
|
||
Chk("F3 피처 off → AddRenderPasses 호출 0", PaletteDitherFeature.PassesAdded == 0 && PaletteDitherFeature.PassesSkipped == 0,
|
||
"added " + PaletteDitherFeature.PassesAdded + " · skipped " + PaletteDitherFeature.PassesSkipped + "(URP 가 비활성 피처를 아예 안 부른다)");
|
||
Chk("F4 피처 off → 픽셀 동일", Hash(f_off) == hOrig, "해시 " + Hash(f_off) + " vs " + hOrig);
|
||
|
||
// ③ 다시 켜면 달라진다(대조군)
|
||
PaletteDither.SetFeature(true);
|
||
var on = ReadLow(_camA);
|
||
Chk("F5 대조군 — 켜면 달라진다", Hash(on) != hOrig, "해시 " + Hash(on) + " != " + hOrig);
|
||
|
||
_camA.cullingMask = -1;
|
||
}
|
||
|
||
// ─────────────────────────────────────────── G. 도트 모드 전환 3회 왕복
|
||
static GameObject _mainCamGo, _infoGo;
|
||
static Camera _srcCam;
|
||
|
||
static void G_ModeCycle()
|
||
{
|
||
H("G. 도트 모드 전환 3회 왕복 — 예외 0 · 대상 카메라가 리그 게임 카메라로 따라온다");
|
||
PaletteDitherFeature.TargetCamera = null; // 🔴 리그 폴백 경로를 실제로 태운다
|
||
|
||
var pf = AssetDatabase.LoadAssetAtPath<GameObject>(kCamPrefab);
|
||
if (pf == null) { Chk("G0 원본 카메라 프리팹", false, kCamPrefab); return; }
|
||
_mainCamGo = UnityEngine.Object.Instantiate(pf);
|
||
_mainCamGo.name = "__WL814j_MainCamera";
|
||
_mainCamGo.hideFlags = HideFlags.DontSave;
|
||
_srcCam = _mainCamGo.GetComponent<Camera>();
|
||
// 리그는 Camera.main 을 원본으로 쓴다. 임시 씬에 이미 MainCamera 태그 카메라가 있으면 그쪽이 뽑힐 수 있는데,
|
||
// 검증 대상은 「도트 모드에서 리그 게임 카메라가 대상이 되는가」이므로 Camera.main 이 있기만 하면 된다.
|
||
Chk("G1 Camera.main 존재", Camera.main != null,
|
||
"Camera.main = " + (Camera.main != null ? Camera.main.name : "null") + " · 프로브가 넣은 것 = " + (_srcCam != null ? _srcCam.name : "null"));
|
||
|
||
// 가짜 InGameInfo — 로비 판정만 인게임으로(에디트 모드 Awake 미실행 · Ins 는 순수 static)
|
||
_infoGo = new GameObject("__WL814j_InGameInfo");
|
||
_infoGo.hideFlags = HideFlags.DontSave;
|
||
var info = _infoGo.AddComponent<InGameInfo>();
|
||
InGameInfo.Ins = info;
|
||
var fld = typeof(InGameInfo).GetField("m_GameMode", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
||
if (fld != null) fld.SetValue(info, eGameMode.Stage);
|
||
Chk("G2 인게임 판정", !LookModeHub.InLobby, "InLobby=" + LookModeHub.InLobby);
|
||
|
||
LookModeHub.ResetAll();
|
||
PaletteDither.ClearFeatureCache();
|
||
PaletteDither.ResetForProbe();
|
||
PaletteDither.EnsureSubscribed();
|
||
|
||
int ex = 0;
|
||
L("");
|
||
L(" | 왕복 | 모드 | 리그조립 | 대상 카메라 | RT | 팔레트 on | 피처 active |");
|
||
L(" |---|---|---|---|---|---|---|");
|
||
for (int round = 1; round <= 3; round++)
|
||
{
|
||
for (int m = 1; m >= 0; m--)
|
||
{
|
||
try
|
||
{
|
||
LookModeHub.SetCameraMode(m);
|
||
LookModeHub.Tick();
|
||
PaletteDither.Tick();
|
||
}
|
||
catch (Exception e) { ex++; L(" EXCEPTION mode " + m + " — " + e.GetType().Name + " " + e.Message); }
|
||
|
||
var t = PaletteDitherFeature.ResolvedTarget;
|
||
var mgr = PixelCameraRig.Manager;
|
||
string rt = mgr != null ? mgr.GameResolution.x + "×" + mgr.GameResolution.y : "—";
|
||
L(" | " + round + " | " + m + " | " + (PixelCameraRig.IsAssembled ? "O" : "-") +
|
||
" | " + (t != null ? t.name : "(없음)") + " | " + rt +
|
||
" | " + PaletteDither.IsOn + " | " + PaletteDither.FeatureActiveNow + " |");
|
||
|
||
if (m == 1)
|
||
{
|
||
Chk("G" + round + "a 도트 모드 → on", PaletteDither.IsOn && PaletteDither.FeatureActiveNow,
|
||
"on=" + PaletteDither.IsOn + " 피처=" + PaletteDither.FeatureActiveNow);
|
||
Chk("G" + round + "b 대상 = 리그 게임 카메라", t != null && ReferenceEquals(t, PixelCameraRig.GameCamera),
|
||
t != null ? t.name : "(없음)");
|
||
}
|
||
else
|
||
{
|
||
Chk("G" + round + "c 기본 모드 → off", !PaletteDither.IsOn && !PaletteDither.FeatureActiveNow,
|
||
"on=" + PaletteDither.IsOn + " 피처=" + PaletteDither.FeatureActiveNow);
|
||
}
|
||
}
|
||
}
|
||
Chk("G9 예외 0", ex == 0, ex + "건");
|
||
L(" " + PaletteDither.Dump());
|
||
|
||
// 로비 강등 = off
|
||
if (fld != null) fld.SetValue(info, eGameMode.Lobby);
|
||
LookModeHub.SetCameraMode(1);
|
||
LookModeHub.Tick();
|
||
PaletteDither.Tick();
|
||
Chk("G10 로비 강등 → off", !PaletteDither.IsOn, "AppliedMode " + LookModeHub.AppliedMode + " · on " + PaletteDither.IsOn);
|
||
if (fld != null) fld.SetValue(info, eGameMode.Stage);
|
||
}
|
||
|
||
// ─────────────────────────────────────────── Z. 정리
|
||
static void Z_Cleanup()
|
||
{
|
||
H("Z. 정리");
|
||
try
|
||
{
|
||
PaletteDitherFeature.TargetCamera = null;
|
||
PaletteDither.SetFeature(false);
|
||
PaletteDither.ResetForProbe();
|
||
LookModeHub.ResetAll();
|
||
PixelCameraRig.Teardown("814j probe end");
|
||
InGameInfo.Ins = null;
|
||
DestroyGo(ref _goA); DestroyGo(ref _goB); DestroyGo(ref _goPrev);
|
||
DestroyGo(ref _mainCamGo); DestroyGo(ref _infoGo);
|
||
_camA = _camB = _camPrev = _srcCam = null;
|
||
var strays = UnityEngine.Object.FindObjectsByType<Camera>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||
L(" 남은 카메라 " + strays.Length + "개 · 리그조립 " + PixelCameraRig.IsAssembled + " · 피처 active " + PaletteDither.FeatureActiveNow);
|
||
L(" AssetDatabase.SaveAssets 호출 0 · 씬 저장 0");
|
||
}
|
||
catch (Exception e) { L(" 정리 중 예외 " + e.GetType().Name + " " + e.Message); }
|
||
}
|
||
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
// Capture — 임시 씬(저장 0) · 도트A(ortho 3.4 · px 240) · 135×240 → 1080×1920 Point(CPU 정수 8배) · 4장
|
||
// ═════════════════════════════════════════════════════════════════════════
|
||
public static void Capture()
|
||
{
|
||
s_sb = new StringBuilder(); s_pass = 0; s_fail = 0;
|
||
L("# WL-814j 팔레트 캡처 " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
|
||
L("도트A 기준 = ortho 3.4 · pixelHeight 240 → RT " + kLowW + "×" + kLowH + " → CPU 정수 " + kScale + "배 = " + kHiW + "×" + kHiH + " PNG");
|
||
|
||
var pc = WLPaletteSettings.Instance;
|
||
var ec = WLEnvLookSettings.Instance;
|
||
Scene temp = default(Scene);
|
||
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(kMapPrefab, Vector3.zero);
|
||
Chk("K0 맵", map != null, "WL_Nature");
|
||
if (map == null) return;
|
||
|
||
// 814c 와 같은 지점·같은 구도(811o h 76 · yaw 233) — 나무·물이 있는 곳
|
||
const float h = 76f, yaw = 233f, dUp = 2.4f, ortho = 3.4f, camBack = 25f, farClip = 120f;
|
||
Vector3 dir = (Quaternion.Euler(h, yaw, 0f) * Vector3.one).normalized;
|
||
Vector3 fwd0 = new Vector3(dir.x, 0f, dir.z).normalized;
|
||
|
||
string aname;
|
||
Vector3 scenic = ScenicSpot(map, out aname);
|
||
Vector3 pcPos = scenic - fwd0 * 11f;
|
||
RaycastHit hit;
|
||
if (Physics.Raycast(pcPos + Vector3.up * 300f, Vector3.down, out hit, 700f)) pcPos = hit.point;
|
||
L(" 기준점(" + aname + ") " + scenic.ToString("0.0") + " → PC " + pcPos.ToString("0.0"));
|
||
|
||
var camGo = new GameObject("__WL814j_OrthoCam");
|
||
camGo.hideFlags = HideFlags.DontSave;
|
||
var cam = camGo.AddComponent<Camera>();
|
||
var acd = camGo.AddComponent<UniversalAdditionalCameraData>();
|
||
acd.renderShadows = true;
|
||
acd.renderPostProcessing = false;
|
||
acd.antialiasing = AntialiasingMode.None;
|
||
cam.orthographic = true;
|
||
cam.orthographicSize = ortho;
|
||
cam.nearClipPlane = 0.1f;
|
||
cam.farClipPlane = farClip;
|
||
cam.clearFlags = CameraClearFlags.Skybox;
|
||
Vector3 lookAt = pcPos + new Vector3(0f, 1.06f, 0f) + fwd0 * 2f;
|
||
cam.transform.position = pcPos + Vector3.up * dUp - dir * camBack;
|
||
cam.transform.LookAt(lookAt);
|
||
Vector3 fwd = pcPos - cam.transform.position; fwd.y = 0f; fwd.Normalize();
|
||
Vector3 right = Vector3.Cross(Vector3.up, fwd);
|
||
L(" 카메라 직교 size " + ortho + " · pos " + cam.transform.position.ToString("0.0") + " · look " + lookAt.ToString("0.0") + " · near 0.1 / far " + farClip);
|
||
|
||
var pcGo = Inst("Assets/Res_Addr/PC/Ai01.prefab", pcPos);
|
||
if (pcGo != null) pcGo.transform.rotation = Quaternion.LookRotation(fwd, Vector3.up);
|
||
string[] mobs = { "Batty_A", "Batty_B", "Batty_C" };
|
||
Vector2[] off = { new Vector2(2.4f, 1.2f), new Vector2(3.4f, -1.5f), new Vector2(4.4f, 0.5f) };
|
||
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("K1 PC · 잡몹", pcGo != null && mobGos.Count == 3, "PC " + (pcGo != null) + " · 몹 " + mobGos.Count + "/3");
|
||
Pose(pcGo); foreach (var m in mobGos) Pose(m);
|
||
|
||
// A안(814i) 상태 재현 = 배경 룩(Critter Toon · _Shades 3) 적용 — 도트A 프리셋은 envLookOn 1 이다
|
||
EnvLook.ResetForProbe();
|
||
EnvLook.BeginForProbe();
|
||
int swMap = EnvLook.SweepRoot(map.transform);
|
||
EnvLook.SetFeature(true);
|
||
L(" 배경 룩(814c/814i) 적용 — 맵 렌더러 " + swMap + "개 스왑 · 외곽선 피처 on · Toon _Shades = " + (ec != null ? ec.shades.ToString() : "?"));
|
||
|
||
PaletteDither.ClearFeatureCache();
|
||
PaletteDitherFeature.TargetCamera = cam;
|
||
|
||
int savedLevels = pc != null ? pc.levels : 6;
|
||
float savedDither = pc != null ? pc.ditherStrength : 0.6f;
|
||
bool saved8 = pc != null && pc.dither8x8;
|
||
|
||
var rows = new List<string>();
|
||
|
||
// ① 디더 off = A안만 (팔레트 패스 자체를 넣지 않는다)
|
||
PaletteDither.SetFeature(false);
|
||
rows.Add(Shot(cam, kOutDir + @"\1_dither_off.png", "① A안만(팔레트 패스 0)", "—", "—"));
|
||
|
||
// ②~④ 팔레트 on
|
||
PaletteDither.SetFeature(true);
|
||
int[] ls = { 6, 4, 8 };
|
||
float[] ds = { 0.6f, 0.8f, 0.3f };
|
||
string[] files = { @"\2_levels6_dither0.6.png", @"\3_levels4_dither0.8.png", @"\4_levels8_dither0.3.png" };
|
||
string[] labels = { "② 권장 기본", "③ 더 도트답게", "④ 약하게" };
|
||
for (int i = 0; i < 3; i++)
|
||
{
|
||
if (pc != null) { pc.levels = ls[i]; pc.ditherStrength = ds[i]; pc.dither8x8 = false; }
|
||
rows.Add(Shot(cam, kOutDir + files[i], labels[i], ls[i].ToString(), ds[i].ToString("0.0")));
|
||
}
|
||
|
||
if (pc != null) { pc.levels = savedLevels; pc.ditherStrength = savedDither; pc.dither8x8 = saved8; }
|
||
|
||
L("");
|
||
L(" | 파일 | 무엇 | levels | dither | 고유 색 수(전수) | 814c 방식 표본(1/331) | 크기 |");
|
||
L(" |---|---|---|---|---|---|---|");
|
||
foreach (var r in rows) L(" " + r);
|
||
|
||
Chk("K9 캡처 4장", Directory.GetFiles(kOutDir, "*.png").Length >= 4, Directory.GetFiles(kOutDir, "*.png").Length + "장");
|
||
}
|
||
catch (Exception ex) { s_fail++; L("EXCEPTION " + ex); }
|
||
finally
|
||
{
|
||
try { PaletteDitherFeature.TargetCamera = null; PaletteDither.SetFeature(false); } catch { }
|
||
try { EnvLook.SetFeature(false); EnvLook.RestoreNow("814j capture end"); } catch { }
|
||
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>저해상도 RT 로 1프레임 렌더하고 RGB24 픽셀을 돌려준다(Blit 0 = 색 왜곡 0).</summary>
|
||
static Color32[] ReadLow(Camera cam)
|
||
{
|
||
var low = new RenderTexture(kLowW, kLowH, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
|
||
low.filterMode = FilterMode.Point; low.antiAliasing = 1; low.Create();
|
||
var prev = cam.targetTexture;
|
||
cam.targetTexture = low;
|
||
cam.Render();
|
||
cam.targetTexture = prev;
|
||
|
||
var active = RenderTexture.active;
|
||
RenderTexture.active = low;
|
||
var tex = new Texture2D(kLowW, kLowH, TextureFormat.RGB24, false);
|
||
tex.ReadPixels(new Rect(0, 0, kLowW, kLowH), 0, 0);
|
||
tex.Apply();
|
||
RenderTexture.active = active;
|
||
var px = tex.GetPixels32();
|
||
UnityEngine.Object.DestroyImmediate(tex);
|
||
low.Release(); UnityEngine.Object.DestroyImmediate(low);
|
||
RenderTexture.active = null;
|
||
return px;
|
||
}
|
||
|
||
static void RenderToLow(Camera cam, RenderTexture unused)
|
||
{
|
||
var low = new RenderTexture(kLowW, kLowH, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
|
||
low.filterMode = FilterMode.Point; low.antiAliasing = 1; low.Create();
|
||
var prev = cam.targetTexture;
|
||
cam.targetTexture = low;
|
||
cam.Render();
|
||
cam.targetTexture = prev;
|
||
low.Release(); UnityEngine.Object.DestroyImmediate(low);
|
||
}
|
||
|
||
/// <summary>렌더 → PNG(정수 8배 Point 확대 · CPU) + 고유 색 수. 표 1줄을 돌려준다.</summary>
|
||
static string Shot(Camera cam, string file, string label, string levels, string dither)
|
||
{
|
||
var low = ReadLow(cam);
|
||
int distinct = Distinct(low);
|
||
|
||
// CPU 정수 배율 확대 = Point · 색 왜곡 0(Graphics.Blit 를 쓰면 sRGB 왕복에서 ±1 이 생길 수 있다)
|
||
var hi = new Color32[kHiW * kHiH];
|
||
for (int y = 0; y < kHiH; y++)
|
||
{
|
||
int sy = y / kScale;
|
||
int srow = sy * kLowW, drow = y * kHiW;
|
||
for (int x = 0; x < kHiW; x++) hi[drow + x] = low[srow + (x / kScale)];
|
||
}
|
||
var tex = new Texture2D(kHiW, kHiH, TextureFormat.RGB24, false);
|
||
tex.SetPixels32(hi);
|
||
tex.Apply();
|
||
File.WriteAllBytes(file, tex.EncodeToPNG());
|
||
|
||
// 814c 와 같은 방법(1080×1920 을 331 간격으로 표본) — 이전 보고서 숫자와 비교하려고 같이 센다
|
||
var seen = new HashSet<int>();
|
||
for (int i = 0; i < hi.Length; i += 331) seen.Add((hi[i].r << 16) | (hi[i].g << 8) | hi[i].b);
|
||
int sampled = seen.Count;
|
||
|
||
UnityEngine.Object.DestroyImmediate(tex);
|
||
var fi = new FileInfo(file);
|
||
Chk("K " + Path.GetFileName(file), fi.Exists && fi.Length > 2048 && distinct > 2,
|
||
label + " · 고유 색 " + distinct + "종(표본 " + sampled + ") · " + fi.Length + " B");
|
||
return "| `" + Path.GetFileName(file) + "` | " + label + " | " + levels + " | " + dither + " | **" + distinct + "** | " + sampled + " | " + (fi.Length / 1024) + " KB |";
|
||
}
|
||
|
||
static int Distinct(Color32[] px)
|
||
{
|
||
var seen = new HashSet<int>();
|
||
for (int i = 0; i < px.Length; i++) seen.Add((px[i].r << 16) | (px[i].g << 8) | px[i].b);
|
||
return seen.Count;
|
||
}
|
||
|
||
static long Hash(Color32[] px)
|
||
{
|
||
long h = 1469598103934665603L;
|
||
for (int i = 0; i < px.Length; i++)
|
||
{
|
||
h = (h ^ px[i].r) * 1099511628211L;
|
||
h = (h ^ px[i].g) * 1099511628211L;
|
||
h = (h ^ px[i].b) * 1099511628211L;
|
||
}
|
||
return h;
|
||
}
|
||
|
||
static string Rgb(Color32 c) { return "rgb(" + c.r + "," + c.g + "," + c.b + ")"; }
|
||
|
||
/// <summary>p(x,y) 와 p(x+dx, y+dy) 가 다른 픽셀 수.</summary>
|
||
static int Period(Color32[] px, int dx, int dy)
|
||
{
|
||
int bad = 0;
|
||
for (int y = 0; y + dy < kLowH; y++)
|
||
for (int x = 0; x + dx < kLowW; x++)
|
||
{
|
||
var a = px[y * kLowW + x];
|
||
var b = px[(y + dy) * kLowW + (x + dx)];
|
||
if (a.r != b.r || a.g != b.g || a.b != b.b) bad++;
|
||
}
|
||
return bad;
|
||
}
|
||
|
||
// ── 헬퍼 ─────────────────────────────────────────────────────────────────
|
||
static GameObject MakeCam(string name, out Camera cam)
|
||
{
|
||
var go = new GameObject(name);
|
||
go.hideFlags = HideFlags.DontSave;
|
||
cam = go.AddComponent<Camera>();
|
||
var acd = go.AddComponent<UniversalAdditionalCameraData>();
|
||
acd.renderPostProcessing = false;
|
||
acd.antialiasing = AntialiasingMode.None;
|
||
cam.orthographic = true;
|
||
cam.orthographicSize = 3.4f;
|
||
cam.clearFlags = CameraClearFlags.Skybox;
|
||
cam.nearClipPlane = 0.1f;
|
||
cam.farClipPlane = 120f;
|
||
cam.transform.position = new Vector3(0f, 5f, -10f);
|
||
cam.transform.rotation = Quaternion.Euler(20f, 0f, 0f);
|
||
return go;
|
||
}
|
||
|
||
static void DestroyGo(ref GameObject go)
|
||
{
|
||
if (go != null) { UnityEngine.Object.DestroyImmediate(go); go = null; }
|
||
}
|
||
|
||
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 void Pose(GameObject go)
|
||
{
|
||
if (go == null) return;
|
||
foreach (var an in go.GetComponentsInChildren<Animator>(true))
|
||
{
|
||
if (an.runtimeAnimatorController == null) continue;
|
||
AnimationClip idle = null;
|
||
foreach (var cl in an.runtimeAnimatorController.animationClips)
|
||
{
|
||
if (cl == null) continue;
|
||
var n = cl.name.ToLowerInvariant();
|
||
if (n.Contains("idle")) { idle = cl; break; }
|
||
if (idle == null) idle = cl;
|
||
}
|
||
if (idle != null) idle.SampleAnimation(an.gameObject, 0.5f);
|
||
}
|
||
}
|
||
|
||
/// <summary>814c 와 같은 「나무·수풀이 촘촘하고 물이 가까운」 지점 선정.</summary>
|
||
static Vector3 ScenicSpot(GameObject map, out string why)
|
||
{
|
||
var pts = new List<Vector3>();
|
||
var waters = new List<Bounds>();
|
||
foreach (var r in map.GetComponentsInChildren<Renderer>(true))
|
||
{
|
||
if (r == null || r is ParticleSystemRenderer) continue;
|
||
var m = r.sharedMaterial;
|
||
if (m == null) continue;
|
||
if (m.name == "Trees" || m.name == "Vegetation" || m.name == "Trees_Toon" || m.name == "Vegetation_Toon") pts.Add(r.bounds.center);
|
||
else if (m.name.IndexOf("Water", StringComparison.OrdinalIgnoreCase) >= 0) waters.Add(r.bounds);
|
||
}
|
||
if (pts.Count == 0) { why = "(나무 0 · 원점)"; return Vector3.zero; }
|
||
|
||
int step = Mathf.Max(1, pts.Count / 300);
|
||
Vector3 best = pts[0]; float bestScore = -1f; int bestN = 0; float bestWater = 9999f;
|
||
for (int i = 0; i < pts.Count; i += step)
|
||
{
|
||
var p = pts[i];
|
||
int n = 0;
|
||
for (int j = 0; j < pts.Count; j += step)
|
||
if ((pts[j] - p).sqrMagnitude < 25f * 25f) n++;
|
||
float wd = 9999f;
|
||
for (int w = 0; w < waters.Count; w++) wd = Mathf.Min(wd, Vector3.Distance(waters[w].ClosestPoint(p), p));
|
||
float score = n + (wd < 40f ? 12f : 0f) + (wd < 20f ? 12f : 0f);
|
||
if (score > bestScore) { bestScore = score; best = p; bestN = n; bestWater = wd; }
|
||
}
|
||
best.y = 0f;
|
||
RaycastHit hit;
|
||
if (Physics.Raycast(new Vector3(best.x, 400f, best.z), Vector3.down, out hit, 900f)) best = hit.point;
|
||
why = "나무·수풀 군락 " + bestN + "그루/25 m · 물까지 " + (bestWater > 9000f ? "없음" : bestWater.ToString("0") + " m");
|
||
return best;
|
||
}
|
||
}
|