Project_WL/Assets/WL/Look/Palette/PaletteDither.cs

218 lines
11 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// PaletteDither.cs — 팔레트 양자화 + 디더 후처리를 **도트 모드에서만** 켜는 연결 코드 (WL-814j · #814)
//
// PD 지시 #814 「도트같은 느낌이 전혀 나지 않고 해상도만 저하된 느낌」 → B안 · 발주서 WL-814j §1-3
//
// ■ 이 파일이 하는 것 — 원본 훅 0 · 새 Manager/Singleton 0(정적 + 러너 GameObject 1개)
// ① 814d `LookModeHub.CameraModeChanged` 를 구독해 모드가 바뀌는 순간 즉시 반영한다.
// ② 그리고 매 프레임 「지금 켜져 있어야 하는가」를 비교만 해서(참조·정수 비교 = GC 0) 맞춘다.
// — 허브 이벤트는 **PD 가 고른 값이 바뀔 때만** 오고, 로비 강등·맵 이탈·리그 재조립은 알려 주지 않는다.
// 리그가 다시 조립되면 대상 카메라 참조가 바뀌므로 폴링이 필요하다(814d 보고 §5 계약).
// ③ 켜짐 = URP 렌더러의 PaletteDitherFeature 를 SetActive(true). 꺼짐 = 원래 값으로 되돌린다.
// (814c EnvLook.SetFeature 와 같은 방식 — 디스크 에셋에는 쓰지 않는다 · SetDirty 0)
//
// ■ 켜지는 조건(전부 AND)
// · WLPaletteSettings.Enabled (C8)
// · LookModeHub.AppliedMode >= minCameraMode (기본 1) — 로비·기본 모드에서는 AppliedMode 가 0 이라 꺼진다
// · requirePixelRig 이면 814b 리그가 조립돼 있고 게임 카메라가 살아 있을 것
//
// 🔴 814b/814d 파일은 **읽기·구독만** 한다(수정 0).
// 🔴 이 파일은 Debug.LogError / Debug.LogException 을 쓰지 않는다(813z · ErrorLogHookManager 팝업).
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
using UnityEngine.Rendering.Universal;
using WL.Look.Toggle;
using WL.PixelArt;
namespace WL.Look.Palette
{
/// <summary>팔레트 후처리 on/off. 정적 · 전용 러너 1개.</summary>
public static class PaletteDither
{
// ── 진단(프로브가 읽는다 · 실측만) ────────────────────────────────────
public static int Ons, Offs, HubEvents, FeatureMisses;
public static string LastLog = "";
/// <summary>지금 후처리가 켜져 있는가.</summary>
public static bool IsOn { get { return s_on; } }
/// <summary>찾아 둔 렌더러 피처(진단). 못 찾으면 null.</summary>
public static ScriptableRendererFeature Feature { get { return s_feature; } }
/// <summary>피처가 지금 활성인가(진단).</summary>
public static bool FeatureActiveNow { get { return s_feature != null && s_feature.isActive; } }
// ── 내부 상태 ─────────────────────────────────────────────────────────
static ScriptableRendererFeature s_feature;
static int s_featureLookups;
static bool s_featureOrig, s_featureChanged;
static bool s_on;
static bool s_subscribed;
static GameObject s_runner;
static WLPaletteSettings Cfg { get { return WLPaletteSettings.Instance; } }
// ─────────────────────────────────────────────────────────────────────
// 틱 · 판정
// ─────────────────────────────────────────────────────────────────────
/// <summary>러너가 매 프레임 부른다(에디트 모드에서는 프로브가 직접 부를 수 있다). 비교뿐 = GC 0.</summary>
public static void Tick()
{
bool want = Want();
if (want != s_on) Apply(want);
}
/// <summary>지금 켜져 있어야 하는가(참조·정수 비교만).</summary>
public static bool Want()
{
if (!WLPaletteSettings.Enabled) return false;
var c = Cfg;
if (c == null) return false;
if (LookModeHub.AppliedMode < c.minCameraMode) return false;
if (c.requirePixelRig)
{
if (!PixelCameraRig.IsAssembled) return false;
if (PixelCameraRig.GameCamera == null) return false;
}
return true;
}
/// <summary>on/off 를 실제로 적용한다. 같은 값이면 무동작.</summary>
public static bool Apply(bool on)
{
if (on == s_on && (!on || FeatureActiveNow)) return s_on;
s_on = on;
if (on) Ons++; else Offs++;
bool ok = SetFeature(on);
var c = Cfg;
if (c != null && c.verboseLog)
LastLog = Log("팔레트 후처리 " + (on ? "ON" : "OFF")
+ " · 모드 " + LookModeHub.AppliedMode
+ " · 리그 " + PixelCameraRig.IsAssembled
+ " · 피처 " + ok);
return s_on;
}
// ─────────────────────────────────────────────────────────────────────
// 렌더러 피처
// ─────────────────────────────────────────────────────────────────────
/// <summary>URP 렌더러의 팔레트 피처를 켜고 끈다. 없으면 아무 것도 하지 않는다(후처리만 안 걸린다).</summary>
public static bool SetFeature(bool on)
{
var f = FindFeature();
if (f == null) { FeatureMisses++; return false; }
if (on)
{
if (!s_featureChanged) { s_featureOrig = f.isActive; s_featureChanged = true; }
if (!f.isActive) f.SetActive(true); // 🔴 SetDirty 0 (디스크 에셋 무변경)
return true;
}
if (!s_featureChanged) return false;
f.SetActive(s_featureOrig);
s_featureChanged = false;
return true;
}
const int kMaxLookups = 8;
static ScriptableRendererFeature FindFeature()
{
if (s_feature != null) return s_feature;
// 🔴 배열을 새로 만드는 호출이라 전환 때만 · 최대 kMaxLookups 회만 찾는다(프레임당 반복 호출 0).
if (s_featureLookups >= kMaxLookups) return null;
s_featureLookups++;
var all = Resources.FindObjectsOfTypeAll<PaletteDitherFeature>();
if (all != null && all.Length > 0) s_feature = all[0];
return s_feature;
}
/// <summary>프로브: 피처 캐시를 비운다(에디트 모드 재실행).</summary>
public static void ClearFeatureCache() { s_feature = null; s_featureLookups = 0; }
/// <summary>프로브: 상태·카운터를 시작값으로 되돌리고 피처도 원복한다.</summary>
public static string ResetForProbe()
{
SetFeature(false);
s_on = false;
Ons = Offs = HubEvents = FeatureMisses = 0;
LastLog = "";
return "리셋";
}
public static string Dump()
{
var c = Cfg;
return "[Palette] on=" + s_on + " 피처활성=" + FeatureActiveNow
+ " 대상=" + (PaletteDitherFeature.ResolvedTarget != null ? PaletteDitherFeature.ResolvedTarget.name : "(없음)")
+ " · 모드 " + LookModeHub.AppliedMode + " 리그 " + PixelCameraRig.IsAssembled
+ " · levels " + (c != null ? c.levels : 0) + " dither " + (c != null ? c.ditherStrength : 0f)
+ " (" + (c != null && c.dither8x8 ? "8x8" : "4x4") + ")"
+ " · 패스 추가 " + PaletteDitherFeature.PassesAdded + " 건너뜀 " + PaletteDitherFeature.PassesSkipped
+ " · on " + Ons + " off " + Offs + " 허브이벤트 " + HubEvents;
}
static string Log(string msg)
{
LastLog = msg;
var c = Cfg;
if (c != null && c.verboseLog) Debug.Log("[WL814j] " + msg); // 🔴 LogError 금지(813z)
return msg;
}
// ─────────────────────────────────────────────────────────────────────
// 허브 구독 · 러너
// ─────────────────────────────────────────────────────────────────────
static void OnCameraModeChanged(int mode)
{
HubEvents++;
Tick(); // 모드 전환 프레임에 바로 반영(폴링 1프레임을 기다리지 않는다)
}
/// <summary>프로브: 구독을 보장한다(에디트 모드에서도 부를 수 있다).</summary>
public static void EnsureSubscribed()
{
if (s_subscribed) return;
s_subscribed = true;
LookModeHub.CameraModeChanged += OnCameraModeChanged;
}
static void Unsubscribe()
{
if (!s_subscribed) return;
s_subscribed = false;
LookModeHub.CameraModeChanged -= OnCameraModeChanged; // 허브는 구독자를 모른다 — 스스로 뗀다
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
static void Boot()
{
if (s_runner != null) return;
if (!WLPaletteSettings.Enabled) return; // C8 — 러너 자체가 뜨지 않는다
EnsureSubscribed();
s_runner = new GameObject("[WL814j] PaletteDitherRunner");
s_runner.hideFlags = HideFlags.HideAndDontSave;
s_runner.AddComponent<PaletteDitherRunner>();
Object.DontDestroyOnLoad(s_runner);
}
internal static void OnRunnerGone()
{
Unsubscribe();
Apply(false);
PaletteDitherFeature.TargetCamera = null;
}
}
/// <summary>팔레트 후처리의 시간 축. GameObject 1개 · 코루틴 0.</summary>
internal sealed class PaletteDitherRunner : MonoBehaviour
{
void Update() { PaletteDither.Tick(); }
void OnApplicationQuit() { PaletteDither.OnRunnerGone(); }
void OnDestroy() { PaletteDither.OnRunnerGone(); }
}
}