TECHARTNOMAD | MAZELINE.TECH

TECH.ART.FLOW.IO

[SIGGRAPH 2026] SPEEDING UP PATH TRACING VIA ORCA (ONLINE RADIANCE CACHE ACCELERATION)

jplee 2026. 8. 5. 19:22

원문 : SIGGRAPH 2026 Advances in Real-Time Rendering in Games course

 

SIGGRAPH 2026 Advances in Real-Time Rendering in Games course

Course Description Modern video games employ a variety of sophisticated algorithms to produce groundbreaking 3D rendering pushing the visual boundaries and interactive experience of rich environments. This course brings state-of-the-art and production-prov

advances.realtimerendering.com

초록: 안타깝게도 패스 트레이싱은 최신 하드웨어에서도 상당히 느리며, 그 결과 실시간 게임 렌더링에 구현하기가 매우 어렵습니다. 이 발표에서는 실시간 렌더링 용도로 특별히 설계한 사용자 정의 래디언스 캐시를 활용해 패스 트레이싱 속도를 획기적으로 높이는 간결한 방법을 소개합니다.

ORCA라 부르는 이 기법은 이전 프레임의 시간적 히스토리에 의존하지 않습니다. 모든 데이터 구조는 현재 프레임에서 즉시 생성되며 다음 프레임까지 유지할 필요가 없으므로, 완전히 동적인 장면에 특히 적합합니다. ORCA의 구현 방식과 더불어, 기존 실시간 패스 트레이서에 최소한의 변경만으로 이 기법을 통합하는 방법도 함께 설명합니다.

발표자 소개:

Jon Greenberg는 4세대에 걸친 콘솔 게임 하드웨어를 경험하며 26년 넘게 게임 업계에서 일해 왔습니다. Midway Games Chicago에서 경력을 시작한 뒤 NetherRealm으로 자리를 옮겨, 높은 성능이 요구되는 격투 게임 개발에 참여했습니다.

Mortal Kombat 프랜차이즈의 렌더링 팀을 이끌면서 게임 속 폭력적이고 잔혹한 표현 기술을 발전시키는 문제에…… 어쩌면 지나치게 많은 시간을 들여 고민하기도 했습니다. 말 그대로 무언가를 ‘베어 내는’ 그래픽이라는 점에서, 이는 “최첨단 그래픽(cutting-edge graphics)”이라는 표현을 완전히 다르게 해석한 셈입니다.

Injustice 2와 Mortal Kombat 11 개발을 마친 뒤 NetherRealm을 떠나 Electronic Arts의 SEED 팀에 합류했으며, 현재는 새로운 렌더링 기법을 연구하고 있습니다.

 

안녕하세요 여러분. 저는 Jon Greenberg입니다. 바로 시작하겠습니다.

 

먼저 path tracing에 대해 간단히 소개하겠습니다. Path tracing은 빛이 세계 곳곳을 튕겨 다니다 최종적으로 눈에 도달하는 과정을 모델링하려는 기법입니다. 빛을 입자의 연속으로 취급하며, 단순화를 위해 진행 방향을 뒤집어 경로가 눈에서 출발한다고 가정합니다. 즉, 프레임의 모든 픽셀에서 경로를 출발시켜 무엇에 부딪히는지 확인하고, 경로가 최종적으로 종료될 때까지 튕겨 다니며 조명 정보를 누적합니다.

PT는 매우 사실적인 결과를 만들어내며, 그 덕분에 photorealistic 이미지를 생성하는 사실상의 표준이 되었습니다. 하지만 아쉽게도 노이즈가 매우 심하고, 수렴하려면 보통 픽셀당 많은 샘플(samples-per-pixel)이 필요합니다. 이렇게 높은 픽셀당 샘플 수가 고해상도와 결합되면서 PT는 대체로 매우 느리다고 여겨져 왔습니다.

 

많은 테라플롭에 달하는 현대 GPU 성능, 특히 하드웨어 ray-tracing의 가속에 힘입어 사람들은 게임 엔진에서 PT를 구동하는 것을 진지하게 검토하기 시작했습니다. PT 지원이 점점 늘어나고는 있지만 아직은 소수의 게임에만 탑재되어 있으며, 대체로 많은 근본적인 타협을 동반한다는 점을 고려할 필요가 있습니다.

여기서 핵심은, 각 경로를 구성하는 비용이 매우 크기 때문에 보통 픽셀당 최대 하나의 경로 샘플만 사용한다는 점입니다. 이는 매우 노이즈가 많은 결과를 낳기 때문에, PT 솔루션은 수렴에 의존하지 않고 대신 denoising을 활용합니다. 그럼에도 PT는 여전히 매우 비용이 크며, 보통 고사양 GPU에 한정되거나 공간적·시간적 프레임 업스케일러와 결합되어 사용됩니다.

 

PT(Path Tracer)는 고사양 GPU에서는 꽤 쓸 만하지만, 안타깝게도 하드웨어는 저렴해지기는커녕 점점 비싸지고 있습니다. 어떤 면에서는 오히려 뒷걸음질치고 있으며, 하드웨어가 더 많은 기능을 갖추더라도 저사양 GPU는 사라지지 않습니다.

그래서 아쉽게도 PT를 실용적으로 만들고 자원을 투자하려면, PT를 점점 더 낮은 사양의 하드웨어로 확장하는 더 나은 방법을 찾아야 할 것입니다. 달리 말하면, PT를 1SPP보다 훨씬 낮은 수준으로 확장하는 방법을 찾아야 합니다.

