작성일 :

보이지 않는 것은 그리지 않는다

Unity 렌더 파이프라인 (2) - 드로우콜과 배칭에서 드로우콜과 배칭으로 GPU에 렌더링 명령을 제출하는 비용을 줄이는 방법을 살펴보았습니다. 머티리얼을 공유하고, Static Batching과 SRP Batcher로 드로우콜을 합치거나 상태 변경을 줄이면 CPU가 GPU에 보내는 명령 수가 줄어들고, 렌더 스테이트 전환 비용도 감소합니다.

하지만 배칭이 아무리 효율적이어도, 화면에 보이지 않는 오브젝트까지 렌더링한다면 GPU는 결과가 화면에 반영되지 않는 작업에 시간을 소비합니다. 가장 빠른 드로우콜은 아예 호출하지 않는 드로우콜입니다.

렌더링 파이프라인에 진입하기 전에 화면에 보이지 않는 오브젝트를 걸러내는 과정이 컬링(Culling)입니다.


컬링은 렌더링 파이프라인의 가장 앞 단계에서 동작합니다. CPU가 씬의 모든 오브젝트를 대상으로 GPU에 보내야 하는지 판단하고, 필요 없는 오브젝트를 렌더링 대상에서 제외합니다. 씬에 오브젝트가 1,000개 있어도 카메라에 보이는 100개만 GPU에 제출하면, 나머지 900개에 대한 드로우콜, 정점 처리, 프래그먼트 처리가 모두 생략됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
컬링의 위치:

씬의 전체 오브젝트 (1,000개)
        │
        ▼
┌──────────────┐
│    컬링      │  ← 보이지 않는 오브젝트 제거
│  (Culling)   │
└──────┬───────┘
       │
       ▼
  렌더링 대상 (100개)
       │
       ▼
┌──────────────┐
│   배칭       │  ← 드로우콜/상태 변경 줄이기
│  (Batching)  │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│  GPU 렌더링  │
└──────────────┘


컬링으로 렌더링 대상을 먼저 줄이면 배칭이 처리해야 할 오브젝트 수도 줄어들고, GPU에 제출되는 전체 작업량이 감소합니다. 그래서 컬링이 배칭보다 앞에 위치합니다. Unity에서 사용되는 컬링 기법에는 Frustum Culling, Occlusion Culling, 레이어별 컬링 거리가 있습니다. 이와 함께 LOD와 텍스처 아틀라스도 렌더링 비용 절감에 기여합니다.


Frustum Culling

카메라가 볼 수 있는 공간은 무한하지 않습니다.

카메라에는 세 가지 설정이 있습니다.

시야각(Field of View)은 카메라가 한 번에 볼 수 있는 각도 범위를 결정합니다.

가까운 클리핑 평면(Near Clip Plane)은 카메라에 가장 가까운 렌더링 경계면으로 기본값은 0.3 단위입니다.

먼 클리핑 평면(Far Clip Plane)은 카메라에서 가장 먼 렌더링 경계면으로 기본값은 1,000 단위입니다.

이 세 값이 만들어내는 공간이 뷰 프러스텀(View Frustum)이며, 잘린 사각뿔 형태를 갖습니다. 뷰 프러스텀 안에 들어오는 오브젝트만 카메라에 보입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
뷰 프러스텀 (위에서 본 모습):

                          Far Clip Plane
     ┌───────────────────────────────────────────┐
      \                                         /
       \                                       /
        \                                     /
         \          카메라 시야              /
          \          (View Frustum)         /
           \                              /
            \                            /
             \                          /
              └────────────────────────┘
                    Near Clip Plane
                          ▲
                          │
                       카메라


Frustum Culling은 이 뷰 프러스텀 밖에 있는 오브젝트를 렌더링 대상에서 제외하는 기법입니다. Unity는 별도의 설정 없이 매 프레임 자동으로 Frustum Culling을 수행합니다.

바운딩 볼륨과 교차 검사

오브젝트가 프러스텀 안에 있는지 판단하려면 메쉬의 형태와 프러스텀을 비교해야 하는데, 정확한 메쉬 형태를 그대로 사용하면 비용이 큽니다. 5,000개의 삼각형으로 이루어진 메쉬 하나를 프러스텀의 6개 평면과 삼각형 단위로 비교하려면, 한 오브젝트당 수만 회의 교차 연산이 필요합니다. 그래서 메쉬 대신, 오브젝트를 감싸는 단순한 도형으로 교차 검사를 수행합니다.

Unity가 사용하는 도형은 AABB(Axis-Aligned Bounding Box)입니다. AABB는 오브젝트를 감싸는 가장 작은 직육면체로, 각 변이 월드 좌표축(x, y, z)에 평행합니다. “축 정렬”은 직육면체가 회전 없이 항상 좌표축 방향을 유지한다는 뜻입니다. 변이 항상 축에 평행하므로 회전 정보가 필요 없고, 각 축의 최솟값과 최댓값만으로 형태가 결정됩니다.

