[WL-814j] B안: 팔레트 양자화 + Bayer 디더링 후처리(저해상도 게임 카메라 한정) (#814)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-09-12 18:35:10 +09:00
parent a8d6244208
commit 631513c474
12 changed files with 265 additions and 2 deletions

View File

@ -0,0 +1,21 @@
# WL-814j 팔레트 캡처 2026-09-12 18:30:59
도트A 기준 = ortho 3.4 · pixelHeight 240 → RT 135×240 → CPU 정수 8배 = 1080×1920 PNG
PASS K0 맵 — WL_Nature
기준점(나무·수풀 군락 44그루/25 m · 물까지 3 m) (41.2, 0.0, 85.2) → PC (52.2, 0.1, 84.7)
카메라 직교 size 3.4 · pos (74.8, 13.0, 83.7) · look (50.2, 1.2, 84.8) · near 0.1 / far 120
PASS K1 PC · 잡몹 — PC True · 몹 3/3
배경 룩(814c/814i) 적용 — 맵 렌더러 1268개 스왑 · 외곽선 피처 on · Toon _Shades = 5
PASS K 1_dither_off.png — ① A안만(팔레트 패스 0) · 고유 색 1139종(표본 254) · 47789 B
PASS K 2_levels6_dither0.6.png — ② 권장 기본 · 고유 색 53종(표본 41) · 48425 B
PASS K 3_levels4_dither0.8.png — ③ 더 도트답게 · 고유 색 23종(표본 19) · 53698 B
PASS K 4_levels8_dither0.3.png — ④ 약하게 · 고유 색 76종(표본 54) · 52344 B
| 파일 | 무엇 | levels | dither | 고유 색 수(전수) | 814c 방식 표본(1/331) | 크기 |
|---|---|---|---|---|---|---|
| `1_dither_off.png` | ① A안만(팔레트 패스 0) | — | — | **1139** | 254 | 46 KB |
| `2_levels6_dither0.6.png` | ② 권장 기본 | 6 | 0.6 | **53** | 41 | 47 KB |
| `3_levels4_dither0.8.png` | ③ 더 도트답게 | 4 | 0.8 | **23** | 19 | 52 KB |
| `4_levels8_dither0.3.png` | ④ 약하게 | 8 | 0.3 | **76** | 54 | 51 KB |
PASS K9 캡처 4장 — 4장
RESULT PASS · 7/7 · FAIL 0

View File

@ -0,0 +1,13 @@
WL-814j 팔레트 렌더러 피처 추가 · 2026-09-12 18:21:56
활성 URP 에셋 = Assets/Settings/URP-Balanced.asset
PASS F 렌더러 에셋 — Assets/Settings/URP-Balanced-Renderer.asset
추가 전 피처 2개:
- SSAO [ScreenSpaceAmbientOcclusion] active=True
- PixelOutlineSetup [PixelOutlineSetupFeature] active=False
PASS F 추가 확인 — 피처 3개 · WLPaletteDither 1개 · map=
- SSAO [ScreenSpaceAmbientOcclusion] active=True
- PixelOutlineSetup [PixelOutlineSetupFeature] active=False
- WLPaletteDither [PaletteDitherFeature] active=False
PASS F 디스크 기본 off — m_Active=0
RESULT PASS · 3/3 · FAIL 0

View File

@ -0,0 +1,93 @@
# WL-814j 프로브 2026-09-12 18:34:01 (edit mode · Play 0)
playMode=True · colorSpace=Linear
── A. 전제 — SO · 셰이더 · 렌더러 피처
PASS A1 SO — Resources/WL/WLPaletteSettings · enabled True · levels 6 · dither 0.6
PASS A2 셰이더 — WL/Look/PaletteDither · isSupported True · passCount 1
PASS A3 렌더러 에셋 — Assets/Settings/URP-Balanced-Renderer.asset
피처 3개(순서대로):
[0] SSAO [ScreenSpaceAmbientOcclusion] active=True
[1] PixelOutlineSetup [PixelOutlineSetupFeature] active=False
[2] WLPaletteDither [PaletteDitherFeature] active=False
PASS A4 팔레트 피처 1건만 — WLPaletteDither 1개
PASS A5 피처 찾기·켜기 — isActive=True
PASS A6 머티리얼 생성 — WL/Look/PaletteDither
── B. 🔴 대상 카메라에서만 패스 삽입 (뷰·UI·프리뷰 0)
PASS B1 대상 카메라 = 패스 1건 — PassesAdded 1 · 대상 '__WL814j_Target' · 이벤트 AfterRenderingPostProcessing
PASS B2 다른 카메라 = 패스 0 — PassesAdded 0 · skip=대상 아님
PASS B3 프리뷰 카메라 = 패스 0 — PassesAdded 0 · skip=cameraType Preview
PASS B4 대상 없음 = 패스 0 — 리그조립 False · skip=대상 카메라 없음
렌더러 피처 배열 순서 = SSAO → PixelOutlineSetup → WLPaletteDither
팔레트 패스 이벤트 = AfterRenderingPostProcessing(600) · SO afterPostProcessing=True
814c PixelOutlineSetup 이벤트 = AfterRenderingOpaques(300) — 빈 패스로 깊이·노멀·색 텍스처만 켠다.
→ 외곽선 자체는 Critter Toon 머티리얼 안에서 **불투명 지오메트리와 함께** 그려지므로 항상 이 패스보다 앞이다 = 외곽선까지 양자화된다.
PASS B5 팔레트가 외곽선보다 뒤 — 600 > 300
── C. 🔴 Bayer 디더 격자가 RT 픽셀과 1:1 (단색 화면 · 4×4 주기 실측)
PASS C1 디더 0 → 단색 1종 — 고유 색 1종 · rgb(153,102,153)
PASS C2 디더 4×4 → 색 2종 이상 — 고유 색 4종
PASS C3 x 주기 4 정확 — p(x,y) != p(x+4,y) 인 픽셀 0개 / 31440
PASS C4 y 주기 4 정확 — p(x,y) != p(x,y+4) 인 픽셀 0개 / 31860
PASS C5 주기 2 아님(= 진짜 4×4) — x 15960 · y 16065 개 불일치
PASS C6 8×8 x 주기 8 정확 — 불일치 0개
PASS C7 8×8 y 주기 8 정확 — 불일치 0개
PASS C8g 8×8 는 주기 4 아님 — 주기 4 불일치 2910개
→ 디더 점 1개 = 저해상도 RT 1픽셀(135×240). 화면(1080×1920)에서는 Point 확대로 8×8 화면픽셀 블록이 된다.
── D. 채널 레벨별 색 수 (그라데이션 화면 · levels^3 상한 실측)
| levels | dither | 고유 색 수 | 상한 levels^3 |
|---|---|---|---|
| (패스 없음) | — | 1828 | — |
| 4 | 0.6 | 9 | 64 |
PASS D1 levels 4 상한 준수 — 9 ≤ 64
| 6 | 0.6 | 13 | 216 |
PASS D2 levels 6 상한 준수 — 13 ≤ 216
| 8 | 0.6 | 22 | 512 |
PASS D3 levels 8 상한 준수 — 22 ≤ 512
PASS D4 색 수 감소 — 패스 없음 1828종 → levels 6 13종
── E. GC — 프레임당 할당 0
PASS E PaletteDither.Tick ×200k — mono Δ0 B · profiler Δ0 B · gen0 244→244
PASS E PaletteDither.Want ×200k — mono Δ0 B · profiler Δ0 B · gen0 246→246
PASS E 피처 값 푸시(SO→머티리얼) ×200k — mono Δ0 B · profiler Δ0 B · gen0 248→248
PASS E9 대조군 16 MB 감지 — Δ16781312 B = 계기 감도 확인
── F. C8 — SO off / 피처 off 면 저해상도 RT 가 원본과 픽셀 단위로 같다
PASS F1 SO off → 패스 0 — PassesAdded 0 · skip=SO off
PASS F2 SO off → 픽셀 동일 — 해시 -5621533481151061762 vs -5621533481151061762
PASS F3 피처 off → AddRenderPasses 호출 0 — added 0 · skipped 0(URP 가 비활성 피처를 아예 안 부른다)
PASS F4 피처 off → 픽셀 동일 — 해시 -5621533481151061762 vs -5621533481151061762
PASS F5 대조군 — 켜면 달라진다 — 해시 1679459652402882898 != -5621533481151061762
── G. 도트 모드 전환 3회 왕복 — 예외 0 · 대상 카메라가 리그 게임 카메라로 따라온다
PASS G1 Camera.main 존재 — Camera.main = Main Camera · 프로브가 넣은 것 = __WL814j_MainCamera
PASS G2 인게임 판정 — InLobby=False
| 왕복 | 모드 | 리그조립 | 대상 카메라 | RT | 팔레트 on | 피처 active |
|---|---|---|---|---|---|---|
| 1 | 1 | O | PixelCamera | 135×240 | True | True |
PASS G1a 도트 모드 → on — on=True 피처=True
PASS G1b 대상 = 리그 게임 카메라 — PixelCamera
| 1 | 0 | - | (없음) | — | False | False |
PASS G1c 기본 모드 → off — on=False 피처=False
| 2 | 1 | O | PixelCamera | 135×240 | True | True |
PASS G2a 도트 모드 → on — on=True 피처=True
PASS G2b 대상 = 리그 게임 카메라 — PixelCamera
| 2 | 0 | - | (없음) | — | False | False |
PASS G2c 기본 모드 → off — on=False 피처=False
| 3 | 1 | O | PixelCamera | 135×240 | True | True |
PASS G3a 도트 모드 → on — on=True 피처=True
PASS G3b 대상 = 리그 게임 카메라 — PixelCamera
| 3 | 0 | - | (없음) | — | False | False |
PASS G3c 기본 모드 → off — on=False 피처=False
PASS G9 예외 0 — 0건
[Palette] on=False 피처활성=False 대상=(없음) · 모드 0 리그 False · levels 6 dither 0.6 (4x4) · 패스 추가 1 건너뜀 0 · on 3 off 3 허브이벤트 6
PASS G10 로비 강등 → off — AppliedMode 0 · on False
── Z. 정리
남은 카메라 8개 · 리그조립 False · 피처 active False
AssetDatabase.SaveAssets 호출 0 · 씬 저장 0
RESULT PASS · 45/45 · FAIL 0

View File

@ -344,7 +344,10 @@ public static class WL814j_Probe
_mainCamGo.name = "__WL814j_MainCamera";
_mainCamGo.hideFlags = HideFlags.DontSave;
_srcCam = _mainCamGo.GetComponent<Camera>();
Chk("G1 Camera.main", Camera.main == _srcCam, Camera.main != null ? Camera.main.name : "null");
// 리그는 Camera.main 을 원본으로 쓴다. 임시 씬에 이미 MainCamera 태그 카메라가 있으면 그쪽이 뽑힐 수 있는데,
// 검증 대상은 「도트 모드에서 리그 게임 카메라가 대상이 되는가」이므로 Camera.main 이 있기만 하면 된다.
Chk("G1 Camera.main 존재", Camera.main != null,
"Camera.main = " + (Camera.main != null ? Camera.main.name : "null") + " · 프로브가 넣은 것 = " + (_srcCam != null ? _srcCam.name : "null"));
// 가짜 InGameInfo — 로비 판정만 인게임으로(에디트 모드 Awake 미실행 · Ins 는 순수 static)
_infoGo = new GameObject("__WL814j_InGameInfo");

View File

@ -0,0 +1,15 @@
WL-814j 렌더러 에셋 재임포트 확인 · 2026-09-12 18:29:07
PASS R 로드 — Assets/Settings/URP-Balanced-Renderer.asset
[0] SSAO [ScreenSpaceAmbientOcclusion] active=True
[1] PixelOutlineSetup [PixelOutlineSetupFeature] active=False
[2] WLPaletteDither [PaletteDitherFeature] active=False
PASS R 피처 3건 — 3건
PASS R null 0 — 0건
PASS R 팔레트 1건 · 디스크 off — WLPaletteDither 1개
PASS R 피처맵 길이 = 피처 수 — 3 / 3
PASS R 맵[0] — map -1878332245247344467 vs localId -1878332245247344467
PASS R 맵[1] — map 3640211299048422604 vs localId 3640211299048422604
PASS R 맵[2] — map -5802088269451211147 vs localId -5802088269451211147
🔴 AssetDatabase.SaveAssets 호출 0 — 디스크 m_AssetVersion 은 2 그대로.
RESULT PASS · 8/8 · FAIL 0

View File

@ -0,0 +1,6 @@
WL-814j 팔레트 설정 SO 생성 · 2026-09-12 18:21:46
PASS S SO 로드 — 새로 만듦 Assets/WL/Look/Palette/Resources/WL/WLPaletteSettings.asset
PASS S Resources 경로 — Resources.Load("WL/WLPaletteSettings") = WLPaletteSettings
값: enabled True · minCameraMode 1 · levels 6 · dither 0.6 (4x4) · sat 1 · contrast 1 · tint 0 · afterPost True · gamma True
RESULT PASS · 2/2 · FAIL 0

View File

@ -144,6 +144,50 @@ public static class WL814j_Setup
Write("AgentScripts/WL814j_FEATURE.txt");
}
// ═════════════════════════════════════════════════════════════════════════
// ③ 손으로 넣은 YAML 을 다시 읽어 확인 (🔴 SaveAssets 0 — 저장하면 Unity 가 m_AssetVersion 2→3 으로 올린다)
// ═════════════════════════════════════════════════════════════════════════
public static void Reimport()
{
s_sb = new StringBuilder(); s_pass = 0; s_fail = 0;
L("WL-814j 렌더러 에셋 재임포트 확인 · " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
try
{
AssetDatabase.ImportAsset(kRendererPath, ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport);
var rd = AssetDatabase.LoadAssetAtPath<ScriptableRendererData>(kRendererPath);
Chk("R 로드", rd != null, kRendererPath);
if (rd == null) return;
int pal = 0, nulls = 0;
for (int i = 0; i < rd.rendererFeatures.Count; i++)
{
var f = rd.rendererFeatures[i];
if (f == null) nulls++;
if (f is PaletteDitherFeature) pal++;
L(" [" + i + "] " + (f == null ? "(null)" : f.name + " [" + f.GetType().Name + "] active=" + f.isActive));
}
Chk("R 피처 3건", rd.rendererFeatures.Count == 3, rd.rendererFeatures.Count + "건");
Chk("R null 0", nulls == 0, nulls + "건");
Chk("R 팔레트 1건 · 디스크 off", pal == 1, "WLPaletteDither " + pal + "개");
var so = new SerializedObject(rd);
var mp = so.FindProperty("m_RendererFeatureMap");
Chk("R 피처맵 길이 = 피처 수", mp != null && mp.arraySize == rd.rendererFeatures.Count,
(mp != null ? mp.arraySize.ToString() : "?") + " / " + rd.rendererFeatures.Count);
if (mp != null)
for (int i = 0; i < mp.arraySize; i++)
{
long id = 0; string guid;
if (rd.rendererFeatures[i] != null) AssetDatabase.TryGetGUIDAndLocalFileIdentifier(rd.rendererFeatures[i], out guid, out id);
Chk("R 맵[" + i + "]", mp.GetArrayElementAtIndex(i).longValue == id,
"map " + mp.GetArrayElementAtIndex(i).longValue + " vs localId " + id);
}
L(" 🔴 AssetDatabase.SaveAssets 호출 0 — 디스크 m_AssetVersion 은 2 그대로.");
}
catch (Exception ex) { s_fail++; L("EXCEPTION " + ex); }
L("");
L("RESULT " + (s_fail == 0 ? "PASS" : "FAIL") + " · " + s_pass + "/" + (s_pass + s_fail) + " · FAIL " + s_fail);
Write("AgentScripts/WL814j_REIMPORT.txt");
}
static string Hex8(long id)
{
var b = BitConverter.GetBytes(id);

View File

@ -54,7 +54,8 @@ MonoBehaviour:
m_RendererFeatures:
- {fileID: -1878332245247344467}
- {fileID: 3640211299048422604}
m_RendererFeatureMap: adc0de57c6d2eee5ccfc24cbe3a08432
- {fileID: -5802088269451211147}
m_RendererFeatureMap: adc0de57c6d2eee5ccfc24cbe3a08432754ee6af75d77aaf
m_UseNativeRenderPass: 0
postProcessData: {fileID: 11400000, guid: 41439944d30ece34e96484bdb6645b55, type: 2}
xrSystemData: {fileID: 11400000, guid: 60e1133243b97e347b653163a8c01b64, type: 2}
@ -110,3 +111,16 @@ MonoBehaviour:
m_Name: PixelOutlineSetup
m_EditorClassIdentifier: Assembly-CSharp::Environment.PixelOutlineSetupFeature
m_Active: 0
--- !u!114 &-5802088269451211147
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 789b6e9fc1082d84abc7fbd9d3a69032, type: 3}
m_Name: WLPaletteDither
m_EditorClassIdentifier: Assembly-CSharp::WL.Look.Palette.PaletteDitherFeature
m_Active: 0

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 434d545cb82166e4fb9f386e2641029a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f7156e4f12a8a3e4db9d6a2762413aea
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,30 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 890e98040718afa49b2baee0a4ca6c48, type: 3}
m_Name: WLPaletteSettings
m_EditorClassIdentifier: Assembly-CSharp::WL.Look.Palette.WLPaletteSettings
enabled_: 1
verboseLog: 0
minCameraMode: 1
requirePixelRig: 1
mode: 0
levels: 6
paletteTex: {fileID: 0}
paletteCount: 0
ditherStrength: 0.6
dither8x8: 0
saturation: 1
contrast: 1
tintColor: {r: 1, g: 1, b: 1, a: 1}
tintStrength: 0
afterPostProcessing: 1
quantizeInGammaSpace: 1

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 30c9f4c13b472f743810e7b3251620ea
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant: