Surfel GI는 어떻게 동작하는가 — EA GIBS 뜯어보기

장면 표면의 원판들이 G-buffer, ray tracing, persistent cache를 연결하는 조명 샘플이 되는 과정.

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

Surfel GI의 디버그 화면을 보면 벽과 바닥에 작은 원판들이 잔뜩 붙어 있다. 4편의 설명을 떠올리면, 원판 하나가 작은 표면 영역을 대표하는 조명 샘플임을 알 수 있다.

이번에는 그 샘플을 실제로 관리하고 계산하는 시스템을 살펴본다. GIBS(Global Illumination Based on Surfels)는 하드웨어 광선 추적(hardware ray tracing)과 표면의 조명 캐시를 결합해 확산 간접광(diffuse indirect lighting) 계산을 공간과 시간에 분산하는 접근이다. 사전 베이크(precomputation / baking)나 전용 lightmap UV를 요구하지 않는다는 점이 EA의 공식 소개에 강조되어 있다.

이 글의 구현 설명은 SIGGRAPH 2021 발표 시점을 기준으로 한다. 이후 버전의 모든 동작을 설명하는 글은 아니다. 수식과 파이프라인은 이해를 위한 단순화이고, 구체적인 구현 사실에는 발표 PDF의 페이지를 함께 적었다.

EA SEED의 SIGGRAPH 2021 발표 영상, 「Global Illumination Based on Surfels」.

두 개의 흐름: 화면에서 샘플을 만들고, 장면에서 빛을 구한다

GIBS를 처음 이해할 때는 배치와 조명 계산을 나누면 편하다.

Scene geometry ------------> Ray tracing acceleration structure
      |                                      |
      v                                      |
G-buffer                                     |
      |                                      |
Coverage 확인                               |
      |                                      |
새 surfel 생성                              |
      |                                      |
      +--> Persistent surfel cache <---------+
                     |             ray로 조명 갱신
                     |
            주변 surfel 조회 / 필터
                     |
            현재 표면의 diffuse shading

화면에서 보이는 표면은 샘플을 생성할 후보가 된다. 하지만 빛을 계산할 때 광선이 검사하는 것은 광선 추적 구조의 실제 기하(geometry)다. 화면에서 배치한다고 조명 계산까지 화면 공간(screen space)에 한정되는 것은 아니다.

또 이 도식은 데이터 관계다. 실제 GPU 패스(pass)의 순서와 의존성을 모두 옮긴 프레임 스케줄은 아니다.

1. G-buffer에서 표면 커버리지(surface coverage)가 부족한 곳을 찾는다

G-buffer의 깊이(depth)와 법선(normal)으로 현재 픽셀이 가리키는 표면을 알아낼 수 있다. 깊이로부터 월드 공간(world-space) 위치를 복원하는 과정을 개념적으로 쓰면 다음과 같다.

$$ h=(PV)^{-1}p_{\mathrm{clip}},\qquad x=h_{xyz}/h_w $$

$P$와 $V$는 투영 행렬(projection matrix)과 뷰 행렬(view matrix)다. 실제 복원에서는 API의 depth 범위, reversed-Z, 화면 좌표 규약을 맞춰야 한다.

2021 GIBS는 화면을 16×16 texel tile로 나누고, 기존 surfel coverage가 가장 부족한 texel을 후보로 선택한다. 무작위 임계값(randomized threshold) 조건을 통과하면 G-buffer 정보로 surfel을 생성한다. 충분히 덮인 타일에서는 생성을 멈춘다. 이는 발표 PDF 14–17쪽의 내용이다.

16×16 화면 타일에서 Surfel 커버리지가 부족한 위치를 찾는 도식

GIBS는 화면 타일마다 coverage가 가장 낮은 위치를 찾는다. 주황색 칸처럼 기존 surfel의 영향이 부족한 곳이 새 샘플 생성 후보가 된다.

한 tile의 coverage

+---+---+---+---+
| + | + | + | + |    + : 기존 샘플의 영향을 받음
+---+---+---+---+    . : coverage가 부족함
| + | . | + | + |
+---+---+---+---+
      |
      +--> 후보 위치에서 새 surfel 생성

즉 모든 픽셀에 샘플 하나를 대응시키지 않는다. 샘플의 대표 영역으로 여러 픽셀을 덮고, 부족한 곳만 채운다. 디버그 원판은 이 분포를 보여주는 시각화이며, 주 렌더링 mesh를 원판으로 교체했다는 의미가 아니다.

2. 생성한 샘플을 유지하고(persistence) 필요에 따라 재활용한다(recycling)

샘플이 생긴 뒤에는 프레임이 끝났다고 버리지 않는다. 표면이 움직이면 변환 식별자(transform identifier)와 로컬 위치(local position)를 이용해 위치를 갱신한다. Surfel 반지름은 화면에 투영된 크기가 대략 일정하도록 조절하고, 고정된 자원 풀 안에서 관련성이 낮은 샘플을 재활용한다. 발표 PDF 18–29쪽에서 설명하는 관리 방식이다.