짚고 넘어갈 점은, 지금부터 제가 말하는 path tracing은 사실 primary ray의 직접 조명(direct lighting)을 건너뛴 것이라는 점입니다. 저사양에서의 속도를 노린다면 가장 마지막에 할 일이 primary view를 ray tracing으로 처리하는 것이기 때문입니다. 따라서 이 발표의 나머지 부분에서 path tracing이라고 하면 간접 조명(indirect lighting)의 path tracing을 가리킵니다.

 

실시간 path tracing의 속도를 높이려 한다면, 흔히 쓰이는 offline path tracing 최적화를 아주 간단히 살펴보는 것이 좋겠습니다. 상당히 단순화한 것이지만, 일반적으로 주된 공략 방향은 취한 샘플의 품질을 개선하는 것입니다. 더 의미 있는 샘플을 얻기 위한 더 나은 importance sampling 같은 것도 있고, 이미 취한 샘플을 더 알뜰하게 활용하려는 방법도 있습니다.

후자의 대표적인 두 예가 Vertex Connection and Merging과 Metropolis Light Transport입니다. 둘 다 이미 생성한 경로를 재활용·변형하거나 다시 연결해, 통상적인 트레이싱 작업을 모두 하지 않고도 서로 다른 출발점에서 새 경로를 확장하는 방식입니다.

또 하나의 매우 유용한 예는 path filtering이라는 아이디어로, 최종 경로 결과만 필터링하는 것이 아니라 개별 경로 정점(vertex)의 결과를 이웃과 함께 필터링하는 것입니다. 이렇게 하면 매 bounce마다 노이즈가 줄고, 결과적으로 최종 경로 결과의 전체 노이즈가 줄어듭니다.

 

그래서 결론부터 말씀드리면, 이 발표는 실시간 path tracing을 빠르게 하는 접근법에 관한 내용입니다. 다만 제가 이 접근법에 도달하게 된 과정을 잠깐 되짚어보는 것도 의미가 있다고 생각합니다:

 

몇 주 전 기준으로, 저는 렌더링 관련 문제를 다루며 게임 업계에서 26년을 보냈다고 자랑스럽게 말할 수 있습니다. 저는 Midway Chicago에서 시작해 결국 Mortal Kombat 팀에 합류했습니다. Midway가 사라진 뒤 Mortal Kombat 팀은 Warner Bros. 산하의 Netherrealm Studios로 부활했습니다. 통틀어 그곳에서 20년 가까이 있으면서 MK와 Injustice 프랜차이즈의 여러 격투 게임을 출시했고, 매우 높은 성능의 렌더링에 집중했습니다.

 

저는 2020년에 EA의 SEED에 합류했습니다. 합류 당시에는 17명 정도였지만 이후 50명 이상으로 성장했습니다. 렌더링 엔지니어로 Netherrealm을 떠났으니, 당연하게도 렌더링을 다루는 SEED의 Future Graphics 팀에 합류하는 것이 자연스러웠습니다.

 

SEED에서 저는 마감에 쫓기는 일반적인 게임 팀이라면 위험 부담이 크다고 여길 만한 것들을 탐구할 상당한 자유를 누렸습니다. 그래서 제가 맡은 프로젝트 중 하나가 Ray Tracing 관점을 특히 염두에 둔 Texture Space Shading 탐구였습니다. EA의 프로젝트에는 코드명이 필요해서 우리는 그것을 Ray Machine이라고 불렀습니다. SEED의 렌더링 프레임워크인 Halcyon을 기반으로, 저는 완전히 GPU 주도로 동작하는 Decoupled Shading 버전을 만드는 데 상당한 시간을 들였습니다. GPU 주도가 무슨 뜻이냐면, 텍스처 공간 메모리 관리까지 포함해 거의 모든 것이 전적으로 GPU 쪽에서 처리되며, 할당은 즉각적인 페이지 요청에 기반해 Just-In-Time으로 이루어졌다는 뜻입니다.

 

Ray Machine에는 언젠가 이야기할 만한 흥미로운 점들이 많습니다. 예를 들어 Callable Shaders를 사용해 흔한 PSO 문제를 해결한 것 등인데, 대부분은 오늘 주제에서 크게 벗어난 이야기일 것입니다. 다만 짚고 넘어갈 핵심은, Dan Baker의 Decoupled Shading 구현과 마찬가지로 Ray Machine도 텍셀 할당에 8x8 패치를 사용했다는 점입니다.

Ray Machine의 큰 아이디어 중 하나는 texture-space shading의 중간 결과를 조명·셰이딩 캐시로 활용하는 것이었습니다. 제가 하고 싶었던 것은 간접 ray를 트레이싱해 텍셀에 부딪히면 그곳에 캐시된 셰이딩을 반환하는 것이었습니다. 해당 텍셀을 포함한 패치가 아직 할당되지 않았다면 할당한 뒤 일부 조명으로 초기화해 반환하고, 이미 존재한다면 부딪힌 텍셀을 radiance cache로 취급해 그 조명을 가져오는 방식이었습니다.

 

사실 지연 갱신 패치 리스트(deferred update patch list) 때문에 실제로는 더 복잡했지만, 그게 요점은 아닙니다. 핵심 문제는 직접 조명의 coherent한 ray tracing과 달리 간접 ray tracing은 세계에서 서로 연관성이 없는 위치에 부딪히는 경향이 있다는 점입니다. 단 하나의 ray가 부딪히기 위해 패치가 할당되는데 정작 필요한 것은 텍셀 하나뿐인 경우가 흔했습니다. 이는 갑작스럽게 엄청난 메모리 압박을 유발하고, 최악의 경우 한 패치에 대해 64대 1의 낭비적인 overshade를 초래하는 등 여러 후속 문제를 일으켰습니다.

