올해 초 NVIDIA는 RTX Mega Geometry라는 새 레이트레이싱 기술을 조라(Zorah) 데모와 함께 공개했다. 이 데모는 약 100GB짜리 언리얼 엔진 씬으로 배포됐고, 그것도 NvRTX라는 특수 브랜치에서만 열 수 있었다. 핵심은 드라이버가 노출하는 새 레이트레이싱 기능, 특히 클러스터 단위 레이트레이싱을 나나이트(Nanite)의 클러스터형 LOD 파이프라인과 결합한 점이다. 이렇게 하면 언리얼 엔진이 지금 레이트레이싱용으로 따로 만드는 나나이트 프록시 메시 없이도, 매우 정교한 씬을 스트리밍하며 완전한 레이트레이싱으로 렌더링할 수 있다. 다만 지나치게 언리얼에 종속돼 외부 실험이 어려웠는데, 9월 초 NVIDIA가 오픈소스 샘플 vk_lod_clusters를 업데이트하며 조라 씬을 glTF로 함께 배포했다. 이 글은 meshoptimizer 개발자가 그 파일을 다루며 계층적 클러스터 LOD 지원을 개선한 기록이다.
나나이트식 파이프라인이 하는 일
기법의 뼈대는 이렇다. 삼각형이 아주 많은 메시가 주어지면, 임의의 상세도로 표현 가능한 계층 구조를 만들고, 그 일부를 적절한 상세도로 스트리밍하며, 보이는 부분만 알맞은 LOD로 렌더링한다. 여기서 선택된 구조는 클러스터들의 방향성 비순환 그래프(DAG)다. 각 클러스터는 최대 128개 정도의 삼각형으로 이뤄진 작은 패치이며, 특정 상세도에서 메시의 한 조각을 나타낸다. 런타임은 어떤 클러스터를 더 거친 것으로 대체할지 결정하는데, 교체로 생기는 시각적 오차가 1픽셀 미만일 때만 바꾸고, 그 전환은 TAA 같은 시간적 필터로 가려진다.
구조를 만드는 절차는 재귀적이다. 메시를 클러스터로 나누고, 인접 클러스터를 조금 더 큰 그룹으로 묶은 뒤, 각 그룹을 경계 에지를 보존한 채 독립적으로 단순화한다. 그 결과를 다시 클러스터로 쪼개고, 더 만들 수 있는 클러스터가 없을 때까지 반복한다. 서로 다른 LOD의 클러스터 사이에 균열이 생기지 않도록 알고리즘을 조합하는 데는 상당한 미묘함이 있으며, 이 주제만으로 여러 편의 학위 논문이 나왔다. meshoptimizer는 2024년부터 자체 알고리즘을 조합해 이 구조를 만드는 예제 코드를 제공해 왔고, 조라 씬 처리도 그 예제에서 출발했다.
36GB 위치 전용 glTF와 메모리의 벽
막상 열어 본 glTF는 만만치 않았다. 크기는 36GB인데 기하 정보만 담겨 있고, 대다수 메시는 정점 속성 없이 위치(position)만 들어 있었다. 셰이딩용 노멀은 셰이더가 위치로부터 유도하는 방식이다. 이 파일을 블렌더로 임포트하면 약 10분간 돌다가 메모리 부족으로 죽고, 언리얼 엔진은 더 빠르게, 즉 5분이 채 안 돼 크래시했다. 192GB RAM으로도 부족했던 셈이다. 다행히 임포트가 아니라 NVIDIA 샘플의 처리 코드를 돌리는 것이 목적이었는데, 9월 초 기준 이 코드도 16스레드로는 메모리가 터졌다. 실험 끝에 다른 작업이 없을 때 8스레드(--processingthreadpct 0.25)로 약 180GB 이상을 쓰며 겨우 돌릴 수 있었고, 컴퓨터를 병행 사용하려면 7스레드로 약 30분이 걸렸다.
처리에 필요한 세 알고리즘, 즉 메시를 클러스터로 나누는 클러스터화, 클러스터를 묶는 파티셔닝, 클러스터 그룹을 줄이는 단순화는 모두 meshoptimizer가 제공한다. 버전 0.25 기준 클러스터화 알고리즘은 둘이다. 하나는 래스터화와 메시 셰이더용으로 메시렛 수를 최소화하며 8년간 다듬어졌고, 다른 하나는 레이트레이싱과 새 클러스터 레이트레이싱 확장을 위해 최근 개발됐다. 레이트레이싱에서는 클러스터 경계 위치가 매우 민감한데, 최적 클러스터화가 되어야 각 클러스터로 마이크로 BVH를 만들고 그 위에 전체 BVH를 쌓아 광선을 추적할 수 있기 때문이다. vk_lod_clusters 샘플은 레이트레이싱 최적 클러스터만 필요하지만, 원래 데모는 래스터 최적 클러스터를 써 왔다.
저자는 알고리즘 연구용이던 데모 코드를 재사용 가능한 인터페이스로 다시 썼다. 정점 속성을 별도로 전달받게 해 위치만 있는 메시에서 노멀 처리에 시간을 낭비하지 않도록 했고, 불필요한 STL 복사를 없앴다. 36GB 파일을 통째로 올리는 부담을 피하려 cgltf 기반 메모리 매핑을 썼고, 이를 쉽게 쓰도록 cgltf에 작은 PR도 기여했다. 재인덱싱도 중요했다. 원본에는 30M 삼각형에 90M 정점을 쓰는 식의 비효율적 메시가 있었는데, 이는 단순화 품질을 떨어뜨리고 처리 시간도 해쳤다. 이렇게 손본 프로그램은 리눅스·16스레드에서 래스터 기준 약 9분 20초에 54.6GB, 레이트레이싱 기준 약 7분 10초에 57.6GB로 전체 파일을 처리했다.
memset이 삼킨 시간
7~9분도 짧지 않아 저자는 Superluminal 프로파일러로 병목을 파고들었다. 눈에 띈 것은 뜻밖에도 memset이었다. 두 클러스터화 알고리즘 모두 정점 인덱스로 색인되는 배열을 두고 어떤 정점이 현재 메시렛에 배정됐는지 추적하는데, 이 방식은 정점 수가 작을 때만 빠르다. 30M 삼각형 메시의 부분집합을 반복 클러스터화하는 상황에서는 오히려 독이 됐다. 유사한 문제는 단순화기에도 있었다. 2024년 클러스터 LOD 대응 때 입력이 메시의 작은 부분집합이라 가정하고 O(정점 수) 작업을 피하는 meshopt_SimplifySparse 플래그를 넣었지만, 필터링용 비트 배열을 초기화하는 부분이 남아 있었다. 정점당 1비트는 16비트보다 훨씬 싸지만, 1억 삼각형에 육박하면 그것마저 쌓였다. 그동안 테스트한 최대 메시가 6M 삼각형·3M 정점으로 한 자릿수 배 이상 작았다는 점이 이런 가정의 배경이었다.
해결책은 과하지 않았다. 전면적인 해시 맵 전환도 가능하지만 비용과 복잡성이 따르므로, 우선 희소 접근(인덱스 수가 정점 수보다 작은 경우)이 감지될 때 인덱스 버퍼가 실제로 쓰는 배열 항목만 초기화하도록 고쳤다. 이 수정만으로 래스터 버전은 3분 31초, 레이트레이싱 버전은 3분 57초로 떨어졌다. 실무 관점에서 이 기록은 두 가지를 일깨운다. 자료구조를 정점 전체 크기에 비례해 초기화하는 관행은 데이터 규모가 한두 자릿수 커지는 순간 곧바로 병목이 되고, 프로파일러 없이는 memset 같은 평범한 연산이 시간을 삼키는 사실조차 알기 어렵다. 다만 이 측정에는 한계가 있다. 저자의 테스트 코드는 결과를 디스크에 저장하지 않으므로, 직렬화·저장 비용을 포함한 완전한 파이프라인과는 동일 조건 비교가 아니다.