218 lines
11 KiB
C#
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(); }
|
|
}
|
|
}
|