URP와 MaterialPropertyBlock을 결합한 캐릭터 오버레이

한눈에 보기

대화 중 현재 발화자는 원래 색을 유지하고 나머지 캐릭터에는 오버레이를 적용하는 연출을 구현했습니다. URP Custom Render Feature가 캐릭터 레이어를 커스텀 셰이더로 다시 그리고, VisibleChangerMaterialPropertyBlock을 통해 캐릭터별 색상과 전환 타이밍을 독립적으로 제어합니다.

항목내용
프로젝트3D 캐릭터 기반 비주얼 노벨 게임
역할Unity 클라이언트 개발
기술Unity, C#, URP, ScriptableRendererFeature, Shader, Stencil Buffer
직접 기여Render Feature, 오버레이 셰이더, 캐릭터별 전환 로직과 시나리오 연동

1. 요구사항

여러 캐릭터가 등장하는 대화 장면에서 현재 발화자는 원래 색을 유지하고, 나머지 캐릭터는 회색 오버레이로 시각적 우선순위를 낮춰야 했습니다.

단순히 한 번 색을 바꾸는 기능이 아니라 다음 조건을 만족해야 했습니다.

  • 캐릭터가 사용하는 원본 셰이더 종류와 관계없이 같은 연출이 적용돼야 합니다.
  • 여러 캐릭터가 동시에 등장해도 각자의 오버레이 상태를 독립적으로 제어해야 합니다.
  • 공유 머티리얼을 캐릭터마다 복제하지 않아야 합니다.
  • 등장·퇴장 투명화와 오버레이 전환이 동시에 실행돼도 시각적으로 튀지 않아야 합니다.
  • 몸통 오버레이와 외곽선이 서로 중복되어 탁해지지 않아야 합니다.

2. 구조

최종 구조는 “누구에게 적용할지”와 “어떻게 그릴지”를 분리했습니다.

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: 캐릭터를 구성하는 여러 SkinnedMeshRenderer의 오버레이 색상을 보간합니다.
  • MaterialPropertyBlock: 공유 오버레이 머티리얼을 유지하면서 렌더러별 _OverlayColor 값을 전달합니다.
  • OverlayEffectRenderFeature: 캐릭터 레이어를 오버레이 전용 머티리얼로 다시 렌더링합니다.
  • UnlitOverlay.shader: Stencil을 이용해 몸통과 외곽선 영역을 구분합니다.

3. 현재 발화자와 적용 대상을 분리하기

시나리오 데이터의 화자 이름을 캐릭터 ID로 변환하고, 활성 캐릭터 목록을 순회하며 현재 화자를 제외한 캐릭터에만 오버레이를 요청했습니다. 화자를 찾을 수 없는 독백이나 시스템 대사에서는 모든 캐릭터를 기본 색으로 되돌렸습니다.

int speakerId = FindCharacterId(currentSpeakerName);
 
foreach (CharacterController character in characters)
{
    int characterId = FindCharacterId(character.name);
    character.SetOverlayColor(characterId != speakerId);
}

배열의 인덱스를 캐릭터 ID처럼 사용하지 않고 실제 이름에서 얻은 ID를 비교했습니다. 캐릭터 배치 순서가 달라져도 잘못된 대상에 오버레이가 적용되지 않도록 하기 위한 선택이었습니다.


4. 공유 머티리얼을 유지하면서 캐릭터별 색상 제어하기

Render Feature의 오버레이 머티리얼은 공유됩니다. 공유 머티리얼의 색을 직접 변경하면 한 캐릭터를 바꾼 값이 다른 캐릭터에도 적용됩니다. 반대로 캐릭터마다 머티리얼을 복제하면 관리 대상과 런타임 인스턴스가 늘어납니다.

그래서 MaterialPropertyBlock_OverlayColor를 기록해 각 SkinnedMeshRenderer에 적용했습니다.

var block = new MaterialPropertyBlock();
renderer.GetPropertyBlock(block);
block.SetColor(OverlayColorId, color);
renderer.SetPropertyBlock(block);

이 방식으로 오버레이 머티리얼과 셰이더는 공유하면서 캐릭터별 색상과 알파는 독립적으로 유지할 수 있었습니다.

여러 Body Part 처리

캐릭터는 몸, 얼굴, 머리카락과 의상 등 여러 SkinnedMeshRenderer로 나뉩니다. VisibleChanger가 렌더러 목록 전체를 순회하며 같은 보간 값을 적용해, 일부 파츠만 늦게 변하거나 원래 색으로 남는 문제를 막았습니다.


5. URP Custom Render Feature와 Stencil

OverlayEffectRenderFeature는 지정된 캐릭터 레이어를 오버레이 머티리얼로 다시 렌더링합니다. 오버레이 셰이더는 몸통과 외곽선 두 패스로 구성했습니다.

Body Pass

