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/키라이트)을 방치