예를 들어 어떤 오브젝트의 AABB가 x축으로 2~5, y축으로 1~4, z축으로 3~7 범위를 차지한다면, 이 여섯 개의 숫자만으로 직육면체의 형태와 위치가 결정됩니다.

프러스텀과의 교차 검사도 각 축의 범위가 겹치는지 비교하면 되므로 연산이 단순합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
AABB (Axis-Aligned Bounding Box)

                y
                ↑
          (5,4,7)
          ┌───────────────┐
         /│              /│
        / │             / │
       /  │            /  │
      ┌───────────────┐   │
      │   │  오브젝트  │   │
      │   │   (메쉬)   │   │
      │   └───────────│───┘ → x
      │  /            │  /
      │ /             │ /
      │/              │/
      └───────────────┘
    (2,1,3)         /
                   z

  AABB 범위: x = 2~5, y = 1~4, z = 3~7
  각 변이 좌표축에 평행 → 여섯 개의 숫자로 형태 결정


뷰 프러스텀은 6개의 평면(상, 하, 좌, 우, Near, Far)으로 둘러싸인 공간입니다.

AABB가 프러스텀의 6개 평면 모두의 안쪽에 있으면 프러스텀 내부로 판정합니다. AABB 전체가 프러스텀의 6개 평면 중 하나라도 완전히 바깥에 있으면 프러스텀 외부로 판정하여 제외합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
프러스텀과 AABB의 교차 검사 (위에서 본 모습)

     ┌───────────────────────────────────────────┐  Far
      \                                         /
       \   ┌───┐                               /
        \  │ A │                               /
         \ └───┘                              /
          \                                  /
           \                                /
            \              ┌─────┐         /
             \             │     │        /
              └────────────│─ B ─│───────┘  Near
                           │     │                    ┌───┐
                       ▲   └─────┘                    │ C │
                       │                              └───┘
                    카메라

  A: AABB 전체가 프러스텀 안에 포함       → 렌더링
  B: AABB가 프러스텀 경계에 걸침          → 렌더링 (보수적 판정)
  C: AABB 전체가 프러스텀 밖에 위치       → 제거


AABB가 프러스텀 경계에 걸치는 경우(B)에는 보수적으로 렌더링 대상에 포함합니다. 실제로는 메쉬의 일부만 보일 수 있지만, AABB 단위로는 이를 정확히 구분할 수 없기 때문입니다. 이 보수적 판정 덕분에 보여야 할 오브젝트가 누락되는 일은 발생하지 않습니다.


Frustum Culling의 비용과 효과

AABB와 평면의 교차 검사는 연산량이 작습니다. 하나의 오브젝트에 대해 6개 평면과의 비교 연산만 수행하므로, 오브젝트 1,000개를 검사해도 CPU에서 수십 마이크로초(µs) 수준에 끝납니다. 이 작은 비용으로 프러스텀 밖의 오브젝트 전부를 렌더링에서 제외할 수 있으므로, 비용 대비 효과가 큽니다.

특히, 카메라가 씬의 일부만 바라보는 일반적인 상황에서 Frustum Culling은 렌더링 대상을 크게 줄여줍니다. 넓은 오픈 월드에서 카메라가 한 방향을 바라보면, 뒤쪽과 좌우 먼 곳의 오브젝트가 모두 제거됩니다.

하지만 프러스텀 안에 있으면서도, 다른 오브젝트에 가려져 실제로는 보이지 않는 오브젝트까지 제거하지는 못합니다. 이 한계를 보완하는 것이 Occlusion Culling입니다.


Occlusion Culling

Frustum Culling은 카메라 시야 밖의 오브젝트를 제거하지만, 시야 안에 있어도 다른 오브젝트에 의해 완전히 가려진(occluded) 오브젝트까지 걸러내지는 못합니다. 건물 뒤에 있는 가구, 벽 뒤의 방, 산 뒤편의 마을이 대표적인 예입니다. 이런 오브젝트들은 프러스텀 안에 있으므로 Frustum Culling을 통과하지만, 화면에는 한 픽셀도 나타나지 않습니다.

Occlusion Culling은 프러스텀 안에 있으면서 다른 오브젝트에 의해 가려진 오브젝트를 렌더링 대상에서 제외하는 기법입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Occlusion Culling 개념:

          카메라
            │
            ▼
    ┌───────────────┐
    │               │
    │    건물 (벽)  │  ← Occluder (가리는 오브젝트)
    │               │
    └───────────────┘
            │
            │  가려진 영역
            ▼
    ┌───────────────┐
    │    가구       │  ← Occludee (가려진 오브젝트)
    │    (테이블)   │     → Frustum 안에 있지만 보이지 않음
    └───────────────┘     → Occlusion Culling으로 제거

