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

147 lines
7.0 KiB
GLSL
Raw Permalink 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.

// ─────────────────────────────────────────────────────────────────────────────
// 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
}