Skip to content

브라우저에서 오픈 월드 만들기, 22부: 빛을 줄 수 있는 구름, 그리고 잘 먹여야 하는 컬링

글: Oleg Sidorkin, Cinevva CTO 겸 공동 창업자

처음 오셨나요? 시리즈 가이드를 보세요. spike가 무엇인지 설명하고 모든 부분을 링크해 둡니다.

21부는 결과적으로 별 소득이 없었던 렌더링 기법에 관한 것이었습니다. 이번 부분에는 소득이 있었던 것 하나, 그리고 동작하게 만들려면 세심한 수정이 필요했던 것 하나가 있습니다. Spike 43은 하늘입니다. 물리 기반 대기와 볼류메트릭 구름, 즉 어떤 장면을 기술 데모가 아니라 하나의 장소로 읽히게 만드는 토대입니다. Spike 44는 meshlet 방식의 GPU 컬링이고, 거기서 얻은 교훈은 오클루전 테스트의 좋고 나쁨은 거기에 무엇을 먹이느냐에 달려 있다는 것입니다.

그래디언트가 아니라 물리에서 나온 하늘

새 탭에서 Spike 43 열기 ↗ · 소스 보기

거의 모든 영화적인 날씨 효과는 두 가지 기반 위에 놓입니다. 하나는 물리 기반 대기입니다. 하늘색과 태양색이 손으로 조정한 그래디언트가 아니라 물리로부터 시간대를 따라가게 합니다. 다른 하나는 볼류메트릭 구름 볼륨입니다. 하늘이 구운 큐브맵 대신 3D 구조를 갖게 합니다. Spike 43은 기존 WebGPU와 TSL 스택 위에 바로 이 한 쌍을 만들 뿐 그 이상은 하지 않습니다. 이 둘이 존재하고 나면 날씨 스택의 나머지(안개, 신의 광선, 젖은 표면, 눈)는 이미 알려진 더 작은 후속 작업들의 연속이 되기 때문입니다.

대기는 Hillaire 2020 모델입니다. WGSL 컴퓨트 셰이더에서 계산하는 룩업 테이블 묶음이죠. 투과율 테이블은 Rayleigh, Mie, 오존 밀도 프로파일을 통과하는 태양광을 적분하고 태양이 움직일 때만 다시 계산합니다. 스카이뷰 테이블은 매 프레임 다시 굽습니다. 게이팅하느라 코드를 쓸 가치가 없을 만큼 충분히 싸기 때문이며, 지평선 주변에서 밴딩을 피하려고 비선형 파라미터화를 씁니다. 다중 산란은 지금은 제대로 된 테이블 대신 해석적 피팅을 쓰는데, 노을이 제대로 읽히니 그 지름길이 잘 숨어 있는 셈입니다. 구름은 Schneider Nubis 방식으로 수평 슬래브를 가로지르는 레이 마치이고, 형태는 32³ Worley 텍스처로 침식된 128³ Perlin-Worley 텍스처에서 나오며, 둘 다 부팅 시 컴퓨트로 굽고 네트워크 요청은 없습니다. Beer 법칙 감쇠, 이중 로브 위상 함수, powder 근사로 빛을 줍니다. 핵심 결합은 구름의 태양색이 매 스텝마다 같은 투과율 테이블을 샘플링한다는 점입니다. 그래서 구름 조명이 두 번째 튜닝 패스 없이도 노을을 따라갑니다.

M1에서는 절반 해상도 구름 기준으로 전체가 1.1에서 2.0 ms에 들어옵니다. 6 ms 예산을 한참 밑돌고, GPU 메모리는 약 14 MB를 쓰며, 100 FPS를 넘겨 돌아갑니다. 두 가지 명제는 실전에서 모두 버텼습니다. 노을은 히어로 샷, 렌더러가 영화처럼 느껴지게 하는 그 순간이고, 시간대별 튜닝 없이 물리에서 떨어져 나옵니다. 그리고 행복한 우연도 있었습니다. 구름 슬래브를 800 m와 4000 m 사이로 파라미터화하니, 낮은 카메라에서 보면 지평선의 먼 구름이 어두운 산등성이처럼 읽힙니다. 누구도 배경 지형을 모델링하지 않았는데 세계에 배경 지형이 생긴 셈입니다.

