빛을 어디에 저장할 것인가: Texel, Voxel, Probe

표면의 UV, 공간의 셀, 공간의 대표 지점에 빛을 저장할 때 생기는 차이.

Series 빛을 어디에 저장할 것인가 — Real-Time GI와 Light Caching 2 / 7

1편에서는 간접광(indirect lighting)을 매 픽셀(pixel)에서 처음부터 계산하는 대신, 대표 샘플을 만들고 재사용할 수 있다는 아이디어를 살펴봤다. 이제 남은 질문은 저장 위치다.

같은 방을 렌더링해도 벽을 UV로 펼쳐 빛을 저장할 수 있고, 방의 공간을 작은 셀로 나눌 수도 있다. 몇몇 위치에만 조명 샘플을 놓는 방법도 있다. 이 선택은 메모리뿐 아니라 서로 어떤 조명을 공유할 수 있는가를 바꾼다.

저장 위치와 저장 내용은 서로 다른 축이다

우선 용어부터 분리하자.

샘플 무엇을 나누거나 대표하는가? 조명 캐시로 쓸 때의 예
Pixel 최종 화면의 작은 영역 화면의 간접광 history
Texel 텍스처의 작은 영역 Lightmap의 표면 조명(surface lighting)
Voxel 3차원 공간의 작은 셀 점유율(occupancy)과 방향별 방사휘도(radiance)
Probe 선택한 위치에서의 측정 주변 방향에 대한 조명 분포
Surfel 표면의 작은 영역 법선(normal)을 가진 표면 조명 샘플

Texel이라고 반드시 albedo를 저장하는 것은 아니다. Probe라고 반드시 SH를 사용하는 것도 아니다. 샘플을 놓는 영역과 그 샘플 안의 데이터 형식은 별개다.

이 시리즈에서는 다음 도식을 기준으로 사용한다. 분류 기준은 캐시 샘플이 대표하는 영역이다.

Lighting samples
    |
    +-- Screen domain ------ Pixel history
    |
    +-- Surface domain ----- UV texel / Surfel
    |
    +-- Volume domain ------ Voxel cell / Spatial probe

이는 역사적인 발전 순서가 아니다. Voxel과 probe는 같은 공간 영역을 다루지만, 셀 단위 표현과 지점 단위 측정이라는 차이가 있다. Surfel의 위치도 월드 공간(world space)으로 표현된다. 월드 공간은 좌표계(coordinate system)이고, 표면 영역(surface domain)과 공간 부피 영역(volume domain)은 샘플이 대표하는 영역이다.

텍셀(texel): 표면을 UV로 펼쳐 저장한다

라이트맵(lightmap)은 표면과 조명 텍스처 사이의 대응 관계를 만든다.

3D mesh surface         Lightmap atlas

  +---------+   UV      +---+---+---+---+
  |   x     | --------> |   | E |   |   |
  +---------+           +---+---+---+---+
                        |   |   |   |   |
                        +---+---+---+---+

표면점의 lightmap UV가 $(u,v)$라면 런타임에는 해당 위치를 샘플링한다. 확산 조명의 복사조도(diffuse irradiance)를 저장한 단순한 경우는 다음과 같다.

$$ E_{\mathrm{ind}}(x,n)\approx T_{\mathrm{light}}(u(x),v(x)) $$

정확히 무엇을 텍스처에 넣는지는 엔진과 모드에 따라 다르다. RGB irradiance일 수도 있고, 방향성 조명 표현이나 직접광까지 포함한 데이터일 수도 있다. Texture space에 저장한다는 사실이 bake 시점까지 결정하지는 않는다. 여기서는 가장 익숙한 정적 베이크 라이트맵(baked lightmap)을 대표 사례로 생각하자.

정적인 방에서는 많은 샘플로 미리 계산하고, 게임 실행 중에는 텍스처 조회(texture lookup)로 읽을 수 있다. 카메라가 움직여도 벽의 UV는 같으므로 같은 표면 정보를 쉽게 재사용한다.

대신 서로 다른 표면 조명이 같은 texel을 공유하지 않도록 UV 차트(UV chart)를 배치해야 한다. Chart 사이에는 필터링(filtering)을 위한 여백(padding)도 필요하다. Unity의 Lightmap UV 생성 문서는 겹침, 왜곡, 간격 조건을 설명한다.

큰 벽을 작은 UV 영역에 넣으면 조명 해상도가 낮아지고, 작은 표면을 크게 넣으면 공간을 많이 쓴다. 또 정적 조명을 bake한 뒤 문이나 광원을 바꾸면 저장한 결과가 현재 장면을 반영하지 못한다. 별도 갱신이나 다른 조명 시스템이 필요한 이유다.

복셀(voxel): 공간을 셀로 나눈다

Voxel은 volume element다. 아래는 3차원 셀을 한 단면에서 본 그림이다.

Voxelized scene: 2D slice

+---+---+---+---+---+
|   |   | # |   |   |     # : geometry가 차지한 셀
+---+---+---+---+---+
|   |   | # |   |   |
+---+---+---+---+---+
| # | # | # | # | # |
+---+---+---+---+---+

