포스트

미분 가능 렌더링 — 사진에서 장면으로 기울기를 되돌리기

미분 가능 렌더링 — 사진에서 장면으로 기울기를 되돌리기

서론

지금까지 렌더링은 한 방향이었습니다.

\[\text{장면 파라미터} \theta \;\longrightarrow\; \text{이미지 } I\]

이제 화살표를 뒤집습니다. 사진 여러 장이 주어졌을 때, 그 사진들을 만들어 냈을 법한 장면을 찾는 문제입니다. 이걸 역렌더링(inverse rendering) 또는 합성을 통한 분석(analysis by synthesis) 이라고 부릅니다.

\[\theta^\star = \arg\min_\theta \; \big\| \text{render}(\theta) - I_{\text{photo}} \big\|\]

1편2편에서 본 도구가 그대로 쓰입니다. 손실을 정의하고, 기울기를 구하고, Adam으로 갱신합니다. 딱 하나만 있으면 됩니다.

\[\frac{\partial L}{\partial \theta} = \underbrace{\frac{\partial L}{\partial I}}_{\text{쉽다}} \cdot \underbrace{\frac{\partial I}{\partial \theta}}_{\textbf{이게 문제다}}\]

렌더러를 미분할 수 있어야 합니다. 그런데 앞서 본 래스터화는 미분이 되지 않습니다. 이 글은 왜 안 되는지, 그리고 3DGS가 그 문제를 어떻게 피해 갔는지에 대한 것입니다.


1. 렌더러의 어디가 미분 불가능한가

렌더링 과정 대부분은 사실 매끄럽습니다. 행렬 곱, 원근 나눗셈, 색 계산, 알파 블렌딩 — 전부 미분됩니다. 문제는 이산적인 결정을 내리는 세 지점입니다.

  1. 안팎 판정. 픽셀 중심이 삼각형 안이면 1, 밖이면 0. 계단 함수입니다.
  2. 가시성 결정. z-버퍼의 “더 가까운 쪽을 남긴다”는 $\min$ 연산이고, 어느 쪽이 이기느냐가 뒤바뀌는 순간 불연속입니다.
  3. 정렬 순서. 두 프리미티브의 앞뒤가 뒤바뀌면 합성 결과가 튑니다.

첫 번째가 가장 근본적입니다.

딱딱한 경계와 부드러운 경계의 손실 지형 프리미티브 하나를 가로로 움직이면서 목표 영상과의 손실을 잰 것. 딱딱한 경계(빨강)의 손실은 계단이다. 계단의 평평한 부분에서 기울기는 정확히 0이고, 모서리에서는 정의되지 않는다. 게다가 목표에서 충분히 멀어지면 겹치는 픽셀이 아예 없어 손실이 완전히 평평해진다 — “어느 쪽으로 가야 하는지”라는 정보 자체가 없다. 가우시안(파랑)은 어디서나 매끄럽고, 멀리서도 기울기가 살아 있다.

이 그림이 문제의 전부입니다. 정리하면 이렇습니다.

딱딱한 경계를 가진 프리미티브에서, 픽셀 값은 파라미터에 대해 조각별로는 매끄럽지만 경계에서 불연속이다. 그런데 “물체를 움직여라”는 정보는 전부 그 불연속에 들어 있다. 매끄러운 조각 안에서는 기울기가 0이다.


2. 세 가지 해결책

(A) 경계를 부드럽게 만든다

계단을 시그모이드 같은 매끄러운 함수로 바꾸는 방법입니다. 픽셀이 삼각형 밖에 있어도 거리에 따라 조금씩 기여하게 만듭니다. SoftRas1, DIB-R2 같은 미분 가능 래스터라이저가 이 길을 갑니다.

기울기가 생기지만 순전파 결과가 원래 렌더링과 달라집니다. 경계가 흐릿한 이미지를 만들고 그걸로 최적화한 뒤, 실제 렌더링은 딱딱한 경계로 하게 되죠. 그 불일치가 오차로 남습니다.

(B) 경계를 정확히 다룬다

불연속을 근사하지 않고, 픽셀 값의 미분에 경계선을 따라가는 항을 명시적으로 추가하는 방법입니다. 렌더링 적분을 미분하면 경계에서 델타 함수 항이 나오는데, 그걸 경계 위에서 따로 표본추출해 계산합니다.3