그래서 어떤 경우에는 꽤 잘 동작하기도 했지만, 단순한 diffuse GI는 사실상 최악의 경우였습니다. 필요한 mip 레벨을 낮출 수는 있었지만 그것으로 문제가 해결되지는 않았고, 그저 덜 고통스럽게 만들 뿐이었으며, 그마저도 씬 복잡도가 높아질수록 확장성이 나빴고 다른 문제도 있었습니다.

 

그래서 이를 어떻게 다룰지 브레인스토밍하던 중 저는 Screen Space Reflections(SSR), 더 일반적으로는 스크린 공간 ray tracing을 다시 들여다봤습니다. 하드웨어 Ray Tracing 자체는 많은 부분이 잠겨 있고 fixed function이 많아서 원리적으로 게임 개발자의 창의적 편법이 많이 적용되지 못했습니다. 하지만 SSR은 상상의 여지가 열려 있어, 그 영역의 문제를 새롭게 재고한 영감 어린 방법이 많습니다.

SSR에서 나온 한 가지 흥미로운 아이디어는 높은 roughness를 다룰 때 조명이 적용된 색상 소스의 mipmap을 활용하는 것입니다. 적어도 처음 보기에는 유사 cone-tracing에 준하는 여러 이점이 있습니다. 메모리 지역성이 좋아지고, cone이 더 거친 mip에 닿아 인근 샘플을 집계하면서 더 많은 ray가 필요할 법한 denoising의 일부를 대신 해주는 셈일 수도 있습니다. 아쉽게도 depth 불일치 때문에 color-mip을 만들거나 사용하는 것은 상당히 지저분하고 실제로 다루기가 꽤 어렵습니다. 다른 사람들은 해냈을지 몰라도, 저는 견고하고 만족스러운 방식으로 동작시킨 적이 한 번도 없습니다. 하지만 그 아이디어는 계속 남아 '이것만 된다면…' 하는 유형의 해법으로 남아 있었습니다.

약간 주제에서 벗어나지만, SSR이 mip에서 겪는 것과 같은 이유로 TSS에서 더 거친 mip을 활용하는 것도 통하지 않는다는 점을 언급해야겠습니다. 인근 텍셀들이 들어오는 ray를 기준으로 방향적·공간적으로 정렬되어 있다는 보장이 없기 때문에, 단순히 그것들을 집계하는 것은 위험하며, 특히 더 거친 mip으로 갈수록 그렇습니다.

 

texture-space에는 이런 문제가 있으니… world-space는 어떨까요? world-space에서 조명이나 조명 비슷한 것을 집계하는 것은 딱히 새로운 일이 아닙니다. voxel을 사용하는 방식은 충분히 흔합니다. 씬 자체를 voxel화해서 이를테면 voxel 공간에서 직접 조명을 하는 것은 범위를 벗어난 노력을 요하는 엄청난 복잡성을 더했을 것입니다. 하지만 voxel에 데이터를 저장하는 것 자체는 큰 부담이 아닙니다. 적어도 voxel 공간에 볼륨 텍스처가 필요 없고 대신 hash-map을 쓸 수 있다는 것을 깨닫고 나면 그렇습니다. 볼륨 텍스처를 버리면서 잃는 주된 기능은 하드웨어 필터링인데, 정말 필요하다면 급한 대로 stochastic filtering으로 어느 정도 해결할 수 있습니다. 실제로는 voxel을 단순한 공간적 위치보다 더 다양하게 분류할 수 있는 제어권을 갖게 되어, hashmap이 여러 면에서 오히려 더 우수할 수도 있습니다.

 

3D 공간을 해싱하는 것 자체는 새로운 일이 아니지만, 2018년의 이 path filtering 논문은 제가 아는 한 radiance를 voxel을 통해 집계(따라서 필터링)하고 그 voxel을 hashmap에 저장하는 것을 다룬 최초의 논문이었습니다. hashmap이 GPU에서 즉석으로 생성되기 때문에, 많은 표준 CPU 접근법은 병렬 생성에 잘 들어맞지 않습니다. 그래서 아이디어는 voxel에 대해 두 개의 해시를 생성하는 것입니다. 첫 번째는 기본 맵 주소를 정의하고, 두 번째는 linear probing을 통해 충돌을 체크섬으로 확인하는 용도입니다. linear probing이란 그저 다음 N개 항목의 체크섬과 비교하는 것을 그럴듯하게 표현한 말입니다. 논문의 경우, 그들이 한 것은 path-vertex의 3D 위치, 양자화된 vertex-normal, level of detail을 조합해 해싱하는 것이었습니다. 여기서 level of detail은 본질적으로 카메라로부터의 거리를 양자화한 로그 스케일입니다.

 

짚고 넘어갈 점은, GPU hashmap이 어떤 면에서는 양날의 검이라는 것입니다. 결코 완벽하지 않습니다. 설계상 해싱 알고리즘이 좋을수록 메모리 접근은 더 비일관적(incoherent)이 됩니다. 실제로 hashmap이 클수록 이는 더 심해집니다. 그리고 충돌을 완화하는 데 많은 제약이 있기 때문에, linear probing이 빈 슬롯을 찾기 쉽도록 hashmap을 과다 할당(over-allocate)해야 합니다.

 

