본문으로 건너뛰기
피드

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

frontend 약 10분
vote
0
댓글
북마크

이 글은 파란 하늘, 노을, 행성 대기를 셰이더로 렌더링하는 과정을 단계별로 파고든다. 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

개인 웹사이트에 JSON-LD 넣는 법, 검색엔진과 크롤러가 내 사이트를 제대로 읽게 만들기

개인 웹사이트에 JSON-LD 구조화 데이터를 추가해 검색엔진과 크롤러가 사이트, 사람, 글, 프로젝트를 더 정확히 이해하게 만드는 실전 가이드야. WebSite, Person, ProfilePage, BlogPosting 같은 노드를 어떻게 연결하고 어느 페이지에 넣어야 하는지 예시 중심으로 설명해.

frontend

Deno, 웹 프로젝트를 데스크톱 앱으로 묶는 `deno desktop` 공개

Deno가 TypeScript 파일 하나부터 Next.js 앱까지 데스크톱 앱으로 패키징하는 `deno desktop`을 공개했다. 아직 안정 릴리스는 아니고 Deno v2.9.0 canary에서만 쓸 수 있지만, 운영체제 WebView 기반의 작은 바이너리, 프레임워크 자동 감지, 내장 자동 업데이트까지 한 번에 노린다.

frontend

파비콘 안에 웹사이트를 숨겨 넣은 개발자, 진짜 됨

한 개발자가 웹사이트의 파비콘 이미지를 작은 저장소처럼 사용해 HTML을 픽셀 RGB 값 안에 넣고, 브라우저에서 다시 읽어 렌더링하는 실험을 했다. 208바이트짜리 HTML payload에 4바이트 길이 헤더를 붙여 총 212바이트를 만들었고, 이를 9x9 픽셀 PNG 안에 87% 사용률로 저장했다.

frontend

스크린이 절대 못 보여주는 색은 어디에 있을까

이 글은 우리가 화면에서 보는 색이 인간이 볼 수 있는 색 전체가 아니라, sRGB와 Display-P3 같은 색역 안에 갇힌 일부라는 점을 파고든다. 특히 숲, 바닷속, 새와 나비의 구조색, 생물발광, 교통신호 LED 같은 실제 세계에는 모니터와 카메라가 제대로 담지 못하는 청록색과 녹색 계열이 꽤 많다는 얘기다. 디스플레이, 카메라, 조명, 렌더링을 다루는 개발자라면 “색상값 하나”가 생각보다 물리와 표준의 타협이라는 걸 체감하게 된다.

frontend

크롬, 매니페스트 버전 2 우회로까지 닫는다

구글 크롬이 매니페스트 버전 2 확장 지원을 사실상 최종 종료 단계로 밀어넣고 있다. 기존에는 플래그나 레지스트리 설정으로 유블록 오리진 같은 확장을 살리는 우회가 있었지만, 크로미움 150과 151을 거치며 그 우회 코드까지 제거되는 흐름이다.