본문으로 건너뛰기
CHOI HONGSU
1 min read

캐릭터 바운즈 섀도우

 

로비 16명 한정 고품질 캐릭터 그림자 — Atlas 슬롯 + 마스크 방식으로 모바일 1024 해상도에서 실루엣 가독성을 확보.

  • 256²  캐릭터당 텍셀 (1024 atlas)
  • 16 동시 캐스터 한도 (Player 우선 + NPC 카메라 거리순)

Problem

URP 기본 캐스케이드 섀도우맵은 화면 전체를 한 장에 담아 캐릭터 1명당 할당되는 텍셀이 매우 적습니다. 그림자만의 영역을 분리해 캐릭터당 텍셀을 보장할 필요가 있었습니다.

Approach

Cascade Shadow Map 해상도 상향

Chosen

Pros

  • 기존 파이프라인 그대로, 구현 비용 적음

Cons

  • 메모리·대역폭 비용 증가, 캐릭터당 텍셀 보장 안 됨, 전체 씬 영향

Atlas 슬롯 + 깊이 비교

Pros

  • 슬롯당 256² 텍셀 보장, 캐릭터별 캡처, URP 셰도우 비교 로직 재활용

Cons

  • 16-bit depth + 작은 슬롯에서 acne, bias 튜닝 부담이 큼

Atlas 슬롯 + 마스크 방식 (chosen)

Pros

  • bias 파라미터 제거, 16-bit depth로 충분, 셰이더 간결

Cons

  • 깊이 정보 없음 → 다층 맵에서 그림자 오류 가능

Why Cascade Shadow Map 해상도 상향: 로비는 캐릭터가 단일 평면 위에 있어 깊이 비교가 주는 이점이 작은 반면, 16-bit depth + 256² 슬롯에서 acne·bias 튜닝 비용이 큽니다. 깊이 비교 자체를 제거하고 normalWS.y 기반 SlopeFade로 천장·벽 영향만 차단했습니다

Architecture

RendererFeature가 BeforeRenderingOpaques 시점에 1024 atlas RT를 클리어하고 16개 슬롯에 각 캐스터 메쉬를 depth-only로 렌더링합니다. 캐스터별 union bounds 8 corner를 view space로 변환해 tight orthographic AABB를 만들어 슬롯 텍셀을 빠듯이 채웁니다. 리시버 셰이더는 _CharacterShadowVP[i]로 worldPos를 슬롯 UV로 변환하고, 클리어 값이 아닌 픽셀을 그림자로 판정합니다.

diagram: /images/projects/ovensmash/character-shadow-atlas/architecture.svg

diagram caption: Caster → Manager → AtlasPass → 글로벌 → Receiver Shader

Implementation

  1. 01

    Step 1. Caster 컴포넌트 + Union Bounds 히스테리시스

    캐릭터 루트에 부착하는 MonoBehaviour. 자식 SkinnedMeshRenderer만 자동 수집해 union bounds를 계산합니다. 본 애니메이션 때문에 bounds가 매 프레임 출렁이는 '펌핑'을 막기 위해 extents에 히스테리시스(확장 즉시 / 축소 lerp)를 적용하고, center는 즉시 추적해 이동 응답성은 유지했습니다.

    • code (csharp): CharacterShadowCaster.GetUnionBounds의 히스테리시스 부분 (Vector3.MaxVector3.Lerp).
  2. 02

    Step 2. Manager — 16 슬롯 우선순위

    씬 싱글톤이 캐스터를 Player/NPC로 분리 등록하고, 매 프레임 Player 전원 → 카메라 거리순 NPC 순서로 최대 16명을 선별합니다. 도메인 리로드 후 static 참조가 끊긴 경우 씬에서 재수집하는 복구 경로도 두었습니다.

  3. 03

    Step 3. Tight AABB Frustum (이번 wip의 핵심)

    이전 버전은 라이트 기울기로부터 그림자 수평 드리움 거리를 추정해 frustum을 정사각형 r로 키웠습니다. 이번에는 bounds 8 corner를 view space로 변환해 tight orthographic AABB(xMin/xMax/yMin/yMax)를 직접 계산하도록 바꿔, 같은 256² 슬롯 안에서 캐릭터 실루엣이 차지하는 텍셀을 최대화했습니다.

    • code (csharp): ComputeMatrices의 8-corner 루프와 Matrix4x4.Ortho(xMin, xMax, yMin, yMax, near, far).
  4. 04

    Step 4. 마스크 방식 그림자 판정

    기존 깊이 비교(coord.z vs stored + bias)를 폐기하고 "아틀라스에 캐스터가 그려진 픽셀 = 그림자"로 단순화했습니다. reversed Z에서는 clear=0이므로 stored > 0이면 hit. _CharShadowBias 파라미터가 사라지고, normalWS.y 기반 SlopeFade로 천장·벽에 그림자가 새는 것을 막습니다.

    • code (hlsl): EvalCharacterShadow의 stored 샘플링 + UNITY_REVERSED_Z 분기 hit 판정.

Before / After

1024 해상도 · 동일 캐릭터

 

After
Before
BEFOREAFTER

디버깅 모드

Conclusion

같은 텍스쳐 해상도로 그림자 퀄리티 보완