# NiloToonCharToneAdjust 작업 요약 > 새 대화에서 이 내용을 읽고 이어서 작업할 수 있도록 작성된 인수인계 문서입니다. --- ## 프로젝트 환경 | 항목 | 내용 | |------|------| | **작업 환경** | **Unity 6000.3.8f1 (Unity 6) / URP 17.0.3** | | NiloToon 버전 | 프로젝트 내 로컬 패키지 (`Assets/NiloToonURP/`) | | 애드온 경로 | `Assets/YAMO/Shader/NiloToonCharToneAdjust/` | --- ## 구현 목표 **NiloToon 캐릭터 셰이더를 사용하는 오브젝트에만** 색조 보정을 적용하는 URP Volume 애드온. - Unity의 **Lift Gamma Gain** + **Shadows Midtones Highlights** Volume과 동일한 사용감 - NiloToon 원본 소스 **무수정** (완전 애드온 방식) - 배경·소품 등 비캐릭터 오브젝트는 영향 없음 - **Auto Match**: 캐릭터/배경 색상을 분석하여 보정 파라미터를 자동 추천하는 원샷 버튼 --- ## 구현 방식: 스텐실 마스킹 포스트프로세스 NiloToon이 캐릭터 픽셀에 **stencil bit7 = 128** 을 기록하는 구조를 활용합니다. ``` [NiloToonExtraThickToonOutlinePass] AfterRenderingTransparents = 600 1. CharacterAreaStencilBufferFill -> 캐릭터 픽셀: stencil = 128 2. ExtraThickOutline -> stencil != 128 픽셀에 아웃라인 3. CharacterAreaColorFill -> stencil = 128 픽셀에 색 오버레이 -> 끝나면 Invert -> stencil = 0 <-- 여기서 지워짐 [우리 Pass] AfterRenderingTransparents + 1 = 601 Step 1: DrawRendererList(overrideMaterial=Pass 5) -> stencil = 128 복원 Step 1b: (Match 요청 시) Pass 9 StencilToMask -> 마스크 텍스처 생성 Step 2: (Match 요청 시) 분석 다운샘플 체인 (full-res -> 64 -> 16 -> 4 -> 1) Step 3: colorHandle -> tempHandle 복사 (Pass 0) Step 4: tempHandle -> colorHandle, Stencil Equal(128)로 색조 보정 (Pass 1) [NiloToonUberPostProcessPass] BeforeRenderingPostProcessing = 900 -> Bloom, Tonemapping (우리 보정 결과에 적용됨) ``` ### 스텐실 구조 - `bit7 (=128)`: NiloToon 내부 예약 -- 캐릭터 영역 마킹 - `bit0~6 (=127)`: 사용자 제어 영역 ### 알려진 한계 - **반투명(Transparent queue) 캐릭터 파츠는 스텐실 마스킹 제외** - NiloToon의 `CharacterAreaStencilBufferFill`이 Opaque queue만 처리 (의도적 설계) --- ## 파일 목록 ``` Assets/YAMO/Shader/NiloToonCharToneAdjust/ +-- NiloToonCharToneAdjustVolume.cs <- VolumeComponent 정의 +-- NiloToonCharToneAdjustFeature.cs <- ScriptableRendererFeature + RenderGraph Pass +-- NiloToonCharToneAdjust.shader <- 풀스크린 색조 보정 셰이더 (Pass 0~9) +-- Editor/ | +-- NiloToonCharToneAdjustVolumeEditor.cs <- 색상 휠 + Reset/Match 버튼 커스텀 Inspector | +-- YAMOTrackballUIDrawer.cs <- URP 내장 TrackballUIDrawer 복사본 +-- WORK_SUMMARY.md <- 이 파일 ``` --- ## 각 파일 상세 ### NiloToonCharToneAdjustVolume.cs - **네임스페이스**: `YAMO` - **메뉴 경로**: `NiloToon/Char Tone Adjust (YAMO)` - `VolumeComponent` 상속, `active`는 베이스 클래스의 `bool active` 사용 (URP 17) | 파라미터 | 타입 | 기본값 | 설명 | |----------|------|--------|------| | `lift` | Vector4Parameter | (1,1,1,0) | 어두운 영역 색조 | | `gamma` | Vector4Parameter | (1,1,1,0) | 중간 영역 감마 | | `gain` | Vector4Parameter | (1,1,1,0) | 밝은 영역 배율 | | `shadows` | Vector4Parameter | (1,1,1,0) | 어두운 영역 색조 (SMH) | | `midtones` | Vector4Parameter | (1,1,1,0) | 중간 영역 색조 (SMH) | | `highlights` | Vector4Parameter | (1,1,1,0) | 밝은 영역 색조 (SMH) | | `shadowsStart/End` | ClampedFloat | 0.0 / 0.3 | SMH 그림자 범위 | | `highlightsStart/End` | ClampedFloat | 0.55 / 1.0 | SMH 하이라이트 범위 | | `saturation` | ClampedFloat | 1.0 | 채도 (0=흑백, 1=원본, 2=과채화) | | `postExposure` | ClampedFloat | 0.0 | 노출 보정 (EV) | | `blendAmount` | ClampedFloat | 1.0 | 보정 강도 | **파라미터 변환 규칙** (Volume -> 셰이더): - Lift/Shadows/Midtones/Highlights: `shader = (xyz - 1) + w` (중립값 = 0) - Gamma/Gain: `shader = xyz + w` (중립값 = 1) ### NiloToonCharToneAdjustFeature.cs - **네임스페이스**: `YAMO` - **renderPassEvent**: `AfterRenderingTransparents + 1` - **RenderGraph 방식**: `AddUnsafePass` 단일 Pass - `UnsafeCommandBuffer`를 통해 render target을 명시적으로 관리 - 3개 RasterPass 분리 시 pass 간 stencil 상태 유실 문제 -> UnsafePass 단일 통합으로 해결 #### Settings - `debugMode`: `DebugMode` enum - `Normal` -> 스텐실 재마킹 + 복사 + 캐릭터 픽셀만 색조 보정 (Pass 1) - `FullScreen` -> 전체 화면 색조 보정 (Pass 2, 디버그) - `StencilView` -> 스텐실 재마킹 + 복사 + 마젠타 오버레이 (Pass 3, 디버그) #### Auto Match 원샷 시스템 에디터 Inspector의 Match 버튼 클릭으로 작동하는 원샷 분석 시스템: 1. **요청**: 에디터에서 `autoMatchRequested = true` 설정 (static 필드) 2. **분석 (Frame N)**: `RecordRenderGraph`에서 분석 체인 포함한 UnsafePass 등록 - Pass 5 (MeshStencilFill): DrawRendererList로 캐릭터 스텐실 재마킹 - Pass 9 (StencilToMask): 스텐실 128 픽셀을 마스크 텍스처에 1.0 기록 - Pass 7 (MaskedDownsample): 마스크 기반 가중 다운샘플 (full-res -> 64x64) - Pass 8 (WeightedDownsample): 알파 기반 다운샘플 (64 -> 16 -> 4 -> 1x1 persistent RT) - 캐릭터(_InvertMask=0)와 배경(_InvertMask=1) 각각 별도 체인 실행 3. **리드백 (Frame N+1)**: `AsyncGPUReadback.Request`로 1x1 RT 읽기 - `Time.frameCount` 비교로 같은 프레임 멀티카메라 중복 방지 4. **값 적용**: 리드백 완료 시 `OnReadbackComplete()`에서 Volume 파라미터 직접 입력 - PostExposure: `log2(bgLum / charLum) * strength` - Midtones: `(bgTint - charTint) * strength` -> Vector4(1+dx, 1+dy, 1+dz, 0) - Saturation: `lerp(1, bgChroma/charChroma, strength)` - Undo 지원 **중요 구현 노트**: - `_InvertMask`는 반드시 `cmd.SetGlobalFloat()`로 설정해야 함 - `mat.SetFloat()`는 커맨드 버퍼 실행 시점에 마지막 값만 남음 (타이밍 이슈) - 분석 상태 머신: `Idle -> Running -> ReadbackPending -> Idle` - `_analysisStartFrame`으로 프레임 경계 확인 (멀티카메라 안전) ### NiloToonCharToneAdjust.shader - **셰이더 경로**: `YAMO/NiloToonCharToneAdjust` - `Properties {}` 비워둠 -- `_MainTex`를 Properties에 선언하면 Material 기본값이 Global 값보다 우선하여 복사가 안 됨 | Pass | 이름 | 스텐실 | 설명 | |------|------|--------|------| | 0 | Copy | 없음 | color -> temp 단순 복사 | | 1 | ToneAdjust | Equal 128 | 캐릭터 픽셀만 색조 보정 | | 2 | ToneAdjustFull | 없음 | 전체 화면 색조 보정 (디버그) | | 3 | StencilView | Equal 128 | 마젠타 오버레이 (디버그) | | 4 | DebugStencilFill | Always->Replace | 전체 화면 stencil=128 기록 (진단) | | 5 | MeshStencilFill | Always->Replace | 메시 기반 stencil=128 기록 (ColorMask 0) | | 6 | MeshMaskFill | Always->Replace | 미사용 (Pass 5+9 조합으로 대체) | | 7 | MaskedDownsample | 없음 | 마스크 기반 4x4 가중 다운샘플 | | 8 | WeightedDownsample | 없음 | 알파 기반 4x4 가중 다운샘플 | | 9 | StencilToMask | Equal 128 | stencil=128 -> color=1.0 마스크 변환 | **셰이더 적용 순서**: PostExposure -> LiftGammaGain -> ShadowsMidtonesHighlights -> Saturation -> Blend ### Editor/NiloToonCharToneAdjustVolumeEditor.cs - `VolumeComponentEditor` 상속, `[CustomEditor(typeof(NiloToonCharToneAdjustVolume))]` - `YAMOTrackballUIDrawer`로 Lift/Gamma/Gain, Shadows/Midtones/Highlights를 색상 휠 UI로 표시 - **Reset All Parameters** 버튼: 모든 파라미터를 기본값으로 초기화 (Undo 지원) - **Auto Match** 접이식 섹션: - Strength 슬라이더 (0~1) - Affect Brightness / Tint / Saturation 토글 - Match 버튼 (에디터/플레이 모드 모두 사용 가능) - 에디터 모드에서는 `SceneView.RepaintAll()`로 렌더링 트리거 ### Editor/YAMOTrackballUIDrawer.cs - URP 내장 `TrackballUIDrawer`의 복사본 (`internal sealed`이라 직접 사용 불가) - URP 내장 셰이더 `Hidden/Universal Render Pipeline/Editor/Trackball` 재사용 --- ## URP 17 핵심 API 메모 ```csharp // RenderGraph UnsafePass 기본 패턴 using (var builder = renderGraph.AddUnsafePass("PassName", out var pd)) { builder.UseTexture(handle, AccessFlags.ReadWrite); builder.UseRendererList(listHandle); builder.AllowPassCulling(false); builder.SetRenderFunc(static (PassData data, UnsafeGraphContext ctx) => { ... }); } // UnsafeCommandBuffer 텍스처/프로퍼티 설정 ctx.cmd.SetGlobalTexture(id, data.textureHandle); // TextureHandle 직접 지원 ctx.cmd.SetGlobalFloat(id, value); // 커맨드 버퍼에 순서대로 기록 // 주의: mat.SetFloat()는 즉시 적용되므로 draw 사이에 값이 바뀌는 경우 cmd.SetGlobalFloat 사용 // RendererList 생성 var rlParams = new RendererListParams(cullResults, drawSettings, filterSettings); var handle = renderGraph.CreateRendererList(rlParams); // DrawRendererList + overrideMaterial (스텐실 재마킹용) var drawSettings = new DrawingSettings(shaderTagId, sortSettings) { overrideMaterial = material, overrideMaterialPassIndex = passIndex, }; // 임시 텍스처 생성 (프레임 스코프) renderGraph.CreateTexture(new TextureDesc(w, h) { format = graphicsFormat, filterMode = FilterMode.Bilinear, name = "name" }); ``` --- ## 사용 방법 1. **URP Renderer Asset** -> Renderer Features -> `+` -> `NiloToonCharToneAdjustFeature` 추가 2. Feature의 Settings > Shader에 `YAMO/NiloToonCharToneAdjust` 셰이더 할당 3. Scene의 Volume 오브젝트 -> Add Override -> `NiloToon/Char Tone Adjust (YAMO)` 4. Volume 컴포넌트 체크박스(베이스 클래스 `active`) ON 5. 파라미터 수동 조절 또는 **Auto Match** 사용 ### Auto Match 워크플로우 1. Volume Inspector 하단 **Auto Match** 섹션 펼치기 2. Strength, Affect 옵션 설정 3. **Match** 버튼 클릭 -> PostExposure, Midtones, Saturation 값 자동 입력 4. **Blend Amount**로 적용 강도 조절 5. 필요시 Match를 반복하여 최적값 탐색 6. 초기화가 필요하면 **Reset All Parameters** 버튼 사용 > **디버그 순서**: Feature Inspector에서 `Debug Mode` 선택 > 1. `Full Screen` -> 전체 화면 동작 확인 > 2. `Stencil View` -> 스텐실 재마킹 정상 여부 확인 (캐릭터 마젠타 오버레이) > 3. `Character Only` -> 정식 모드, 캐릭터에만 색조 보정 적용 --- ## 트러블슈팅 기록 ### DrawRendererList + NiloToon 스텐실 패스가 동작하지 않는 문제 - **증상**: `DrawRendererList`로 NiloToon의 `NiloToonCharacterAreaStencilBufferFill` 패스를 재실행해도 stencil=128이 기록되지 않음 - **원인**: UnsafePass 컨텍스트에서 NiloToon 셰이더의 스텐실 패스가 정상 동작하지 않음 (정확한 원인 미상) - **해결**: `DrawingSettings.overrideMaterial`로 우리 셰이더의 Pass 5 (`MeshStencilFill`)를 강제 적용 ### MeshMaskFill(Pass 6)로 마스크+스텐실 동시 기록 실패 - **증상**: Pass 6으로 DrawRendererList 실행 시 캐릭터가 하얗게 빛남, charAvg==bgAvg - **원인**: UnsafePass에서 maskHandle+depthHandle을 바인딩하고 메시를 렌더링하면 colorHandle에도 영향 - **해결**: 2단계 분리 -- Pass 5 (stencil only) + Pass 9 (fullscreen StencilToMask) ### mat.SetFloat()로 _InvertMask 설정 시 char/bg 결과 동일 - **증상**: 캐릭터 분석과 배경 분석 결과가 완전히 동일 - **원인**: `mat.SetFloat()`는 머티리얼 프로퍼티를 즉시 변경하지만, `cmd.DrawProcedural()`은 나중에 실행됨. 두 체인 실행 전 마지막 값(invertMask=1)만 GPU에 적용됨 - **해결**: `cmd.SetGlobalFloat()`로 변경하여 커맨드 버퍼에 순서대로 기록 ### 멀티카메라 환경에서 리드백 실패 - **증상**: Auto Match 리드백 실패 (유효 픽셀 부족) -- 간헐적 - **원인**: Game+Scene 뷰 동시 사용 시 같은 프레임 내 `RecordRenderGraph`가 2회 호출되어 GPU 실행 전 리드백 요청 - **해결**: `Time.frameCount` 비교로 최소 1프레임 경과 후에만 리드백 요청 --- ## 현재 상태 (2026-04-06) | 항목 | 상태 | |------|------| | VolumeComponent + 에디터 GUI (색상 휠) | 완료 | | ScriptableRendererFeature + RenderGraph UnsafePass | 완료 | | 셰이더 (Pass 0~9) | 완료 | | 전체 화면 적용 (FullScreen 모드) | 동작 확인 | | 스텐실 마스크 시각화 (StencilView 모드) | 동작 확인 | | 캐릭터에만 적용 (Normal 모드, 스텐실 마스킹) | 동작 확인 | | Auto Match (원샷 분석 + 값 자동 입력) | 동작 확인 | | Reset 버튼 | 완료 |