앞서 말했듯이 hashmap 성능은 기본적으로 설계상 최선이 아니며 상당히 비일관적입니다. 이를 어느 정도 완화하는 방법을 제가 아는 것은 몇 가지 있지만… 얼마나 효과가 있는지는 데이터와 플랫폼에 매우 의존적이고 상황에 따라 다릅니다 — 결과는 제각각일 수 있습니다.

첫 번째는 첫 해시를 할 때 normal이나 크기를 빼고 체크섬 해시에만 유지하는 것입니다. 원래 Path Filtering 논문에 설명되어 있습니다. 이렇게 하면 원래라면 비슷했을 서로 다른 해시들이 같은 메모리 영역을 linear probe하게 됩니다. 그래서 충돌 해소의 탐색 부분이 때때로 더 빨라질 수 있습니다. 하지만… 이제 잠재적으로 충돌이 훨씬 많아져서 오히려 더 느려질 수도 있고, 더 많은 과다 할당과 더 긴 linear-probe 길이를 요구합니다. 참고로 저는 보통 이 방법은 피합니다.

두 번째는 적어도 더 작은 voxel 크기에 대해 공간적으로 거칠게 버킷화(bucket)하는 것입니다. 이를 위해 맵 버퍼를 소수의 bin으로 나눕니다. voxel 크기에 상대적으로 위치를 거칠게 해싱해 bin을 선택합니다. 그런 다음 주소를 해당 bin으로 제한하면서 표준 해싱 절차를 적용합니다. 이렇게 하면 인근 voxel들이 매우 거칠게 함께 묶이며, 실제로 성능을 어느 정도 개선할 수 있습니다.

 

자, voxel을 통해 hit를 참조할 수 있게 되었으니, 우리 오브젝트(또는 gbuffer)에서 간접 경로를 트레이싱하고, 들어오는 방향과 첫 hit 위치로 해싱해 hashmap에 저장할 수 있습니다. 기본적으로 이렇게 하면 간단한 voxel radiance cache가 완성됩니다. voxel 크기를 정하는 것은 흥미로운 문제인데, 앞서 언급한 LOD 메커니즘으로 어느 정도 완화됩니다. 다만 world-space voxel 크기를 스크린 공간 커버리지나 카메라 거리에 기반해 정하는 방식은, ray 원점 대비 hit 위치의 디테일을 제대로 반영하지 못한다는 점은 짚어둘 만합니다.

그러면 아이디어는, path tracing을 할 때 첫 간접 hit까지 트레이싱하고 캐시를 확인해서, hit가 있으면 그것을 사용하고 없으면 나머지 경로 트레이싱을 마저 완료하는 것입니다. 실제로는 셀 초기화 주위에 임계 구역(critical section)을 걸 수 없기 때문에, 캐시를 채우는 것과 읽는 것을 보통 두 개의 패스로 나눠야 합니다 — 대개는 소수의 경로를 미리 완전히 트레이싱해 시드(seed)를 만들고, 프레임 내 나머지 경로에 대해서는 trace-and-test를 수행합니다. 제가 방금 설명한 것은 사실상 NVIDIA의 SHARC radiance caching과 거의 같은 내용으로, 다소 공격적으로 사용됩니다.

 

그렇다면 왜 그냥 SHARC를 그대로 쓰지 않느냐고요? 주된 문제는, voxel이 (Path Filtering 논문에서 설명하듯) 일관된 스크린 면적 커버리지를 촉진하기 위해 카메라 거리에 상대적으로 크기가 정해진다는 점입니다. voxel을 너무 작게 만들면 채워야 할 voxel이 훨씬 많아지고, 너무 크게 만들면 시각적 품질에 명백한 우려가 생깁니다. 그래서 어느 정도는 게임 콘텐츠에 맞춰 이 절충을 합리적으로 하는 '골디락스 존'을 골라야 합니다.

그런데 유리한 voxel 크기를 골랐더라도, 씬을 적당히 커버하려면 캐시를 그럴듯하게 채우기 위해 실제로 엄청나게 많은 경로가 필요합니다. 캐시를 만드는 목적 자체가 가능하면 비싼 path tracing을 피하려는 것이므로, 이는 결국 오랜 시간에 걸쳐 캐시를 아주 서서히 채운다는 뜻이 됩니다. 잘해야 조명에 심한 지연(latency)이 생기고, 더 흔하게는 캐시가 동적 오브젝트를 전혀 반영하지 못하게 됩니다.

 

기껏해야 캐시를 워밍업 스타트(warm-start)해야 한다는 것도 이상적이지 않지만, 그렇다고 더 정적인 조명 솔루션으로 몰아가는 것도 그리 좋지 않습니다. 그래서 질문은, 우리가 더 잘할 수 있을까? 입니다.

먼저, 큰 문제 중 하나가 골디락스 문제로 인한 캐시 커버리지 부족이라는 점을 생각해 봅시다. 그렇다면 이걸 어떻게든 실제로 해결할 수 있다면 어떨까요? mip-mapping 문제 기억나시나요? 우리는 이미 voxel 크기를 해시에 포함하는 것만으로 다양한 크기의 voxel을 hashmap에 저장하고 있습니다. 그러니 이를 다양한 크기의 여러 voxel을 사용해 정점을 저장하는 방식으로 해결하면 어떨까요? 물론 더 큰 hashmap이 필요하겠지만, 반드시 엄청나게 더 커야 하는 것은 아닙니다.

 