수학적으로 옳지만 비쌉니다. 실루엣이 될 수 있는 모서리를 찾아 그 위에서 추가 표본을 뽑아야 합니다. 물리 기반 역렌더링 연구에서 쓰이지만, 3DGS 같은 실시간 규모에는 무겁습니다.

(C) 애초에 경계가 없는 표현을 쓴다

문제를 푸는 대신 문제가 없는 표현을 고릅니다. NeRF와 3DGS가 이쪽입니다.

  • NeRF — 장면이 연속 밀도장 $\sigma(\mathbf{x})$ 입니다. 표면도 경계도 없습니다. 광선 위에서 샘플링해 적분하는 과정이 통째로 매끄럽습니다.
  • 3DGS — 프리미티브가 가우시안입니다. 무한히 뻗어 나가며 지수적으로 옅어질 뿐, 어디에도 “여기부터 밖” 같은 선이 없습니다.

3DGS 논문의 진짜 기여가 “가우시안을 쓰자”는 아이디어보다 빠른 미분 가능 타일 래스터라이저를 만든 데 있다고 하는 이유가 이것입니다. 표현을 바꾸면 미분 가능성이 공짜로 따라오지만, 그걸 실시간 속도로 구현하는 건 별개의 일이니까요.


3. 그래서 3DGS는 완전히 미분 가능한가

아닙니다. 정직하게 말하면 매끄러운 부분이 압도적으로 많을 뿐입니다. 여전히 끊기는 곳들이 있습니다.

지점미분 가능?실제로 어떻게 되나
3차원 → 2차원 투영○ (야코비 근사)원근 투영을 1차 근사해 2D 공분산을 얻는다4
가우시안 감쇠 $\exp(-\frac12 q)$매끄러움
알파 블렌딩곱과 합뿐
SH 색 평가선형
$3\sigma$ 바깥 절단실용상 무시할 만큼 기여가 작은 지점에서 자름
깊이 정렬 순서순서가 바뀌면 결과가 튄다 (popping)
조기 종료 ($T < \epsilon$)가려진 것은 기울기를 못 받는다
밀도 조정 (복제·분할·제거)기울기와 무관한 이산적 개입

아래 세 줄이 중요합니다. 3DGS의 학습은 순수한 경사하강이 아닙니다. 매끄러운 경사하강 사이사이에, 기울기로는 표현할 수 없는 이산적 조작이 끼어듭니다.

그리고 그 조작의 판단 기준이 기울기라는 점이 재미있습니다. 3DGS는 각 가우시안에 대해 화면 좌표에서의 위치 기울기 $|\partial L / \partial \mu_{\text{2D}}|$ 를 누적해 두고, 이 값이 크면 밀도를 조정합니다.

이 기울기가 크다는 건 “이 가우시안을 화면에서 움직이면 손실이 많이 줄어든다”는 뜻입니다. 그런데 움직여도 손실이 계속 크다면, 그건 위치가 틀린 게 아니라 가우시안 하나로는 감당이 안 되는 영역이라는 신호입니다. 그래서 나눕니다. 작은 가우시안이면 복제하고(빈 곳을 메워야 함), 큰 가우시안이면 분할합니다(너무 넓은 영역을 혼자 맡고 있음).

미분 불가능한 조작의 트리거를 미분에서 뽑아 쓴 셈입니다.


4. 직접 해 보기 — 2차원 가우시안 스플래팅

말로만 하면 와닿지 않으니 실제로 돌려 봅니다. 3차원을 2차원으로 줄이고, SfM도 카메라도 없애고, 미분 가능 렌더러 + Adam 만 남긴 최소 버전입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import torch

H = W = 144
yy, xx = torch.meshgrid(torch.linspace(-1, 1, H), torch.linspace(-1, 1, W), indexing="ij")
grid = torch.stack([xx, yy], -1)                       # (H, W, 2)

