포스트

알파 블렌딩과 볼륨 렌더링 — 사실은 같은 식이다

알파 블렌딩과 볼륨 렌더링 — 사실은 같은 식이다

서론

앞 글은 z-버퍼로 끝났습니다. 그리고 z-버퍼에는 조건이 하나 붙어 있었습니다.

불투명할 때만 그렇습니다.

이 글은 그 조건이 깨졌을 때의 이야기입니다. 그리고 그 이야기를 끝까지 따라가면 뜻밖의 곳에 도착합니다. 반투명 이미지를 겹치는 규칙(알파 블렌딩)과 NeRF의 볼륨 렌더링 적분이 완전히 같은 식이라는 사실입니다.

이건 우연이 아닙니다. 그리고 이 사실이 3DGS를 이해하는 데 결정적입니다. 3DGS는 NeRF의 볼륨 렌더링을 물려받았지만, 적분을 샘플링하는 대신 유한 개의 알파 블렌딩으로 바로 씁니다. 왜 그게 가능하고 왜 그게 빠른지는 두 식이 같다는 걸 알아야 보입니다.


1. over 연산자

앞에 불투명도 $\alpha$, 색 $c_f$ 인 것이 있고 뒤에 색 $C_b$ 가 있을 때, 최종 색은 이렇습니다.1

\[C = \alpha\, c_f + (1-\alpha)\, C_b\]

앞의 것이 $\alpha$ 만큼 자기 색을 내고, 나머지 $(1-\alpha)$ 만큼 뒤가 비쳐 보인다는 뜻입니다. 이걸 over 연산자라고 하고 $c_f \text{ over } C_b$ 로 씁니다.

이 연산의 성질이 중요합니다.

  • 결합법칙은 성립합니다. $(A \text{ over } B) \text{ over } C = A \text{ over } (B \text{ over } C)$. 어디서부터 묶어 계산하든 같습니다.
  • 교환법칙은 성립하지 않습니다. $A \text{ over } B \ne B \text{ over } A$.

두 번째가 전부입니다. 순서를 바꾸면 결과가 달라집니다.

섞는 순서에 따른 결과 차이 같은 원반 세 장, 같은 불투명도 0.55. 왼쪽은 깊이 순서대로, 가운데는 뒤집어서 합성했다. 겹치는 영역의 색이 완전히 달라진다. z-버퍼가 “가장 가까운 것 하나”만 남기고 순서를 신경 쓰지 않아도 됐던 것은, 뒤에 있는 것의 기여가 정확히 0이었기 때문이다.


2. 앞에서 뒤로, 뒤에서 앞으로

합성은 두 방향 모두 가능합니다.

뒤에서 앞으로(back-to-front). 가장 먼 것부터 화면에 덧칠합니다.

\[C \leftarrow \alpha_i c_i + (1-\alpha_i) C\]

구현이 가장 단순합니다. 프레임버퍼 하나만 있으면 되고, 하드웨어 블렌딩 유닛이 하는 일이 정확히 이것입니다.

앞에서 뒤로(front-to-back). 가까운 것부터 처리하되, 남은 투과율 $T$ 를 따로 들고 다닙니다.

\[C \mathrel{+}= T\,\alpha_i c_i, \qquad T \mathrel{\times}= (1-\alpha_i)\]

$T$ 는 “여기까지 오는 동안 살아남은 빛의 비율”입니다. 처음엔 1이고, 불투명한 것을 지날 때마다 줄어듭니다.

1
2
3
4
5
6
7
8
9
10
11
import numpy as np

def composite_front_to_back(colors, alphas, eps=1e-3):
    """가까운 것부터 정렬된 (color, alpha) 목록을 합성한다."""
    C, T = np.zeros(3), 1.0
    for c, a in zip(colors, alphas):
        C += T * a * c              # 이 층이 실제로 기여하는 몫
        T *= (1.0 - a)              # 뒤쪽에 남겨 줄 빛
        if T < eps:                 # 이미 다 막혔다면 나머지는 볼 필요가 없다
            break
    return C, T                     # T 는 곧 배경이 비쳐 보이는 비율

두 방향의 결과는 수학적으로 같습니다. 그런데 앞에서 뒤로 하면 공짜로 얻는 것이 있습니다. $T$ 가 충분히 작아지면 나머지를 아예 건너뛸 수 있습니다. 이것이 break 한 줄이고, 실전 성능에 큰 차이를 만듭니다. 3DGS의 타일 래스터라이저도, NeRF의 광선 마칭도 전부 앞에서 뒤로 갑니다.

