포스트

[TIL] 2026-07-08 — 레벨 드레싱 & 아웃라인 경계 차등: 볼륨 안만 아웃라인

[TIL] 2026-07-08 — 레벨 드레싱 & 아웃라인 경계 차등: 볼륨 안만 아웃라인

TeamCarry 레벨 정리(바리케이드·원경) 구현 기록. 작업 레벨은 /Game/Developers/goldb/Maps/L_LevelProto_Dressed (팀 원본 L_LevelProto는 무변경 — 복제본에서 작업 후 리뷰를 거쳐 반영 예정). 셰이더 M_PP_OutlineHLSL v10, 브랜치 feat/barricade-imposter.

1. 맵 가장자리 바리케이드 + 원경 임포스터

문제: 맵 가장자리에서 바닥이 뚝 끊기고 허공이 보인다. 인비저블월(BlockingVolume)이 있어 물리는 막혀 있지만, 시각적으로는 미완성 상태.

가장자리에 바리케이드 줄, 그 너머로 원경 바닥과 스카이라인 임포스터. 허공이 사라졌다.

  • 바리케이드 152개 — 바닥 경계를 라인 트레이스(아래→위, 바닥 액터만 필터)로 120방향 스캔해 폴리라인을 얻고, 그 위에 construction-barrier(Kenney CityRoads)를 260cm 간격으로 배치. 림에 건물이 서 있는 구간은 트레이스가 자동으로 걸러낸다.
  • 스카이라인 임포스터 29장 — 임포스터는 원경을 실제 메시 대신 그림 카드로 대체하는 최적화 기법. 절차 생성한 도시 실루엣 텍스처(T_CitySkyline, 2048×512) 한 장을 Unlit 마스크드 머티리얼로 카드에 얹어 2겹 링(근경 진한 톤 / 원경 옅은 톤)으로 배치했다. 카드당 폴리곤 2개, 충돌·그림자 없음. 서쪽은 기존 스카이스크래퍼 지구가 있어 제외.
  • 원경 바닥 700m 플레인 — 플레이트와 스카이라인 사이 파란 보이드를 메운다.

에셋: /Game/Developers/goldb/CityImposter/ (T_CitySkyline, M_CityImposter, MI_CityImposter_Far, M_ImposterGround)

2. 경계 가시화 — 공사장 해저드 월

문제: 인비저블월은 보이지 않아서 플레이어가 “왜 못 가지?”를 인지할 수 없다.

플레이 경계 볼륨 4개(Collision12~15) 위치에 세운 반투명 해저드 월. 공사장 느낌으로 "여기까지"를 전달한다.

월드 좌표 기반 대각 줄무늬 frac((x+y+z)/250)로 노랑/검정 패턴을 만들고 Unlit 트랜스루선트로 출력. 충돌·그림자 없는 순수 시각용이며, 물리 차단은 기존 볼륨이 그대로 담당한다. 색은 WallColor 파라미터로 조절.

에셋: /Game/Developers/goldb/Boundary/M_BoundaryWall

3. 아웃라인 경계 차등 — “볼륨 안만 아웃라인”

요구: 캐릭터와 플레이 경계 안쪽만 아웃라인을 유지하고, 경계 밖 배경(도시·원경)은 아웃라인을 제거해 시선 분산을 줄일 것.

BeforeAfter
beforeafter

벽 너머 건물·원거리 가로등의 실루엣 윤곽이 사라졌다(남은 선은 메시 텍스처의 창틀 디테일). 구역 안쪽 가로등(오른쪽)은 윤곽 유지.

스텐실 규약 (CustomDepthStencil)

의미설정 주체
1인터랙션 하이라이트 (노란 링)C++ OnFocus/OnUnfocus 훅 — 런타임
2경계 밖 배경 — 아웃라인 억제레벨 배치 시 일괄 지정 — 에디터 (419개 액터)

경계 밖 판정: 플레이 직사각형(x −3880~560, y −970~5070 — 볼륨 벽 기준)과 액터 바운드를 비교한다. 완전히 밖이면 배경, 걸치면 중심 위치로 판정, 대형 공유 바닥은 예외로 안쪽 취급.

핵심 — 커스텀뎁스↔씬뎁스 일치 검사