def render(mu, logs, theta, color, logit):
    """N개의 2D 가우시안을 앞에서 뒤로 알파 블렌딩한다."""
    s = torch.exp(logs)                                # 스케일은 항상 양수여야 하므로 로그로 저장
    c, sn = torch.cos(theta), torch.sin(theta)
    R = torch.stack([torch.stack([c, -sn], -1),
                     torch.stack([sn,  c], -1)], -2)   # (N, 2, 2)

    d  = grid[None] - mu[:, None, None, :]             # 중심에서의 변위
    dl = torch.einsum("nij,nhwj->nhwi", R.transpose(1, 2), d)     # 가우시안 국소 좌표로
    q  = (dl ** 2 / (s ** 2)[:, None, None, :]).sum(-1)

    a = torch.sigmoid(logit)[:, None, None] * torch.exp(-0.5 * q)  # 불투명도는 시그모이드로 [0,1]
    a = a.clamp(0, 0.999)

    T = torch.cumprod(torch.cat([torch.ones(1, H, W), 1 - a[:-1]], 0), 0)   # 누적 투과율
    return ((T * a).unsqueeze(-1) * torch.sigmoid(color)[:, None, None, :]).sum(0)

세 가지만 짚겠습니다.

  • 스케일은 exp, 불투명도와 색은 sigmoid로 감쌌습니다. 제약 조건을 클리핑으로 강제하면 경계에서 기울기가 끊깁니다. 함수로 흡수하면 파라미터는 실수 전체를 자유롭게 돌아다니면서도 결과는 항상 유효 범위 안에 있습니다. 3DGS가 스케일을 로그로, 불투명도를 로짓으로, 회전을 쿼터니언으로 저장하는 이유가 전부 이것입니다.
  • cumprod가 곧 투과율 $T_i = \prod_{j<i}(1-\alpha_j)$ 입니다. 앞 글의 앞→뒤 합성이 그대로 텐서 연산 한 줄이 됩니다.
  • 미분 코드는 한 줄도 쓰지 않았습니다. 전부 매끄러운 연산이니 자동미분이 알아서 합니다. 이것이 “미분 가능한 렌더러”의 실질적인 의미입니다.

옵티마이저는 2편에서 이야기한 대로 파라미터군마다 학습률을 다르게 줍니다.

1
2
3
4
5
6
7
8
9
10
11
opt = torch.optim.Adam([
    dict(params=[mu],    lr=0.006),     # 위치는 조심스럽게
    dict(params=[logs],  lr=0.010),
    dict(params=[theta], lr=0.020),
    dict(params=[color], lr=0.030),     # 색은 크게 움직여도 기하가 안 흔들린다
    dict(params=[logit], lr=0.030),
])

for it in range(1200):
    loss = ((render(mu, logs, theta, color, logit) - target) ** 2).mean()
    opt.zero_grad(); loss.backward(); opt.step()

결과입니다.

2차원 가우시안 스플래팅 학습 과정 무작위로 흩뿌린 가우시안 64개에서 출발해 1200번 반복. 60번 만에 큰 덩어리들의 자리가 잡히고, 300번이면 PSNR 35dB에 도달한다. 밀도 조정도, SH도, 카메라도 없이 미분 가능한 렌더러와 Adam만으로 여기까지 온다.

이 60줄이 3DGS의 핵심 골격입니다. 3차원으로 올리고, 카메라 투영을 붙이고, 색을 SH로 바꾸고, 밀도 조정을 넣으면 나머지가 채워집니다.


5. 자주 나오는 질문들

Q1. 그냥 유한차분으로 기울기를 구하면 안 되나?

파라미터 하나당 렌더링을 두 번 해야 합니다. 3DGS는 가우시안 하나당 59개, 백만 개면 5900만 개의 파라미터입니다. 렌더링을 1억 번 하고 나서야 한 스텝을 뗄 수 있습니다. 1편에서 역전파가 왜 필요한지 이야기한 그 이유가 여기서도 똑같이 성립합니다.

Q2. 미분 가능하기만 하면 최적화가 되나?

