TECHARTNOMAD | MAZELINE.TECH

TECH.ART.FLOW.IO

[번역] 싱글 Pass 체적 수체 Shader(계속 업데이트 중)

jplee 2026. 6. 16. 18:41

저자: YuWong

고품질 수체 렌더링 — 표면 렌더링에서 체적 렌더링로의 진화

전통적인 표면 렌더링에서 빛은 매질의 경계면을 들어가고 나오는 그 순간에만 변화한다. 이런 모델은 기껏해야 디퓨즈, 반사, 굴절 같은 단순한 효과를 지탱할 뿐, 현실의 물이 가진 그런 "안쪽에서부터 배어 나오는" 투명한 느낌을 시뮬레이션하기는 어렵다.

고품질의 수체 렌더링을 구현하려면, 빛이 체적 매질 내부를 전파하는 과정에서 발생하는 에너지 손실과 이득을 반드시 고려해야 한다. 미시적인 물리 관점에서 보면, 체적 매질 속의 빛은 사실 딱 두 가지 일만 겪는다:

흡수 — 빛 에너지가 물 분자와 용해 물질에 삼켜져 열에너지로 전환된다. 파장마다 흡수되는 정도가 다르며, 붉은빛이 가장 먼저 사라지고 청록빛이 가장 멀리까지 관통한다. 이것이 바로 수심이 깊어질수록 물이 점점 푸르게 변하는 기본 톤을 만든다.

산란 — 광자가 물속의 미세한 부유물, 이를테면 진흙, 플랑크톤, 기포 등에 부딪힌 뒤 순수하게 진행 방향만 바꾼다. 에너지가 허공에서 사라지는 것이 아니라, 사방으로 다시 흩어지는 것이다. 이 방향 변화야말로 수체가 탁해 보이고, 물속에서 빛줄기와 후광이 생기는 물리적 근원이다.

전체 광로에서 벌어지는 모든 일은 결국 이 두 가지의 중첩이다. 빛은 아주 짧은 거리를 이동할 때마다 조금씩 흡수되고, 조금씩 산란된다. 그리고 산란되어 나간 그 일부 중 또 아주 작은 몫이 마침 카메라 방향으로 꺾여 들어온다. 그것이 바로 우리가 최종적으로 보게 되는 빛이다.

오프라인 렌더링: 3차원 공간 기반의 랜덤 추적, 즉 수치해

오프라인 렌더러는 어떤 거시적 근사에도 의존하지 않는다. 말 그대로 3차원 공간 안에서 흡수와 산란을 한 걸음 한 걸음 정직하게 계산한다.

시선을 따라 아주 작은 스텝으로 전진하면서, 매 스텝마다 두 가지 일을 한다. 첫째, 그 구간에서 흡수되고 산란되어 사라진 빛을 광선에서 덜어낸다. 둘째, 현재 위치에서 광원 방향으로 또 하나의 광선을 쏘아, 얼마나 많은 빛이 카메라 방향으로 산란되어 들어오는지를 평가한다. 빛이 물속에서 두 번, 세 번 이동하고, 반복적으로 산란된 뒤 다시 빠져나오는 고차 경로들도 실제 광로를 통해 자연스럽게 누적된다. 이 방식은 절대적인 물리 정확성을 보장하지만, 대가는 천문학적인 연산 비용과 샘플링이 수렴하기 전까지 끈질기게 따라붙는 노이즈다.

실시간 렌더링: 스크린 공간 두께 기반의 차원 축소, 즉 해석해

실시간 게임 파이프라인은 픽셀마다 스텝 추적을 수행하는 비용을 도저히 감당할 수 없다. 그래서 현대의 고품질 수체 Shader는 깊이 버퍼를 이용해 빛이 수체를 관통하는 "총 두께"를 구한 뒤, 경로상의 미시적 샘플링을 과감히 포기하고 균일 매질에서의 복사 전달 방정식 해석해를 직접 사용한다.

흡수 처리: 총 두께를 지수 감쇠 법칙, 즉 exp 함수에 바로 대입해 수중 배경색에 한 번 곱해 준다. 이렇게 물리적인 어두워짐이 끝난다.

산란 처리: 경로를 따라 카메라 방향으로 산란되어 들어오는 에너지를 수학적으로 한 번 적분한다. 이 단계에서는 위상 함수, 예를 들어 전방/후방 산란을 제어하는 g 값을 추가로 도입할 수 있으며, 총 흡수에 의해 엄격히 제한되는 수렴 함수, 즉 1 - exp 형태를 유도할 수 있다. 이 함수는 아주 절묘하게도 "빛이 수체에 들어와 산란되는 과정"과 "산란된 빛이 남은 경로에서 다시 흡수되는 과정"을 동시에 포괄한다. 덕분에 깊은 물 영역의 산란 밝기는 자연스럽게 매질 자체 색의 상한으로 수렴하며, 결코 잘못해서 무한히 밝아지지 않는다.