기록해 둘 만한 아키텍처 메모가 하나 있습니다. 가장 자연스러운 형태는 하늘을 먼저 스왑 체인(swap chain)에 그린 다음 three.js가 autoClear = false로 그 위에 지오메트리를 그리게 하는 것입니다. 이건 r184의 WebGPU 렌더러에서는 살아남지 못합니다. 그 플래그가 WebGL에서처럼 컬러 load op를 게이팅하지 않아서 three.js가 매 프레임 하늘을 덮어 버리기 때문입니다. 해결책은 three.js를 오프스크린 타깃에 렌더링하고, 스왑 체인을 소유하는 우리 자신의 패스에서 최종 합성(mix(skyCloud, scene, scene.alpha) 다음 ACES 다음 sRGB)을 하는 것입니다.

오클루더만큼만 좋은 컬링

새 탭에서 Spike 44 열기 ↗ · 소스 보기

Spike 44는 네 가지 렌더링 모드를 서로 벤치마크합니다. 평범한 포워드, CPU 클러스터 컬링, GPU 컴퓨트 컬링, 그리고 Hi-Z 오클루전 컬링이 붙은 visibility buffer입니다. 흥미로운 건 Hi-Z 경로인데, 여기에 조용한 버그가 하나 있었습니다. HUD는 오클루전이 켜져 있다고 말했지만 "Hi-Z killed" 카운터는 영원히 정확히 0.0%에 머물렀습니다. 프러스텀 컬링은 동작했으니 컬 셰이더의 앞단은 문제가 없었습니다. 오클루전 쪽 절반이 풀 비용을 치르는 무동작이었던 겁니다.

Hi-Z 오클루전 테스트는 클러스터의 바운딩 박스를 화면에 투영하고, 화면 사각형이 약 2×2 텍셀이 되도록 깊이 피라미드의 mip 레벨을 고르고, 그 사각형 안에서 가장 깊은 오클루더 깊이를 샘플링한 뒤, 클러스터의 가장 가까운 점이 그 오클루더보다도 멀면 클러스터를 거부합니다. 깊이 피라미드는 매 프레임 만들어지는데, 불투명 깊이 프리패스로 mip 0을 시드한 다음 위로 max-reduce 합니다. 이 불투명 프리패스는 의도적으로 단단한 오클루더, 즉 지면과 나무별 줄기 프록시만 포함합니다. 알파 테스트된 잎은 구멍을 뚫어 max-reduce를 속이기 때문입니다.

버그는 기하학적이었지 논리적인 게 아니었습니다. 줄기 프록시는 0.5 m × 4 m × 0.5 m 박스였습니다. 30 m에서는 화면에 약 17픽셀로 투영됩니다. 그런데 전형적인 50 m 풀 클러스터는 mip 5를 고르고, 거기서는 텍셀 하나가 소스 픽셀 32개를 덮습니다. 17픽셀짜리 줄기는 단일 mip-5 텍셀을 완전히 덮지 못하므로, 줄기에 닿는 모든 텍셀은 주변 지면에도 닿습니다. 맨 처음의 2×2 max-reduce는 더 큰 깊이 값, 즉 줄기 뒤의 더 먼 지면을 고르고, 그래서 줄기의 깊이는 첫 번째 리덕션에서 지워집니다. mip 5에 이르면 피라미드는 거의 어디서나 지면 깊이를 들고 있고, 클러스터는 결코 지면보다 멀지 않으며, 그래서 어떤 것도 결코 오클루드되지 않습니다.

