---
title: "브라우저에서 진짜 하늘과 행성 대기를 렌더링하는 법"
published: 2026-05-12T13:26:46.000Z
canonical: https://jeff.news/article/2360
---
# 브라우저에서 진짜 하늘과 행성 대기를 렌더링하는 법

이 글은 파란 하늘, 노을, 행성 대기를 셰이더로 렌더링하는 과정을 단계별로 파고든다. Rayleigh 산란, Mie 산란, 오존 흡수, 깊이 버퍼, 행성 스케일 처리, LUT 기반 최적화까지 다뤄서 WebGL·React Three Fiber 쪽 개발자에게 꽤 실전적인 자료다.

- 단순히 파란 그라디언트를 깔아서는 ‘진짜 하늘’ 느낌이 안 나는 이유부터 짚고 들어감
  - 하늘색은 배경색이 아니라 햇빛이 공기, 먼지, 오존을 통과하면서 산란되고 흡수된 결과임
  - 관찰자 고도, 태양 각도, 공기 밀도, 대기 두께까지 다 볼륨 안에서 계산해야 그럴듯해짐

- 첫 출발은 raymarching으로 대기 밀도를 샘플링하는 방식임
  - 카메라에서 광선을 쏘고, 그 광선을 따라 여러 지점을 찍으면서 공기 밀도를 누적함
  - 예시에서는 대기 높이를 100km, Rayleigh scale height를 8km, primary step을 24로 두고 시작함
  - 이렇게 누적한 optical depth로 빛이 얼마나 살아남는지 transmittance를 계산함

- 낮 하늘의 파란색은 Rayleigh 산란이 담당함
  - 짧은 파장일수록 더 강하게 산란되기 때문에 빨강보다 파랑이 훨씬 많이 관찰자 쪽으로 튐
  - 그래서 낮에는 낮은 고도에서 밝은 파란색이 쌓이고, 위로 갈수록 공기가 얇아져 더 어둡고 깊은 파랑이 나옴
  - 수평선 쪽은 광선이 더 많은 대기를 통과하니 흰 안개처럼 밝아지는 것도 자연스럽게 생김

- Rayleigh만으로는 부족해서 Mie 산란과 오존 흡수를 추가함
  - Mie 산란은 먼지나 에어로졸 같은 큰 입자 때문에 생기는 산란이라 태양 주변의 뿌연 광휘와 노을 헤이즈를 만든다
  - 오존 흡수는 빛을 다시 흩뿌리는 게 아니라 특정 파장을 제거해서 수평선과 황혼의 색을 더 깊게 잡아줌
  - 결과적으로 하늘색은 더 자연스러운 ‘스카이 블루’가 되고, 해가 낮을 때 보라빛·주황빛 변화도 살아남

> [!IMPORTANT]
> 이 글의 핵심은 “하늘을 그리는 법”이 아니라 “빛이 대기 안에서 얼마나 사라지고, 얼마나 카메라 쪽으로 꺾이는지”를 계속 누적하는 모델임.

- 노을을 제대로 만들려면 카메라 방향만 계산해서는 안 됨
  - 처음 구현은 카메라에서 샘플 지점까지 빛이 얼마나 줄어드는지만 봄
  - 하지만 실제로는 태양빛이 샘플 지점에 도착하기 전에도 대기를 통과하며 많이 약해짐
  - 그래서 각 샘플 지점에서 태양 방향으로 한 번 더 light marching을 돌려 sun optical depth를 더함
  - 이 중첩 루프 덕분에 해가 수평선 근처로 내려갈 때 빛이 길게 통과하며 붉어지는 효과가 나옴

- 평면 배경 셰이더를 실제 3D 장면에 붙이려면 depth buffer가 필요함
  - 배경만 칠하는 게 아니라 카메라와 오브젝트 사이 공간에 대기 안개를 채워야 함
  - screen UV와 depth buffer에서 world position을 복원하고, 카메라 위치에서 그 점까지 3D ray를 구성함
  - 가까운 오브젝트는 선명하고, 멀리 있는 오브젝트는 대기를 더 많이 통과하니 흐려지는 식으로 처리됨

- 행성 대기 렌더링에서는 ray-sphere intersection이 핵심이 됨
  - 대기는 행성보다 살짝 큰 구 형태의 shell로 보고, 광선이 대기권에 들어오고 나가는 지점을 찾음
  - 광선이 행성 표면에 먼저 닿으면 거기서 raymarching을 멈춤
  - 다른 장면 오브젝트가 표면보다 앞에 있으면 scene depth에서 멈춰야 오브젝트가 대기 뒤에 파묻히는 버그를 피할 수 있음

- 행성 스케일에서는 logarithmic depth buffer도 필요해짐
  - 행성 반지름에 비해 대기 두께는 몇 km 수준이라, 먼 거리에서는 깊이 차이를 구분하기 어려움
  - React Three Fiber의 Canvas에서 logarithmicDepthBuffer를 켜고, 셰이더에서는 로그 깊이를 다시 ray distance로 변환함
  - 이걸 안 하면 대기 shell과 행성 표면 사이에서 depth fighting이 튀기 쉬움

- 보너스로 일식까지 처리함
  - 샘플 지점에서 태양 방향과 달 방향의 각도 차이를 비교해서 달이 태양 원반을 가리는지 계산함
  - 단순 dot product만 쓰면 크기 차이를 반영하지 못하니, 태양과 달의 angular radius를 비교함
  - 완전히 가리는 경우, 부분적으로 겹치는 경우, 아예 안 겹치는 경우를 나눠 sun visibility를 0~1 범위로 곱함