여기서 persistent는 영구적이라는 뜻이 아니다. 같은 표면을 다시 볼 때 유효한 조명 계산을 재사용할 수 있도록 유지한다는 뜻이다. 샘플을 너무 빨리 지우면 다시 들어오는 영역에서 초기 상태(cold start)가 생기고, 모두 남겨두면 한정된 자원을 새 표면에 쓰기 어렵다.

표면 추적과 조명 갱신도 구분해야 한다. 움직이는 상자에 surfel이 따라붙었다고 해서, 이전 위치에서 계산한 빛이 새 위치에서도 맞는 것은 아니다.

3. Surfel에서 광선(ray)을 보내 입사 방사휘도(incident radiance)를 추정한다

Surfel은 자신의 법선 쪽 반구(hemisphere)로 ray를 보낸다. Ray가 다른 표면에 닿으면 그 위치의 직접 diffuse 조명과 기존 surfel 조명을 평가한다. 이 결과를 다시 출발 surfel의 irradiance에 반영한다. 이 연결은 발표 PDF 62쪽에 설명되어 있다.

surfel x ---- ray ----> hit point y
                         |
                         +-- 광원에 의한 조명과 가시성
                         +-- y 주변의 기존 surfel 조명
                         |
                 y에서 x로 오는 radiance 추정
                         |
                 x의 irradiance 갱신

흡수(absorption)·산란(scattering)하는 매질이 없는 ray 구간에서는 $y$에서 $x$ 방향으로 나오는 radiance가 $x$에서 해당 방향으로 들어오는 radiance와 연결된다. Ray 자체가 조명값을 반환하는 것은 아니다. 교차점에서 재질과 빛을 평가해야 한다.

반구 적분을 $M$개의 방향 표본으로 추정하는 일반적인 식은 다음과 같다.

$$ \widehat E(x,n)=\frac{1}{M}\sum_{k=1}^{M} \frac{\widehat L_i(x,\omega_k)\max(0,n\cdot\omega_k)}{p(\omega_k)} $$

$p(\omega)$는 방향을 선택하는 확률밀도(probability density)다. 위 식은 몬테카를로 추정량(Monte Carlo estimator)의 원리를 보여준다. 실제 ray guiding이나 필터의 세부 구현식은 아니다.

코사인 가중 반구 샘플링(cosine-weighted hemisphere sampling)에서 $p(\omega)=(n\cdot\omega)/\pi$라면 식이 다음처럼 단순해진다.

$$ \widehat E(x,n)=\frac{\pi}{M}\sum_{k=1}^{M}\widehat L_i(x,\omega_k) $$

밝은 방향에 더 많은 ray를 보내는 guiding을 적용하면 그 분포에 맞는 확률 보정도 필요하다. 방향만 편향되게 고른 뒤 같은 평균을 내면 추정 대상 자체가 바뀐다. EA는 공식 소개에서 ray guiding, ray binning, spatial filtering과 많은 광원의 처리도 구현 요소로 소개한다.

4. 이전 캐시(cache)가 다음 반사(bounce)를 전달한다

교차점에서 직접광(direct lighting)만 평가하면 출발 surfel에는 그 표면에서 한 번 반사된 빛이 전달된다. 여기에 교차점의 이전 간접광(indirect lighting) 캐시도 읽으면, 앞서 계산한 반사광이 다시 다른 표면으로 전달될 수 있다.

설명용 반복식을 쓰면 다음과 같다.

$$ E^{(t+1)}=\mathcal T\bigl(L_{\mathrm{direct}}+L_{\mathrm{emission}}+ L_{\mathrm{indirect}}[E^{(t)}]\bigr) $$

$\mathcal T$는 ray sampling으로 표면에 들어오는 빛을 적분하는 연산을 뜻한다. 실제 코드의 pass나 버퍼 구조와 동일한 식은 아니다.

처음:      직접광을 받은 벽의 빛 --> 바닥 캐시
다음 갱신: 바닥에 쌓인 간접광   --> 다른 벽의 캐시
반복:      여러 표면을 거친 빛이 점차 반영됨

발표는 surfel coverage가 있는 영역에서 이런 반복으로 여러 bounce를 시간에 걸쳐 반영한다고 설명한다. 매 프레임 모든 긴 path를 완전히 추적한다는 뜻은 아니다. 캐시의 초기값, coverage, 갱신 횟수에 따라 수렴 지연과 근사 오차가 남는다.

발광 표면(emissive surface)을 ray로 직접 만나는 경로도 있을 수 있으므로, “cache ray로 얻은 모든 값이 물리적으로 간접광”이라고 단정하면 안 된다. 다른 조명 pass와 결합할 때 어느 경로를 누가 처리하는지 정해 중복을 피해야 한다.

