147 lines
7.0 KiB
GLSL
147 lines
7.0 KiB
GLSL
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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
|
||
}
|