Unity의 Occlusion Culling: 베이크 방식

Occlusion Culling은 Frustum Culling과 달리 런타임에 실시간으로 계산하기 어렵습니다. “오브젝트 A가 오브젝트 B를 완전히 가리는가”를 모든 오브젝트 쌍에 대해 매 프레임 계산하면, 오브젝트가 N개일 때 비교 횟수가 N × (N-1), 즉 N²에 비례하여 증가합니다. 오브젝트가 100개이면 약 10,000번, 1,000개이면 약 1,000,000번입니다.

컬링으로 절약하는 비용보다 컬링 연산 자체의 비용이 더 커질 수 있습니다.

Unity는 이 문제를 베이크(Bake) 방식으로 해결합니다. 에디터에서 미리 씬을 분석하여 각 위치에서 어떤 오브젝트가 보이는지를 계산해 두고, 런타임에는 이 결과를 조회만 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Occlusion Culling 베이크 과정:

1. 씬을 셀(Cell)로 분할
┌─────┬─────┬─────┬─────┐
│ C00 │ C01 │ C02 │ C03 │
├─────┼─────┼─────┼─────┤
│ C10 │ C11 │ C12 │ C13 │
├─────┼─────┼─────┼─────┤
│ C20 │ C21 │ C22 │ C23 │
├─────┼─────┼─────┼─────┤
│ C30 │ C31 │ C32 │ C33 │
└─────┴─────┴─────┴─────┘

2. 각 셀에서 보이는 오브젝트 목록 계산
   C11에서 → {건물A, 나무B, 도로C, ...}
   C12에서 → {건물A, 울타리D, ...}
   C21에서 → {나무B, 도로C, 건물E, ...}

3. 결과를 바이너리 데이터로 저장


베이크 과정의 첫 단계는 씬을 3D 격자(Cell)로 분할하는 것입니다. 3D 격자는 씬 공간을 가로, 세로, 높이 방향으로 균등하게 나눈 직육면체 블록의 집합이며, 각 셀은 카메라가 위치할 수 있는 공간의 한 단위입니다.

베이크 과정에서 각 셀에 카메라를 놓고 전 방향으로 가시성 검사를 수행합니다.

이때 기록되는 것은 건물이나 벽 같은 정적 오브젝트(Occluder)가 어디서 시야를 막는가라는 정보입니다. Occluder의 위치와 형태가 베이크 데이터에 저장되며, 이 정보는 런타임에도 변하지 않습니다. Occluder가 시야를 막으면, 그 뒤의 오브젝트는 해당 셀의 가시 목록에서 제외됩니다.

런타임 동작

런타임에는 카메라의 현재 위치가 어느 셀에 해당하는지를 찾고, 그 셀의 가시 목록을 조회합니다. 가시 목록에 없는 오브젝트는 렌더링 대상에서 제외됩니다.

1
2
3
4
5
6
7
8
9
10
11
런타임 Occlusion Culling 조회:

카메라 위치 → 셀 C11에 해당
                  │
                  ▼
         C11의 가시 목록 조회
         {건물A, 나무B, 도로C}
                  │
                  ▼
         가시 목록에 없는 오브젝트 → 렌더링 제외
         (건물E, 울타리D, 가구F 등)


이 조회는 씬 로드 시 RAM에 올라온 베이크 데이터를 읽는 것이므로, 런타임 비용이 작습니다. 매 프레임 교차 검사를 수행하는 대신, 베이크 시점에 한 번 계산한 결과를 반복 사용하는 구조입니다.

베이크 방식의 장점과 제약

베이크 방식은 런타임 비용이 작습니다. 셀 조회와 목록 비교만 수행하므로, 오브젝트 수가 많아도 CPU 비용이 크게 늘지 않으며, 실시간 Occlusion Culling에 비해 프레임 예산을 거의 소모하지 않습니다.


하지만 제약도 있습니다.

첫째는 베이크 시간입니다. 씬이 크고 복잡할수록 베이크에 오래 걸립니다. 셀 수가 많고 오브젝트가 많을수록 각 셀에서의 가시성 계산량이 증가하며, 대규모 씬에서는 수십 분 이상 소요되기도 합니다.

둘째, 정적 오브젝트만 Occluder로 사용 가능합니다. 앞서 설명한 것처럼 Occluder의 위치와 형태가 베이크 데이터에 기록되어 있으므로, 런타임에 위치가 바뀌는 동적 오브젝트(캐릭터, 이동하는 플랫폼 등)는 Occluder로 사용할 수 없습니다. 반면, 동적 오브젝트를 Occludee(가려지는 역할)로 등록하는 것은 가능합니다. Occluder의 위치가 고정되어 있으므로, 런타임에 동적 오브젝트의 현재 위치가 Occluder 뒤에 있는지를 매 프레임 검사할 수 있기 때문입니다.