5. 적은 ray를 변화가 큰 곳에 쓴다

2021 발표의 적응적 적분(adaptive integration)은 장기 평균과 단기 평균·분산(variance)을 사용해 변화에 대한 반응을 조절한다. 새 surfel, variance가 큰 surfel, 현재 화면 조명에 기여하는 surfel에 더 많은 ray를 요청하게 하고 전체 광선 예산(ray budget)으로 제한한다. 발표 PDF 74–79쪽의 내용이다.

예를 들어 이미 안정된 벽과 새로 보이는 골목이 있다면, 둘에 같은 계산량을 배분할 이유가 적다. 안정된 벽은 변화 감지를 위한 표본(sample)을 유지하고, 새 골목은 빠르게 초기 조명을 얻어야 한다.

하지만 variance가 높다는 사실만으로 원인을 확정할 수는 없다. Noise가 클 수도 있고 조명이 바뀌었을 수도 있다. 갱신량과 history의 신뢰도를 함께 조절하는 일이 필요하다. 이 문제는 다음 편의 중심 주제다.

6. 가시성(visibility)을 포함해 현재 표면의 빛을 재구성(reconstruction)한다

모든 픽셀에서 전체 surfel 목록을 순회하면 캐시 조회 자체가 비싸진다. GIBS는 카메라 근처의 균일 격자(uniform grid)와 주변의 비선형 사다리꼴 격자(trapezoidal grid)를 사용해 가까운 샘플을 찾는다. 위치·방향 가중치로 irradiance를 모으고, 기여가 부족할 때 cell 평균을 보완값으로 사용한다. 발표 PDF 30–40쪽의 설명이다.

이 grid는 surfel 검색을 위한 가속 구조(acceleration structure)다. 앞에서 다룬 voxel GI처럼 공간 셀에 장면 전체의 점유율과 radiance를 표현하는 것과 역할이 다르다.

또한 surfel만으로 벽 너머 빛의 혼합이 사라지지는 않는다. GIBS는 반구 방향별 깊이의 평균과 제곱 평균을 저장하고, 이를 가시성 가중치에 사용한다. 2021 발표의 기본 해상도는 surfel 반구당 4×4 texel이다. 발표 PDF 41–59쪽은 이 방사형 깊이 함수(radial depth function)로 누출을 줄이는 과정을 보여준다.

필터의 역할을 단순화하면 다음과 같다.

주변 샘플 검색
      |
위치·표면 방향이 맞는가?
      |
깊이 정보상 조명을 공유할 수 있는가?
      |
유효한 irradiance를 조합
      |
현재 픽셀의 재질로 diffuse shading

마지막 Lambertian 적용은 이전 편들과 같다.

$$ L_{o,\mathrm{ind}}(x)\approx\frac{\rho(x)}{\pi}\widetilde E_{\mathrm{ind}}(x,n) $$

현재 픽셀의 albedo detail과 캐시의 조명 해상도를 분리할 수 있다는 장점이 있다. 대신 거친 irradiance 표현이 날카로운 조명 변화나 법선 변화까지 보존하는 것은 아니다.

Surfel만 쓰는 시스템일까?

G-buffer에서 surfel을 만드는 방식은 투명 표면에 그대로 적용하기 어렵다. 2021 GIBS는 이를 보완하기 위해 ray-traced probe와 SH, volume clipmap을 함께 사용한다. 발표 PDF 156–176쪽의 내용이다.

이 사례는 probe와 surfel을 경쟁 관계로만 보지 않아도 된다는 점을 보여준다. 불투명 표면에는 표면 캐시를 쓰고, 다른 조회 요구에는 공간 캐시를 쓸 수 있다.

다만 화면에서 생성된 surfel cache가 모든 미관측 표면까지 자동으로 채워주는 것은 아니다. 최초 배치의 관측 범위와 ray tracing의 탐색 범위는 서로 다른 문제이며, coverage가 부족한 위치의 간접광에는 근사상의 한계가 있다.

디버그 원판 뒤에 있는 설계

GIBS의 원판은 단순한 장면 장식이 아니다. 표면의 대표 위치, 조명 계산의 시작점, 이전 결과의 저장소, 픽셀 보간의 이웃을 한 샘플이 함께 담당한다.

핵심 선택은 ray를 어디서 얼마나 보내고, 그 계산을 얼마나 오래 공유할지다. Surfel은 그 선택을 표면에 맞춰 구현하는 표현이다. 계산을 여러 프레임에 나누면 noise를 줄일 수 있지만, 장면이 바뀌었을 때 이전 값이 얼마나 유효한지도 판단해야 한다.

다음 글 「한 프레임의 빛을 다음 프레임에서 써도 될까?」에서는 공간 캐시의 수명과 temporal accumulation을 구분하고, 오래된 조명을 다루는 방법을 살펴본다.


References