아닙니다. 미분 가능성은 필요조건일 뿐입니다. 기울기가 있어도 다음 함정들이 남습니다.

  • 국소 최솟값. 가우시안이 엉뚱한 자리에 자리 잡으면 거기서 빠져나오지 못할 수 있습니다.
  • 보이지 않는 것. 어떤 학습 이미지에도 안 잡힌 영역은 손실에 기여하지 않으니 기울기가 0입니다. 그 부분은 아무렇게나 남습니다. 3DGS 결과를 학습에 없던 시점에서 보면 이상한 덩어리가 나타나는 이유입니다.
  • 가려진 것. 앞이 불투명하면 뒤는 기울기를 못 받습니다. 이건 올바른 동작이지만, 초기에 잘못 생긴 큰 가우시안이 뒤를 영원히 가리면 회복이 어렵습니다.

Q3. 미분 불가능한 밀도 조정이 끼는데도 학습이 되나?

됩니다. 요령은 불연속을 파라미터가 아니라 “구조”에만 두는 것입니다. 가우시안을 하나 복제해 둘로 나눌 때, 새 둘의 합이 원래 하나와 거의 같은 이미지를 만들도록 초기화합니다. 즉 순전파 결과는 거의 연속이고, 파라미터 개수만 바뀝니다. 손실이 튀지 않으니 Adam의 상태도 크게 흔들리지 않습니다.

이건 딥러닝에서 층을 키우는 기법(net2net)이나 트리 기반 모델의 분할과 같은 발상입니다. “기울기로 언제 나눌지 정하고, 나눌 때는 값이 변하지 않게 나눈다.”

Q4. 그럼 딱딱한 경계 표현은 이제 쓸모없나?

그렇지 않습니다. 메시는 여전히 게임·시뮬레이션·제조의 표준이고, 명시적인 표면이 필요한 모든 일에서 대체 불가입니다. 그래서 “가우시안이나 밀도장으로 최적화한 뒤 메시로 뽑아내는” 연구가 활발합니다. 미분 가능 렌더링은 메시를 없앤 게 아니라, 최적화하는 동안만 잠시 다른 표현을 빌려 쓰는 방법을 제공한 것에 가깝습니다.


마치며

  • 역렌더링은 $\partial I / \partial \theta$ 를 요구한다. 고전 래스터화는 안팎 판정·가시성·정렬이라는 세 개의 이산 결정 때문에 이걸 주지 못한다.
  • 가장 근본적인 문제는 “움직여라”는 정보가 전부 불연속에 들어 있다는 것이다. 매끄러운 조각 안에서는 기울기가 0이다.
  • 해결은 세 갈래 — 경계를 부드럽게 하거나, 경계를 정확히 다루거나, 경계가 없는 표현으로 갈아타거나. NeRF와 3DGS는 세 번째다.
  • 3DGS도 완전히 미분 가능하지는 않다. 절단·정렬·밀도 조정은 이산적이다. 다만 순전파가 거의 연속이면 학습은 잘 돌아간다.
  • 제약은 클리핑이 아니라 함수로 흡수한다. exp, sigmoid, 쿼터니언이 전부 그 목적이다.
  • 60줄짜리 2차원 버전으로도 원리는 그대로 재현된다.

이제 마지막 한 조각이 남았습니다. 지금까지 본 것 — MLP, Adam, 볼륨 렌더링, 미분 가능성 — 을 전부 합치면 무엇이 나오는가. 그게 NeRF이고, 3DGS가 무엇을 이어받고 무엇을 버렸는지도 거기서 분명해집니다.


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

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

참고문헌

  1. Liu, S., Li, T., Chen, W., & Li, H. (2019). Soft Rasterizer: A differentiable renderer for image-based 3D reasoning. ICCV. ↩︎

  2. Chen, W., et al. (2019). Learning to predict 3D objects with an interpolation-based differentiable renderer. NeurIPS. — DIB-R. ↩︎

  3. Li, T.-M., Aittala, M., Durand, F., & Lehtinen, J. (2018). Differentiable Monte Carlo ray tracing through edge sampling. ACM TOG, 37(6). — 불연속을 근사하지 않고 경계 위에서 표본추출하는 접근. ↩︎

  4. Zwicker, M., Pfister, H., van Baar, J., & Gross, M. (2001). EWA volume splatting. IEEE Visualization. — 3DGS가 쓰는 2D 공분산 투영(야코비 근사)의 출처. ↩︎

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

License

CC BY-NC-ND 4.0

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