이것이 바로 언리얼 엔진 SingleLayerWater 셰이딩 모델의 체적 셰이딩 알고리즘이다.

SingleLayerWater의 한계

SingleLayerWater처럼 스크린 공간 두께를 기반으로 차원을 축소한 해석해는 게임 업계에서 널리 쓰인다. 하지만 극단적인 성능 타협인 만큼, 바닥의 광로 추적 관점에서는 여전히 심각한 한계를 갖고 있다. 핵심 결함은 바로 이것이다. 전체 광로가 하나의 "두께 스칼라"로 납작하게 눌려 버리고, 그 경로에서 일어나는 모든 공간적 디테일이 한 번의 적분 속에 통째로 뭉개진다는 점이다.

  • 해석해가 성립하려면 전체 광로상의 흡수와 산란 강도가 어디서나 동일해야 한다. 이는 곧 수체 전체가 하나의 흡수/산란 파라미터 세트를 공유해야 한다는 뜻이다. 탁한 물과 맑은 물의 전이, 진흙의 침전, 서로 섞이는 물흐름처럼 매질 기울기가 존재하는 장면을 표현할 수 없다.
  • 1 - exp 수렴 함수의 본질은 "빛이 입사 → 한 번 산란 → 출사"라는 한 경로만을 설명한다. 경로상의 각 미소 지점에서 직접광이 보이는지, 구름 그림자가 만드는 빛기둥, godray 같은 고주파 체적 그림자 현상은 이 해석 적분에 참여할 수 없다. 왜냐하면 지점별 그림자를 넣는 순간, 적분은 더 이상 해석해를 갖지 않기 때문이다.
  • 해석해는 단일 산란만을 적분한다. 그러나 실제의 탁한 매질, 예를 들어 근해의 모래 섞인 물, 우유, 피부 같은 곳에서는 광자가 수십 번씩 반복 산란된 뒤에야 빠져나온다. 그래서 SingleLayerWater는 산란량이 높은 상황에서 색이 어둡고 딱딱하게 치우치며, 내부에서 스스로 빛나는 듯한 부드러움이 부족하다.
  • 위상 함수가 상수로 퇴화한다. 전체 광로가 하나의 산란각을 공유하기 때문에, SingleLayerWater는 보통 위상 함수를 전역 스케일 팩터처럼 사용할 수밖에 없다. 시선 방향을 따라 산란 방향성이 변화하는 느낌을 표현할 수 없고, 태양을 마주볼 때의 글로우처럼 강한 전방 산란의 표현력도 제한된다.

Ray Marching으로 해석해를 다시 수치해로 끌어올리기

GPU 연산 능력의 비약적인 발전, Compute Shader의 보급, 그리고 TAA 노이즈 제거가 기본 파이프라인 구성 요소로 통합된 덕분에, 실시간 렌더링도 마침내 오프라인 렌더링의 "스텝별 진행, 샘플별 평가"라는 사고방식을 다시 집어 들 수 있게 되었다. 다만 샘플 수는 극한까지 줄이고, 그 대가는 시간 축 누적과 공간 필터링이 함께 감당하게 한다.

핵심 전략은 SingleLayerWater가 납작하게 접어 버린 그 광로 위에 샘플 지점을 다시 꽂아 넣는 것이다. 해석해가 뭉개 버린 흡수와 산란의 모든 디테일을 하나씩 되살린다.

1. 시선 방향의 지수 스텝 Ray Marching

시선 방향을 따라 6회 지수 스텝 샘플링을 수행한다. 가까운 곳은 촘촘하고 먼 곳은 성기게 배치한다. 이는 "가까운 영역일수록 시각적 기여가 크고, 먼 영역일수록 흡수가 지배적"이라는 물리 법칙에 들어맞는다. 각 스텝은 현재 위치의 흡수와 산란 기여를 독립적으로 평가한다.

시작점은 블루 노이즈 Dither로 흔들어 준다. 샘플링 밴딩 artifact를 흩뜨리기 위한 것이다. 여기에 TAA의 시간 누적을 결합하면, 6회 샘플링의 노이즈도 몇 프레임 안에 평균화되어 거의 수백 회 샘플링에 가까운 인상을 낼 수 있다.

2. 샘플 지점별 체적 그림자

이것이야말로 해석해로는 도저히 닿을 수 없는 차원이다. 각 샘플 지점에서 shadow map을 다시 샘플링해, 해당 지점이 광원에 직접 비추어지는지 판정한다. 빛을 받는 지점만 산란 에너지를 기여한다. 햇빛이 구름을 뚫고 지나가며 만드는 빛기둥, 바위와 수면 파동이 수중에 드리우는 광선, godray, 수중 그림자의 부드러운 변화가 모두 여기서 자연스럽게 생겨난다.