셋째, 메모리 사용입니다. 베이크된 가시성 데이터는 씬과 함께 저장되며, 런타임에 메모리에 로드됩니다. 셀이 세밀할수록, 오브젝트가 많을수록 데이터 크기가 커집니다. 모바일처럼 가용 메모리가 제한된 환경에서는 셀 크기를 지나치게 줄이지 않도록 주의해야 합니다.


Occlusion Culling이 효과적인 씬

Occlusion Culling의 효과는 씬의 구조에 따라 크게 달라집니다. 벽과 구조물이 시야를 많이 차단하는 씬에서 효과가 큽니다. 건물 내부에서 카메라가 방 하나를 바라보면 벽 뒤 다른 방의 오브젝트가 모두 제거되고, 도시에서 골목을 바라보면 건물 뒤편의 오브젝트가 모두 제거됩니다.

반면, 평원이나 사막처럼 탁 트인 야외 환경에서는 효과가 작습니다. 시야를 차단하는 큰 구조물이 없으므로, 프러스텀 안의 오브젝트 대부분이 실제로 보이고 Occlusion Culling으로 제거할 대상이 적기 때문입니다.

대형 차단물이 많은 실내나 도시 씬일수록 Occlusion Culling의 효과가 커집니다.


레이어별 컬링 거리

Frustum Culling과 Occlusion Culling은 오브젝트의 위치(프러스텀 안/밖, 가려짐/보임)를 기준으로 컬링합니다. 하지만 위치만으로는 해결되지 않는 경우가 있습니다. 풀 한 포기, 작은 돌, 파티클 이펙트 같은 오브젝트가 카메라에서 200m 떨어져 있으면, 프러스텀 안에 있고 가려지지도 않았지만 화면에서 한두 픽셀 크기로 나타납니다. 렌더링해도 GPU 비용만 소모되고 시각적 효과는 거의 없습니다.

레이어별 컬링 거리(Per-Layer Culling Distance)는 이런 오브젝트를 거리 기준으로 걸러냅니다. Unity에서는 각 오브젝트를 32개 레이어 중 하나에 직접 지정할 수 있으므로, 풀은 Grass 레이어, 소품은 SmallProps 레이어처럼 종류별로 분류한 뒤 Camera.layerCullDistances 배열로 레이어마다 다른 컬링 거리를 지정하면, 그 거리 밖의 오브젝트는 렌더링에서 제외됩니다. 풀 레이어는 30m, 소품 레이어는 80m처럼 작고 덜 중요한 오브젝트일수록 짧은 거리를 지정하여 드로우콜과 정점 처리 비용을 줄일 수 있습니다.

컬링 거리 측정 방식

Camera.layerCullDistances는 32개 요소의 float 배열입니다. 각 인덱스가 해당 레이어 번호에 대응하며, 컬링 거리를 0으로 설정한 레이어는 카메라의 Far Clip Plane을 그대로 사용합니다.

기본적으로 컬링 거리는 카메라 전방 축(z축)에 대한 투영 거리로 측정됩니다.

투영 거리란 카메라에서 오브젝트까지의 직선 거리가 아니라, 그 직선 거리에서 카메라 전방 방향 성분만 취한 값입니다. 화면 중앙의 오브젝트는 직선 거리와 투영 거리가 거의 같지만, 화면 가장자리의 오브젝트는 카메라 전방과의 각도가 크므로 투영 거리가 짧아집니다. 그래서 실제로는 멀리 있어도 컬링 경계 안에 남게 됩니다.

이 방식에는 문제가 있습니다. 오브젝트의 실제 위치는 변하지 않아도, 카메라를 회전하면 그 오브젝트가 화면 중앙에서 가장자리로 이동하면서 투영 거리가 달라집니다. 화면 중앙에 있을 때는 컬링 거리를 넘어 제거되던 오브젝트가, 가장자리로 이동하면 투영 거리가 짧아져 컬링 경계 안으로 들어옵니다. 그 결과, 카메라를 회전하는 것만으로 오브젝트가 갑자기 나타나거나 사라지는 현상이 발생합니다.

Camera.layerCullSpherical을 true로 설정하면 이 문제를 해결할 수 있습니다. 투영 거리 대신 카메라로부터의 직선 거리(반경)로 컬링하므로, 카메라를 회전해도 오브젝트의 컬링 여부가 바뀌지 않습니다.


효과와 주의점

레이어별 컬링 거리는 적용이 간단하면서 드로우콜 감소 효과가 큽니다. 풀, 작은 돌, 파티클처럼 수가 많고 크기가 작은 오브젝트를 먼 거리에서 일괄 제거하면, 수백 개의 드로우콜이 한 번에 줄어듭니다.

다만 컬링 거리를 너무 짧게 설정하면, 플레이어가 이동하여 컬링 경계를 넘는 순간 오브젝트가 갑자기 나타나는 팝인(Pop-in) 현상이 눈에 띕니다.

