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

227 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// PaletteDitherFeature.cs — 팔레트 양자화 + Bayer 디더 풀스크린 패스를 **저해상도 게임 카메라에만** 넣는 렌더러 피처
//
// PD 지시 #814 「도트같은 느낌이 전혀 나지 않고 해상도만 저하된 느낌」 → B안 · 발주서 WL-814j §1-2
//
// ■ 🔴 왜 카메라를 골라야 하나 (이 태스크의 핵심 제약)
// 814b 리그 구조(실측) = WL_PixelCameraRig → **게임 카메라**(저해상도 RT 135×240 등) → UpscaledDisplay
// → ViewCamera(Overlay · 픽셀 쿼드만) + QuadCanvas. UI 는 원본 카메라 스택의 Overlay 다.
// 양자화·디더를 화면(업스케일 뒤)이나 뷰/UI 카메라에 걸면 **디더 점 1개가 1 게임픽셀이 아니라 1 화면픽셀**이
// 되어 도트가 아니라 노이즈로 보인다. → 대상 카메라가 아니면 **패스를 아예 넣지 않는다**(씬뷰·프리뷰·반사 포함).
//
// ■ 대상 카메라 결정 순서
// ① TargetCamera(정적 · 프로브/캡처가 직접 지정) → ② PixelCameraRig.GameCamera(리그가 조립하며 알려 준 것)
// 둘 다 없으면 패스 0. 814b 파일은 **읽기만** 한다(수정 0).
//
// ■ 렌더 순서
// 기본 AfterRenderingPostProcessing — 814c 픽셀 외곽선은 Toon 머티리얼 안에서 **불투명 지오메트리와 함께**
// 그려지므로 이보다 앞이다 → 외곽선까지 같이 양자화된다. SO 로 AfterRenderingTransparents 로 내릴 수 있다.
//
// ■ C8 — WLPaletteSettings.enabled_ = 0 이면 패스 0건(피처가 활성이어도). 디스크 기본 m_Active: 0(814c 선례).
//
// 🔴 이 파일은 Debug.LogError / Debug.LogException 을 쓰지 않는다(813z · ErrorLogHookManager 팝업).
// 🔴 Assets/WL/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일).
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.RenderGraphModule.Util;
using UnityEngine.Rendering.Universal;
using WL.PixelArt;
namespace WL.Look.Palette
{
/// <summary>저해상도 게임 카메라의 RT 에만 팔레트 양자화 + Bayer 디더를 거는 URP 렌더러 피처.</summary>
public sealed class PaletteDitherFeature : ScriptableRendererFeature
{
public const string ShaderName = "WL/Look/PaletteDither";
// ── 대상 카메라 ───────────────────────────────────────────────────────
/// <summary>프로브·캡처가 직접 지정하는 대상 카메라(우선). null 이면 814b 리그의 게임 카메라를 쓴다.</summary>
public static Camera TargetCamera;
/// <summary>지금 패스가 들어갈 카메라(없으면 null). 814b 파일은 읽기만 한다.</summary>
public static Camera ResolvedTarget
{
get
{
if (TargetCamera != null) return TargetCamera;
var c = WLPaletteSettings.Instance;
if (c != null && !c.requirePixelRig) return null; // 리그 없이 쓰려면 TargetCamera 를 직접 지정해야 한다
return PixelCameraRig.GameCamera;
}
}
// ── 진단(프로브가 읽는다 · 실측만) ────────────────────────────────────
public static int PassesAdded, PassesSkipped, MaterialFaults;
public static string LastSkipReason = "";
/// <summary>마지막으로 패스를 넣은 카메라 이름(진단).</summary>
public static string LastTargetName = "";
/// <summary>마지막으로 실제로 쓴 렌더 이벤트(진단).</summary>
public static RenderPassEvent LastEvent;
/// <summary>프로브: 카운터를 비운다.</summary>
public static void ResetCounters()
{
PassesAdded = PassesSkipped = MaterialFaults = 0;
LastSkipReason = ""; LastTargetName = "";
}
// ── 셰이더 프로퍼티 ID (문자열 → ID 는 1회만 = 프레임당 GC 0) ─────────
static readonly int kLevels = Shader.PropertyToID("_WLLevels");
static readonly int kDitherStrength = Shader.PropertyToID("_WLDitherStrength");
static readonly int kDitherSize = Shader.PropertyToID("_WLDitherSize");
static readonly int kSaturation = Shader.PropertyToID("_WLSaturation");
static readonly int kContrast = Shader.PropertyToID("_WLContrast");
static readonly int kTintColor = Shader.PropertyToID("_WLTintColor");
static readonly int kTintStrength = Shader.PropertyToID("_WLTintStrength");
static readonly int kGammaEncode = Shader.PropertyToID("_WLGammaEncode");
static readonly int kPaletteCount = Shader.PropertyToID("_WLPaletteCount");
static readonly int kPaletteTex = Shader.PropertyToID("_WLPaletteTex");
const string kPaletteKeyword = "_WLPALETTE_TEX";
Material m_Material;
Shader m_Shader;
PaletteDitherPass m_Pass;
public override void Create()
{
if (m_Pass == null) m_Pass = new PaletteDitherPass();
m_Pass.renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing;
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
var cfg = WLPaletteSettings.Instance;
if (cfg == null || !WLPaletteSettings.Enabled) { PassesSkipped++; LastSkipReason = "SO off"; return; }
var cam = renderingData.cameraData.camera;
if (cam == null) { PassesSkipped++; LastSkipReason = "camera null"; return; }
// 🔴 씬뷰·프리뷰·반사는 무조건 제외(대상 비교 전에 잘라 낸다 — 에디터 오염 0)
var ct = renderingData.cameraData.cameraType;
if (ct != CameraType.Game) { PassesSkipped++; LastSkipReason = "cameraType " + ct; return; }
var target = ResolvedTarget;
if (target == null) { PassesSkipped++; LastSkipReason = "대상 카메라 없음"; return; }
if (!ReferenceEquals(cam, target)) { PassesSkipped++; LastSkipReason = "대상 아님"; return; }
if (!EnsureMaterial()) { PassesSkipped++; LastSkipReason = "머티리얼 없음"; return; }
PushValues(cfg);
if (m_Pass == null) m_Pass = new PaletteDitherPass();
m_Pass.renderPassEvent = cfg.afterPostProcessing
? RenderPassEvent.AfterRenderingPostProcessing
: RenderPassEvent.AfterRenderingTransparents;
m_Pass.Material = m_Material;
renderer.EnqueuePass(m_Pass);
PassesAdded++;
LastEvent = m_Pass.renderPassEvent;
LastTargetName = cam.name;
}
/// <summary>셰이더 → 머티리얼(1회). 셰이더가 없으면 false(패스 0 · 로그 1줄).</summary>
bool EnsureMaterial()
{
if (m_Material != null) return true;
if (m_Shader == null) m_Shader = Shader.Find(ShaderName);
if (m_Shader == null || !m_Shader.isSupported)
{
MaterialFaults++;
var c = WLPaletteSettings.Instance;
if (c != null && c.verboseLog) Debug.Log("[WL814j] 셰이더 없음/미지원 — " + ShaderName);
return false;
}
m_Material = CoreUtils.CreateEngineMaterial(m_Shader);
return m_Material != null;
}
/// <summary>SO 값을 머티리얼에 얹는다. SetFloat/SetColor(int) 는 관리 힙 할당 0.</summary>
void PushValues(WLPaletteSettings c)
{
m_Material.SetFloat(kLevels, c.levels);
m_Material.SetFloat(kDitherStrength, c.ditherStrength);
m_Material.SetFloat(kDitherSize, c.dither8x8 ? 8f : 4f);
m_Material.SetFloat(kSaturation, c.saturation);
m_Material.SetFloat(kContrast, c.contrast);
m_Material.SetColor(kTintColor, c.tintColor);
m_Material.SetFloat(kTintStrength, c.tintStrength);
m_Material.SetFloat(kGammaEncode, c.quantizeInGammaSpace ? 1f : 0f);
bool usePalette = c.mode == WLPaletteMode.PaletteTexture && c.paletteTex != null;
if (usePalette)
{
int n = c.paletteCount > 0 ? c.paletteCount : c.paletteTex.width;
if (n < 1) n = 1;
if (n > 64) n = 64;
m_Material.SetTexture(kPaletteTex, c.paletteTex);
m_Material.SetFloat(kPaletteCount, n);
if (!m_Material.IsKeywordEnabled(kPaletteKeyword)) m_Material.EnableKeyword(kPaletteKeyword);
}
else
{
m_Material.SetFloat(kPaletteCount, 0f);
if (m_Material.IsKeywordEnabled(kPaletteKeyword)) m_Material.DisableKeyword(kPaletteKeyword);
}
}
/// <summary>프로브: 머티리얼 준비 + SO 값 얹기 경로만 실행한다(프레임당 할당 측정용).</summary>
public bool ProbePushValues()
{
var c = WLPaletteSettings.Instance;
if (c == null) return false;
if (!EnsureMaterial()) return false;
PushValues(c);
return true;
}
/// <summary>프로브: 지금 피처가 들고 있는 머티리얼(없으면 만든다).</summary>
public Material ProbeMaterial { get { return EnsureMaterial() ? m_Material : null; } }
protected override void Dispose(bool disposing)
{
CoreUtils.Destroy(m_Material);
m_Material = null;
m_Pass = null;
}
// ═════════════════════════════════════════════════════════════════════
// 패스 — Render Graph 전용(Unity 6000.3 은 Compatibility Mode 가 없다)
// URP FullScreenPassRendererFeature 와 같은 방식: 활성 색을 임시 텍스처로 복사 → 머티리얼로 되돌려 그린다.
// ═════════════════════════════════════════════════════════════════════
sealed class PaletteDitherPass : ScriptableRenderPass
{
public Material Material;
public PaletteDitherPass()
{
profilingSampler = new ProfilingSampler("WL814j PaletteDither");
requiresIntermediateTexture = true; // 백버퍼 직행이면 읽을 수 없다 → 중간 텍스처를 요구한다
}
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
if (Material == null) return;
var res = frameData.Get<UniversalResourceData>();
if (res == null || !res.cameraColor.IsValid()) return;
if (res.isActiveTargetBackBuffer) return; // 백버퍼는 읽기 불가 — 조용히 건너뛴다
var copyDesc = renderGraph.GetTextureDesc(res.cameraColor);
copyDesc.name = "_WL814jPaletteCopy";
copyDesc.clearBuffer = false;
var source = res.activeColorTexture;
var copy = renderGraph.CreateTexture(copyDesc);
renderGraph.AddBlitPass(source, copy, Vector2.one, Vector2.zero, passName: "WL814j Copy Color");
var p = new RenderGraphUtils.BlitMaterialParameters(copy, res.activeColorTexture, Material, 0);
renderGraph.AddBlitPass(p, passName: "WL814j Palette Dither");
}
}
}
}