3. 레일리–미 산란 혼합 위상 함수

각 샘플 지점에서 위상 함수를 다시 한 번 평가하고, 레일리 산란 5% + 미 산란 95%의 혼합 모델을 사용한다. 레일리 항은 파장 의존적인 분자 산란의 푸른 톤을 담당하고, 미 항은 Henyey–Greenstein, g = 0.8이라는 수체의 전형적인 값으로 큰 입자의 강한 전방 산란을 담당한다. 덕분에 수체는 "심해의 푸름"과 "역광의 후광"이라는 원래는 서로 충돌하기 쉬운 두 색감을 동시에 갖게 된다.

4. 다중 산란의 차원 축소 근사

진짜 다중 산란을 하려면 재귀적인 경로 추적이 필요하고, 실시간 렌더링이 그것을 감당할 리 없다. 그래서 여기서는 꽤 영리한 근사를 사용한다. 흡수+산란 중에서 산란이 차지하는 비중에 따라 위상 함수의 방향성을 동적으로 낮춘다. 산란 비중이 높을수록, 광자가 출사하기 전에 더 많이 산란되었고 방향이 더 균일하게 흩어졌다는 뜻이다. 이때 HG 위상을 등방성, 즉 phase → 1 쪽으로 보간한다. 이 한 단계는 거의 무비용으로 "탁한 물이 스스로 빛나는 듯한 느낌"을 되찾아 주며, 단일 산란 모델의 색조 결함을 크게 메운다.

5. 장면 내부 산란, Scene In-Scattering

수중 바닥 물체가 반사한 빛이 수체를 지나 카메라로 돌아올 때도, 그 경로에서 다시 산란된다. 여기서는 단순화된 공식을 사용해 수중 장면광도 한 번 산란된 것처럼 처리한다. 이렇게 하면 수중의 모든 픽셀에 대해 다시 완전한 ray marching을 수행하지 않아도, 먼 곳의 바닥이 자연스럽게 물의 산란 주색으로 흐려진다. 깊은 물의 시각적 두께감이 크게 올라간다.

굴절 색수차의 영리한 처리

어차피 어떤 식으로든 굴절 샘플링을 3번 해야 한다면, 각 샘플마다 서로 다른 노이즈를 쓰는 편이 샘플 활용도를 높일 수 있다.

왜 굴절은 실제 법선 기준으로 스크린 공간 샘플링을 하면 안 되는가

오프라인 렌더링의 굴절은 실제 법선을 따라 계산되므로, 현실에서 "젓가락을 물에 넣으면 휘어 보이는" 현상을 완벽히 시뮬레이션할 수 있다.

하지만 우리가 스크린 공간에서 경로 추적을 이렇게 해 버리면, 화면 하단에 있는 물의 샘플링 경로가 명백히 화면 범위를 벗어나게 되고, 가장자리 단절이 생긴다. 게다가 스크린 공간에서의 샘플링 경로가 이전보다 훨씬 길어지기 때문에, 올바른 결과를 얻으려면 Ray Marching에 더 심하게 의존하게 된다.

그래서 굴절은 접선 방향 법선을 기준으로만 교란을 주는 식으로 처리해야 한다.

왜 색수차는 실제 법선 기준으로 스크린 공간 샘플링을 해도 되는가

색수차는 RGB 각각에 아주 미세한 오프셋을 주는 것에 불과하기 때문이다. 따라서 굴절 부분처럼 타협할 필요가 없다.

굴절 블러의 샘플링 알고리즘

Interleaved Gradient Noise를 사용하면 분포의 상관성 문제 때문에, 국소적인 편향으로 인해 RGB 세 채널의 색수차가 서로 다른 정도로 틀어질 뿐 아니라, 더 큰 문제는 샘플이 분포 함수를 제대로 따르지 못하게 된다는 점이다.

「시탐법」으로 굴절 블러의 깊이 경계 문제 해결

블러 반경은 깊이 차와 양의 상관관계를 갖는다. 그래서 현재 픽셀의 깊이 차를 블러 반경의 곱셈 계수로 고려해야 한다. 하지만 이 규칙은 깊이 경계에서는 성립하지 않는다.

Ray Marching으로 경로 추적을 흉내 낼 수 있다. 미세 표면 분포에 의해 무작위로 결정된 굴절 광선을 따라 깊이에 대해 Ray Marching 교차 판정을 수행하고, 그 교차점을 색상 샘플 위치로 삼으면 이 광로의 실제 색을 얻을 수 있다.