앞서 설명한 투영 거리 문제는 카메라 회전만으로 발생하며 layerCullSpherical로 해결할 수 있지만, 팝인은 컬링 거리를 사용하는 한 항상 존재하는 트레이드오프입니다. 풀이 30m 밖에서 갑자기 사라지면 시각적으로 어색하지만, 100m 밖에서 사라지면 대부분의 플레이어가 인지하지 못합니다.

오브젝트가 작을수록 짧은 컬링 거리에서도 팝인이 눈에 띄지 않으므로, 오브젝트 크기에 맞는 거리를 설정하는 것이 중요합니다.


LOD 시스템 실전

렌더링 기초 (1) - 메쉬의 구조에서 LOD(Level of Detail)의 개념을 살펴보았습니다. LOD는 카메라와 오브젝트 사이의 거리에 따라 메쉬의 복잡도를 단계적으로 줄여 정점 처리 비용을 절약하는 기법입니다.

컬링이 오브젝트 자체를 렌더링 대상에서 제외하는 것이라면, LOD는 렌더링 대상에 남아 있는 오브젝트의 정점 수를 줄여 GPU 부담을 낮춥니다.

LODGroup 컴포넌트

Unity에서 LOD를 적용하려면 오브젝트에 LODGroup 컴포넌트를 추가합니다. LODGroup은 여러 LOD 단계(LOD 0, LOD 1, LOD 2, …)를 하나의 컴포넌트에서 관리하며, 각 단계에 서로 다른 복잡도의 메쉬를 등록합니다. LOD 0이 가장 정밀한 원본 메쉬이고, 숫자가 올라갈수록 삼각형 수가 줄어든 단순한 메쉬입니다.

각 LOD 단계의 전환 시점은 카메라와의 절대 거리가 아니라 화면 점유 비율(Screen Relative Height)로 결정됩니다. 오브젝트의 바운딩 볼륨이 화면 높이의 몇 퍼센트를 차지하는지를 기준으로, 비율이 줄어들 때 다음 LOD 단계로 전환합니다.

절대 거리 대신 화면 점유 비율을 사용하면, 오브젝트마다 별도의 거리 기준을 설정하지 않아도 크기에 따라 전환 시점이 자연스럽게 달라집니다. 큰 건물은 100m 떨어져 있어도 화면에서 크게 보이므로 LOD 0을 유지하고, 작은 돌은 20m만 떨어져도 화면에서 작게 보이므로 LOD 2로 전환됩니다.

같은 거리라도 오브젝트 크기에 따라 적절한 단계가 자동으로 선택되는 구조입니다.

LOD 전환과 Cross-Fade

LOD 단계가 바뀔 때 메쉬가 순간적으로 교체되면, 삼각형 수 차이로 인해 실루엣이 급변하는 팝핑(Popping) 현상이 발생합니다. 이를 완화하기 위해 Unity의 LODGroup은 Fade Mode 설정을 제공합니다. 세 가지 옵션이 있습니다.

None은 전환 시점에서 메쉬를 즉시 교체합니다. 추가 비용이 없지만, 팝핑이 발생합니다.

Cross Fade는 전환 시점 전후로 이전 LOD와 다음 LOD를 동시에 렌더링하면서 투명도를 전환합니다. 이전 LOD가 점점 투명해지고 다음 LOD가 점점 불투명해지므로 전환 자체는 부드럽습니다. 하지만 전환 구간에서 두 메쉬를 동시에 그리므로 오버드로우가 2배가 됩니다. 드로우콜도 전환 구간에서 일시적으로 증가합니다.

SpeedTree는 나무와 식생 모델 생성 미들웨어인 SpeedTree 에셋 전용 모드입니다. Cross Fade와 유사하게 전환 구간에서 블렌딩을 수행하지만, 나뭇잎이나 가지처럼 복잡한 실루엣의 식생에 맞게 조정된 방식을 사용합니다.


디더링 기반 Cross-Fade

Cross Fade의 오버드로우 문제를 줄이는 방법으로 디더링(Dithering) 기반 Cross-Fade가 있습니다. LODFadeMode.CrossFade와 함께 셰이더에서 디더링 패턴을 사용하면, 두 LOD를 동시에 완전히 그리는 대신 픽셀 단위로 교차하여 그립니다.

1
2
3
4
5
6
7
8
9
10
11
디더링 기반 Cross-Fade (전환 50% 시점):

일반 Cross Fade (투명도 블렌딩):
  LOD 0: ████████████████████  (전체 픽셀, 50% 투명)
  LOD 1: ████████████████████  (전체 픽셀, 50% 투명)
  → 모든 픽셀을 두 번 처리

디더링 Cross Fade:
  LOD 0: █ █ █ █ █ █ █ █ █ █  (체크무늬 패턴의 절반 픽셀)
  LOD 1:  █ █ █ █ █ █ █ █ █   (나머지 절반 픽셀)
  → 각 픽셀을 한 번만 처리