미리 곱해 둔 알파(premultiplied alpha). 색을 $c$ 대신 $c’ = \alpha c$ 로 저장해 두면 over 연산이 $C = c’_f + (1-\alpha)C_b$ 가 되어 곱셈이 하나 줄고, 무엇보다 연산이 결합적으로 깔끔해집니다. 여러 층을 미리 하나로 합쳐 둘 수 있게 되죠. 렌더링 파이프라인이 거의 전부 이 표현을 쓰는 이유입니다.


3. 정렬이라는 골칫거리

순서가 결과를 바꾼다면 정렬해야 합니다. 그런데 정렬은 생각보다 어렵습니다.

  • 무엇을 기준으로 정렬하나. 프리미티브의 중심? 가장 가까운 점? 어느 것을 골라도 삼각형 두 개가 서로를 관통하면 하나의 순서로는 표현할 수 없습니다.
  • 순서가 화면 픽셀마다 다를 수 있습니다. 한 픽셀에서는 A가 앞이고 다른 픽셀에서는 B가 앞인 상황이 실제로 생깁니다.
  • 정렬 자체가 비쌉니다. 프리미티브가 수백만 개면 매 프레임 정렬해야 합니다.

그래서 순서와 무관하게 반투명을 처리하려는 연구(order-independent transparency)가 오래 있었지만, 어느 것도 완전하지는 않습니다.

3DGS는 여기서 실용적인 타협을 합니다. 화면을 타일로 나눈 뒤, 타일마다 걸치는 가우시안을 중심의 깊이 기준으로 한 번 정렬하고 그 순서를 그 타일의 모든 픽셀에 씁니다. 픽셀별 정확한 순서가 아니라 타일별 근사 순서입니다. 대부분의 경우 눈에 띄지 않지만, 카메라가 움직일 때 정렬 순서가 뒤바뀌면서 색이 툭 튀는 현상(popping)이 나타날 수 있습니다. 3DGS 결과물을 돌려 보다 보면 가끔 보이는 그 깜빡임이 이것입니다.


4. 알파는 어디서 오는가 — 볼륨 렌더링

지금까지 $\alpha$ 는 “그냥 주어진 값”이었습니다. 그런데 안개나 연기처럼 연속적으로 퍼져 있는 매질에서는 $\alpha$ 를 물리에서 유도할 수 있습니다.

광선을 따라 가는 빛의 세기 $I(t)$ 를 생각합니다. 매질의 밀도가 $\sigma(t)$ 라면, 아주 짧은 구간 $dt$ 동안 흡수되는 양은 밀도와 거리에 비례합니다.

\[\frac{dI}{dt} = -\sigma(t)\, I(t)\]

이 미분방정식의 해가 투과율입니다.

\[T(t) = \exp\left(-\int_{0}^{t} \sigma(s)\, ds\right)\]

여기에 매질이 스스로 색 $c(t)$ 를 낸다고 두면(방출), 카메라에 도달하는 최종 색은 각 지점의 기여를 전부 더한 것이 됩니다.

\[C = \int_{0}^{t_{\max}} T(t)\,\sigma(t)\,c(t)\,dt\]

이 식이 NeRF의 렌더링 방정식입니다.23 읽는 법은 이렇습니다.

위치 $t$ 가 화면 색에 기여하는 정도는 거기에 물질이 있고($\sigma$가 크고) 거기까지 빛이 도달했을 때($T$가 크고)뿐이다.

볼륨 렌더링 적분과 그 이산화 왼쪽: 옅은 안개 하나와 얇고 진한 표면 하나가 있는 광선. 투과율 $T$(파랑)는 물질을 지날 때마다 계단처럼 떨어지고, 기여도 $w = T\sigma$(주황)는 표면에 도달하는 순간 뾰족하게 솟았다가 그 뒤로는 0이 된다. 가려진 뒤쪽은 아무리 밀도가 높아도 기여하지 못한다. 가운데: 같은 적분을 8개·48개 구간으로 나눈 것. 오른쪽: 샘플이 표면 두께보다 성기면 오차가 널뛰다가, 표면을 해상할 수 있게 되는 순간부터 규칙적으로 수렴한다.

$w(t) = T(t)\sigma(t)$ 는 적분하면 $1 - T(t_{\max})$ 가 되는 확률 밀도 비슷한 양입니다. “빛이 어디서 멈췄는가”의 분포라고 읽으면 됩니다. NeRF에서 깊이를 뽑을 때 $\hat{d} = \int w(t)\, t\, dt$ 로 계산하는 것이 이 해석에서 나옵니다.


5. 이산화하면 알파 블렌딩이 된다

이제 위 적분을 컴퓨터로 계산합니다. 구간을 $N$ 개로 나누고 각 구간에서 $\sigma$, $c$ 가 상수라고 두면, 구간 $i$ 안에서 투과율은 정확히 계산됩니다.