자, 우리가 할 수 있는 것은 고정된 크기들의 범위를 골라 각 위치에서 hit 지점을 다시 해싱하고, 작은 것부터 큰 것 순으로 스캔하는 것입니다. 여러 위치가 같은 voxel로 해싱되면 그것들을 함께 집계해 voxel에 평균으로 넣습니다. 이렇게 하면 어느 정도 절충이 이뤄져, 디테일이 존재하는 곳에서는 세밀한 결과를, 없는 곳에서는 더 거친 결과로 폴백하면서, 우리의 무수한 경로 정점을 커버할 가능성이 훨씬 커집니다. 처음에는 폴백할 때 점 샘플에 가까운 것 대신 잠재적으로 큰 voxel을 얻게 되므로 매우 미심쩍어 보입니다. 하지만 실제로는 그렇지 않습니다. 완벽한 금속 거울 반사를 다루는 게 아닌 한, 우리가 보통 하려는 것은 수많은 점 샘플을 통해 lobe를 적분하며 그것들을 함께 집계하는 것이기 때문입니다. 따라서 집계된 voxel이 lobe 안에 온전히 들어있는 한, 그것은 사실 이미 평균낸 수많은 샘플을 대표하며, 이는 path filtering과 유사합니다. 사실 SHARC와 마찬가지로 이 적합(fit) 제약을 반드시 엄격하게 지킬 필요는 없지만, 물론 이는 약간의 오차를 도입합니다.

 

이 방식은 놀라울 정도로 잘 동작합니다. 실제로 voxel 크기를 다양화하기 위해, 저는 대략 gbuffer 픽셀의 world-space 크기에 해당하는 작은 값에서 시작해, 가장 큰 것이 적당히 거대해질 때까지 voxel을 8의 거듭제곱으로 키웁니다. 이를 시도하는 방법은 여러 가지가 있는데, 적어도 지금까지 제가 찾은 가장 잘 되는 방법은 이를 세 단계(phase)로 나눠 수행하는 것이었습니다.

 

첫 번째 단계는 해싱을 통해 모든 스케일에서 first-hit에 해당하는 모든 voxel과, 현재 크기 대비 한 단계 더 큰 연관 자식(child) voxel을 식별하고, voxel radiance를 0으로 초기화합니다.

 

두 번째 단계는 가장 작은 voxel 전체를 해당 first-hit 데이터로 채우며, 노출된(exposed) radiance를 저장합니다.

 

세 번째 단계는 루프를 돕니다. 가장 작은 집합에서 시작해 모든 voxel을 가져와, 그 radiance를 한 단계 더 거친 자식(child) voxel에 atomic add로 더하고, 그 자식을 새 voxel 리스트에 추가합니다. 다음 voxel 리스트에 대해 이를 반복하고, 크기 한계에 도달할 때까지 계속합니다.

 

이 작업은 꽤 빠르게 수행되며, 점점 더 많은 voxel이 함께 집계될수록 각 루프 패스도 그만큼 빨라집니다. 참고로 radiance 집계는 컴포넌트별 고정소수점(fixed point)으로 수행합니다. 조회 비용을 낮게 유지하기 위해, 집계가 끝난 뒤에는 이를 패킹된 32비트 컬러 포맷으로 변환합니다.

 

잠깐 곁가지로 말씀드리면 — 이것은 texture space shading을 위한 해법으로 시작됐지만, 사실 그것에 대한 기술적 종속성은 전혀 없습니다. 더 전통적인 렌더링 파이프라인에 이를 도입하지 못할 이유가 없습니다. 그래서 EA 내부 팀들이 PT를 탐구하고 있었던 만큼, 우리는 deferred 렌더링 엔진에서 볼 법한 표준적인 화면 지향 간접 path tracing을 처리하는 쪽으로 방향을 틀었습니다.

 

이렇게 voxel 계층 구조로 radiance cache를 구성하면, 상대적으로 적은 수의 시드 경로만으로도 매우 높은 hit rate를 얻을 수 있습니다 — 캐시를 시간적으로 누적할 필요가 더 이상 없을 정도로요! 사실 이를 훨씬 더 밀어붙일 수 있습니다. hit rate가 충분히 높아서 아주 적은 수의 경로로 시드할 수 있으며, hit를 '어쩌면' 발생하는 이벤트로 취급하는 대신 SHARC와 유사하게 픽셀을 미리 두 범주 — update와 render — 로 나눌 수 있는 수준입니다. SHARC는 캐시를 갱신·기록하기 위해 미리 실행하는 시딩 경로와, 캐시를 읽어 path trace하는 보다 표준적인 범주를 갖고 있습니다.

마찬가지로 우리는 픽셀을 sparse와 reconstruction 두 범주로 나눕니다. sparse 픽셀은 완전한 path trace를 수행하되, 그 경로의 첫 간접 hit에서의 radiance도 추적·저장합니다. reconstruction 픽셀은 완전한 경로를 구성하려 하지 않고, 각자 단순히 first hit까지 ray를 트레이싱해 캐시를 조회하려 합니다. 이 reconstruction 아이디어는 잘 알려진 것으로 결코 독창적인 것이 아닙니다 — 경로를 인근의 다른 경로 꼬리에 다시 연결하는, offline path tracing의 널리 검증된 다양한 아이디어에서 상당 부분 빌려온 것입니다.

 

이 모든 것을 종합해 봅시다: 먼저 sparse 경로를 트레이싱하면서, gbuffer에서의 첫 bounce, 그것이 최종적으로 부딪히는 위치(즉 파란 점), 그리고 연관된 경로 radiance를 추적합니다.

 

그런 다음 그 hit에 해당하는 voxel들을 식별하고, radiance를 해당 voxel들에 주입합니다. 이렇게 voxel 캐시를 채웁니다.

 