디더링 방식에서는 전환이 일어나는 인접한 두 LOD 단계(예: LOD 0 → LOD 1, LOD 1 → LOD 2) 중 각 픽셀을 한쪽만 담당합니다.

전환이 진행될수록 이전 단계가 담당하는 픽셀이 줄어들고 다음 단계의 픽셀이 늘어납니다. 각 픽셀이 한 번만 기록되므로, 일반 Cross Fade에서 발생하는 오버드로우가 없습니다.

다만 셰이더 실행 비용은 줄어들지 않습니다. GPU는 양쪽 LOD의 프래그먼트 셰이더를 모든 픽셀에 대해 실행한 뒤, 디더링 패턴에 해당하지 않는 픽셀을 clip() 함수로 폐기합니다.

폐기는 셰이더 실행 이후에 일어나므로, 연산 자체는 이미 수행된 상태입니다. 드로우콜도 양쪽 LOD 각각 한 번씩, 총 2회 발생합니다.

정리하면, 디더링 기반 Cross-Fade는 오버드로우를 제거하지만, 셰이더 실행 비용과 드로우콜은 일반 Cross Fade와 동일합니다. 전환 구간에서 디더링 패턴이 미세하게 보일 수 있다는 시각적 트레이드오프도 있습니다.


모바일 환경에서는 GPU가 초당 프레임버퍼에 기록할 수 있는 픽셀 수인 필레이트(Fill Rate)가 제한적입니다.

모바일 GPU는 데스크톱 대비 필레이트가 낮아 오버드로우의 영향이 더 크므로, 오버드로우를 유발하는 일반 Cross Fade보다 디더링 기반 Cross-Fade가 더 적합합니다.

디더링 패턴이 눈에 띄는 것을 감수하더라도 성능을 확보할지, 일반 Cross Fade로 부드러운 전환을 유지할지는 대상 기기의 필레이트 여유와 프로젝트의 아트 스타일에 따라 결정합니다.

모바일에서의 LOD 설계

모바일에서는 GPU 성능과 메모리가 제한적입니다. LOD 단계가 많으면 그만큼 메쉬 에셋이 늘어나 메모리 부담이 커지고, 화면에서 충분히 작아질 때까지 고폴리곤 메쉬를 유지하도록 설정하면 GPU에 부담이 됩니다.

그래서 단계 수는 적게, 전환은 일찍 일어나도록 보수적으로 설정합니다.

1
2
3
4
5
6
7
8
9
10
11
모바일 LOD 구성 예시:

  화면 점유 비율
  100%          25%         10%          3%          0%
    ├────────────┼───────────┼────────────┼───────────┤
    │   LOD 0    │   LOD 1   │   LOD 2    │  Culled   │
    │  (100%)    │   (40%)   │   (15%)    │   (0%)    │
    ├────────────┼───────────┼────────────┼───────────┤
    가까움 ─────────────────────────────────────▶ 멀어짐

  괄호 안의 값은 LOD 0 대비 삼각형 수 비율


LOD 단계 사이의 삼각형 감소율이 클수록 정점 처리 비용 절약이 크지만, 전환 시 실루엣 차이가 커져 팝핑이 눈에 띄기 쉽습니다. 위 다이어그램에서 LOD 1의 삼각형 수는 LOD 0의 40%이므로, 정점 처리 비용은 60% 줄어들면서도 실루엣 변화는 비교적 완만합니다.


마지막 Culled 단계는 오브젝트를 아예 렌더링하지 않는 단계입니다. 화면에서 3% 미만을 차지하는 오브젝트는 대부분 눈에 띄지 않으므로 제거해도 시각적 차이가 작습니다. 다만, 랜드마크 건물이나 주요 지형처럼 멀리서도 존재감이 필요한 오브젝트는 Culled 대신 LOD 2를 유지하는 편이 자연스럽습니다.


텍스처 아틀라스

앞의 절들에서 다룬 컬링과 LOD는 렌더링 대상 오브젝트의 수나 복잡도를 줄이는 방법입니다.

텍스처 아틀라스는 렌더링 대상을 줄이는 대신, 여러 오브젝트가 같은 머티리얼을 공유하도록 만들어 배칭이 가능하게 하는 방법입니다.

Unity 렌더 파이프라인 (2) - 드로우콜과 배칭에서 같은 머티리얼을 사용하는 오브젝트들이 배칭 대상이 된다고 설명했습니다.

머티리얼이 같으려면 셰이더와 텍스처가 모두 같아야 합니다. 나무, 풀, 돌, 울타리가 각각 다른 텍스처를 쓰면 머티리얼이 4개가 되어 배칭이 불가능합니다.

