본문으로 건너뛰기
피드

브라우저에서 진짜 하늘과 행성 대기를 렌더링하는 법

frontend 약 10분

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

  • 1

    하늘색은 단순 그라디언트가 아니라 공기 밀도, 파장별 산란, 시선 방향, 태양 방향이 합쳐진 결과임

  • 2

    Rayleigh 산란은 낮의 파란 하늘을, Mie 산란은 태양 주변의 뿌연 빛과 노을 느낌을, 오존 흡수는 수평선과 황혼의 색감을 보강함

  • 3

    행성 대기를 렌더링하려면 깊이 버퍼, 로그 깊이 버퍼, ray-sphere intersection으로 대기권 구간만 샘플링해야 함

  • 4

    실시간 성능을 위해 Transmittance LUT, Sky-view LUT, Aerial Perspective LUT로 비싼 계산을 텍스처 조회로 바꾸는 접근을 실험함

  • 단순히 파란 그라디언트를 깔아서는 ‘진짜 하늘’ 느낌이 안 나는 이유부터 짚고 들어감

    • 하늘색은 배경색이 아니라 햇빛이 공기, 먼지, 오존을 통과하면서 산란되고 흡수된 결과임
    • 관찰자 고도, 태양 각도, 공기 밀도, 대기 두께까지 다 볼륨 안에서 계산해야 그럴듯해짐
  • 첫 출발은 raymarching으로 대기 밀도를 샘플링하는 방식임

    • 카메라에서 광선을 쏘고, 그 광선을 따라 여러 지점을 찍으면서 공기 밀도를 누적함
    • 예시에서는 대기 높이를 100km, Rayleigh scale height를 8km, primary step을 24로 두고 시작함
    • 이렇게 누적한 optical depth로 빛이 얼마나 살아남는지 transmittance를 계산함
  • 낮 하늘의 파란색은 Rayleigh 산란이 담당함

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

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

중요

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

  • 노을을 제대로 만들려면 카메라 방향만 계산해서는 안 됨

    • 처음 구현은 카메라에서 샘플 지점까지 빛이 얼마나 줄어드는지만 봄
    • 하지만 실제로는 태양빛이 샘플 지점에 도착하기 전에도 대기를 통과하며 많이 약해짐
    • 그래서 각 샘플 지점에서 태양 방향으로 한 번 더 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는 카메라와 장면 오브젝트 사이의 대기 안개와 산란광을 저장함

💡

> 처음부터 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처럼 반복되는 비싼 계산을 낮은 해상도 텍스처에 저장해두면, 최종 패스에서는 복잡한 적분 대신 텍스처를 한 번 읽는 쪽으로 바꿀 수 있거든요.

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

댓글

댓글

댓글을 불러오는 중...

frontend

미래적인 텍스트를 만드는 6가지 영화 로고 꼼수

2016년에 나온 타이포그래피 글이지만, SF 영화 로고가 왜 비슷하게 ‘미래적’으로 보이는지 꽤 웃기고 정확하게 해부한다. 기울임, 각진 곡선, V자 형태, 글자 결합, 일부 획 제거, 금속 질감과 별 배경까지 더하면 대충 2092년 느낌이 난다는 식이다.

frontend

블로그 글이 현재 시간을 알려준다? CDN 헤더로 만든 서버리스 시계 실험

이 글은 Cloudflare CDN의 응답 헤더와 브라우저 PerformanceResourceTiming API를 이용해 웹페이지 안에서 현재 시간을 추정하는 실험을 다룬다. NTP처럼 전용 시간 서버를 운영하지 않고도 CDN이 사실상 시간 기준점처럼 동작할 수 있다는 아이디어다. 실제 테스트에서는 웹 시계 오차 범위가 약 60ms로, ntpdig의 약 6ms보다 한 자릿수 정도 덜 정밀했다.

frontend

쿼리 스트링 차단 선언한 개인 웹사이트 운영자의 빡침

한 개인 웹사이트 운영자가 자기 사이트 URL에 임의의 쿼리 스트링을 붙이는 관행을 아예 막겠다고 선언했다. 특히 ref, UTM 같은 추적 파라미터를 남의 URL에 붙이는 건 사용자와 사이트 운영자 모두에게 무례한 일이라는 주장이다.

frontend

번은 좋은데, 이제 앤트로픽 품에 있어서 불안하다는 얘기

글쓴이는 번이 빠르고 실용적인 자바스크립트 런타임이라는 점은 인정하지만, 앤트로픽 인수 이후 장기적인 방향을 신뢰하기 어려워졌다고 말한다. 특히 클로드 코드의 품질 저하, 과금 혼란, 서드파티 하네스 제한 사례를 보며 번도 같은 제품 운영 방식에 휘말릴 수 있다고 우려한다.

frontend

왜 터미널 UI가 다시 뜨고 있나

데스크톱 네이티브 UI 툴킷이 플랫폼마다 흔들리고, Electron 앱은 일관성과 키보드 워크플로를 놓치면서 개발자들이 다시 터미널 사용자 인터페이스(TUI)로 돌아가고 있다는 글이다. 저자는 Claude, Codex 같은 명령줄 도구의 성공을 단순한 복고가 아니라, 운영체제 UI 생태계가 제공하지 못한 빠르고 자동화 가능한 인터페이스에 대한 반응으로 본다.