이제 나머지 픽셀을 reconstruction해야 하므로, gbuffer에서 튕겨 나가는 reconstruction ray를 쏘아 그것들이 처음 부딪히는 지점(빨간 점으로 표시됨)을 확인합니다.

 

이제 reconstruction hit 위치를 캐시에서 조회하려 합니다. 가장 작은 해당 voxel 크기부터 시작해, hit가 나올 때까지 점점 더 큰 크기를 시도합니다.

 

마침내 hit가 나오면, 그 voxel의 radiance를 사용해 reconstruction 경로를 근사합니다.

 

여기서 놓치기 쉬운 점 몇 가지를 짚어보겠습니다.

첫째, 여기에는 암묵적인 시간적 히스토리(temporal history)가 전혀 없습니다. 우리는 매 프레임 voxel 캐시 전체를 처음부터 리셋하고 다시 구성합니다. 캐시 항목을 어떻게 노후화(age)할지 고민할 필요도 없고, 카메라 이동을 다룰 필요도 없습니다. 심지어 카메라 상대(camera-relative) 위치를 사용할 수도 있습니다.

둘째, 이것이 완벽무결한 것은 아닙니다. 계층 구조가 있어도 일부 조회는 캐시 항목을 찾지 못할 것입니다. 실제로는 이것이 1%를 넘는 경우가 드물어 보이지만, 이를 추적해 해당 픽셀에 대해서는 그냥 완전한 경로를 생성할 수도 있습니다. 더 쉬운 방법은 그것들을 길이 0인 경로로 표시해 denoiser가 '고치도록' 두는 것인데, 드물게만 발생한다면 이 방법도 잘 동작합니다.

 

떠오를 만한 한 가지 질문은, 왜 캐시에서 first hit만 사용하느냐는 것입니다. 원한다면 모든 정점을, 그 정점이 소유하는 경로 꼬리(tail) 일부와 함께 저장해 훨씬 더 조밀한 캐시를 만들 수도 있습니다. 그렇게 하지 않는 이유는 thin-wall 및 tunnelling 문제를 피하기 위해서입니다. voxel 집계를 다루다 보니, 실제로는 서로 가려져 있던(occluded) 무관한 경로들을 실수로 붙여버려 light bleeding을 유발하기가 매우 쉽기 때문입니다. first hit만 취하고 위치와 각도(angle)로 함께 해싱함으로써, 잘못된 양성 집계(false-positive-aggregation)가 일어날 가능성을 크게 줄입니다. 더욱 신중을 기하고 싶다면, 경로의 첫 세그먼트 길이(first-segment-length)를 양자화한 로그값을 해시에 포함할 수도 있습니다.

 

다음 질문은 어떤 픽셀을 어느 쪽으로 정할지 어떻게 결정하느냐입니다. 가장 쉬운 시도는 그냥 픽셀을 확률적으로(stochastically) 고르는 것입니다. 예를 들어 픽셀의 25%를 시드 경로로, 75%를 reconstruction으로 하고 싶다면, 각 픽셀마다 주사위를 굴려 25% 미만인지 확인하면 됩니다. 순수한 white noise도 동작하지만, Blue noise나 FAST noise를 사용하면 훨씬 나아집니다. 씬과 정점별 작업량에 따라 비율마다 서로 다른 절충이 있지만, 일반적으로 경로가 적을수록 더 빨라집니다. 그리고 reconstruction 쪽도 완전히 공짜가 아니라는 점을 유념하세요 — 여전히 매 프레임 radiance 캐시를 구성·전파해야 하고, 각 reconstruction 픽셀마다 path-connection ray도 트레이싱해야 합니다.

제가 목표로 삼아온 것은 이상적으로는 16픽셀당 약 하나의 경로, 즉 픽셀의 약 6.25%입니다. 그런데 앞서 설명한 대로 그냥 하면 어떻게 될까요?

 

성능 관점에서 이것은 훌륭합니다. Xbox Series X에서 측정하면, 프레임의 path tracing 부분이 14밀리초 남짓에서 5밀리초로 떨어집니다. 원래 비용의 1/3 남짓에 불과합니다. 다만 여전히 ray tracing이 지배적인데, 이는 상당 부분 reconstruction이 여전히 엄청난 수의 ray를 트레이싱해야 하고 그것이 공짜가 아니기 때문입니다. 실제 경로 수는 예상대로 1/16로 줄었지만, 아쉽게도 specular 반사의 디테일 일부를 잃었고 매우 불안정합니다. 참고로 여기서 제가 적용한 denoising은 단순한 temporal anti-aliasing뿐입니다.

 

specular 반사를 제대로 포착하려면 더 많은 경로가 필요하다는 것이 분명합니다. 임계값을 전역적으로 올리는 것은 역효과를 낳으므로, 우리가 정말 원하는 것은 그것들이 필요할 것으로 의심되는 곳에서만 임계값을 높이는 것입니다 — 이는 기본적으로 BSDF 샘플링이 specular lobe를 선택했을 때 적용되는 일종의 대체 임계값을 뜻합니다. specular lobe 확률을 사용하는 것은 합리적인 선택이지만, 거칠기(roughness)가 높은 금속에서는 과도해질 수 있다는 점을 유념하세요.

 

여기 우리가 적용하는 기본 휴리스틱들이 있습니다 — 시간 관계상 계속 진행하겠습니다. 세부 내용이 궁금하시면 나중에 슬라이드를 참고하세요.

 