텍스처 아틀라스는 이 여러 텍스처를 하나의 큰 텍스처에 합쳐서 하나의 머티리얼로 통일합니다.

나무, 풀, 돌, 울타리의 텍스처를 하나의 아틀라스에 합치면 네 오브젝트 모두 같은 텍스처(아틀라스)를 사용하게 됩니다. 여기에 셰이더까지 같으면 머티리얼이 동일해지므로 배칭 대상이 되고, 드로우콜이 4회에서 1회로 줄어듭니다.

UV 좌표 조정

아틀라스를 사용하면 각 오브젝트의 UV 좌표를 아틀라스 내의 해당 영역에 맞게 조정해야 합니다. UV 좌표는 메쉬의 각 정점이 텍스처의 어느 위치를 참조할지를 지정하는 2D 좌표입니다.

개별 텍스처를 사용할 때는 UV가 (0, 0)에서 (1, 1)까지 전체 텍스처를 참조하지만, 아틀라스에서는 해당 서브 텍스처가 위치한 영역만 참조해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
UV 좌표 조정:

개별 텍스처: UV (0,0) ~ (1,1) → 텍스처 전체를 참조

아틀라스 (1장의 큰 텍스처):
(0, 1.0)                    (1.0, 1.0)
  ┌───────────┬───────────┐
  │           │           │
  │   나무    │    풀     │
  │           │           │
  ├───────────┼───────────┤
  │           │           │
  │    돌     │  울타리   │
  │           │           │
  └───────────┴───────────┘
(0, 0)                      (1.0, 0)

아틀라스 적용 후 각 오브젝트의 UV 범위:
  나무   → (0, 0.5) ~ (0.5, 1.0)     왼쪽 위
  풀     → (0.5, 0.5) ~ (1.0, 1.0)   오른쪽 위
  돌     → (0, 0) ~ (0.5, 0.5)       왼쪽 아래
  울타리 → (0.5, 0) ~ (1.0, 0.5)     오른쪽 아래


나무 오브젝트의 UV를 (0, 0)~(1, 1)에서 (0, 0.5)~(0.5, 1.0)으로 변환하면, 아틀라스에서 나무 텍스처가 위치한 왼쪽 위 영역만 참조하게 됩니다. Unity의 Sprite Atlas나 외부 도구(TexturePacker 등)가 텍스처 배치와 UV 변환을 자동으로 처리하므로, 개발자가 좌표를 직접 계산할 필요는 없습니다.

2D와 3D에서의 아틀라스

Unity에서 텍스처 아틀라스는 2D와 3D 양쪽에서 활용됩니다.

Sprite Atlas (2D) 방식에서는 Unity의 2D 시스템이 제공하는 Sprite Atlas 기능으로 여러 스프라이트를 하나의 아틀라스에 합칩니다. 주로 UI 아이콘, 2D 캐릭터의 스프라이트 시트, 타일맵 등에 사용됩니다.

Unity 에디터에서 Sprite Atlas 에셋을 생성하고 포함할 스프라이트를 지정하면 빌드 시 자동으로 아틀라스가 생성됩니다. 같은 Sprite Atlas에 포함된 스프라이트들은 같은 아틀라스 텍스처를 참조하게 되므로, 셰이더까지 동일하면 같은 머티리얼로 배칭됩니다.

Texture Atlas (3D) 방식에서는 3D 모델링 도구(Blender, Maya 등)에서 여러 오브젝트의 텍스처를 하나의 아틀라스에 베이크합니다. 3D 메쉬는 오브젝트 표면을 감싸는 복잡한 UV 레이아웃을 가지고 있어서, 아틀라스에 맞게 UV를 재배치하는 작업이 필요합니다. Unity에는 3D 메쉬의 텍스처와 UV를 자동으로 합쳐주는 기능이 없으므로, 모델링 단계에서 처리합니다.

예를 들어, 씬에서 자주 함께 등장하는 나무, 풀, 돌의 텍스처를 하나의 아틀라스로 합치면 모두 같은 텍스처를 참조하게 됩니다. 여기에 셰이더까지 동일하면 같은 머티리얼로 배칭할 수 있습니다.


아틀라스의 크기와 제약

아틀라스 텍스처는 크기가 커질수록 메모리 사용량이 증가합니다. 모바일에서는 일반적으로 1024x1024 또는 2048x2048을 사용하며, 4096x4096도 가능하지만 메모리 부담이 큽니다.

아틀라스에 넣을 수 있는 서브 텍스처의 수는 아틀라스 크기와 각 서브 텍스처의 크기에 따라 달라집니다. 예를 들어, 2048x2048 아틀라스에 256x256 서브 텍스처를 넣으면 최대 64개(8x8)가 들어갑니다.

서브 텍스처 사이에 패딩(Padding)을 두면 실제 수는 줄어듭니다. 패딩은 텍스처 샘플링 시 인접한 서브 텍스처의 색상이 경계를 넘어 번지는 현상(블리딩, Bleeding)을 방지합니다.