단순히 “스텐실 2가 찍힌 픽셀을 억제”하면 가려진 배경도 커스텀뎁스 버퍼에 남기 때문에 배경 앞에 선 캐릭터·집의 윤곽선까지 꺼진다(커스텀뎁스 패스는 메인 패스의 가림과 무관하게 그려짐). 그래서 “배경이 실제로 화면에 보이는 픽셀”만 억제한다:

1
2
3
// 배경(스텐실2)이면서, 커스텀뎁스 ≈ 씬뎁스 (= 그 배경이 지금 보이는 표면)일 때만
float bgVisible = step(abs(stencil - 2.0), 0.4)              // 스텐실 == 2
                * step(customDepth, sceneDepth * 1.02 + 50.0);  // 뎁스 일치 검사

캐릭터가 배경 앞에 서 있으면 그 픽셀의 스텐실은 2(뒤의 배경)지만, 커스텀뎁스(배경까지 거리) ≫ 씬뎁스(캐릭터까지 거리)라 검사에서 걸러져 윤곽선이 보존된다. 아웃라인의 5방향 샘플 각각에 이 검사를 적용하고, 기존 “엣지에서 가장 가까운 표면” 판정과 결합했다.

4. 시행착오 — 왜 이 방식인가

  1. 스텐실 단순 억제 — 가려진 배경이 커스텀뎁스에 남아 배경 앞 캐릭터·집 윤곽선까지 꺼짐. 화면 전체가 무윤곽이 되는 부작용.
  2. 월드 좌표 박스 게이트 — PP 도메인에서 WorldPosition 노드·행렬 복원 모두 씬 표면 좌표를 안정적으로 주지 않음.
  3. 내부/외부 PPV 분리 (검토만) — PPV는 카메라 위치 기준으로 전환됨. 화면 픽셀 단위의 안/밖 구분이 원리적으로 불가.
  4. 스텐실 + 커스텀뎁스 일치 검사 — 표준 오클루전 하이라이트 기법의 응용. 픽셀 정확, 스텐실 2가 없는 다른 맵은 동작이 완전히 동일(영향 없음).

5. 확장 방법 & 최종 결과

  • 새 배경 오브젝트 추가 — 메시 컴포넌트에 CustomDepthStencilValue = 2 + Render CustomDepth만 켜면 끝.
  • 새 상호작용 오브젝트OnFocus에서 스텐실 1 + 커스텀뎁스 ON (인터랙션 하이라이트 규약과 동일).
  • 전제 조건r.CustomDepth=3 (DefaultEngine.ini 반영 완료).

최종: 구역 안쪽만 또렷한 윤곽, 해저드 월이 경계를 알리고, 바깥 도시는 배경으로 가라앉는다. 아웃라인·하이라이트 링·배경 억제를 단일 PP 패스로 처리.

오늘 배운 것 정리

  1. 원경은 메시가 아니라 그림이다. 폴리곤 2개짜리 임포스터 카드 + 실루엣 텍스처 한 장이면 스카이라인이 선다 — 충돌·그림자 없음, 2겹 링의 톤 차이로 원근감까지.
  2. 인비저블월은 물리만 막는다 — 시각 경계는 따로 전달해야 한다. 월드 좌표 frac 줄무늬의 Unlit 해저드 월로 “여기까지”를 보여주고, 차단은 기존 볼륨에 그대로 맡긴다. 시각과 물리의 역할 분리.
  3. 커스텀뎁스 패스는 메인 패스의 가림과 무관하게 그려진다. 스텐실만 보고 억제하면 배경 앞 캐릭터·집의 윤곽까지 꺼진다 — “커스텀뎁스 ≈ 씬뎁스” 일치 검사로 실제로 화면에 보이는 배경 픽셀만 걸러야 한다.
  4. PP 도메인에서 화면 픽셀 단위의 안/밖 구분은 스텐실+뎁스뿐이다. WorldPosition 복원은 불안정하고 PPV는 카메라 위치 기준 전환이라 원리적으로 불가 — 표준 오클루전 하이라이트 기법의 응용이 답이었다.
  5. 규약을 정하면 확장이 공짜다. 스텐실 1=하이라이트(런타임 훅)·2=배경 억제(에디터 배치)로 역할을 고정하니, 새 오브젝트는 스텐실 값만 지정하면 끝. 스텐실 2가 없는 맵은 동작이 완전히 동일하다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.