포스트

[TIL] 2026-07-20 — 라이트맵을 구우면 벽이 타는 문제 · 원인 추적과 전 조명 Movable 전환

[TIL] 2026-07-20 — 라이트맵을 구우면 벽이 타는 문제 · 원인 추적과 전 조명 Movable 전환

목차


증상

Build Lighting을 돌렸더니 실내 벽이 그라데이션 없이 균일한 주황 덩어리가 됐다. 빛이 어디서 오는지 알 수 없고, 창가와 구석의 밝기 차이가 사라졌다.

라이트맵 빌드 직후 — 벽 전체가 한 덩어리

원인이 하나일 거라 생각했는데 세 개였고, 다 고친 뒤에도 남는 문제가 있어서 결국 굽는 걸 포기했다. 그 과정을 정리한다.


작업 환경

  • 프로젝트: TeamCarry (UE 5.8 / C++ + BP / 리슨서버 멀티 / 협동 이사 게임)
  • 대상 레벨: L_Lobby, L_Level1, L_Level2
  • 조명 구성: RectLight 35 · PointLight 13 · SpotLight 8 · DirectionalLight 1 · SkyLight 1
  • 렌더링: Lumen + 포스트 프로세스 툰 셀 셰이딩
  • 확인 수단: 빌드 로그(LightingResults) · DumpUnbuiltLightInteractions · 뷰포트 캡처

배경 — 라이트맵이 뭘 하는 물건인가

원인을 이해하려면 라이트맵이 어떻게 저장되는지부터 봐야 한다.

Static·Stationary 라이트의 간접광은 실시간으로 계산하지 않는다. Lightmass가 빌드 시점에 오프라인으로 계산해서 텍스처에 구워 넣는다. 이 텍스처가 라이트맵이고, 각 스태틱메시는 이걸 샘플링할 UV 좌표가 필요하다.

그런데 이 UV는 아무거나 쓸 수 없다. 두 가지 조건이 있다.

  1. 0~1 범위 안에 들어갈 것 — 라이트맵 텍스처는 메시 한 개당 한 장이라 UV가 1을 넘어가면 텍스처 밖을 읽는다.
  2. 면끼리 겹치지 않을 것 — 겹치면 서로 다른 면이 같은 텍셀을 공유하게 된다.

문제는 아티스트가 만든 UV 채널 0은 보통 이 조건을 일부러 어긴다는 것이다. 텍스처를 타일링하려면 UV가 1을 넘어야 하고, 벽돌 텍스처 같은 걸 여러 면에 같은 방식으로 입히려면 면들이 UV 공간에서 겹치는 게 오히려 정상이다.

그래서 언리얼은 라이트맵 전용 UV를 따로 만든다. 스태틱메시 빌드 설정의 Generate Lightmap UVs가 켜져 있으면, 겹치지 않게 재배치(unwrap + pack)한 UV를 다른 채널에 생성한다. 어느 채널에 넣을지는 Dst Lightmap Index가 정한다.

여기서 중요한 건, 생성하는 쪽과 사용하는 쪽이 별개의 설정이라는 점이다.

설정역할소속
bGenerateLightmapUVs라이트맵용 UV를 만들지 여부Build Settings
DstLightmapIndex만든 UV를 몇 번 채널에 넣을지Build Settings
LightMapCoordinateIndex렌더링 때 몇 번 채널을 읽을지StaticMesh 본체

앞의 둘은 “쓰기”, 마지막 하나는 “읽기”다. 이게 어긋나면 곱게 펴놓은 UV를 두고 엉뚱한 채널을 읽는다.


원인 1. 라이트맵이 엉뚱한 UV 채널을 보고 있었다

빌드 로그에 이게 찍혀 있었다.

1
2
LightingResults: Warning: Extrude_233CC0FA Object has overlapping UVs.
LightingResults: Extrude_233CC0FA Lightmap UV are overlapping by 52.5%.

메시 설정을 열어보니:

1
2
3
bGenerateLightmapUVs   = True    ← 생성은 되고 있음
DstLightmapIndex       = 1       ← 채널 1에 잘 만들어짐
LightMapCoordinateIndex = 0      ← 근데 읽는 건 채널 0

위 표의 딱 그 어긋남이다. 채널 1에는 겹침 없는 UV가 정상적으로 만들어져 있는데, 렌더링은 타일링용 채널 0을 라이트맵으로 읽고 있었다.

왜 “탄 것처럼” 보이나. 겹친 UV는 서로 다른 면이 같은 텍셀을 공유한다는 뜻이다. 창가 벽면(밝음)과 구석 벽면(어두움)이 라이트맵의 같은 픽셀에 기록되면, Lightmass는 둘을 한 값으로 합쳐 저장한다. 밝은 쪽 면적이 크면 어두운 쪽까지 그 값으로 칠해진다. 52.5% 겹침이면 절반 이상의 면이 남의 조명 값을 뒤집어쓴다. 그래서 그라데이션이 사라지고 균일한 덩어리가 된다.

LightMapCoordinateIndex를 1로 바꾸자 벽 명암이 바로 돌아왔다.