이게 얼마나 잘 되냐고요? 실제로 아주 잘 됩니다. 우리는 원하는 세밀한 하이라이트를 되찾게 되지만, 이 특정 시점에서는 경로 수가 두 배로 늘었습니다. 영상을 보시면 불안정하던 특징이 안정화되고 반사된 특징이 더 선명해진 것을 확인하실 수 있을 겁니다. 다만 추가된 경로 때문에 비용이 약 30% 증가했습니다.

 

이제 전반적으로는 아주 보기 좋지만, 경로 수가 전적으로 시점 의존적(view dependent)이라는 문제가 있습니다! 휴리스틱에서 어느 정도 이를 완화합니다 — 총 생성률(spawning rate)에 상한을 둡니다… 그렇더라도 이 특정 프레임 시점은 온갖 거울 반사 때문에 약 330,000개의 경로를 유발합니다. 균일 샘플링 대비 프레임 비용이 두 배 이상으로 늘었습니다. 또한 이 모든 추가 경로를 허용하는 것은 단순한 타임라인(timelining) 문제만이 아니라는 점도 언급할 만합니다 — voxel 캐시도 커져야 하고 경로를 추적하는 데이터 버퍼도 커져야 하므로, 실제로는 보이는 것보다 더 나쁩니다.

 

그래서 이것이 누락된 specular ray를 채워주긴 하지만, 경로 수를 줄이려던 원래 목표와는 다소 상충합니다. 사실 이제 우리는 렌더링 비용을 끌어올리는 화면 콘텐츠에 극도로 민감해졌습니다. 이는 분명 최선이 아닙니다! 여기서 게임을 다루는 상황이라면 우리는 분명 안정적이고 일관된 예산(budget)을 원합니다. 이는 임계값을 재정규화(renormalize)함으로써 달성할 수 있는데, 픽셀 점수 계산 작업을 세 부분으로 나눠서 수행합니다:

1.모든 픽셀에 대한 확률 점수(probability score)를 계산한다.
2.모든 확률 점수를 합산하고, 예산을 그 합으로 나눠 스케일 팩터(scale factor)를 만든다.
3.이 값을 원래 계산된 점수들에 스케일 팩터로 적용한다.
 
 

그래서 이 balanced 모드는 기본 sparse 분포에 비해 전반적인 품질이 더 높으면서도, 휴리스틱만으로는 유발됐을 가변적 경로 할당을 피합니다. 꽤 좋은 절충안이며 전체 비용을 상당히 안정적으로 유지합니다. 이것이 진정으로 가능하게 하는 것은, 보다 전형적인 방법인 동적 해상도 스케일링(dynamic resolution scaling)이라는 큰 망치 대신, 경로 수를 예산으로 관리함으로써 간접 조명 비용을 조절하는 것입니다.

또한 눈에 띄는 실제 남은 문제는 대체로 reconstruction ray의 ray tracing 오버헤드를 중심으로 맴돈다는 점도 언급할 만합니다 — 특히 저사양 하드웨어에서는 이것이 꽤 큰 문제이며, 우리의 경로 재분배로 인해 다소 더 나빠진 것으로 보입니다. reconstruction 경로 일부를 그냥 버리고 나머지는 denoiser가 채우도록 맡기는 방식으로 성능을 더 완화할 여지가 있을 수 있는데, 이는 Anno 117 팀이 보여준 바와 같습니다.

 

자, 지금까지 제가 언급한 것들에는 시간적(temporal) 요소가 전혀 없습니다. 휴리스틱을 포함해 모든 것이 프레임 내에서 평가되었습니다. 이제 약간의 시간적 고려를 도입할 의향이 있다면, 더 나은 접근법은 잠재적으로 이전 프레임의 분산(variance)을 기반으로 샘플 할당을 조절하는 것일 수 있습니다. 다만 이는 애초에 분산을 측정할 무언가가 필요하므로 시간적 피드백(temporal feedback)을 요구합니다. 본질적으로 이전 프레임을 재투영(reproject)해 분산을 계산하는 것입니다.

 

이 부분도 넘어가지만, 원하시면 나중에 슬라이드에서 다시 보실 수 있습니다.

 

지금까지 설명한 것이 조금이라도 추구할 가치가 있어 보인다면, 이 시점에서 말씀드릴 것은 제가 이 시스템 — 짐작하셨겠지만 우리는 이를 Orca라고 불렀습니다 — 을 EA의 4개의 서로 다른 path tracer에 연결했다는 점입니다. 처음에 Orca는 두 개의 서로 다른 Halcyon Path Tracer 구현에 연결됐는데, 첫 번째는 공개 시연되었고 팀이 Ray Tracing Gems에서 상세히 설명한 원래의 ray-recursive 구현이었고, 두 번째는 더 새롭고 더 빠른 구현으로 앞선 벤치마킹에 사용된 것이었습니다. 또한 Codemasters의 F1 코드베이스와, 이름을 밝힐 수 없는 멀티 게임 엔진에도 연결했습니다. 일반적으로 이런 서로 다른 Path Tracer들은 두 진영 중 하나로 분류할 수 있습니다 — One Big RayGen 방식이거나 Many Dispatches 방식이거나입니다. 각자의 장단점과 무관하게, 대체로 이런 시스템을 Many Dispatches 방식에 연결하는 것이 결과를 가로채고 흐름을 다시 돌릴 명확한 지점이 더 많아 훨씬 쉽다고 말할 수 있습니다. 그렇지만 이를 동작시키는 데 구현의 세부 사항이 그리 중요하지는 않습니다.

 

