227 lines
12 KiB
C#
227 lines
12 KiB
C#
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|||
|
|
// 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");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|