URP와 MaterialPropertyBlock을 결합한 캐릭터 오버레이
프로젝트 요약
대화 중 현재 발화자는 원래 색을 유지하고, 나머지 캐릭터는 회색 오버레이로 시각적 우선순위를 낮추는 연출을 구현했습니다.
URP Custom Render Feature가 캐릭터 레이어를 전용 셰이더로 다시 그리고,
MaterialPropertyBlock으로 공유 머티리얼을 유지하면서 캐릭터별 색상과 전환 상태를 독립적으로 제어했습니다.
| 항목 | 내용 |
|---|---|
| 프로젝트 | 3D 캐릭터 기반 비주얼 노벨 |
| 담당 | Unity 클라이언트 개발 |
| 기술 | Unity, C#, URP, ScriptableRendererFeature, Shader, Stencil Buffer, MaterialPropertyBlock |
| 구현 내용 | Render Feature, 오버레이 셰이더, 캐릭터별 전환 로직과 시나리오 연동 |
| 핵심 성과 | 원본 캐릭터 셰이더를 수정하지 않고 발화자 중심 오버레이 연출 시스템 |
문제 상황
여러 캐릭터가 등장하는 대화 장면에서 현재 발화자는 원래 색을 유지하고, 나머지 캐릭터는 회색 오버레이로 시각적 우선순위를 낮춰야 했습니다.
캐릭터마다 사용하는 원본 머티리얼과 셰이더 구성이 달랐기 때문에 각 셰이더에 동일한 색상 연산을 추가하면 수정 범위가 넓어집니다. 하나의 공유 머티리얼 값을 직접 바꾸면 여러 캐릭터가 같은 값을 사용하고, 캐릭터마다 머티리얼을 복제하면 런타임 인스턴스와 관리 대상이 늘어나는 문제도 있었습니다.
해결해야 했던 조건
- 원본 캐릭터 셰이더를 개별적으로 수정하지 않을 것
- 여러 캐릭터의 오버레이 상태를 독립적으로 제어할 것
- 캐릭터마다 오버레이 머티리얼을 복제하지 않을 것
- 여러 Body Part에 동일한 전환 값을 적용할 것
- 몸통과 외곽선이 중복 블렌딩되어 탁해지지 않을 것
- 등장·퇴장과 화자 전환이 겹쳐도 색상이 튀지 않을 것
해결 방법
오버레이를 원본 셰이더의 기능으로 넣는 대신, URP의 별도 렌더링 단계에서 캐릭터 레이어를 전용 머티리얼로 다시 그렸습니다. 적용 대상과 렌더링 방식을 분리하고, 렌더러별 값은 MaterialPropertyBlock으로 전달했습니다.
flowchart LR A["ScenarioController<br/>현재 발화자 판별"] --> B["CharacterController<br/>전환 순서 조정"] B --> C["VisibleChanger<br/>캐릭터별 색상 보간"] C --> D["MaterialPropertyBlock<br/>_OverlayColor 전달"] E["URP Renderer"] --> F["OverlayEffectRenderFeature<br/>캐릭터 레이어 재렌더링"] F --> G["UnlitOverlay Shader<br/>Body + Outline"] D --> G
ScenarioController는 현재 화자를 판별하고 비화자에게 오버레이를 요청합니다.CharacterController는 등장·퇴장 연출과 겹치지 않도록 실행 순서를 조정합니다.VisibleChanger는 캐릭터를 구성하는 렌더러 전체의 색상을 보간합니다.OverlayEffectRenderFeature는 지정된 캐릭터 레이어를 전용 머티리얼로 다시 렌더링합니다.UnlitOverlay.shader는 Stencil을 이용해 몸통과 외곽선 영역을 구분합니다.
현재 발화자와 적용 대상 분리
시나리오 데이터의 화자 이름을 캐릭터 ID로 변환하고, 활성 캐릭터 목록에서 현재 화자를 제외한 캐릭터에만 오버레이를 요청했습니다. 화자를 찾을 수 없는 독백이나 시스템 대사에서는 모든 캐릭터를 원래 색으로 되돌렸습니다.
int speakerId = FindCharacterId(currentSpeakerName);
foreach (CharacterController character in characters)
{
int characterId = FindCharacterId(character.name);
character.SetOverlayColor(characterId != speakerId);
}배열 순서를 캐릭터 ID처럼 사용하지 않고 실제 이름에서 얻은 ID를 비교했습니다. 캐릭터의 배치 순서가 달라져도 잘못된 대상에 오버레이가 적용되지 않도록 하기 위한 선택이었습니다.
공유 머티리얼과 캐릭터별 상태 분리
Render Feature의 오버레이 머티리얼은 모든 캐릭터가 공유합니다. 공유 머티리얼의 색을 직접 변경하는 대신 MaterialPropertyBlock에 _OverlayColor를 기록해 각 SkinnedMeshRenderer에 적용했습니다.
var block = new MaterialPropertyBlock();
renderer.GetPropertyBlock(block);
block.SetColor(OverlayColorId, color);
renderer.SetPropertyBlock(block);이 방식으로 머티리얼과 셰이더는 공유하면서 캐릭터별 색상과 알파는 독립적으로 유지했습니다. 몸, 얼굴, 머리카락과 의상처럼 여러 SkinnedMeshRenderer로 구성된 캐릭터는 렌더러 목록 전체에 같은 보간 값을 적용해 일부 파츠만 늦게 바뀌거나 원래 색으로 남는 문제를 막았습니다.
Custom Render Feature와 Stencil
OverlayEffectRenderFeature는 지정된 캐릭터 레이어를 오버레이 전용 머티리얼로 다시 렌더링합니다. 오버레이 셰이더는 몸통을 그리는 Body Pass와 외곽 영역을 그리는 Outline Pass로 구성했습니다.
Body Pass
Stencil 값이 기준값과 같은 픽셀에만 몸통 오버레이를 그립니다. 그린 픽셀의 Stencil 값을 Zero로 변경해 다음 Outline Pass가 몸통 내부를 다시 칠하지 못하도록 했습니다.
Outline Pass
버텍스를 노멀 방향으로 확장하고 앞면을 컬링하는 Inverted Hull 방식으로 외곽 영역을 만들었습니다. Body Pass에서 내부 Stencil을 지웠기 때문에 외곽선이 몸통 위에 중복 블렌딩되는 범위를 줄일 수 있었습니다.
Stencil
{
Ref [_StencilRef]
Comp [_StencilComp]
Pass Zero
}원본 캐릭터 셰이더마다 오버레이 계산을 추가하지 않고 URP의 별도 렌더링 단계에서 공통 연출을 적용하기 위한 구성입니다.
등장·퇴장 연출과 실행 순서 조정
캐릭터가 등장하거나 퇴장하는 도중 발화자가 바뀌면 투명화 코루틴과 오버레이 코루틴이 같은 렌더러의 시각 상태를 동시에 변경할 수 있습니다.
CharacterController에서 등장 코루틴이 진행 중이면 완료를 기다린 뒤 오버레이 전환을 시작하도록 순서를 정했습니다. 같은 상태를 다시 요청받으면 새 코루틴을 만들지 않았고, 전환 마지막 프레임에는 목표 색상을 다시 설정해 부동소수점 오차가 남지 않게 했습니다.
퇴장 중인 캐릭터의 오버레이만 화면에 남지 않도록 오버레이 알파는 캐릭터의 현재 투명도보다 커지지 않게 제한했습니다.
lilToon 머티리얼의 런타임 투명화 과정에서 발생한 셰이더 Variant 불일치는 별도 사례로 정리했습니다.
결과
- 현재 화자와 비화자의 시각적 우선순위를 명확히 구분했습니다.
- 원본 캐릭터 셰이더를 수정하지 않고 공통 오버레이 연출을 적용했습니다.
- 공유 머티리얼을 유지하면서 캐릭터별 색상과 알파를 독립적으로 제어했습니다.
- 여러 Body Part에 동일한 전환 값을 적용했습니다.
- Stencil과 패스 순서를 이용해 몸통과 외곽선의 중복 블렌딩을 줄였습니다.
- 등장·퇴장과 화자 전환이 겹치는 상황의 실행 순서를 명시했습니다.