아틀라스가 너무 크면 메모리 낭비로 이어질 수 있습니다.

GPU는 텍스처를 통째로 메모리에 로드하므로, 아틀라스 안의 서브 텍스처 중 일부만 사용되더라도 전체 아틀라스가 메모리에 올라갑니다. 그래서 씬에서 함께 등장하는 오브젝트들의 텍스처만 같은 아틀라스에 묶는 것이 메모리 효율과 배칭 효과 양쪽에 유리합니다.


컬링 기법의 종합 비교

지금까지 다룬 기법들은 파이프라인의 서로 다른 지점에서 렌더링 비용을 줄입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
기법 비교:

┌──────────────────────┬─────────────────────┬────────────────┬─────────────────┐
│        기법          │        역할         │   동작 시점    │    추가 비용    │
├──────────────────────┼─────────────────────┼────────────────┼─────────────────┤
│ Frustum Culling      │ 프러스텀 밖 제거    │ 매 프레임 자동 │ 거의 없음       │
│ Occlusion Culling    │ 가려진 오브젝트 제거│ 베이크 + 조회  │ 베이크 시간,    │
│                      │                     │                │ 메모리          │
│ 레이어별 컬링 거리   │ 먼 거리 소형 제거   │ 매 프레임 자동 │ 거의 없음       │
│ LOD                  │ 거리에 따라 메쉬    │ 매 프레임 자동 │ LOD 메쉬 에셋   │
│                      │ 복잡도 감소/제거    │                │                 │
│ 텍스처 아틀라스      │ 머티리얼 통일로     │ 사전 준비      │ UV 조정,        │
│                      │ 배칭 가능하게 함    │                │ 아틀라스 메모리 │
└──────────────────────┴─────────────────────┴────────────────┴─────────────────┘


이 기법들은 서로 배타적이지 않고 단계적으로 적용됩니다. Frustum Culling이 프러스텀 밖의 오브젝트를 제거하고, Occlusion Culling이 가려진 오브젝트를 추가로 제거합니다. 레이어별 컬링 거리가 먼 거리의 소형 오브젝트를 걸러내고, LOD가 남은 오브젝트의 삼각형 수를 줄입니다. 텍스처 아틀라스는 머티리얼을 통일하여 배칭을 가능하게 합니다.

각 단계에서 렌더링 부담을 줄여, 최종적으로 GPU에 제출되는 작업량을 최소화하는 구조입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
기법들의 적용 순서 예시:

  씬의 전체 오브젝트 (1,000개)
        │
  Frustum Culling       → 프러스텀 밖 700개 제거       → 남은 300개
        │
  Occlusion Culling     → 가려진 100개 제거            → 남은 200개
        │
  레이어별 컬링 거리    → 먼 거리 소형 50개 제거       → 남은 150개
        │
  LOD                   → 삼각형 수 감소 + Culled 30개  → 남은 120개
        │
  텍스처 아틀라스       → 머티리얼 통일 → 배칭 가능
        │
        ▼
  GPU에 제출 (120개, 드로우콜 최소화)

마무리

  • Frustum Culling은 카메라의 절두체 밖에 있는 오브젝트를 GPU에 제출하지 않으며, Unity가 매 프레임 자동으로 수행합니다.
  • Occlusion Culling은 에디터에서 정적 Occluder의 가시성을 베이크한 뒤, 런타임에 Occludee가 가려져 있는지를 테이블 조회로 판정합니다.
  • Per-Layer Culling Distance는 레이어별로 컬링 거리를 다르게 설정하여, 멀리 있는 작은 오브젝트를 먼저 제거합니다.
  • LOD는 화면 점유 비율에 따라 메쉬의 폴리곤 수를 단계적으로 줄여 GPU 부하를 낮춥니다.
  • 텍스처 아틀라스는 여러 텍스처를 하나로 합쳐 동일 머티리얼 조건을 만들어, 배칭이 가능하게 합니다.

각 기법은 서로 다른 기준으로 GPU에 제출되는 작업량을 줄이며, 단계적으로 적용되어 최종 드로우콜 수를 최소화합니다.


UnityPipeline 시리즈에서는 렌더 파이프라인의 구조(Part 1), 드로우콜과 배칭(Part 2), 컬링과 LOD(Part 3)를 통해 GPU 쪽 렌더링 비용을 줄이는 방법을 살펴보았습니다.

GPU 쪽 비용이 정리되면, 다음 병목은 매 프레임 실행되는 C# 스크립트, Unity API 호출, 가비지 컬렉션 같은 CPU 쪽에서 발생합니다. 스크립트 최적화 시리즈에서 이 CPU 비용을 줄이는 방법을 다룹니다.


관련 글

시리즈

Tags: LOD, Unity, 모바일, 최적화, 컬링

Categories: ,