Project_WL/Assets/WL/Look/Palette/WLPaletteDither.shader

147 lines
7.0 KiB
Plaintext
Raw Permalink Normal View History

// ─────────────────────────────────────────────────────────────────────────────
// WLPaletteDither.shader — 팔레트 양자화 + Bayer 순서 디더링 풀스크린 1패스 (WL-814j · #814)
//
// PD 지시 #814 「도트같은 느낌이 전혀 나지 않고 해상도만 저하된 느낌」 → B안 = 색 수 제한 + 디더링.
//
// 🔴 이 패스는 **저해상도 게임 카메라의 RT** 에만 걸린다(PaletteDitherFeature 가 대상 카메라를 고른다).
// 업스케일 뒤 화면이나 뷰/UI 카메라에 걸면 디더 점이 1 게임픽셀이 아니라 1 화면픽셀이 되어
// 도트가 아니라 노이즈로 보인다.
//
// ■ 왜 감마 공간에서 양자화하나
// 프로젝트는 Linear 색공간(ProjectSettings m_ActiveColorSpace: 1 · 실측)이라 RT 를 샘플하면 선형 값이 온다.
// 선형 값을 그대로 6단계로 나누면 밝은 쪽에만 단계가 몰려 도트 팔레트처럼 안 보인다.
// → sRGB 로 바꿔서 양자화·디더하고 다시 선형으로 되돌린다(_WLGammaEncode 1). 8비트 PNG 의 고유 색 수와
// levels^3 이 정확히 맞아떨어지는 것도 이 때문이다(levels 6 → 상한 216색).
//
// ■ 디더 좌표 = SV_POSITION(= 렌더 타깃 픽셀 좌표) → 격자가 RT 픽셀과 1:1 로 맞는다.
// _ScreenParams 를 쓰지 않는다(카메라가 RT 로 그릴 때 값이 흔들릴 수 있다).
//
// ■ 양자화 두 방식
// ⓐ 채널 레벨(기본) — R/G/B 각 _WLLevels 단계로 반올림. levels^3 색 상한.
// ⓑ 팔레트 텍스처(_WLPALETTE_TEX 키워드 · 배선만) — N×1 텍스처에서 최근접 색을 찾는다.
// ─────────────────────────────────────────────────────────────────────────────
Shader "WL/Look/PaletteDither"
{
Properties
{
_WLLevels ("Levels per channel", Float) = 6
_WLDitherStrength ("Dither strength", Float) = 0.6
_WLDitherSize ("Dither matrix (4 or 8)", Float) = 4
_WLSaturation ("Saturation", Float) = 1
_WLContrast ("Contrast", Float) = 1
_WLTintColor ("Tint color", Color) = (1,1,1,1)
_WLTintStrength ("Tint strength", Float) = 0
_WLGammaEncode ("Quantize in sRGB (1) or render space (0)", Float) = 1
_WLPaletteCount ("Palette entry count", Float) = 0
[NoScaleOffset] _WLPaletteTex ("Palette texture (N x 1)", 2D) = "white" {}
}
SubShader
{
Tags { "RenderPipeline" = "UniversalPipeline" }
ZWrite Off ZTest Always Blend Off Cull Off
Pass
{
Name "WLPaletteDither"
HLSLPROGRAM
#pragma target 3.0
#pragma vertex Vert
#pragma fragment Frag
#pragma multi_compile_local_fragment _ _WLPALETTE_TEX
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"
TEXTURE2D(_WLPaletteTex);
SAMPLER(sampler_WLPaletteTex);
float _WLLevels;
float _WLDitherStrength;
float _WLDitherSize;
float _WLSaturation;
float _WLContrast;
float4 _WLTintColor;
float _WLTintStrength;
float _WLGammaEncode;
float _WLPaletteCount;
// ── Bayer 순서 디더 (재귀 구성 · 배열 인덱싱 0 = 모바일에서 분기·레지스터 비용 최소)
// Bayer2 = [[0, 0.5], [0.75, 0.25]] · Bayer4/Bayer8 은 그 위에 쌓는다. 값 범위 [0, 1).
float WLBayer2(float2 a)
{
a = floor(a);
return frac(a.x * 0.5 + a.y * a.y * 0.75);
}
float WLBayer4(float2 a) { return WLBayer2(a * 0.5) * 0.25 + WLBayer2(a); }
float WLBayer8(float2 a) { return WLBayer4(a * 0.5) * 0.25 + WLBayer2(a); }
float3 WLApplyTone(float3 c)
{
// 대비 — 중간값 0.5 기준
c = (c - 0.5) * _WLContrast + 0.5;
// 채도 — Rec.601 휘도 기준
float l = dot(c, float3(0.299, 0.587, 0.114));
c = lerp(l.xxx, c, _WLSaturation);
// 색조 — tintStrength 0 이면 무보정
c = lerp(c, c * _WLTintColor.rgb, _WLTintStrength);
return c;
}
float4 Frag(Varyings input) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
float4 src = SAMPLE_TEXTURE2D_X_LOD(_BlitTexture, sampler_PointClamp, input.texcoord.xy, _BlitMipLevel);
float3 c = src.rgb;
// 선형 → sRGB (감마 공간에서 양자화해야 사람 눈 기준으로 고르게 나뉜다)
if (_WLGammaEncode > 0.5) c = LinearToSRGB(max(c, 0.0));
c = saturate(c);
c = saturate(WLApplyTone(c));
// ── 디더 오프셋: SV_POSITION = 렌더 타깃 픽셀 좌표 → 격자가 RT 픽셀과 1:1
float2 px = floor(input.positionCS.xy);
float b = (_WLDitherSize >= 8.0) ? WLBayer8(px) : WLBayer4(px);
float d = (b - 0.5) * _WLDitherStrength; // strength 1 = 양자화 1단계를 꽉 채운다
#ifdef _WLPALETTE_TEX
// ⓑ 팔레트 텍스처 — N×1 에서 최근접 색(배선만 · 기본 미사용)
int n = (int)min(max(_WLPaletteCount, 1.0), 64.0);
float pStep = 1.0 / max(1.0, (float)(n - 1));
float3 probe = saturate(c + d * pStep);
float3 best = probe;
float bestD = 1e9;
[loop]
for (int i = 0; i < n; i++)
{
float3 p = SAMPLE_TEXTURE2D_LOD(_WLPaletteTex, sampler_WLPaletteTex,
float2(((float)i + 0.5) / (float)n, 0.5), 0).rgb;
float3 e = p - probe;
float dist = dot(e, e);
if (dist < bestD) { bestD = dist; best = p; }
}
c = best;
#else
// ⓐ 채널 레벨 — levels^3 색 상한
float L = max(2.0, _WLLevels);
float3 v = c * (L - 1.0);
v = floor(v + d + 0.5);
c = saturate(v / (L - 1.0));
#endif
// sRGB → 선형 (하드웨어가 저장할 때 다시 sRGB 로 인코딩한다)
if (_WLGammaEncode > 0.5) c = SRGBToLinear(c);
return float4(c, src.a);
}
ENDHLSL
}
}
Fallback Off
}