그래서 이 모든 것의 실용성을 진정으로 입증한 것은 Codemasters의 F1에서 이를 구동하는 일이었습니다. 꽤 흥미로운 작업이었고, 예상대로 여러 변경과 재작업으로 이어졌는데, 가장 두드러진 것은 voxel downrez 패스의 재작성이었습니다. 다른 구현과 마찬가지로, 전체에서 path tracing이 아닌 부분 중 가장 비싼 것이 트레이싱되는 reconstruction ray라는 점은 짚어둘 만합니다. 또한 F1은 경로 꼬리(path tail)를 처리하기 위해 SHARC를 폭넓은 커버리지의 안정적인 radiance cache로 사용한다는 점도 언급해야겠습니다. 두 시스템은 실제로 서로 꽤 잘 협력할 수 있으며, 우리는 SHARC 갱신 예산을 sparse 경로 예산에서 할당해 그 경로들이 이중 역할을 하도록 합니다. 핵심은 두 시스템이 path tracing 비용의 일부를 공유할 수 있다는 점입니다.

또한 F1은 자신들의 path tracer를 이미 출시했지만, 이를 가속하기 위해 Orca를 활용하는 것은 진행 중인 작업이며 그들이 이 기능을 출시하리라고 장담할 수는 없다는 점을 밝혀둡니다.

 

여기 1080p F1의 PS5 Pro 빌드에서 Orca가 구동되는 짧은 클립과, Orca를 켜고 끈 상태로 Razor 캡처에서 얻은 비교 데이터가 있습니다. F1의 원시 path-tracing 수치가 GDC에서 보여준 것보다 다소 나쁜 이유를 100% 확신하지는 못하지만, 그들이 수많은 품질 개선을 추가했고 경로 꼬리에 SHARC를 사용하도록 전환했다는 점을 감안하면 그것으로 어느 정도 설명될 수 있습니다. 이것은 여전히 GDC 강연에서 언급된 단일(monolithic) RayGen 방식을 사용하지만, 그들은 multi-dispatch도 적극적으로 탐구하고 있습니다.

어쨌든 오른쪽 타이밍에서 보시다시피, 1080p에서 안정적인 30Hz를 유지하는 데 필요한 33ms를 충분히 밑돌고 있어 아주 좋습니다. 그뿐만 아니라, 해상도를 864p로 조금 낮추면 실제로 여유를 두고 60Hz 안에 들어옵니다 — 꽤 멋진 일입니다!

 

물론 이런 접근법에는 발생할 수 있는 잠재적 문제들이 있습니다. 완화 방법과 함께 언급할 만한 몇 가지를 소개합니다:

첫째, fireflies가 때때로 문제가 될 수 있습니다. 좁은 픽셀 이웃에서 샘플링되는 큰 voxel을 정말 밝게 만들면 그것들이 더 두드러지게 나타날 수 있기 때문입니다. 이를 다루는 방법은 두 가지입니다 — 첫 번째는 단순히 기여도를 클램핑(clamp)하는 것이지만, 이는 약간의 에너지 손실로 이어집니다. 더 낫지만 더 비싼 방법은 radiance cache에 주입할 때 확률 점수(probability score)를 가중치로 반영하는 것입니다. 이는 샘플을 그 선택 pdf에 상대적으로 가중하지만, 집계 비용을 조금 더 높이고 고정소수점 오버플로를 피하기 위한 중간 재정규화(intermediate renormalization)를 불가능하게 합니다.

둘째, 우리가 트레이싱하는 경로 수를 최소화하려 하긴 하지만, 캐시는 그것이 대표하는 sparse 경로가 많을수록 더 정확한 결과를 제공합니다. 그래서 이전 프레임들의 경로를 외부에서 추적하다가 Store 단계에서 히스토리 가중치(history weight)와 함께 주입함으로써 시간적 누적 금기를 어느 정도 굽힐 수 있습니다. 이는 다소 결과를 오염시키지만, 오래된 경로 — 또는 단순히 그 첫 정점 — 를 재조명(relight)하고 경로 주입을 첫 정점이 정적 지오메트리에 속한 것으로 제한함으로써 그 영향을 어느 정도 억제할 수 있습니다.

마지막으로, 날카로운 반사가 있으면 그 안에서 눈에 띄는 voxel 아티팩트가 생길 수 있습니다. 이 중 일부는 단순히 언더샘플링(undersampling) 때문이지만, dithering을 사용해 최악의 부분은 숨길 수 있습니다.

 

voxel을 이런 식으로 사용할 때 생기는 불행한 부작용 하나는, denoised 결과가 실제로 voxel을 시각적으로 드러낼 수 있다는 점입니다. 특히 날카로운 하이라이트에서 그렇습니다. voxel 스케일에 상대적으로 위치를 dithering함으로써 이를 어느 정도 완화할 수 있습니다. 이렇게 하면 radiance cache miss가 늘어날 수 있지만… voxel을 가려주고, 대체로 전체적으로 더 보기 좋습니다.

참고로 per-pixel Random 해시는 현재 voxel 스케일과 rayIndex에 대해 계산됩니다. reconstructed 픽셀 리스트가 atomic으로 구성되어(순서가 무작위화됨) rayIndex 값은 뒤섞여 사실상 난수로 변환됩니다.

 


한국어 버전 PPTX ( 구글 드라이브 )
https://docs.google.com/presentation/d/1afsBA4TWcNTNlR2p26IU89R-OedVRM9J/edit?usp=sharing&ouid=101375571592519364220&rtpof=true&sd=true

 

Google Slides 로드 중

 

docs.google.com