2026-09-12 09:11:13 +00:00
// 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 > ( ) ;
2026-09-12 09:35:10 +00:00
// 리그는 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" ) ) ;
2026-09-12 09:11:13 +00:00
// 가짜 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 ;
}
}