헛짚었던 부분 — 로그에 뜬 것만 고쳤다

처음엔 경고에 이름이 뜬 14개만 고치고 재빌드했다. 여전히 탔다.

LightingResults의 겹침 경고는 비율이 임계값을 넘은 것만 출력한다. 30% 겹친 메시는 화면에서 충분히 문제인데 로그에는 안 나온다. 로그를 목록으로 쓰면 안 되고, 레벨 전체를 훑어야 실제 범위가 나온다.

전수 조사 결과:

레벨스태틱메시LightMapCoordinateIndex = 0
L_Level16553
L_Lobby41다수

경고에 뜬 건 14개, 실제 대상은 53개였다.


원인 2. 라이트맵 해상도가 4였다

UV를 고쳐도 일부 오브젝트는 여전히 단색이었다. 이건 다른 문제였다.

light_map_resolution은 그 메시가 받을 라이트맵 텍스처의 한 변 크기(텍셀)다. 이 값이 4인 메시가 8개 있었다 — SM_ToyTruck, SM_Couch_003, SM_Box_Cardboard 등.

4×4 = 16텍셀. 오브젝트 전 표면의 조명을 16개 픽셀로 표현한다는 뜻이고, 여기에 앞서 본 UV 패킹까지 들어가면 면 하나당 1~2텍셀이 돌아간다. 사실상 면당 단색이다.

적정값은 오브젝트 크기에 비례해야 한다. 액터 월드 바운즈를 기준으로 약 12uu/텍셀을 목표로 재계산해 로비 31개를 조정했다.

1
2
2m(200uu) 소파  → 200 / 12 ≈ 16텍셀
10m(1000uu) 벽  → 1000 / 12 ≈ 84텍셀 → 128로 올림

해상도는 메시 애셋 값 말고 컴포넌트별 Overridden Light Map Res로도 덮을 수 있다. 같은 메시를 크게 쓰는 인스턴스만 올리고 싶을 때 쓴다. 이 레벨은 이미 64~512로 제각각 덮여 있었다.


원인 3. 벽·기둥·천장이 전부 같은 Cube 하나였다

조사하다 구조적인 문제를 발견했다. 레벨의 벽·기둥·천장이 모두 같은 Cube 메시 하나를 스케일만 바꿔 배치한 것이었다 (Cube14, Cube37, Cube64, Cube84, Cube103, Cube303…).

라이트맵 UV 레이아웃은 메시 애셋 단위로 만들어진다. 큐브 하나의 UV는 6면을 정사각형에 균등하게 배치한다 — 원본 큐브가 정육면체니까 그게 맞다.

그런데 이걸 (1000, 20, 300)으로 눌러 벽으로 쓰면, 실제 면적 비율은 이렇게 깨진다.

실제 면적UV 할당
앞/뒤 (1000×300)300,0001/6
좌/우 (20×300)6,0001/6
위/아래 (1000×20)20,0001/6

넓은 앞면이 좁은 옆면과 똑같은 텍셀 수를 받는다. 앞면은 텍셀 밀도가 50배 부족하고, 옆면은 그만큼 낭비된다. 해상도를 올려도 이 비율은 안 바뀌므로 넓은 면 기준으로 맞추려면 전체를 과하게 올려야 한다.

해상도는 컴포넌트별로 덮을 수 있지만 UV 레이아웃은 애셋 고정이라 인스턴스별 보정이 안 된다. 그레이박스 단계라 어쩔 수 없는 구조지만, 라이트맵을 제대로 구우려면 용도별로 메시를 분리해야 한다는 뜻이다.


남은 노이즈 — 품질을 8배로 올려도 그대로

세 가지를 고치자 번인은 사라졌다. 대신 천장과 벽 경계에 검은 점이 뿌려졌다.

간접광 노이즈

간접광 샘플이 부족한 걸로 보고 Lightmass 설정을 올렸다.

시도Indirect QualitySmoothness빌드 시간결과
12.01.01분 30초노이즈 있음
26.00.67분 44초노이즈 있음
38.02.012분 57초노이즈 있음

2번 시도에서 IndirectLightingSmoothness를 0.6으로 낮춘 건 방향이 반대였다. 이 값은 간접광 결과를 얼마나 뭉갤지를 정하는데, 낮을수록 선명해지는 대신 노이즈가 그대로 드러난다. 선명하게 만들려다 노이즈를 키웠다.

품질을 8배로 올려도 그대로라는 건 샘플 부족이 아니라는 뜻이다. 여기서 파라미터 튜닝을 멈추고 격리 실험으로 넘어갔다.

실험가설결과
포스트 프로세스 볼륨 OFF툰 셀 셰이더가 밝기 차를 밴딩으로 증폭노이즈 그대로 → 아님
전 조명 Movable + 재빌드라이트맵 데이터 자체의 문제노이즈 즉시 소멸 → 확정

전 조명 Movable 전환 후

Movable로 바꾸면 라이트맵을 아예 안 쓰고 전부 실시간(Lumen) 계산이 된다. 그 상태에서 노이즈가 사라졌으니 원인은 라이트맵 쪽이다. 앞서 본 공유 큐브 구조를 생각하면 놀랄 일은 아니었다.


