역자의 말: 시그라프 2025 어드벤스트 렌더링 자료들이 영상과 함께 공개되기 시작 했어요. 의역이나 읽기 편한 어조로 일부 교정을 했기 때문에 번역문 직역이랑은 좀 다를수 있습니다.
발표자 소개

먼저 오늘 발표를 맡은 두 사람부터 소개드리겠습니다.
룩 르블랑은 레이 트레이싱과 조명 분야를 전문으로 하는 Anvil 렌더링 팀의 테크 리드입니다. 렌더링 쪽 경력만 25년 이상, 비디오 게임 업계 경력은 12년 이상이고, 박사 과정에서 연구를 쭉 해온 사람입니다. 에이도스 몬트리올에서는 글로벌 일루미네이션과 레이 트레이싱 개발을 이끌었고, 지금은 유비소프트 몬트리올에서 《어쌔신 크리드 섀도》의 레이 트레이싱 기반 스페큘러 글로벌 일루미네이션을 책임지고 있습니다.

멜리노 콘테는 같은 Anvil 렌더링 팀에서 레이 트레이싱과 조명 쪽을 담당하고 있는 팀 리더이자 주요 기여자입니다. 렌더링 경력은 10년, 게임 업계 경력은 7년 정도이고요. 이전에는 에이도스 몬트리올에서 《마블 가디언즈 오브 갤럭시》 렌더링을 맡았고, 현재는 유비소프트 몬트리올에서 《어쌔신 크리드 섀도즈》의 레이 트레이싱 관련 작업을 담당하고 있습니다.


저희 둘은 Anvil 렌더링 팀에서 라이팅과 레이 트레이싱에 집중하고 있는 프로그래밍 팀 리드이자 테크 리드입니다. 본격적인 기술 얘기에 들어가기 전에, 이 발표는 저희 팀과 수많은 협력자들의 여러 해에 걸친 작업 결과라는 점을 먼저 말씀드리고 싶습니다. 이 기술을 실제로 게임에 넣기 위해 힘써 준 모든 동료들에게 이 자리를 빌려 감사 인사를 전합니다.

어쌔신 크리드 시리즈는 잘 아시는 것처럼, 아주 체계적으로 구성된 대규모 오픈 월드를 특징으로 합니다. 그중에서도 AC 섀도우는 지금까지 나온 작품들 가운데 가장 역동적인 월드를 가지고 있습니다.

이 게임에는 단순히 낮과 밤이 바뀌는 수준을 넘어, 서로 다른 14가지의 기상 상태와 그 사이 전환, 그리고 재질·기상 효과·시간대(Time of Day, TOD)에 영향을 주는 사계절 시스템이 모두 들어가 있습니다. 조명 입장에서 보면, 이 모든 변수를 동시에 다뤄야 한다는 뜻이죠. 예상하실 수 있듯이, 이런 환경에서는 전통적인 오프라인 베이크 기반 솔루션으로 확장성을 확보하기가 점점 더 어려워집니다.

이 슬라이드에서는, 최근 몇 년 사이에 월드의 다양성과 스케일이 얼마나 커졌는지 한 번 비교해 보겠습니다. 예를 들어 Unity에서 흔히 사용하는 균일 프로브 분포(uniform probe distribution)를 그대로 가져온다고 가정하면, 지금 저희 월드는 그때와는 완전히 다른 규모를 갖게 됩니다.
균일 분포 자체는 직관적이지만, 스케일이 커질수록 비용이 기하급수적으로 늘어나기 때문에 잘 확장되지 않습니다. 그래서 AC 섀도우의 베이크드 GI 버전에서는, 예를 들어 봄과 여름을 하나의 통합 GI로 묶는 식으로 여러 가지 타협을 해야 했습니다.
여기서 말하는 GI는 글로벌 일루미네이션(Global Illumination)이고, 보다 일반적인 개념은 글로벌 일루미네이션 위키를 참고하셔도 좋습니다.

이렇게 월드 스케일이 커지면서, 글로벌 일루미네이션 시스템도 세대별로 계속 진화해 왔습니다.《오리진》 시절에는, 필요 영역에 수동으로 디테일을 추가하는 희소 볼륨(sparse volume) 접근을 사용했습니다.
AC 섀도우에서는 한 단계 더 나아가서, 확산(diffuse)과 스페큘러(specular) 모두에 대해 완전 동적인 레이 트레이스드 글로벌 일루미네이션(ray-traced GI)을 도입했습니다. 레이 트레이싱 자체에 대해서는 Ray tracing (graphics) 항목을 떠올리시면 됩니다.