조명에 쓰는 voxel은 점유율이나 불투명도(opacity)와 함께 빛의 정보를 저장할 수 있다. 예를 들어 벽이 차지한 셀에 반사된 radiance를 넣으면, 다른 위치에서 그 방향을 탐색하며 간접광을 추정할 수 있다.

Voxel Cone Tracing 논문은 계층적 복셀 옥트리(voxel octree)와 원뿔 추적(cone tracing)을 결합한다. 멀어질수록 넓어지는 cone에 맞춰 더 거친 계층의 값을 읽어, 가시성과 들어오는 에너지를 근사한다.

Voxel Cone Tracing의 광원 주입, 계층 필터링, 원뿔 조회 과정

Voxel Cone Tracing의 세 단계: 광원 정보를 복셀에 주입하고, 상위 계층으로 필터링한 뒤, 표면에서 원뿔로 조회한다. 오른쪽은 장면을 저장하는 희소 복셀 옥트리다.

shading point --> cone widens with distance
      x --------> /                 \
                 /   coarse data    \
                /_____________________\

여러 셀의 평균을 한 번에 읽을 수 있다는 것은 효율적이지만, 그 평균이 세부 구조를 지울 수도 있다는 뜻이다. 얇은 벽이 voxel보다 작으면 점유 정보가 충분하지 않을 수 있고, 거친 계층에서 벽 양쪽 조명이 섞일 수도 있다.

공간 해상도(spatial resolution)는 세제곱으로 증가한다

한 축에 $N$개의 셀이 있는 밀집 격자(dense grid)라면 총 셀 수는 $N^3$이다.

$$ 256^3=16{,}777{,}216,\qquad512^3=134{,}217{,}728 $$

설명용으로 셀당 8 byte를 가정하면 각각 128 MiB와 1 GiB다. 각 축의 해상도를 두 배로 늘렸을 뿐인데 메모리는 여덟 배가 된다. 이는 dense grid의 산술 예이며, 모든 voxel GI가 이만큼 메모리를 쓰는 것은 아니다. 희소 구조(sparse structure)는 빈 공간을 줄이고, 클립맵(clipmap)은 관심 영역마다 해상도를 다르게 유지한다.

중요한 선택은 저장 방식 자체보다 어디까지 세밀한 공간 표현을 유지할 것인가다.

프로브(probe): 공간의 대표 지점에서 측정한다

Probe는 선택한 위치에 둔 측정 샘플이다. 셀 전체를 표현하는 대신, 그 위치에서 주변 빛이 어떻게 들어오는지 저장하고 주변 표면에서 보간(interpolation)해 쓴다.

Room

+--------------------------------+
|     o              o           |
|                                |
|             o                  |    o : probe
|       +----------+             |    x : shading point
|       | object x |             |
|     o +----------+  o          |
+--------------------------------+

Probe를 규칙적인 격자에 놓을 수도 있고, 필요한 곳에 불규칙하게 놓을 수도 있다. 격자형 probe도 밀도를 두 배씩 늘리면 개수가 여덟 배가 된다. 따라서 probe의 장점은 무조건적인 메모리 우위가 아니라, 조명이 공유될 수 있는 간격으로 대표 지점을 선택할 수 있다는 점이다. 방향 데이터와 가시성(visibility) 데이터의 크기도 함께 계산해야 한다.

이동하는 캐릭터는 현재 위치 주변의 probe를 읽을 수 있다. 하지만 probe가 공중에 있다고 해서 그 자체로 벽을 구분하는 것은 아니다.

bright room A          dark room B

     o        | wall |       x
              |      |
    가까운 probe라도 벽으로 분리되어 있다.

단순 거리 보간은 밝은 방의 값을 어두운 방으로 가져올 수 있다. DDGI 논문은 방향별 조명과 거리 정보를 사용해 이런 빛 누출(light leaking)을 줄이는 probe 접근을 제시한다. 자세한 내용은 다음 편에서 다룬다.

같은 방에서 어떤 차이가 생길까?

정적 벽, 이동하는 캐릭터, 열리는 문이 있는 방을 생각해보자.

접근 벽의 조명을 어디서 읽는가? 문이 열린 뒤의 주요 과제
Baked lightmap 벽의 UV texel 미리 계산한 조명이 바뀐 가시성을 반영하지 못함
Voxel GI 주변 voxel 표현을 탐색 변경된 geometry·조명과 관련 계층을 갱신
Dynamic probe GI 주변 probe의 조명을 보간 영향받은 probe를 갱신하고 가시성에 맞춰 보간

실제 엔진에서는 하나만 고를 필요가 없다. 정적 표면의 lightmap과 움직이는 물체의 probe를 함께 쓰는 식의 조합이 가능하다. 각 표현이 다루는 빛의 범위를 분명히 해야 중복 계산도 피할 수 있다.

결국 저장 위치는 보간 관계를 정한다. UV상 이웃, voxel grid상 이웃, world-space probe상 이웃은 실제로 같은 조명을 공유할 수 있는 이웃과 항상 일치하지 않는다.

다음 글 「Light Probe: 공간에 빛을 저장한다는 것」에서는 공간의 한 점이 방향별 빛을 어떻게 저장하는지, 그리고 보간에 왜 가시성 정보가 필요한지 살펴본다.


References