\[\exp(-\sigma_i \delta_i), \qquad \delta_i = t_{i+1} - t_i\]

이 구간이 막는 비율은 그 나머지입니다.

\[\alpha_i = 1 - \exp(-\sigma_i \delta_i)\]

여기서 $\alpha$ 가 나왔습니다. 그리고 구간 $i$ 앞까지 살아남은 투과율은 앞 구간들이 남긴 것의 곱입니다.

\[T_i = \prod_{j<i} \exp(-\sigma_j \delta_j) = \prod_{j<i} (1 - \alpha_j)\]

둘을 합치면 적분이 이렇게 됩니다.

\[C \approx \sum_{i=1}^{N} T_i\, \alpha_i\, c_i = \sum_{i=1}^{N} c_i\,\alpha_i \prod_{j<i}(1-\alpha_j)\]

오른쪽 식은 앞에서 뒤로 하는 알파 블렌딩과 글자 하나까지 같습니다. 2절의 composite_front_to_back 코드가 그대로 볼륨 렌더러입니다.

1
2
3
4
5
6
7
8
def volume_render(sigmas, colors, deltas):
    """sigma(밀도), color, 구간 길이 -> 최종 색. 위 알파 블렌딩과 같은 루프다."""
    alphas = 1.0 - np.exp(-sigmas * deltas)     # 유일하게 추가된 줄
    C, T = np.zeros(3), 1.0
    for a, c in zip(alphas, colors):
        C += T * a * c
        T *= (1.0 - a)
    return C, T

정리하면 이렇습니다.

알파 블렌딩은 볼륨 렌더링 적분의 이산 버전이다. 다른 점은 단 하나, $\alpha$ 를 직접 주느냐 밀도에서 $1-e^{-\sigma\delta}$ 로 얻느냐뿐이다.


6. 그래서 NeRF와 3DGS는 어디가 다른가

두 방법 다 위의 식을 씁니다. 차이는 합의 항이 어디서 오느냐입니다.

 NeRF3D Gaussian Splatting
항의 정체광선 위에서 샘플링한 지점그 픽셀에 걸친 가우시안
개수광선당 수십~수백 (내가 정한다)그 픽셀을 덮는 것 전부 (장면이 정한다)
$\alpha$$1 - e^{-\sigma_i \delta_i}$$\sigma_i \cdot \exp(-\tfrac12 d^\top \Sigma’^{-1} d)$
순서광선을 따라가므로 자동으로 정렬됨명시적으로 정렬해야 함
비용샘플마다 MLP 호출가우시안마다 지수 하나

3DGS의 $\alpha$ 에는 $\delta$ 가 없습니다. 구간을 나눈 게 아니라 덩어리 하나가 통째로 하나의 항이기 때문입니다. 대신 화면상의 거리에 따라 부드럽게 옅어지는 항이 붙습니다.

이 차이가 만드는 결과가 큽니다.

  • 3DGS에는 “샘플이 표면을 놓치는” 문제가 없습니다. 위 그림 오른쪽에서 샘플이 성길 때 오차가 널뛰던 그 구간이 아예 존재하지 않습니다. 물질이 있는 곳에만 가우시안이 있으니까요. NeRF가 계층적 샘플링이라는 장치를 따로 둬야 했던 이유가 바로 그 구간을 피하기 위해서입니다.
  • 대신 3DGS는 정렬 비용과 정렬 오류를 떠안습니다. 광선을 따라가면 순서는 저절로 정해지지만, 화면에 뿌려진 덩어리들의 앞뒤는 따로 정해 줘야 합니다.

7. 자주 나오는 질문들

Q1. 앞에서 뒤로와 뒤에서 앞으로가 정말 같은 결과인가?

수학적으로 같습니다. 다만 부동소수점에서는 미세하게 다릅니다. 앞에서 뒤로 가면 $T$ 가 곱해지며 점점 작아지므로 뒤쪽 항들이 작은 수끼리 더해지고, 뒤에서 앞으로 가면 큰 값에 계속 섞입니다. 실용적으로 문제가 되는 수준은 아니지만, 앞에서 뒤로가 조기 종료가 되고 기울기 계산에도 유리해서 학습이 걸린 렌더러는 거의 다 이쪽입니다.

Q2. $\sigma$ 와 $\alpha$ 는 무엇이 다른가?

$\sigma$ 는 길이당 밀도이고 단위가 $1/\text{길이}$ 입니다. 값에 상한이 없습니다. $\alpha$ 는 특정 구간을 통과할 때 막히는 비율이고 $[0,1]$ 입니다. 둘을 잇는 것이 $\alpha = 1 - e^{-\sigma\delta}$ 이고, 여기에 $\delta$ 가 들어간다는 게 핵심입니다. 같은 밀도라도 구간을 길게 잡으면 $\alpha$ 가 커집니다. NeRF 구현에서 샘플 간격을 바꿨는데 결과가 달라졌다면 대개 이 항을 빼먹은 것입니다.