다시 말해, 명중할 때의 스텝 거리가 멀수록 굴절 원점에서 더 많이 벗어난다는 뜻이고, 보이는 결과는 블러 반경이 더 커진 것처럼 된다. 효과는 경로 추적과 거의 차이가 없다.

하지만 Ray Marching의 비용은 정말 너무 크다. 그래서 직관적으로 떠올린 것이 바로 「시탐점 보정법」이다.

하지만 Ray Marching의 비용은 정말 너무 크다. 그래서 직관적으로 떠올린 것이 바로 「시탐점 보정법」이다.

보조 설명: 「시탐점 보정법」이 실제로 하고 있는 일
이 방법은 말하자면, 비싼 Ray Marching을 한 번의 "질문"으로 대신하는 기법이다. 처음에는 현재 픽셀의 깊이 차만 믿고 굴절 오프셋을 대략 예측한다. 하지만 깊이 경계에서는 그 예측이 자주 틀린다. 그래서 예측한 위치, 즉 시탐점에 먼저 가서 깊이를 한 번 더 물어본다. 그곳의 깊이 차가 크면 "실제 광로는 더 멀리 가야 했구나"라고 판단하고, 작으면 "너무 멀리 갔구나"라고 판단해 오프셋을 되돌린다.
핵심은 스크린 공간에서의 굴절 이동량이, 해당 광선이 지나간 깊이 차에 거의 선형적으로 비례한다는 점이다. 그래서 시탐점 깊이 차 / 현재 픽셀 깊이 차라는 단순한 비율이 곧 보정 계수가 된다. 완전한 Ray Marching처럼 여러 번 교차를 찾는 것은 아니지만, 깊이 경계에서 가장 크게 틀어지는 1차 오차를 한 번에 잡아 주는 셈이다.
실전에서는 이 비율을 그대로 쓰기보다 최소/최대값으로 클램프하는 편이 안전하다. 매우 얇은 물체, 화면 밖으로 나가는 샘플, 급격한 깊이 단절에서는 보정값이 과도하게 튈 수 있기 때문이다. 즉 이 기법은 물리적으로 완전한 교차 탐색은 아니지만, 비용 대비 깊이 경계의 굴절 블러 붕괴를 상당히 그럴듯하게 눌러 주는 "1-step ray marching 근사"라고 보면 된다.

현재 픽셀의 깊이 차로 조정한 블러 반경을 이용해 스크린 공간 굴절 오프셋을 얻고, 그것을 시탐점으로 삼는다. 그 시탐점에서 깊이 버퍼를 한 번 샘플링해 시탐점의 깊이 차를 구한 뒤, 시탐점 깊이 차 / 현재 픽셀 깊이 차의 비율을 해당 픽셀 굴절 오프셋의 곱셈 계수로 사용한다.

결과는 경로 추적과 거의 구분되지 않는다. 그리고 나중에 보니, 이 방식은 수학적으로도 실제로 성립한다.

굴절 광로의 스크린 공간 오프셋은 명중점의 깊이 차와 엄격하게 정비례하는 선형 관계를 갖기 때문이다. 따라서 탐색점에서 샘플링한 깊이 차는 "실제 오프셋이 얼마여야 하는지"를 정량적으로 알려 주는 근거로 바로 사용할 수 있다.

현재 픽셀의 깊이 차가 작아 굴절 오프셋도 작아야 하는데, 시탐 결과의 깊이 차가 오히려 매우 크다면, 알고리즘은 이 광로가 3D 공간에서 실제로는 현재 픽셀 깊이가 암시하는 것보다 훨씬 더 긴 스크린 거리를 지나갔다고 판단한다. 그래서 최종 샘플 지점을 더 멀리 밀어낸다.

반대로 현재 픽셀의 깊이 차가 커서 굴절 오프셋도 커야 하는데, 시탐 결과의 깊이 차가 매우 작다면, 알고리즘은 이 광로가 3D 공간에서 실제로는 현재 픽셀 깊이가 암시하는 것보다 훨씬 더 짧은 스크린 거리를 지나갔다고 판단한다. 그래서 최종 샘플 지점을 더 가까이 당겨 온다.

카스틱 처리

카스틱은 깊이로 재구성한 수중 바닥 좌표를 이용해, 평행광 방향으로 투영된 텍스처를 샘플링하는 방식이다. 실제로 바닥이 존재해야 하는 것은 아니다. 이 부분은 더 길게 말할 필요도 없다.

카스틱은 태양광이 물바닥에 닿은 뒤 다시 빠져나오는 현상이므로, 여기서는 흡수가 두 번 일어난 것으로 근사할 수 있다.

카스틱이 산란에 기여하는 효과를 모사하기 위해, 산란 Ray Marching 과정에서도 카스틱을 샘플링한다.

원문
(73 封私信 / 55 条消息) 单Pass体积水体Shader(正在持续更新中) - 知乎