Stencil 값이 기준값과 같은 픽셀에만 몸통 오버레이를 그립니다. 그린 픽셀의 Stencil 값을 Zero로 변경해 다음 외곽선 패스가 몸통 내부를 다시 칠하지 못하도록 했습니다.

Outline Pass

버텍스를 노멀 방향으로 확장하고 앞면을 컬링하는 Inverted Hull 방식으로 외곽 영역을 만듭니다. Body Pass에서 내부 Stencil이 지워졌기 때문에 외곽선이 몸통 위에 중복 블렌딩되는 범위를 줄일 수 있습니다.

Stencil
{
    Ref [_StencilRef]
    Comp [_StencilComp]
    Pass Zero
}

이 구성은 원본 캐릭터 셰이더에 오버레이 색상 계산을 각각 추가하지 않고, URP의 별도 렌더링 단계에서 공통 연출을 적용하기 위한 선택이었습니다.

증빙

Frame Debugger에서 원본 캐릭터 → Overlay Body → Overlay Outline 순서가 보이도록 캡처합니다. 각 캡처에는 어떤 버퍼와 패스가 바뀌는지 짧은 주석을 추가합니다.


6. 등장 연출과 오버레이 전환의 충돌

캐릭터가 등장하는 동안에는 투명도가 0에서 1로 변합니다. 이 시점에 발화자 변경까지 발생하면 투명화 코루틴과 오버레이 코루틴이 같은 렌더러 상태를 동시에 수정할 수 있습니다.

CharacterController에서 등장 코루틴이 진행 중이면 완료를 기다린 뒤 오버레이 전환을 시작하도록 순서를 정했습니다. 같은 오버레이 상태를 다시 요청받으면 새 코루틴을 만들지 않았고, 전환이 끝나면 참조를 비워 다음 요청을 받을 수 있게 했습니다.

VisibleChanger는 시작 색과 목표 색 사이를 시간 기준으로 보간하고, 마지막 프레임에는 정확한 목표값을 다시 설정했습니다. 프레임 누락이나 부동소수점 오차로 최종 알파가 애매하게 남는 일을 줄이기 위한 처리였습니다.

투명화와 오버레이 알파 결합

퇴장 중인 캐릭터의 오버레이만 화면에 남지 않도록, 오버레이 알파가 캐릭터의 현재 투명도보다 커지지 않게 제한했습니다.


7. 결과와 검증

  • 현재 화자와 비화자의 시각적 우선순위를 명확히 구분했습니다.
  • 공유 머티리얼을 유지하면서 캐릭터별 오버레이 값을 독립적으로 제어했습니다.
  • 원본 캐릭터 셰이더마다 별도 오버레이 로직을 추가하지 않았습니다.
  • 등장·퇴장과 화자 전환이 겹치는 상황의 실행 순서를 명시했습니다.

현재 수치 성과는 별도로 측정하지 않았으므로 추정하지 않습니다. 공개 전 다음 항목을 동일 환경에서 측정할 예정입니다.

  • Render Feature 적용 전후 Draw Call
  • 화면에 등장한 캐릭터 수에 따른 CPU·GPU Frame Time
  • 머티리얼 인스턴스 수
  • 오버레이 전환 중 GC Alloc 여부

시연 영상

두 명 이상의 캐릭터가 등장하는 장면에서 발화자가 바뀔 때 오버레이가 교대되고, 캐릭터 등장·퇴장 중에도 색상이 튀지 않는 30~45초 영상을 추가합니다.


8. 한계와 다음 개선

현재 구현은 공통 Stencil 기준값을 사용합니다. 복잡한 겹침이나 오버레이 제외 오브젝트가 증가하면 단일 기준값만으로 영역을 구분하기 어려울 수 있습니다. 이런 요구가 실제로 생긴다면 다음 대안을 검토할 수 있습니다.

  • 오버레이 제외 레이어를 먼저 지우는 마스킹 패스
  • 캐릭터 또는 그룹별 Stencil ID
  • Fullscreen 기반 합성 방식
  • URP Render Graph 기반 재구성

이 항목들은 현재 프로젝트에 적용된 결과가 아니라 향후 요구가 복잡해질 때의 개선 방향입니다. 포트폴리오에서는 실제 구현과 후속 아이디어를 명확히 구분합니다.


9. 이 경험에서 배운 것

  • 렌더링 연출은 패스 구현만으로 끝나지 않고 게임 상태와 전환 타이밍까지 연결해야 합니다.
  • 공유 리소스와 객체별 상태를 분리할 때 MaterialPropertyBlock이 유용합니다.
  • 셰이더의 Stencil 연산은 패스 순서와 함께 설명해야 실제 동작을 이해할 수 있습니다.
  • 문서의 설계 설명보다 현재 코드와 Frame Debugger의 실행 결과를 우선해야 합니다.

← 포트폴리오 홈으로