book/41-tech-character-local-lighting-and-selective-grading.md

N/A

41. 기술 심화(실전): Character Local Lighting + Selective Grading

캐릭터를 배경과 분리해 조명/톤을 선택 보정하는 실전 URP 패턴

41. 기술 심화(실전): Character Local Lighting + Selective Grading

[A/B] 서브컬쳐 렌더링에서 “캐릭터가 묻히지 않는다”는 것은 셰이더 한 줄로 해결되지 않습니다. 배경이 복잡해질수록 캐릭터는 별도의 노출/대비/채도 목표를 가져야 하고, 그 목표를 파이프라인에서 강제하는 장치가 선택 보정(selective grading)입니다.

  • [B] 목표: 배경 노출 체계를 깨지 않으면서, 캐릭터만 “한 단계 더 읽히게” 만든다(과보정 금지).
  • [B] 핵심: 캐릭터 마스크를 안정적으로 만들고, 마스크 경계/후처리 순서/temporal과 충돌하지 않게 운영한다.

목적

  • [A/B] 복잡한 배경에서 캐릭터 가독성을 유지한다.
  • [B] mask + full-screen pass 조합을 표준화한다.

증거 등급 요약(A/B/C)

  • [A] 캐릭터/배경 분리 단서(공식 발표)
  • [B] selective grade 재현 사례
  • [C] 파라미터 강도/순서는 프로젝트 튜닝

핵심 개념

서론: 캐릭터는 배경과 다른 “시각 목표”를 가진다

  • [A/B] 배경은 분위기/조명 설계가 우선이고, 캐릭터는 표정/실루엣/재질 분리가 우선이다.
  • [B] 그래서 전역 톤매핑만으로는 “배경은 좋지만 캐릭터가 어두워지는” 상황이 자주 생긴다.

개론: 두 접근을 구분해 쓰면 유지보수가 쉬워진다

  • [B] 로컬 라이트(캐릭터 전용 조명): 캐릭터에만 라이트/보정을 적용하는 방식(하지만 씬 조명과의 충돌 가능)
  • [B] 선택 보정(selective grading): 씬 결과를 유지하면서 “마스크 영역만” 색/대비를 조정하는 방식
  • [B] 실전에서는 “로컬 라이트는 최소화, 선택 보정으로 마무리”가 회귀가 적은 편이다.

이론: 선택 보정은 결국 ‘색 변환 + 마스크 합성’이다

  • [B] 수식 형태는 단순하다: out = lerp(scene, grade(scene), mask).
  • [C] grade는 Lift/Gamma/Gain, saturation, contrast 같은 단순 연산으로도 충분히 큰 효과가 난다.
  • [C] 중요한 건 “강도”가 아니라 “경계/순서/프리셋 전환”을 운영 규칙으로 고정하는 것이다.

심화: 마스크 품질이 결과를 좌우한다

  • [B] 마스크 생성 방법은 하나로 통일해야 누수가 줄어든다.
  • [B] Rendering Layer 기반: 캐릭터만 별도 레이어로 렌더링해 mask 생성(운영이 단순)
  • [B] Stencil 기반: 다른 효과(T009/T011)와 충돌할 수 있어 bit 예약이 필요
  • [B] 경계 halo는 “마스크 해상도/필터/블러/업샘플” 문제인 경우가 많아, grade 수식을 먼저 의심하지 않는다.

코드 해설

  • [B] lerp(scene, graded, mask)는 보정 책임을 분리해 마스크 오류를 빠르게 추적할 수 있게 한다.
  • [B] 풀스크린 패스는 post stack 순서에 민감하므로 bloom/tonemap 이후 배치를 기본값으로 둔다.
HLSL
float mask = SAMPLE_TEXTURE2D(_CharacterMaskTex, sampler_CharacterMaskTex, uv).r;
float3 scene = SAMPLE_TEXTURE2D(_SceneColorTex, sampler_SceneColorTex, uv).rgb;
float3 graded = ApplyCharacterGrade(scene, _CharLift, _CharGamma, _CharGain);
float3 outColor = lerp(scene, graded, mask);
C#
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public sealed class CharacterSelectiveGradeFeature : ScriptableRendererFeature
{
    [SerializeField] private Material gradeMaterial;
    private CharacterGradePass _pass;

    public override void Create()
    {
        _pass = new CharacterGradePass(gradeMaterial)
        {
            renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing
        };
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        if (gradeMaterial == null) return;
        renderer.EnqueuePass(_pass);
    }

    private sealed class CharacterGradePass : ScriptableRenderPass
    {
        private readonly Material _mat;
        public CharacterGradePass(Material mat) => _mat = mat;
        public override void Execute(ScriptableRenderContext context, ref RenderingData data)
        {
            var cmd = CommandBufferPool.Get("CharacterSelectiveGrade");
            var color = data.cameraData.renderer.cameraColorTargetHandle;
            Blitter.BlitCameraTexture(cmd, color, color, _mat, 0);
            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
        }
    }
}

디버깅 포인트

  • [B] 캐릭터 마스크 경계에서 halo가 생기면 mask 해상도와 bilateral blur 반경을 같이 점검한다.
  • [B] 톤매핑/블룸/선택보정 순서를 바꿔가며 밝은 테두리 누수 지점을 프레임 디버거로 확인한다.
  • [A/B] 캐릭터만 과노출될 때는 selective grade gain보다 scene exposure 우선순위를 먼저 조정한다.

URP 매핑 포인트

설계 해석

  • [B] 마스크 생성은 rendering layer나 stencil 중 하나로 통일해 다중 기준 혼합 누수를 피한다.

  • [A/B] 낮/야간/네온 프리셋별 grade 파라미터를 분리 저장하고 카메라 상태에 따라 전환한다.

  • [B] rendering layer/stencil 기반 mask 생성

  • [A/B] post 이후 pass로 적용

  • [B] 적용 위치를 바꿔야 하는 경우:

  • [B] tonemap 이후(기본): 색이 “최종 디스플레이 공간”에서 안정적, 과보정 위험이 낮음

  • [C] tonemap 이전: HDR에서 더 자연스럽지만, bloom/exposure와 상호작용이 커서 운영 난도가 올라감

실패 패턴/오해

  • [B] mask 해상도 부족으로 halo 발생
  • [B] bloom/tonemap 순서 충돌
  • [B] 캐릭터 마스크를 여러 기준(레이어+스텐실+ID)으로 섞어 누수가 발생
  • [B] “캐릭터가 어두움”을 grade로만 해결해 원인(exposure/키라이트)을 방치

실무 체크리스트

Sources (섹션 단위 인용)