- 화성 같은 다른 행성 대기도 상수만 바꿔서 어느 정도 흉내낼 수 있음
  - 예시 Mars 설정은 planetRadius 3390km, atmosphereRadius 3500km로 약 110km 두께의 대기를 둠
  - Rayleigh, Mie, 오존 관련 계수를 바꾸면 먼지 많은 주황빛 대기와 화성 특유의 푸른 노을을 만들 수 있음
  - 물론 값은 근사치라 과학 시뮬레이터라기보다는 시각적으로 납득 가능한 렌더링에 가까움

- 문제는 이 방식이 꽤 비싸다는 것임
  - primary raymarching 샘플이 많고, 각 샘플마다 태양 방향 light marching까지 들어감
  - 게다가 전체 화면 해상도에서 이 계산을 반복하니 실시간 렌더링에서는 부담이 큼
  - 그래서 글 후반부는 Sebastian Hillaire의 LUT 기반 대기 렌더링 접근으로 넘어감

- LUT 방식은 비싼 대기 계산을 여러 텍스처로 쪼개 저장하는 전략임
  - Transmittance LUT는 특정 고도와 태양 각도에서 빛이 얼마나 살아남는지 저장함
  - 글에서는 이 LUT를 250 x 64 해상도의 Frame Buffer Object에 렌더링함
  - Sky-view LUT는 특정 방향으로 봤을 때 하늘이 어떤 색인지 저장함
  - Aerial Perspective LUT는 카메라와 장면 오브젝트 사이의 대기 안개와 산란광을 저장함

> [!TIP]
> 처음부터 LUT로 가면 구조가 복잡해지니, 글의 흐름처럼 먼저 느리지만 직관적인 raymarching 버전을 만든 뒤 병목을 LUT로 빼는 편이 이해하기 좋음.

- 최종 합성 단계에서는 세 LUT를 조합해서 장면을 완성함
  - 배경 픽셀은 Sky-view LUT에서 하늘색을 가져옴
  - 장면 지오메트리는 Aerial Perspective LUT의 RGB 산란광과 alpha transmittance로 원래 색을 섞음
  - Transmittance LUT는 중간 단계에서 태양빛 생존량을 빠르게 조회하는 역할을 함

- 저자도 LUT 구현이 프로덕션급은 아니라고 선을 그음
  - sky-view 쪽에서 banding과 flickering이 있고, 몇 가지 shortcut 때문에 최적화도 완벽하지 않다고 함
  - WebGPU와 compute shader로 갔으면 FBO 기반 우회가 덜 필요했을 거라고도 언급함
  - 그래도 nested lightmarching을 texture lookup으로 바꾼 것만으로도 체감 가능한 성능 개선이 나옴

---

## 기술 맥락

- 이 글에서 가장 중요한 선택은 처음부터 물리 기반 대기 모델을 썼다는 점이에요. 단순 그라디언트는 빠르지만, 태양 각도나 고도 변화에 반응하지 못하거든요. 그래서 Rayleigh, Mie, 오존 흡수를 분리해서 실제 색 변화가 생기는 이유를 셰이더 안에 넣은 거예요.

- raymarching은 구현을 이해하기 좋지만 비용이 커요. 각 픽셀마다 대기 안을 여러 번 샘플링하고, 노을까지 하려면 각 샘플에서 태양 방향으로 또 march해야 하거든요. 그래서 정확한 그림을 먼저 만든 뒤 LUT로 계산을 빼는 흐름이 자연스러워요.

- 행성 렌더링에서 depth buffer 처리가 까다로운 이유는 스케일 차이 때문이에요. 행성 반지름은 수천 km인데 대기 두께는 수십에서 100km 안팎이라, 일반 깊이 버퍼로는 표면과 대기층을 멀리서 안정적으로 구분하기 힘들어요.

- LUT 방식은 실시간 그래픽스에서 자주 쓰는 타협이에요. Transmittance LUT처럼 반복되는 비싼 계산을 낮은 해상도 텍스처에 저장해두면, 최종 패스에서는 복잡한 적분 대신 텍스처를 한 번 읽는 쪽으로 바꿀 수 있거든요.

## 핵심 포인트

- 하늘색은 단순 그라디언트가 아니라 공기 밀도, 파장별 산란, 시선 방향, 태양 방향이 합쳐진 결과임
- Rayleigh 산란은 낮의 파란 하늘을, Mie 산란은 태양 주변의 뿌연 빛과 노을 느낌을, 오존 흡수는 수평선과 황혼의 색감을 보강함
- 행성 대기를 렌더링하려면 깊이 버퍼, 로그 깊이 버퍼, ray-sphere intersection으로 대기권 구간만 샘플링해야 함
- 실시간 성능을 위해 Transmittance LUT, Sky-view LUT, Aerial Perspective LUT로 비싼 계산을 텍스처 조회로 바꾸는 접근을 실험함

## 인사이트

프론트엔드에서 3D 비주얼을 한다면 이 글은 그냥 예쁜 셰이더 튜토리얼이 아니라, 물리 기반 렌더링을 브라우저 제약 안에서 어떻게 타협하는지 보여주는 케이스임. 특히 ‘일단 raymarching으로 맞는 그림을 만들고, 그다음 LUT로 비용을 줄인다’는 흐름이 꽤 배울 만함.