저희가 고민해야 했던 "스케일러빌리티"는 단순히 월드가 동적인가, 시간과 날씨가 변하는가 하는 문제에 그치지 않습니다. 목표 플랫폼 스펙도 모두 제각각이기 때문에, 하나의 플랫폼 안에서 베이크드 GI와 RTGI 두 솔루션을 모두 지원해야 했습니다.
이제 본격적인 디테일로 들어가기 전에, 먼저 게임 안의 다양한 환경을 보여 주는 짧은 영상 클립을 보시고, 그 다음부터 내부 구현을 차근차근 풀어 보겠습니다.

오늘 발표는 크게 두 파트로 구성되어 있습니다.
먼저 룩이, 우리가 사용한 RTGI(Ray-Traced Global Illumination) 알고리즘과 그 구현을 가능한 한 구체적으로 뜯어서 설명드릴 거고요.[RTGI 개념 참고: https://en.wikipedia.org/wiki/Global_illumination#Ray_tracing_based_approaches]
그 다음에는 제가, AC 섀도우라는 게임 특유의 제약과 도전 과제들, 그리고 그 문제들을 어떤 방식으로 해결했는지 정리해서 말씀드리겠습니다. 마지막에는 성능 지표와, 알고리즘 자체의 한계, 구현에서 남아 있는 제약, 그리고 앞으로 개선하려는 방향까지 간단히 정리하고 마무리하겠습니다.


지금부터 소개드릴 작업은 최근 몇 년간 많이 언급된, 동적 디퓨즈 글로벌 일루미네이션(Dynamic Diffuse Global Illumination, DDGI) 계열 기법들을 기반으로 하고 있습니다.
최신 버전에서는 픽셀 단위 레이 트레이싱 패스를 사용하고, 그 결과를 프로브 볼륨(probe volume)에 캐시해서 카메라 기준 2차 히트(secondary hit)의 GI를 빠르게 재사용합니다. DDGI 자체는 예를 들어 NVIDIA RTXGI 문서 같은 자료에서 잘 정리되어 있습니다.[https://developer.nvidia.com/rtxgi]
비슷한 시스템은 이미 《Metro Exodus》나 스노우드롭(Snowdrop) 엔진 기반의 《Avatar》, 《Star Wars Outlaws》 같은 게임에서도 발표되고 실제로 사용되고 있습니다.
이 세션에서는, 이런 계열의 GI 시스템을 우리가 어떤 구조로 구현했는지, 그리고 식생이 많은 거대한 동적 오픈 월드를 여러 플랫폼에서 동시에 유지하면서도 지금 슬라이드에서 보시는 수준의 품질과 속도를 어떻게 달성했는지를 중심으로 살펴보겠습니다.

먼저 전체 알고리즘을 한 슬라이드에 요약해 보겠습니다.
첫 단계에서는 카메라 관점에서 씬을 래스터라이즈해서 G-버퍼(g-buffer)를 만들고, 그 위에서 직접광(direct lighting)을 계산합니다.
그 다음 각 픽셀에 대해 간접 디퓨즈(indirect diffuse)와 간접 스페큘러(indirect specular)를 계산합니다. 여기서 나오는 BRDF는, 잘 아시는 양방향 반사 분포 함수(BRDF, Bidirectional Reflectance Distribution Function) 개념을 그대로 사용합니다.[BRDF 개념: https://en.wikipedia.org/wiki/Bidirectional_reflectance_distribution_function]

각 픽셀마다, 먼저 BRDF에 맞춰 난수 방향을 하나 선택합니다. 디퓨즈 로브(diffuse lobe)와 스페큘러 로브(specular lobe)를 나누기 위해, 이 과정을 두 번에 걸쳐 돌립니다.
디퓨즈 패스, 그 다음 스페큘러 패스, 이렇게 두 번의 패스로 생각하시면 됩니다.

그다음에는 선택된 방향으로 실제 씬 안에 레이를 쏩니다.
여기서 저희 시스템은 우선 화면 공간 레이마칭(screen-space ray marching)부터 시작합니다. 이유는 두 가지 정도로 보시면 됩니다.
첫째, 일부 콘솔 플랫폼에서는 BVH 기반 레이 트레이싱보다 화면 공간 레이마칭이 순수 속도 측면에서 더 빠릅니다.
둘째, 잠시 후에 보시겠지만 RT용 BVH 월드 표현은 속도를 위해 아주 거칠게(coarse) 만들어져 있습니다. 모든 오브젝트가 들어가 있는 것도 아니고, 들어가 있다 하더라도 래스터 버전보다 훨씬 단순한 LOD를 사용합니다. 그래서 순수 BVH만으로는 작은 오브젝트나 자기 교차(self-intersection) 문제를 다 잡기가 어렵고, 이 부분을 보완하기 위해 먼저 화면 공간에서 레이마칭을 돌리게 됩니다.

만약 화면 공간에서 아무것도 찾지 못했다면, 그다음 단계에서 BVH 씬 표현으로 레이를 넘겨서 트레이싱합니다.

카메라 기준으로 봤을 때 두 번째 히트, 즉 2차 히트(secondary hit)에 도달하면, 먼저 태양과 로컬 라이트에서 오는 직접광을 계산합니다.

그 다음에는 간접 조명 중에서 디퓨즈 성분만 따로 떼어서, 카메라 주변에 배치해 둔 프로브 볼륨(probe volume)을 사용해 계산합니다.

히트 위치 기준으로 주변 8개의 프로브를 잡아서, 이들을 보간(interpolation)해 그 지점의 간접 디퓨즈 조명을 얻습니다.
이 과정을 모든 픽셀에 대해 반복하면, 당연히 노이즈가 많은 결과가 나오게 됩니다. 그래서 마지막에 디노이저(denoiser)를 한 번 통과시켜서, 최종 간접 조명을 얻습니다. 디노이저 쪽 기본 개념은 NVIDIA NRD 문서를 참고하시면 감을 잡기 좋습니다.[https://github.com/NVIDIARTX/NRD]

지금까지는 프로브 볼륨을 "어떻게 쓰는가"에 집중해서 봤고요, 이제부터는 그 프로브 볼륨을 "어떻게 구축하는가"를 설명드리겠습니다. 2차 히트에서 간접 조명을 뽑아 쓰려면, 이 프로브 볼륨이 먼저 잘 준비되어 있어야 합니다.

먼저 볼륨 안에 들어 있는 각 프로브에 대해 어떤 일을 하는지부터 보겠습니다.

각 프로브를 중심으로 한 구(sphere)를 생각하고, 그 표면을 균일하게 커버하도록 여러 방향으로 레이를 쏩니다.
쉽게 말해, 프로브 위치를 기준으로 "이 주변에서 볼 수 있는 세상"을 샘플링한다고 보시면 됩니다.

각 레이가 씬의 기하를 맞으면, 그 히트 지점에 대해 다시 모든 광원으로부터 직접 조명을 계산합니다.
이렇게 해서 각 프로브가 주변 환경을 한 번 "조명해 본 결과"를 모으는 과정이 1차 단계입니다.

여기에 더해, 간접 조명은 현재 프로브 볼륨을 사용하되 이전 프레임 값을 참조해서 멀티 바운스 GI를 근사합니다. 프레임마다 프로브를 조금씩 업데이트하면서, 프레임 간에 정보를 계속 누적해 나가는 구조라고 생각하시면 이해가 빠릅니다.

지금 보시는 그림이, 모든 프로브를 조명하고 난 뒤의 방사휘도(radiance) 결과입니다. 아직은 필터링 전이라, 다소 거칠지만 정보는 충분히 들어 있는 상태입니다.

다음 단계에서는 이 프로브들을 컨볼루션(convolution)해서, 필터링된 방사휘도 캐시(filtered radiance cache)를 만듭니다.
물리적으로 완전히 정확한 대신, 여기에서는 노이즈를 줄이고 결과를 더 안정적으로 만드는 쪽을 택합니다.
이 캐시는 나중에 레이를 쐈는데 아무 기하도 맞지 않을 때, 또는 월드 RT를 줄이고 싶을 때 폴백(fallback)으로 쓰이게 됩니다.

또 하나 중요한 결과물은 조도(irradiance) 캐시입니다.
모든 프로브에 대해 디퓨즈 BRDF와 컨볼브를 한 번 더 돌려서, 어느 위치에서든 간접 디퓨즈 조명을 바로 뽑아 쓸 수 있는 형태로 만들어 둡니다.

여기까지가 알고리즘 개요였고요, 이제는 레이 트레이싱용 씬 정의를 좀 더 구체적으로 들여다보겠습니다.

앞에서 잠깐 언급했듯이, 레이 트레이싱용 씬 표현은 래스터 버전에 비해 훨씬 더 간략화된 형태입니다. 가능한 한 낮은 LOD를 사용하되, 플레이어 입장에서 눈에 띄는 아티팩트가 생기지 않을 정도까지만 품질을 유지합니다. 버텍스 애니메이션은 지원하지 않기 때문에, 스키닝된 캐릭터나 바람에 흔들리는 식생은 RT 월드에 직접 들어오지 않습니다. 이런 요소는 다른 방식으로 처리합니다. 작은 풀, 잔해 같은 소형 지오메트리는 최대한 과감하게 컬링해서, BVH 복잡도를 줄이는 쪽을 택했습니다.또 한 가지 포인트는, 히트 결과를 빨리 가져오기 위해 삼각형 단위 정보를 미리 베이크해 둔다는 점입니다. 이렇게 하면 인덱스를 조회하고, 다시 정점 데이터를 읽어 오는 과정을 건너뛸 수 있습니다. 실제로는 4×32비트 정도의 패치 하나를 가져와 그 안에 법선과 UV를 모두 담아두고, 이 값만 가지고 UV 보간을 해 버리는 방식입니다.

머티리얼 쪽도 같은 철학을 따릅니다. 래스터 파이프라인에서는 머티리얼마다 셰이더가 제각각일 수 있지만, RT 쪽에서는 최대한 통합된(unified) 머티리얼 표현 하나로 수렴시키고 있습니다. 이렇게 하면 DXR 인라인 레이 트레이싱(Inline Raytracing) 같은 기능과도 잘 맞고, 여러 플랫폼에서 더 좋은 성능을 얻을 수 있습니다. 각 머티리얼에 대해서는 PBR 상수들을 먼저 계산해 두고, 지금 슬라이드에는 그 예시가 나와 있습니다. PBR(Physically Based Rendering) 자체는 PBR 위키의 기본 개념을 그대로 떠올리시면 됩니다. 머티리얼 정보는 대략 이런 식으로 접근합니다.
- instanceDataID = ray.CommittedInstanceID + ray.CommittedGeometryIndex
- materialID = instanceData[instanceDataID]

또 알베도(albedo)와 알파(alpha) 같은 일부 파라미터에 대해서는, 필요할 때 텍스처를 직접 사용하는 경로를 열어두어서 씬 품질을 보완합니다.

머티리얼 베이크 파이프라인 자체는 생각보다 단순합니다. 각 PBR 파라미터에 대응하는 텍스처마다 평균값을 구해서, 그걸 상수 형태로 저장하는 식입니다. 트라이플래너(triplanar) 같은 복잡한 머티리얼은 한 번 결과를 텍스처 아틀라스에 구워 넣은 다음, 그 결과 텍스처를 다시 입력으로 사용해서 평균값을 얻습니다. 알베도와 알파는 이렇게 만들어진 텍스처를 바로 사용합니다.

알파 테스트(alpha-test) 머티리얼의 경우에는, 상수를 만들 때 알파 텍스처 값을 함께 고려합니다. 또 한 가지, 이런 머티리얼을 쓰는 메시마다 하나의 불투명도(opacity) 값을 계산해 둡니다. 방식은 이렇습니다. 메시의 삼각형을 알파 텍스처 공간으로 재투영한 뒤, 삼각형 내부 픽셀들만 모아서 알파 값을 평균 내는 식입니다. 이렇게 얻은 값은 레이 트레이싱에서, 이 메시를 어느 정도 "불투명한 것처럼" 처리할지에 대한 기준으로 사용됩니다. 필요에 따라서는 이 값을 가지고, 레이가 불투명 히트를 했다고 볼지, 아니면 통과했다고 볼지 확률적으로 정하기도 하고, 메시를 스케일해서 완전 불투명 메시처럼 근사하기도 합니다.

식생용 머티리얼의 경우에는 계절감을 표현하기 위해 컬러 LUT를 추가로 사용합니다. 각 계절마다 다른 색을 LUT에 정의해 두고, 시즌 값에 따라 머티리얼 컬러를 바꾸는 식입니다. 또, 리프가 있는 버전과 없는 버전 두 가지를 한 알파 텍스처 안에 채널로 나눠 담는 방식도 사용합니다. 이렇게 하면 계절에 맞춰 리프가 있는 채널/없는 채널을 선택해서 그릴 수 있습니다.
- 시즌 값은 각 계절의 시작과 끝을 포함해서 총 8개를 사용합니다.

런타임에서는 매 프레임 BVH 표현을 갱신합니다. 카메라 주변 스트리밍 상황에 따라 인스턴스를 추가하거나 제거하고, 움직이는 오브젝트들의 트랜스폼 행렬도 업데이트합니다. 앞에서 말씀드렸듯이 버텍스 애니메이션은 지원하지 않지만, 트랜스폼 기반 동적 오브젝트는 문제 없이 처리합니다. 지형(terrain) 쪽도 카메라 주변만 따로 갱신합니다. 지형은 일반 지오메트리와 달리 패치 단위로 나뉘고, 독립적인 텍스처 아틀라스를 사용합니다. 마지막으로 카메라 기준에서 인스턴스들을 컬링합니다. 일정 거리 이후부터는, 고체각(solid angle)이 임계값보다 작은 인스턴스를 잘라내는 방식입니다. 인스턴스 수가 워낙 많기 때문에, 이 작업은 한 프레임에 다 하지 않고 타임 슬라이싱(time-slicing)으로 나누어 처리합니다. 전체 씬을 모두 스캔하는 데는 1초가 채 걸리지 않습니다. 참고로 TLAS(Top-Level Acceleration Structure)는 매 프레임 비동기(async) 큐에서 다시 빌드합니다.

BVH 업데이트가 끝나면, 다음으로 프로브 볼륨 데이터를 업데이트합니다.

우리가 사용하는 프로브 볼륨은 카메라를 따라 움직이는 다중 캐스케이드 3D 균일 그리드(multiple cascaded 3D uniform grids) 구조입니다. 지금 셋업에서는 1만 개가 조금 넘는 프로브를 사용하고 있습니다. 각 프로브는 조명 데이터를 담는 버퍼뿐 아니라, 월드 오프셋 같은 메타데이터도 같이 가지고 있어서, 자기 영역 안에서 조금씩 움직일 수 있습니다.
- 캐스케이드 크기와 프로브 해상도는 모두 파라미터로 조정할 수 있습니다.
- 가장 바깥쪽 그리드는 약 512m(32 × 16)를 커버합니다.
- 토로이달(toroidal) 주소 체계를 쓰기 때문에, 각 프로브는 텍스처 아틀라스 안에서 고정된 위치를 갖습니다.
- index = (position / probeSize) % numGridElements

프로브 데이터는 옥타히드럴 인코딩(octahedral encoding)을 적용한 텍스처 아틀라스에 저장됩니다.
우리는 여기서 압축된 G-버퍼를 유지하고, 이 데이터를 기반으로 임시 라이팅 버퍼(방사휘도)를 계산한 뒤, 다시 컨볼루션을 걸어 가시성(visibility), 방사휘도(radiance), 조도(irradiance) 캐시를 만들어 냅니다.
- 프로브 볼륨 전체 메모리 사용량은 약 73MB 정도이며, 세부 내용은 성능 슬라이드에서 다시 보여 드리겠습니다.
- 각 프로브는 자기만의 라이팅 스케일을 가지는 방사휘도·조도 데이터를 가지고 있습니다.
- G-버퍼를 유지하고 있는 이유는, 프로브를 다시 레이 트레이싱하지 않고도 조명을 재계산하기 위해서입니다. 예를 들어 AC 섀도우에서는 플레이어가 라이트를 켜고 끄는 상황이 많은데, 이때 프로브를 다 다시 쏘기보다는 G-버퍼에서 재라이팅하는 편이 훨씬 비용이 적게 듭니다.

물론 1만 개가 넘는 프로브를 매 프레임 전부 업데이트하는 것은 말이 되지 않기 때문에, 여기에는 여러 가지 최적화가 들어가 있습니다.
먼저, 매 프레임 전체 프로브 중 일부만 업데이트합니다. 그리고 업데이트 방식도 세 가지 모드로 나누어 쓰고 있습니다.
- 풀 업데이트(full update): G-버퍼 생성, 라이팅 계산, 컨볼루션, 리로케이션까지 풀 해상도로 전부 수행합니다.
- 풀 업데이트(쿼터 해상도): G-버퍼와 라이팅을 1/4 해상도로 계산해서 레이 트레이싱 비용을 최대 40%까지 줄입니다.
- 라이팅 + 컨볼루션만 수행: 기존 G-버퍼 캐시를 그대로 재사용하면서 라이팅만 다시 계산합니다.
특정 상황, 예를 들어 새 지역을 로딩했거나 텔레포트한 직후에는, 프로브 전체에 풀 업데이트를 한 번씩 돌려서 상태를 맞춰 줍니다.
반대로 평상시에는 업데이트되지 않는 프로브에 대해 라이팅 스케일만 재조정해서, 전체적인 조명 변화에 그럴듯하게 맞춰 가는 방식을 씁니다.

조금 더 구체적으로 보면, 프로브 업데이트는 먼저 "어떤 프로브를 이번 프레임에 건드릴지"를 고르는 단계에서 시작합니다. AC 섀도우에서는 기본적으로 프레임당 1,000개의 프로브를 예산(budget)으로 사용하고 있고, 60fps 모드나 Xbox Series S 같은 저성능 플랫폼에서는 그 절반으로 줄입니다. 프로브 선택에는 3단계 그리디 알고리즘을 사용합니다.
1단계: 새로 생성된 프로브를 전부 우선적으로 잡습니다. 이들은 보통 카메라를 따라 움직이는 3D 그리드의 경계에 있게 됩니다. 카메라 움직임에 따라 새 프로브 수가 크게 요동칠 수 있기 때문에, 이 프로브들은 쿼터 해상도로만 업데이트해서 G-버퍼 비용을 안정화합니다.
2단계: 남은 예산의 약 16%를 각 캐스케이드에 라운드 로빈(round-robin) 방식으로 배분하고, 이 프로브들에 대해서는 풀 해상도 풀 업데이트를 수행합니다.
3단계: 나머지 예산으로, 다시 각 캐스케이드에서 라운드 로빈으로 프로브를 골라서 라이팅과 컨볼루션만 수행합니다. 카메라가 너무 빠르게 움직이지 않는 상황에서는, 이 세 번째 유형이 전체 예산의 80~84% 정도를 차지하게 됩니다.
이 구조 덕분에, 각 프로브는 대략 초당 3번 정도 라이팅 업데이트를 받고, 2초에 한 번 정도는 풀 업데이트를 받게 됩니다.

선택된 프로브들에 대해 가장 먼저 하는 일은 메타데이터를 업데이트하는 것입니다.여기서는 프로브를 실내(indoor)인지, 실외(outdoor)인지 분류합니다. 지형 아래에 있는 프로브들은, 지표 바로 아래에 있는 것 정도만 위로 올려 주고, 그보다 더 깊이 묻혀 있는 것들은 일단 비활성화합니다. 다만 지형 아래라고 하더라도, 인테리어 볼륨 안에 있는 프로브(지하실이나 동굴 등)는 그대로 유지합니다. 이런 영역은 실제로 플레이어가 걸어 다니는 게임플레이 공간이기 때문입니다.

풀 업데이트 대상으로 뽑힌 프로브(풀/쿼터 해상도 포함)는, 각 텍셀의 중심에서 레이를 쏴서 G-버퍼를 새로 만듭니다. 여기서는 지터나 템포럴 누적을 쓰지 않습니다. G-버퍼를 캐시하는 구조와 잘 맞지 않기 때문입니다. 레이 최대 거리는 프로브가 속한 캐스케이드의 크기에 따라 달라지고, 캐스케이드가 클수록 더 멀리까지 쏘게 됩니다.이 단계에서 각 프로브가 맞은 백페이스(back-face) 삼각형 개수도 같이 세어 둡니다. 나중에 프로브를 옮기지 못하는 상황인데 백페이스 비율이 너무 높다면, 그 프로브는 내부에 갇혀 있다고 보고 비활성화합니다.

G-버퍼를 업데이트하는 동안 태양 그림자도 같이 계산합니다. 프로브 라이팅은 프레임마다 일부씩만 갱신되고, 그 사이 태양 위치는 계속 움직이기 때문에, 이 그림자는 완전히 정확한 값은 아닙니다. 하지만 프로브가 대략 2초마다 한 번씩 풀 업데이트를 받는다는 점과, 그 사이 태양이 크게 움직이지 않는다는 점을 감안하면, 실제 플레이 상황에서는 충분히 자연스럽게 보입니다. 그림자 계산은 먼저 카메라 주변에 있는 섀도 맵을 사용하고, 히트 지점이 그 범위를 벗어나면 태양 방향으로 섀도 레이를 한 번 더 쏘는 식으로 처리합니다. 마지막으로 구름 그림자까지 합성해서 최종 결과를 만듭니다.

태양 조명이 빠르게 변하는 상황을 지원하기 위해, 라이팅 계산 단계의 첫 과정은 "이번에 업데이트하지 않는 프로브"들의 스케일을 재조정하는 것입니다. 새로운 태양 위치와 하늘 광도(sky intensity)를 기반으로 각 프로브의 평균 조명을 다시 추정하고, 이전 추정치와 비교해서 라이팅 스케일만 조정합니다. 각 프로브는 자기만의 스케일 값을 가지고 있기 때문에, 텍셀 전체를 다시 쏠 필요 없이 스케일 값만 바꿔도 꽤 괜찮은 결과를 얻을 수 있습니다. 이때 사용하는 데이터는 프로브 메타데이터에 들어 있는 하늘 가시성, 평균 태양/하늘 조명, 평균 로컬 라이트 휘도 등입니다.

그다음에는 선택된 모든 프로브에 대해 실제 라이팅 계산을 수행합니다. 여기에는 이번 프레임에 G-버퍼를 새로 만든 프로브와, 기존 G-버퍼를 재사용하는 프로브 모두 포함됩니다. 계산 자체는 두 패스로 나눠서 진행합니다. 먼저 기하를 맞은 텍셀들을 처리하고, 그 다음에 아무것도 맞지 않은 텍셀들을 처리합니다. 이렇게 그룹을 나누는 것만으로도 라이팅 비용을 약 20% 절감할 수 있었습니다.
- 히트 여부는 히트 거리를 보고 판단하고, 미스는 특수한 거리 값(MISS_DISTANCE)으로 표시합니다.

첫 번째 패스에서는, 모든 라이트에서 오는 직접 조명과 이전 프레임의 프로브 조도 캐시를 사용한 간접 디퓨즈 조명을 함께 계산합니다.

두 번째 패스에서는, 아무 기하에도 맞지 않은 텍셀들을 처리합니다.이 경우에는 먼저 레이 끝 지점이 프로브 볼륨 안에 있는지 확인하고, 안에 있다면 필터링된 방사휘도 캐시를 써서 조명을 얻습니다.레이가 볼륨 범위를 벗어나면, 간단한 하늘 근사(sky approximation)를 대신 사용합니다. 하늘 근사는 8×8짜리 작은 텍스처 하나로 구현되어 있고, 매 프레임 이 값을 갱신합니다.

마지막으로, 프로브 버퍼에는 두 가지 값만 남겨 둡니다. 하나는 전체 방사휘도, 다른 하나는 로컬 라이트 휘도입니다. 이 로컬 라이트 휘도는 나중에 프로브 라이팅 스케일을 다시 맞출 때 사용됩니다.

다음 단계는 선택된 모든 프로브에 대해 컨볼루션을 돌리는 과정입니다. 먼저 깊이 버퍼를 사용해서 Variance Shadow Map(VSM)을 하나 만들고, 이것을 프로브 간 보간 가중치를 계산할 때 사용합니다.[VSM 개념: https://en.wikipedia.org/wiki/Variance_shadow_mapping] 여기서 재미있는 점 하나는, 평균과 평균 제곱값을 계산하기 전에 값을 8비트 범위 [0, 1]로 리스케일하고 클램프한다는 것입니다. 이렇게 하면 실내 모서리 같은 특정 상황에서 섀도 누출을 더 잘 막을 수 있습니다. 출력 단계에서는 테두리 텍셀을 복제해서 안전한 보더를 만들어 줍니다. 예를 들어 5×5 프로브라면 7×7로 확장해서 저장하는 식입니다. 이렇게 해두면 나중에 하드웨어 바이리니어 필터링을 쓸 때 생길 수 있는 엣지 문제를 줄일 수 있습니다.

동시에 방사휘도에 대해서도 컨볼루션을 수행해서, 필터링된 방사휘도·조도 프로브를 만들어 냅니다.
이 과정에서는 하늘 가시성, 평균 로컬 라이트 휘도, 평균 전체 휘도 같은 메타데이터도 함께 계산합니다. 평균 휘도는 프로브별 라이팅 스케일링 팩터로 사용됩니다.
- 이런 방사휘도 스케일링을 사용하면, 한쪽 극단에서는 강한 태양광 아래의 야외, 다른 쪽 극단에서는 어두운 동굴 실내까지, 그리고 낮과 밤을 오가면서도 충분한 정밀도를 확보할 수 있습니다. 노출 값으로 보면 대략 -16에서 +16 사이를 커버하는 셈입니다.

컨볼루션에 사용하는 컴퓨트 셰이더는 성능을 많이 신경 쓴 편입니다. 각 스레드가 출력 텍셀 하나를 담당해서 전체 컨볼루션을 계산하는 구조를 유지하되, 먼저 로컬 데이터 스토어(LDS)에 방사휘도 데이터를 통째로 올려놓고, 그 안에서 연산을 최대한 끝내는 방식입니다. 이때 입력 데이터를 LDS로 옮길 때 바이리니어 필터링을 함께 써서, 읽어야 하는 샘플 수를 4배 줄였습니다. 정확한 가중치를 쓰지는 못하지만, 실제로는 품질 손실이 미미하고, 속도는 약 4배 정도 이득을 볼 수 있었습니다.
- 각 스레드 그룹은 서로 다른 프로브 하나를 맡아서, 방사휘도와 조도 컨볼루션을 동시에 수행합니다. 각 스레드는 출력 텍셀 하나를 계산합니다.
- 모든 스레드는 64텍셀 단위로 방사휘도를 LDS에 나눠서 로딩하고, 그다음 각자의 BRDF 방향에 맞춰 이 값들을 컨볼브합니다. 이 과정을 입력 프로브 전체를 소진할 때까지 반복합니다.
- AC 섀도우에서는 10×10 방사휘도 캐시와 5×5 조도 캐시를 사용합니다. 따라서 한 프로브당 100 + 25 = 125개의 스레드가 필요하고, 실제 구현에서는 64의 배수인 128로 맞춰 사용합니다.
- 방사휘도 필터링용 커널은 클램프된 코사인 분포를 사용합니다: pow(saturate((cos-0.75)/(1-0.75)), 4).

프로브 볼륨 업데이트의 마지막 단계는, 지오메트리 내부에 있거나, 지오메트리와 너무 가까운 프로브들을 더 좋은 위치로 옮기는 작업입니다.
적당한 위치를 찾지 못하면, 해당 프로브는 과감히 비활성화합니다.
- 백페이스 히트 비율이 일정 임계값을 넘으면, 그 프로브는 지오메트리 내부에 있다고 판단합니다.
- 새 위치를 찾을 때는 프로브 G-버퍼의 깊이 값을 전체적으로 살펴보고, 최소 깊이가 너무 작으면 최대 깊이 방향으로 프로브를 이동시킵니다.
2부는 내일... 분량이 너무 많아요. ㅜㅜ
'TECH.ART.FLOW.IO' 카테고리의 다른 글
| [INDEX] SIGGRAPH 2025 ADAVANCED REALTIME RENDERING (0) | 2026.01.08 |
|---|---|
| [발표 번역 2부] Siggraph 2025 Ray tracing the world of Assessin’s Creed Shadow. (2) | 2026.01.08 |
| [번역] Unity에서 스킨드 메시의 GPU Driven 렌더링 구현 (0) | 2026.01.03 |
| [발표 번역] SIGGRAPH 2025 idTech8에서의 글로벌 일루미네이션 (0) | 2026.01.02 |
| [번역] Bindless 에 관한 간략한 논의: UE5에서의 Bindless (1) | 2026.01.02 |