곁가지 — 스테이셔너리 4개 한도와 Static 전환 실수

빌드 로그에 이 경고가 같이 나왔다.

1
Failed to allocate shadowmap channel for stationary light

Stationary 라이트는 절반만 굽는다. 직접광은 실시간, 그림자와 간접광은 라이트맵에 저장한다. 이때 그림자를 라이트맵의 별도 채널에 넣는데 텍셀당 4개까지만 있다. 한 지점에 스테이셔너리 라이트가 5개 이상 겹치면 5번째부터 채널을 못 받고 전부 동적 그림자로 폴백한다 — 굽는 의미가 없어지면서 비용만 올라간다.

겹침을 세어보니 RectLight 10개 + SpotLight 3개가 초과분이었다. 이걸 Static으로 바꿔서 경고를 0건으로 만들었는데, 이게 실수였다.

Static 라이트는 실시간 렌더 기여가 아예 없다. 라이트맵 안에만 존재하므로 라이트맵을 읽지 않는 액터는 그 빛을 못 받는다. 캐릭터와 운반 중인 가구는 Movable이라 그 아래를 지나가도 안 밝아진다. 조명이 꺼진 것처럼 보인다.

경고를 없애는 데만 집중하다가 게임플레이에 뭐가 걸리는지를 안 봤다. 이 프로젝트는 캐릭터가 실내를 계속 돌아다니는 게 핵심이라 특히 나쁜 선택이었다.

모빌리티직접광간접광무버블 액터를 비추나
Static베이크베이크
Stationary실시간베이크
Movable실시간실시간

결론 — 이 프로젝트는 굽지 않는다

원인 세 개를 다 고쳤는데도 노이즈가 남았고, 그걸 잡으려면 메시 구조를 갈아엎어야 하는 상황이었다. 그래서 라이트맵을 굽는 게 이 프로젝트에 실익이 있는지를 다시 봤다.

  • 툰 셰이딩이 라이팅 위를 덮는다. 포스트 프로세스 셀 셰이더가 밝기를 단계로 뭉개기 때문에, 12분 걸려 구운 부드러운 간접광 그라데이션이 화면에서는 몇 단계로 잘려 나간다. 구운 디테일 대부분이 안 보인다.
  • 시간대가 실시간으로 바뀐다. 낮→노을→밤 전환을 쓰는데 구운 조명은 한 시점 고정이다. 밤에도 낮의 간접광이 벽에 남는다.
  • 용량이 붙는다. 맵마다 _BuiltData 바이너리가 생긴다 (L_Level1 13MB, L_Lobby 8.4MB). .umap은 LFS 바이너리라 빌드할 때마다 통째로 새 버전이 쌓인다.

그래서 전 조명을 Movable로 통일했다.

조명개수모빌리티
RectLight35Movable
PointLight13Movable
SpotLight8Movable
DirectionalLight1Movable
SkyLight1Movable

미처리 라이트가 0이 돼서 뷰포트의 “LIGHTING NEEDS TO BE REBUILT” 경고도 안 뜬다. 실시간 비용은 Lumen이 이미 켜져 있어 체감 차이가 없었다.

최종 결과

베이크가 나쁜 기법이라는 게 아니라, 조건이 안 맞았다. 고정 시점 조명 + 사실적 렌더링 + 최적화된 메시 구조라면 반대 결론이 나온다. 하루 종일 파라미터를 돌린 끝에 “이 기능이 우리한테 필요한가”를 처음에 물었어야 했다는 걸 알았다.


정리

  • 라이트맵 UV는 LightMapCoordinateIndex가 가리키는 채널을 읽는다. Generate Lightmap UVs가 켜져 있어도 이 인덱스가 0이면 타일링용 UV를 라이트맵으로 쓰게 된다. 만드는 설정(DstLightmapIndex)과 읽는 설정이 별개다.
  • UV가 겹치면 서로 다른 면이 같은 텍셀을 공유해 조명 값이 섞인다. 이게 “벽이 통째로 탄” 것처럼 보이는 이유다.
  • LightingResults의 겹침 경고는 임계값 초과분만 출력한다. 경고 목록을 작업 목록으로 쓰면 절반을 놓친다.
  • IndirectLightingSmoothness는 낮출수록 선명 + 노이즈 증가. 노이즈가 문제면 올려야 한다.
  • 품질을 몇 배 올려도 그대로면 샘플 부족이 아니다. 파라미터 튜닝 대신 격리 실험으로 넘어가는 게 빠르다.
  • 라이트맵 UV 레이아웃은 메시 애셋 단위라 인스턴스별 보정이 안 된다. 하나의 큐브를 스케일만 바꿔 벽·기둥·천장에 다 쓰면 면적 비율이 깨진 채로 고정된다.
  • Stationary 라이트의 그림자 채널은 겹침 4개까지. 초과하면 동적 그림자로 폴백된다.
  • Static 라이트는 실시간 기여가 없어 캐릭터 같은 무버블 액터를 못 비춘다. 경고를 지우려고 Stationary → Static으로 바꾸면 조명이 꺼진 것처럼 보인다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.