해결책은 프록시를 그것이 떨어지는 텍셀들을 지배할 만큼 충분히 크게, 나무의 목재가 아니라 실루엣에 맞춰 크기를 정하는 것입니다. 대략 2 m × 6 m × 2 m 프록시는 여전히 실제 수관보다 작아서 틈으로 보이는 잎이 과도하게 컬링되는 일은 없지만, 중요한 거리까지 max-reduce를 견딜 만큼은 크고, 그러자 오클루전 카운터가 즉시 0이 아니게 되었습니다. 이 교훈은 프로덕션 엔진을 위한 규칙으로 일반화됩니다. Hi-Z 오클루더로 신뢰받는 것은 무엇이든 물리적 본체가 아니라 화면상 실루엣에 맞춰 크기를 정해야 합니다. 트인 식생 장면에서 Hi-Z의 효과는 깊이 테스트 수식의 우아함이 아니라 해당 mip에서의 오클루더 커버리지가 좌우하기 때문입니다. 풀 대 풀 오클루전은 어차피 발동할 수 없습니다. 풀잎은 그 아래 지면과 같은 깊이에 있으니까요. 그래서 진짜 이득은 나무가 먼 식생을 가리는 것, 그리고 나무가 나무를 가리는 것입니다.

이 장에서 언급한 기술

Hillaire 2020 대기 LUT. 투과율 테이블(Rayleigh, Mie, 오존 프로파일을 통과하는 태양광)은 태양이 움직일 때만 다시 계산하고, 여기에 지평선 비선형 파라미터화를 곁들인 프레임별 스카이뷰 테이블을 더하면, 손으로 조정한 그래디언트 없이 시간대를 따라가는 물리 기반 하늘색과 태양색이 나옵니다. 어떤 아티팩트가 제대로 된 굽기를 강요하기 전까지는 해석적 다중 산란 피팅이 완전한 테이블을 대신합니다. 노을은 시간대별 튜닝 없이 물리에서 떨어져 나옵니다.

Schneider Nubis 볼류메트릭 구름. 수평 슬래브를 가로지르는 레이 마치로, 부팅 시 구운 128³ Perlin-Worley 텍스처를 32³ Worley 텍스처로 침식해 형태를 잡고, Beer 법칙 감쇠와 이중 로브 위상과 powder 항으로 빛을 줍니다. 매 스텝마다 같은 투과율 테이블에서 구름의 태양색을 샘플링하면 구름 조명이 일출과 일몰을 공짜로 따라갑니다. 절반 해상도 레이 마치는 전체 해상도보다 대략 4배 싸고, 전형적인 거리에서 눈에 띄는 품질 손실이 없습니다. 표준적인 프로덕션 트레이드오프죠.

r184에서 raw WebGPU와 three.js 합성하기. 하늘을 스왑 체인에 그린 다음 autoClear = false로 그 위에 three.js 지오메트리를 그리는 방식은 실패합니다. 그 플래그가 WebGPU 백엔드에서 컬러 load op를 게이팅하지 않기 때문입니다. three.js를 오프스크린 RGBA16F 타깃에 렌더링하고, 스왑 체인을 소유하는 패스에서 최종 mix 더하기 톤맵 더하기 sRGB를 하세요.

Hi-Z 오클루전 컬링과 오클루더 크기 정하기. max-reduce로 만든 깊이 피라미드는 GPU 컬 패스가 화면 사각형 내 가장 깊은 오클루더보다 가장 가까운 점이 뒤에 있는 클러스터를 거부하게 해줍니다. 오클루더가 선택된 mip에서 텍셀을 지배하기에 너무 작으면 이 테스트는 조용히 아무것도 하지 않습니다. 첫 max-reduce가 오클루더의 깊이를 그 뒤의 더 먼 배경으로 바꿔 버리기 때문입니다. 오클루더는 물리적 본체가 아니라 화면상 실루엣에 맞춰 크기를 정해야 합니다. GPU 주도 LOD를 보세요.


29부 중 22부. 이전: 21부 - 더 빠르지 않았던 더 빠른 렌더러 다음: 23부 - 아바타 쉰 명과 방 안의 목소리 시리즈 가이드: /ko/blog/2026-02-25-open-world-browser-series-guide