Q3. 그럼 3DGS의 불투명도는 물리적으로 무슨 뜻인가?

엄밀히 말하면 물리적 밀도가 아닙니다. 3DGS는 볼륨 렌더링 식의 형태를 가져다 쓰되, $\alpha$ 를 학습 가능한 파라미터로 직접 둡니다. 그래서 “이 장면의 밀도장이 무엇인가”라는 질문에는 답하지 않습니다. 3DGS의 결과에서 깨끗한 표면을 뽑아내는 게 생각보다 어려운 이유가 여기 있습니다 — 애초에 표면을 표현하려고 만든 물건이 아닙니다.

Q4. 조기 종료를 하면 기울기도 안 흐르나?

그렇습니다. $T$ 가 임계값 아래로 떨어져 건너뛴 가우시안은 순전파에서 기여하지 않았으니 역전파에서도 기울기를 못 받습니다. 이건 의도된 동작입니다. 완전히 가려진 물체가 화면에 기여하지 않는다면, 그 물체를 어떻게 바꿔도 손실이 변하지 않는 게 맞습니다. 기울기가 0인 게 옳은 답입니다.

Q5. 왜 밀도를 직접 안 쓰고 굳이 지수를 거치나?

$\alpha = 1 - e^{-\sigma\delta}$ 는 $\sigma \ge 0$ 이기만 하면 자동으로 $\alpha \in [0,1)$ 을 보장합니다. 클리핑이 필요 없다는 뜻이고, 그래서 매끄럽게 미분 가능합니다. 만약 $\alpha$ 를 직접 최적화하면서 $[0,1]$ 로 자르면 경계에서 기울기가 끊깁니다. 3DGS는 이 문제를 다른 방식으로 피합니다 — 불투명도를 로짓으로 저장하고 시그모이드를 씌웁니다. 발상은 같습니다. 제약을 함수로 흡수해 버리는 것입니다.


마치며

  • over 연산자는 결합적이지만 교환적이지 않다. 그래서 반투명에는 정렬이 필요하고, z-버퍼로는 안 된다.
  • 앞에서 뒤로 합성하면 투과율 $T$ 를 들고 다니게 되는데, 그 대가로 조기 종료를 얻는다.
  • 볼륨 렌더링 적분 $C = \int T\sigma c\,dt$ 를 이산화하면 $\alpha_i = 1 - e^{-\sigma_i\delta_i}$ 가 나오고, 나머지는 알파 블렌딩과 완전히 같은 식이 된다.
  • NeRF는 광선 위 샘플이 항이 되고, 3DGS는 가우시안이 항이 된다. 전자는 샘플링 문제를, 후자는 정렬 문제를 안는다.

이제 합성하는 법은 알았습니다. 그런데 각 항의 $c_i$ 는 어디서 오는 걸까요. 상수 하나면 될 것 같지만, 그러면 젖은 바닥의 반사도 금속의 광택도 표현할 수 없습니다. 다음 글은 “보는 방향에 따라 달라지는 색”을 어떻게 몇 개의 숫자로 담는가에 대한 것입니다.


이 시리즈 — 3DGS를 읽기 위한 기초지식

  1. MLP와 역전파
  2. SGD에서 Adam까지
  3. SfM 파이프라인
  4. MVS — 밀집 복원
  5. 래스터화
  6. 알파 블렌딩과 볼륨 렌더링 ← 지금 읽는 글
  7. 구면 조화 함수
  8. 미분 가능 렌더링
  9. NeRF
  10. 3D Gaussian Splatting ← 도착지

참고문헌

  1. Porter, T., & Duff, T. (1984). Compositing digital images. SIGGRAPH ‘84, 253–259. — over 연산자와 미리 곱한 알파의 출처. ↩︎

  2. Max, N. (1995). Optical models for direct volume rendering. IEEE TVCG, 1(2), 99–108. — 흡수·방출 모델과 그 이산화. NeRF가 인용하는 바로 그 논문. ↩︎

  3. Mildenhall, B., Srinivasan, P. P., Tancik, M., Barron, J. T., Ramamoorthi, R., & Ng, R. (2020). NeRF: Representing scenes as neural radiance fields for view synthesis. ECCV. — 4절이 위 이산화 식. ↩︎

이 기사는 저작권자의 CC BY-NC-ND 4.0 라이센스를 따릅니다.

License

CC BY-NC-ND 4.0

이 포스트는 CC BY-NC-ND 4.0 라이선스로 제공됩니다. 출처 표시는 필요하며, 상업적 이용과 변경은 허용되지 않습니다.