13 KiB (Stored with Git LFS)

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<PassData> 단일 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 메모

// RenderGraph UnsafePass 기본 패턴
using (var builder = renderGraph.AddUnsafePass<PassData>("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 버튼 완료