TECHARTNOMAD | MAZELINE.TECH

TECH.ART.FLOW.IO

[SIGGRAPH 2026] SLIM: 로블록스(Roblox)의 사용자 생성 3D 월드 대규모 확장 기법

jplee 2026. 8. 17. 02:09

요약 (Abstract):

로블록스(Roblox) 게임은 수백만 명의 크리에이터들이 키트배싱(Kitbash) 방식으로 자유롭게 조합한 사용자 생성 콘텐츠(UGC)로 구축됩니다. 여기에는 전통적인 아트 디렉션 제약, 고정된 리소스 예산(Content Budget), 수작업으로 제작된 LOD, 혹은 예측 가능한 런타임 동작 같은 기준이 존재하지 않습니다. 그럼에도 이러한 게임들은 연산 능력, 메모리, 그래픽 성능 및 네트워크 환경이 천차만별인 매우 광범위한 기기에서 원활하게 구동되어야 합니다.

본 발표에서는 원작자가 의도한 외형, 동작 및 시맨틱(의미적 속성)을 그대로 보존하면서, 로블록스 월드에 대한 가볍고 기기 맞춤형(Device-adaptive) 런타임 표현(Representation)을 자동으로 생성하는 클라우드 기반 시스템 SLIM을 소개합니다. 수 페타바이트(PB)에 달하는 기존 UGC를 대상으로 SLIM을 대규모로 구축하고 배포하는 과정에서의 기술적 동기, 시스템 아키텍처, 엔진 통합, 그리고 프로덕션 단계의 도전 과제를 다룹니다. 또한 표현 생성, 클라이언트 선택, 시각적 충실도(Fidelity) 유지, 완전한 런타임 시맨틱이 요구될 때 원본 콘텐츠로 되돌아가는 폴백(Fallback) 간의 트레이드오프를 논의합니다. 마지막으로 기기 맞춤형 표현 방식과 서버 지원 렌더링(Server-assisted rendering)의 향후 발전 방향을 제시합니다.

발표자 소개 (Speaker Bio):

세르게이 마키브(Sergey Makeev)는 현재 로블록스(Roblox)에서 시니어 테크니컬 디렉터(Senior Technical Director)로 재직 중인 베테랑 그래픽 엔지니어이자 테크니컬 리더입니다. AAA 게임 개발과 대규모 플랫폼 분야에서 20년 이상의 경력을 쌓았으며, 고성능 렌더링 파이프라인 아키텍처와 코어 엔진 기술을 전문으로 다룹니다. 로블록스에서 수백만 대의 디바이스에 걸쳐 로블록스 월드의 성능과 그래픽 품질을 자동으로 최적화하는 플랫폼인 SLIM을 설계했습니다. 또한 다층 레이어 의상이 어떤 아바타 체형에도 자동으로 피팅되는 '레이어드 클로딩(Layered Clothing)' 시스템을 주도하여 개발했습니다. 아울러 로블록스 렌더링 엔진을 물리 기반 렌더링(PBR) 및 고급 라이팅 시스템으로 현대화한 "Future Is Bright" 프로젝트를 총괄했습니다.

이전에는 테크니컬 디렉터로서 여러 멀티플랫폼 엔진을 밑바닥부터 직접 구축했으며, Skyforge, Warface, Armored Warfare 등 주요 타이틀의 기술 리더십을 맡았습니다. 실시간 그래픽스 커뮤니티의 활발한 기여자로서 GPU Pro 5와 GPU Zen 2에 기술 논문을 게재하기도 했습니다.


안녕하세요, 여러분. 저는 세르게이 마케예프(Sergey Makeev)이며, Roblox의 수석 기술 디렉터(Senior Technical Director)입니다.

오늘은 사용자 제작 3D 월드를 확장하는 방법에 대해 이야기하겠습니다. Roblox에서 렌더링이 왜 어려운 문제인지, 그리고 저희가 이를 어떻게 해결했는지 다루겠습니다.

먼저 플랫폼에 대한 배경을 설명하고, 데모를 보여드린 다음, 이것이 어떻게 작동하는지 자세히 살펴보겠습니다.

먼저 다양한 로블록스(Roblox) 게임을 보여주는 영상을 함께 보시죠.

[영상 재생]

사람들은 "로블록스"라고 하면 보통 단순하고 각진(블록 형태의) 게임을 떠올립니다 — 지금 화면에 보이는 것 같은 게임들이죠.

그리고 그건 당연합니다 — 그런 게임들은 우리 역사의 큰 부분을 차지하니까요. 하지만 로블록스 크리에이터들은 다양한 장르와 비주얼 스타일에 걸쳐 온갖 종류의 게임을 만듭니다.

그중 일부 월드는 정말 방대하고 정교합니다.

영상을 보시면서 한 가지 염두에 두셨으면 하는 점이 있습니다: 여기 나오는 모든 게임은 동일한 엔진에서 구동되며, 동일한 버전의 코드와 콘텐츠가 우리가 지원하는 모든 기기에서 원활하게 실행되어야 합니다.

바로 그 광범위함(다양성)이 로블록스에서 렌더링이 어려운 큰 이유 중 하나입니다.

그럼 영상을 즐겨 주세요.

로블록스란 정확히 무엇일까요?

그것은 하나의 게임도, 단일 가상 세계도, 다른 사람이 라이선스로 빌려 쓸 수 있는 엔진도 아닙니다. 로블록스는 하나의 플랫폼입니다: 엔진, 클라우드 서비스, 콘텐츠 시스템, 그리고 그 위에서 활동하는 크리에이터와 플레이어들의 커뮤니티까지 아우르는 것이죠.

그 영상에서 보신 모든 것은 사용자가 직접 만든(user-generated) 것입니다. 크리에이터들이 월드, 아바타, 의상, 도구를 만들고 — 플랫폼은 이 모든 것을 콘솔, PC, 휴대폰, 태블릿, VR 헤드셋, 심지어 스마트 TV에까지 전달합니다.

렌더링 엔지니어의 입장에서 보면, 이는 평소에 기대어 왔던 모든 전제를 사라지게 만듭니다. 단일한 아트 디렉션도 없고, 고정된 콘텐츠 예산(budget)도 없으며, 정해진 하드웨어 타깃도 없습니다. 렌더러는 크리에이터가 다음에 무엇을 만들든 그것을 처리해 낼 수 있어야 합니다.

왜 그것이 어려운지 이해하기 위해, 이제 이 세계를 렌더링 관점에서 살펴보겠습니다.

 

모든 로블록스 월드의 중심에는 데이터 모델(data model)이 있습니다. 이는 객체, 속성, 스크립트, 그리고 에셋에 대한 참조로 이루어진 살아 있는(live) 트리 구조입니다. HTML의 DOM과 비슷하다고 생각하시면 되는데, 다만 3D 월드를 위한 것이죠.

게임 서버가 시뮬레이션을 소유하며 이 트리를 업데이트합니다. 스크립트와 물리 연산은 언제든지 무엇이든 바꿀 수 있습니다. 각 클라이언트는 자신에게 필요한 트리의 일부분만 전달받습니다.

우리의 에디터인 로블록스 스튜디오(Roblox Studio)도 동일한 데이터 모델과 동일한 엔진 코드를 사용하며, 그 위에 협업 멀티플레이어 편집 기능이 얹혀 있습니다.

이것이 중요한 이유는 다음과 같습니다: 로블록스에서 "정적(static)"이라는 것은 하나의 가정일 뿐, 결코 보장이 아닙니다. 이 때문에 오프라인 최적화나 일회성 베이크(bake)에 의존하는 대부분의 표준적인 기법들을 쓸 수 없게 됩니다.

전형적인 로블록스 월드는 계속해서 변화하며, 결코 안정적인 상태에 도달하지 않습니다.

 

이제 같은 문제를 콘텐츠 전달(content delivery) 측면에서 살펴보겠습니다.

크리에이터는 단 한 번의 클릭으로 게임을 모든 플랫폼에 배포합니다. "Publish(게시)" 버튼을 누르면, 새 버전이 즉시 모든 기기에서 사용 가능해집니다. 플레이어는 빌드를 설치하거나 패치를 기다리지 않습니다 — 로블록스 게임에 접속하는 것은 오히려 웹 페이지를 여는 것에 더 가깝습니다.

플레이어가 접속하면, 서버는 살아 있는 데이터 모델을 복제(replicate)하기 시작하고, 그와 동시에 메시(mesh), 텍스처, 오디오는 CDN에서 별도로 도착합니다. 그리고 이 모든 과정이 진행되는 동안에도 모든 것은 계속 변합니다 — 시뮬레이션은 돌아가고, 플레이어가 이동함에 따라 에셋은 스트리밍되어 들어오고 나갑니다.

그러니 다시 한번, 안정적인 상태(steady state)란 없으며, "이제 월드가 최종 완성되었다"라고 말할 수 있는 단일한 시점도 존재하지 않습니다.

 

그리고 콘텐츠의 양은 어마어마합니다.

매일 크리에이터들은 약 120만 개의 메시(mesh)와 160만 개의 이미지를 업로드합니다. 임의의 어느 하루를 봐도, 상위 1,000개 게임 중 약 26퍼센트가 수백만 명의 플레이어에게 실시간으로 반영되는 업데이트를 배포합니다.

이 모든 것이 여덟 개의 플랫폼에서 작동해야 하는데, 우리의 고사양 기기는 저사양 기기보다 약 800배 더 많은 연산 능력을 가지고 있음에도 그렇습니다.

또한 우리는 20년치의 콘텐츠를 축적해 왔습니다 — 700페타바이트가 넘죠. 그리고 이 모든 것은 정작 그 콘텐츠가 만들어질 당시에는 존재하지도 않았던 기기에서도 여전히 구동되어야 합니다.

그러니 "콘텐츠 완료(content complete)"라는 단계 또한 없습니다. 새로운 콘텐츠는 계속 도착하고, 오래된 콘텐츠는 계속 작동해야 하며, 하드웨어 타깃은 결코 멈추지 않고 움직입니다.

 

이번 강연을 위해, 저는 전형적인 로블록스 월드를 대표하는 데모를 하나 만들었습니다.

미리 말씀드리자면: 저는 그래픽스 엔지니어이지 아티스트가 아닙니다. 하지만 많은 크리에이터들이 하는 방식 그대로 만들었습니다 — 카탈로그에서 수천 개의 에셋을 가져다 짜깁기(kitbashing)하는 방식으로요.

그리고 이 데모는 여러분이 예상하실 법한 특성을 정확히 가지고 있습니다: 고르지 않은 삼각형 및 텍스처 밀도, 엄청나게 많은 작은 객체들, 그리고 서로 겹치는 수많은 지오메트리. 각각의 에셋은 그 자체로는 문제가 없더라도, 그것들이 수십만 개 모이면 결코 효율적인 장면(scene)이 되지 않습니다.

이 장면에는 약 172,000개의 데이터 모델 인스턴스가 있습니다. 아무런 최적화 없이 단순하게(naively) 렌더링하면, 프레임당 5,940만 개의 삼각형과 약 3,500번의 드로우 콜(draw call)이 발생합니다.

이는 데스크톱에서조차 렌더링하기에 너무 부담스러운 수준입니다 — 그런데도 이것이 100달러짜리 휴대폰에서 구동되어야 합니다.

 

하지만 여기, 보시죠.

[영상 재생]

이것은 데스크톱에서 4K 해상도로 실시간 구동되고 있으며, 우리가 SLIM이라고 부르는 최적화된 표현(representation) 방식을 사용하고 있습니다.

 

그리고 이것은 동일한 장면을 휴대폰에서 구동한 모습입니다.

동일한 데이터 모델, 동일한 에셋, 그리고 동일한 엔진입니다.

별도의 모바일 버전이란 없습니다 — 유일한 차이는 런타임 표현(runtime representation) 방식뿐입니다.

 

여기 수치를 보시죠. 단순하게(naively) 렌더링하면, 이 장면은 프레임당 약 5,900만 개의 삼각형을 필요로 합니다. SLIM을 적용하면 이 수치가 데스크톱에서는 470만 개, 태블릿에서는 240만 개, 그리고 휴대폰에서는 단 110만 개로 떨어집니다.

그렇다면 이것이 어떻게 가능할까요?

우리는 이 시스템을 세 가지 기둥(pillar) 위에 구축했습니다.

첫째, 에셋 트랜스코더(asset transcoder): 우리는 크리에이터의 콘텐츠를 안정적이고 버전 관리되는 소스 데이터로 보관하고, 필요한 런타임 형태는 무엇이든 여기서 파생시킵니다.

둘째, SLIM 월드 트랜스코더(world transcoder): 같은 개념이지만, 단일 에셋이 아니라 월드 전체에 적용한 것입니다.

셋째, 런타임 오케스트레이션(runtime orchestration): 각 클라이언트가 자신의 메모리, 렌더링, 네트워크 예산(budget) 맞는 최적의 디테일 수준을 선택합니다.

 

에셋 트랜스코더(asset transcoder)부터 시작하겠습니다. 개념은 단순합니다: 소스는 보존하고, 런타임 형태는 파생시킨다는 것입니다.

 

앞서 보셨듯이, 로블록스 월드는 결코 안정적으로 정착하지 않습니다. 인스턴스는 생겼다 사라지고, 스크립트는 속성을 바꾸며, 물리 연산은 사물을 이리저리 움직입니다. 이는 예외적인 경우(edge case)가 아니라 — 지극히 정상적인 동작입니다.

우리는 월드를 얼려 놓고 오프라인 빌드나 베이크(bake) 실행할 수 없습니다.

하지만 그렇다고 해서 모든 것이 항상 변한다는 뜻은 아닙니다.

 

살아 있는 데이터 모델은 가변적(mutable)입니다. 하지만 그것이 참조하는 에셋은 그렇지 않습니다.

에셋 참조(asset reference)는 정확히 하나의, 변경 불가능한(immutable) 버전을 가리킵니다: 특정한 텍스처 바이트와 특정한 메시 데이터를 말이죠. 크리에이터가 에셋을 편집하면, 우리는 새로운 버전을 생성합니다 — 기존 버전의 바이트는 절대 건드리지 않습니다.

그래서 월드가 계속 변하는 와중에도, 에셋 참조는 우리에게 안정적인 입력값을 제공합니다. 이것이 바로 다른 모든 것이 그 위에 서 있는 토대입니다.

 

하지만 안정적인 소스가 곧 고정된 런타임 포맷을 의미하는 것은 아닙니다.

에셋이 생성되는 시점에, 우리는 그것의 미래 요구사항을 알지 못합니다. 압축 포맷은 플랫폼마다 다릅니다. 인코더는 끊임없이 개선됩니다. 우리는 새로운 플랫폼을 추가하고, 우리의 자체(in-house) 포맷도 발전합니다 — 콘텐츠가 게시되고 한참이 지난 후에도 말이죠.

단 하나의 영구적인 인코딩으로는 모든 기기를 감당할 수 없습니다.

그리고 우리는 크리에이터에게 오래된 콘텐츠를 다시 게시(republish)해 달라고 절대 요청하지 않습니다. 기존 콘텐츠는 자동으로 상위 호환(forward-compatible)이 되어야 하는데, 이는 처음 들으면 불가능한 일처럼 들립니다.

 

그 해답은 바로 로블록스 콘텐츠 플랫폼(content platform)입니다. 하나의 에셋 페치(fetch) 요청 과정을 함께 살펴보겠습니다.

클라이언트는 데이터 모델에서 에셋 참조를 가져와, 에셋 ID, 정확한 버전, 그리고 자신이 이해할 수 있는 포맷들의 순서가 매겨진 목록을 전송합니다.

이 예시에서는 텍스처를 요청합니다: 먼저 BC1, 그다음 ETC2, 그다음 원본 소스 순으로요. 이 순서가 중요합니다 — 첫 번째 항목이 선호하는(preferred) 포맷이고, 나머지는 대체(fallback) 수단입니다.

콘텐츠 플랫폼은 그 ID, 버전, 포맷을 실제 콘텐츠 바이트로 변환(resolve)합니다.

 

가장 이상적인 경우는 캐시 히트(cache hit)입니다. 요청된 형태가 이미 존재하므로, 플랫폼은 CDN URL을 반환하고 클라이언트는 그것을 직접 다운로드합니다.

아무것도 새로 빌드되지 않습니다 — 동일한 것을 요청하는 모든 클라이언트는 동일한 결과를 받습니다.

 

이제 캐시 미스(cache miss)의 경우를 살펴보겠습니다. 기억하세요, 크리에이터는 소스만 업로드했고, 런타임 포맷들은 그로부터 파생된다는 것을요.

원하는 표현(representation)을 사용할 수 없을 때, 플랫폼은 트랜스코더 작업(job)을 제출합니다. 이 작업은 큐(queue)에 들어가고, 트랜스코더가 이를 집어 들어 정확한 소스 버전을 가져온 뒤 처리합니다 — 이 경우에는 밉맵(mipmap)을 생성하고 텍스처를 압축하는 방식으로요. 그 결과물은 해당 에셋의 새로운 표현으로 게시됩니다.

그 이후로는, 그저 캐시된 콘텐츠일 뿐입니다 — 이후의 모든 요청은 그것을 재사용합니다. 하지만 이 작업은 비동기(asynchronous)로 이루어지므로, 미스 경로(miss path)에는 안전한 대체 수단(fallback)이 필요합니다.

 

클라이언트는 트랜스코딩이 끝나기를 기다리지 않습니다.

선호하는 포맷이 없으면, 플랫폼은 목록에 있는 다음 포맷을 시도합니다. 우선순위가 낮은 포맷이 준비되어 있으면, 클라이언트는 선호 포맷이 클라우드에서 생성되는 동안 그것을 사용합니다. 아무것도 준비되지 않았다면, 서비스는 "준비 안 됨 — 나중에 다시 시도하세요"라고 응답하고, 클라이언트는 정확히 그대로 합니다.

여기서 핵심은 대체(fallback)가 오류 경로(error path)가 아니라는 점입니다. 그것은 콘텐츠 계약(content contract)의 일부이며, 게임을 결코 멈추게 하지 않으면서도 새로운 포맷이 등장할 수 있게 해 줍니다.

 

자, 이것이 전체 그림입니다. 전통적인 에셋 쿠킹(asset cooking)이 마법처럼 그냥 사라진 것은 아닙니다 — 그것은 콘텐츠 플랫폼 내부로 옮겨가, 사용자에게는 보이지 않은 채 필요할 때(on demand) 실행됩니다.

하나의 안정적인 소스가 얼마든지 많은 런타임 형태를 만들어 내고, 그 모든 결과물은 캐시되어 공유됩니다.

 

물론 이 다이어그램은 단순화한 것입니다. 실제 시스템은 콘텐츠 검열(moderation), 권한(permission), 현지화(localization), 부하 분산(load balancing) 등 훨씬 더 많은 것을 처리합니다.

프로덕션 규모에서, 이 시스템은 초당 약 1,200만 건의 요청을 처리합니다. 우리의 트랜스코더 서버군(fleet)은 960대의 서버로 구성되어 있으며, 총 약 168,000기가헤르츠의 CPU 용량과 8.3테라바이트의 RAM을 갖추고 있습니다.

이제 이 파생된 형태(derived form)들을 제대로 정의해 보겠습니다.

 

우리는 각각의 파생된 형태를 대체 콘텐츠 표현(alternative content representation), 줄여서 ACR이라고 부릅니다.

하나의 에셋 ID는 여러 개의 변경 불가능한 소스 버전을 가지며, 각 소스 버전은 다시 여러 개의 ACR을 만들어 냅니다: 텍스처 인코딩, 메시 LOD, 오클루전(occlusion) 데이터 등이죠.

여기서 두 개의 독립적인 버전 축(version axis)이 있다는 점을 기억하세요. 크리에이터가 에셋을 편집하면 새로운 소스 버전이 생깁니다. 트랜스코더 코드가 바뀌면 — 동일한 소스로부터 — 새로운 ACR 포맷 버전이 생깁니다.

ACR은 콘텐츠 주소 지정(content-addressed) 방식으로 관리되고, 캐시되며, 중복 제거(deduplicate)됩니다. 소스는 영구적이지만, 모든 ACR은 폐기 가능(disposable)합니다. 시스템이 언제든지 그것을 다시 생성할 수 있기 때문이죠.

 

이는 우리에게 또 하나의 매우 유용한 특성을 제공합니다: 에셋 포맷이 바뀌더라도, 구버전과 신버전의 엔진이 공존할 수 있다는 점입니다.

각 엔진 버전은 자신이 이해할 수 있는 포맷만 요청하고, 플랫폼은 동일한 소스로부터 서로 다른 ACR을 제공합니다. 전역적인 변환(global conversion)도, 조율된(coordinated) 업데이트도 없습니다.

그리고 구버전 클라이언트들이 더 이상 특정 구형 포맷을 요청하지 않게 되면, 플랫폼은 그저 그것을 가비지 컬렉션(garbage-collect)으로 정리해 버립니다.

 

또 하나의 좋은 특성이 있습니다: 코드가 어디에서 실행되든, 동일한 입력은 항상 동일한 출력을 만들어 냅니다. 그래서 실행 위치(execution location)는 하나의 배포(deployment) 선택지가 됩니다.

클라우드에서는, 트랜스코더가 만들어 낸 결과물이 수백만 명의 플레이어와 공유됩니다.

우리는 동일한 트랜스코더를 스튜디오나 클라이언트에서도 실행할 수 있으며, 동일한 코드가 즉각적인 로컬 미리보기(local preview) 생성합니다.

또는 그것을 게임 서버에서 실행할 수도 있는데, 여기서는 실시간 게임 세션 내부에서 생성된 콘텐츠를 처리할 수 있습니다.

배치(placement) 결정하는 것은 지연 시간(latency), 연산량, 재사용성, 그리고 신뢰(trust)이지 — 코드 자체가 아닙니다.

 

여기 두 가지 실제 예시가 있습니다.

위쪽 영상은 Cube4D입니다 — 실행 중인 게임 내부에서 콘텐츠를 생성하는 우리의 생성형 AI 모델이죠. 플레이어가 어떤 객체를 설명하면, 모델이 게임 세션 안에서 바로 메시, 텍스처, 스크립트를 만들어 냅니다.

이 경우, 트랜스코딩은 게임 서버에서 이루어집니다. 그 결과물은 해당 세션에 사용되거나, 플랫폼에 게시되거나, 혹은 플레이어가 마음에 들어 하지 않으면 폐기될 수 있습니다.

오른쪽 영상은 로블록스 메이크업(makeup)입니다 — 플레이어가 직접 편집하는 레이어드(layered) 텍스처죠. 이는 두 개의 트랜스코더 작업을 발생시킵니다: 하나는 즉각적인, 플레이어 전용 미리보기를 위한 로컬 작업이고, 다른 하나는 안전성 검사, 더 나은 압축, 그리고 게임 간 공유를 위한 클라우드 작업입니다.

두 경우 모두 동일한 계약(contract)입니다 — 작업이 필요한 곳에서 실행될 뿐이죠.

 

이 모델 아래에서 텍스처 파이프라인이 어떻게 발전해 왔는지 보여드리겠습니다.

초창기에는, 모든 클라이언트가 PNG를 다운로드하고, 디코딩한 뒤, 밉맵(mipmap)을 생성했습니다. 그런 다음 런타임에 압축되지 않은 RGBA 텍스처를 만들었죠. 작동은 했지만, 모든 클라이언트가 그 비용을 치러야 했습니다.

2013년경, 우리는 임시방편의(ad hoc) 클라우드 텍스처 변환기를 사용해 GPU 네이티브(GPU-native) 블록 압축 텍스처를 생성하기 시작했습니다. 이것은 텍스처만 처리했고, 하드코딩된 몇 가지 포맷만 지원했습니다.

2019년에는 PBR 재질(material)을 추가했습니다. 이 텍스처 레이어들은 함께 처리되어야 하는데, 밉맵을 생성할 때 노멀 맵(normal map)이 러프니스(roughness)에 영향을 주기 때문입니다. 이로 인해 우리는 트랜스코더 시대로 접어들게 되었습니다.

트랜스코더를 사용해, 우리는 2025년에 텍스처를 여러 개의 ACR로 분할하여 CDN에서 점진적으로(progressively) 스트리밍될 수 있게 했습니다. 또한 레이트-왜곡 최적화(rate-distortion optimization)를 도입했는데, 이는 텍스처 다운로드 크기를 눈에 띄게 줄여 주었습니다.

오늘날, 트랜스코더는 최신 인코더를 사용해 KTX2 텍스처를 생성하며, 점진적 텍스처 스트리밍을 지원합니다.

그리고 소스는 결코 바뀌지 않았습니다. 20년 전에 업로드된 텍스처도 이 모든 것을 자동으로 적용받습니다.

 

메시(mesh)도 비슷한 길을 걸었습니다 — 중간에 약간의 우회로가 있긴 했지만요.

우리는 2016년에 단순한 텍스트 기반 포맷으로 시작했습니다; 모든 클라이언트가 로드할 때마다 그 텍스트 파일을 파싱했죠. 2017년에는 임포트(import) 과정에서 바이너리 파일을 생성하기 시작했습니다 — 업로드 전에 이루어지는 클라이언트 측 처리 단계였죠. 2021년에는 임포트가 스킨드 메시(skinned mesh)와 자동 생성 LOD도 지원하게 되었습니다. 하지만 이 모든 처리가 업로드 전에 일어났고, 이는 사실상 소스 에셋의 정의 자체를 바꾸는 것이었습니다.

그리고 여기에 문제가 있습니다: 우리는 기존 메시를 바꿀 수 없었기 때문에, 개선이 이루어질 때마다 또 다른 메시 포맷이 생겨났습니다. 정점에 달했을 때는, 엔진이 원래의 텍스트 기반 포맷을 포함해 무려 열 가지의 서로 다른 메시 포맷을 지원했습니다.

LOD도 같은 문제를 겪었습니다. LOD는 임포트 시에 단 한 번만 생성되었기 때문에, 더 나은 알고리즘이나 버그 수정이 기존 에셋을 결코 개선해 주지 못했습니다.

그 후 우리는 트랜스코더로 전환했습니다. 다운로드 크기를 줄이기 위해 Draco 압축을 도입했습니다. 최신 메시 리듀서(mesh reducer)를 트랜스코더에 연결하자, 기존의 모든 메시가 아무도 무언가를 다시 업로드하지 않고도 더 나은 LOD를 갖게 되었습니다.

그리고 이제 우리는 그 위에 점진적 메시 스트리밍(progressive mesh streaming)까지 추가하고 있습니다.

 

그래서 핵심 요점은 이것입니다: 게시(publishing)는 소스를 고정시키지만, 최적화까지 고정시키지는 않습니다.

우리가 더 나은 인코더나 더 나은 LOD 알고리즘을 만들 때마다, 오래된 콘텐츠는 다음 요청 시에 그 모든 개선 사항을 적용받습니다. 우리는 누구에게도 다시 제작(reauthor)해 달라고 요청하지 않으면서 20년치의 콘텐츠를 개선합니다.

그리고 이것은 엄청난 규모로 돌아갑니다: 매분 약 27,000개의 텍스처 ACR과 24,000개의 메시 ACR이 생성됩니다. 매시간 1억 3,600만 개가 넘는 ACR이 CDN에서 페치(fetch)됩니다.

하지만 에셋 트랜스코더에는 한 가지 명확한 한계가 있습니다: 한 번에 하나의 에셋만 볼 수 있다는 점입니다. 개별적으로 최적화된 수백만 개의 에셋으로 이루어진 월드는 여전히 렌더링하기에 지나치게 부담스럽습니다.

그래서 질문은 이것입니다: 우리가 이 동일한 개념을 월드 전체에 적용할 수 있을까요?

 

그리고 이것이 두 번째 기둥입니다 — 월드 트랜스코더(world transcoder). 이제 하나의 에셋에서 월드 전체로 규모를 확장해 보겠습니다.

 

로블록스 월드는 작고 재사용 가능한 수많은 부품들로 만들어집니다. 큐브 하나는 이미 최적일 수 있습니다. 하지만 백만 개의 큐브는 그렇지 않습니다.

에셋 트랜스코더는 각 메시를 개별적으로 봅니다. 그것은 이 큐브들이 하나의 건물을 이룬다는 사실을 알지 못합니다. 그것들의 상대적 중요도, 크기(scale), 혹은 서로의 사이에 가려진 표면들을 볼 수 없습니다.

그러한 최적화는 오직 전체 구성(composition)을 함께 볼 때에만 가능해집니다. 그런 넓은 시야를 가지면, 우리는 지오메트리와 재질을 병합하고, 가려진 표면을 제거하며, 집합체(aggregate)에 대한 LOD를 만들 수 있습니다.

그러므로 최적화 단위는 커져야 합니다 — 하나의 에셋에서 월드 전체로 말이죠.

 

그리고 여기에 핵심이 있습니다: 트랜스코더 계약(contract)은 사실 애초에 에셋에 관한 것이 아니었습니다. 그것은 안정적이고 버전 관리되는 입력과 결정론적(deterministic) 처리를 필요로 하며, 콘텐츠 주소 지정 방식으로 관리되고 캐시되며 재사용되는 출력을 만들어 냅니다. 에셋은 그저 첫 번째 작업 부하(workload)였을 뿐입니다.

SLIM은 Scalable Lightweight Interactive Models(확장 가능한 경량 인터랙티브 모델)의 약자입니다. 이는 동일한 메커니즘을 살아 있는 월드의 선택된 영역(region)에 적용합니다.

소스 데이터 모델은 여전히 권위 있는(authoritative) 원본이자 편집 가능한 상태로 유지됩니다. 월드 트랜스코더는 해당 영역의 렌더링 관점(rendering view)을 취해 더 가벼운 런타임 형태를 파생시키고, 클라이언트는 그것을 다른 여느 ACR과 똑같이 사용합니다.

하지만 그에 앞서, 우리는 살아 있는 가변적(mutable) 데이터 모델을 안정적인 트랜스코더 입력으로 변환해야 합니다.

 

우리는 그 입력을 SLIM 디스크립터(descriptor)라고 부릅니다. 서버가 전체 데이터 모델을 소유하고 월드 전체를 볼 수 있기 때문에, 이 디스크립터는 서버가 생성합니다.

현재는, 크리에이터가 데이터 모델의 하위 트리(subtree) SLIM 적용 가능(SLIM-able)한 것으로 표시합니다. 자동 경계 선택(automatic boundary selection)은 아직 진행 중인 작업입니다 — 우리는 자동 분할(automatic segmentation)을 비롯한 여러 아이디어를 탐구하고 있습니다.

디스크립터는 트랜스코더가 렌더링에 필요로 하는 것만 저장합니다: 지오메트리 및 재질 참조, 트랜스폼(transform), 그리고 색상이죠. 에셋의 실제 데이터(payload)는 바깥에 남아 있습니다 — 디스크립터는 오직 참조만 담고, 실제 바이트는 절대 담지 않습니다.

살아 있는 월드가 변하면, 서버는 디스크립터를 업데이트하고, 클라이언트는 파생된 SLIM 출력을 소비합니다.

여기에 반가운 보너스가 하나 있습니다: 클라이언트는 이제 렌더링만을 위해 전체 데이터 모델을 필요로 하지 않습니다. 데이터 모델 뷰(view)를 상당히 줄일 수 있으며, 이는 메모리를 절약하고, 성능을 향상시키며, 네트워크 대역폭을 감소시킵니다.

 

이제 우리는 간결하고 안정적인 소스를 갖게 되었지만, 여기에는 안정적인 정체성(identity)이 필요합니다.

일반 에셋은 에셋 ID와 버전을 가집니다. 하지만 SLIM 디스크립터는 결코 명시적으로 게시되지 않으므로, ID가 없습니다. 대신, 콘텐츠 그 자체가 자신의 정체성을 정의합니다: 우리는 디스크립터 바이트를 해싱(hash)하고, 그 해시값이 곧 주소가 됩니다.

만약 그 해시가 이미 존재한다면, 우리는 그 디스크립터와 그로부터 파생된 모든 것을 재사용합니다. 새로운 것이라면, 서버가 한 번 업로드하고, 그 후 플랫폼이 트랜스코딩을 처리하며 결과물을 캐시합니다.

여기서 좋은 점은 이것입니다: 콘텐츠 주소 지정(content addressing)은 업로드 전에 중복을 잡아냅니다. 그래서 동일한 모델이 서로 다른 게임 세션에 걸쳐, 심지어 완전히 다른 사람들이 만든 서로 다른 게임에 걸쳐서도 재사용됩니다.

 

여기에 한 가지 중요한 함정이 있습니다: 화면상에서 똑같아 보이는 모델은 반드시 바이트 단위로 완전히 동일한(byte-for-byte identical) 디스크립터를 만들어 내야 합니다. 그렇지 않으면 전체 중복 제거(deduplication) 시스템이 무너져 버립니다. 그렇기 때문에 우리는 해싱하기 전에 정규화(normalize)를 합니다.

두 모델은 매우 다른 데이터 모델 하위 트리를 사용하면서도 동일한 객체를 표현할 수 있습니다: 자식(children)의 순서가 다르거나, 인스턴스 이름이 다르거나, 트랜스폼에 부동소수점(floating-point) 노이즈가 있을 수 있죠. 만약 우리가 원시 데이터를 그대로 해싱한다면, 그 각각은 서로 다른 정체성이 되어 버립니다.

그래서 우리는 렌더링에 중요하지 않은 모든 것을 제거합니다. 런타임 파라미터에 속하는 상태 — 트랜스폼, 색상, 재질 — 는 따로 분리해 냅니다. 부동소수점 값을 양자화(quantize)하고, 인스턴스와 속성을 정규(canonical) 순서로 정렬한 뒤, 나머지는 버립니다.

정규화를 거치고 나면, 동등한 모델들은 동일한 바이트와 동일한 해시를 만들어 내고, 그 해시가 곧 주소가 됩니다.

 

여기 이 밴(van) 대한 전체 과정을 보여드리겠습니다.

서버는 살아 있는 데이터 모델 하위 트리를 가져와, 렌더링 상태를 추출하고, 구조를 정규화하며, 파라미터를 분리해 내고, 이미지에 영향을 주지 않는 모든 것을 제거합니다. 이렇게 정규화된 바이트가 우리에게 SLIM 주소를 제공합니다.

다른 어딘가에 있는 동등한 밴은 — 이름, 트랜스폼, 혹은 이력(history)이 다르더라도 — 동일한 주소로 변환(resolve)됩니다.

이렇게 우리는 살아 있는 월드의 한 조각을 안정적이고 재사용 가능한 입력으로 바꾸었습니다. 이제 그것을 렌더링 가능한(renderable) 상태로 만들어 보겠습니다.

 

그렇다면 월드 트랜스코더는 실제로 무엇을 할까요?

그것은 모든 부품을 모델 공간(model space)의 렌더링 가능한 지오메트리로 변환합니다. 호환되는 재질과 셰이더 상태별로 지오메트리를 묶고, 그것을 더 큰 덩어리(chunk)로 병합하며, 가려진 내부 표면을 제거하고, 집합체에 대한 여러 단계의 디테일(LOD)을 만듭니다.

각 디테일 단계는 자신의 바이트 크기와 기하학적 오차(geometric error) 기록합니다.

그 출력물은 여러 개의 SLIM ACR로 패키징됩니다 — 충실도(fidelity) 단계마다 하나씩요. 트랜스코더는 특정 기기를 겨냥하지 않습니다. 런타임에, 각 기기는 카메라 절두체(frustum), 화면 크기, 그리고 자신의 성능 및 메모리 예산에 따라 충실도 단계를 선택합니다.

가까이서는, 각 재질을 원본 텍스처와 함께 개별적으로 렌더링합니다. 멀리서는, 여러 재질을 하나의 런타임 재질로 결합합니다. 텍스처의 더 작은 버전들을 텍스처 배열(texture array)에 넣습니다. 이렇게 하면 밴 전체를 단 한 번의 드로우 콜로 렌더링할 수 있습니다. 더 멀리서는, 텍스처를 단색(solid color)으로 대체합니다.

밴의 경우, 60개의 소스 메시가 하나의 지오메트리 덩어리가 되고, 13개의 소스 재질이 하나의 런타임 재질이 됩니다.

이제 이 파이프라인을 단계별로 살펴보겠습니다.

 

이 여섯 단계 다이어그램을 사용해 밴이 전체 과정을 거치는 모습을 따라가 보겠습니다.

1단계 — 디스크립터 생성 — 는 이미 자세히 다루었습니다. 그것이 작게 유지된다는 점만 기억하세요: 트랜스폼, 색상, 그리고 에셋 참조는 담지만, 실제 데이터(payload)는 절대 담지 않습니다. 이는 모든 입력을 명시적이고 재현 가능한(reproducible) 상태로 유지해 줍니다.

 

다음 단계는 실제 에셋 데이터를 가져오는(fetch) 것입니다...

 

한 가지 짚고 넘어갈 만한 점이 있습니다: 월드 트랜스코더는 샌드박스(sandbox) 처리된 게임 엔진 코드라는 것입니다. 그것은 모든 로블록스 포맷을 별도로 재구현한 것이 아닙니다. 런타임과 동일한 코드를 실행합니다.

그 코드는 디스크립터를 검증하고, 인스턴스들을 순회하며, 고유한 에셋 ID들을 수집하고, 각각을 정확한 소스 버전으로 변환(resolve)합니다. 먼저 공유된 로컬 디스크 캐시를 확인하고; 없는 것은 소스 에셋이나 에셋 ACR의 형태로 CDN에서 가져옵니다. 그런 다음 모든 것을 메모리에 로드합니다.

디스크립터는 작지만, 모든 의존성(dependency)은 정확하고 명시적입니다.

 

에셋이 로드되면, 다음 단계는 집합체(aggregate) 구축하는 것입니다...

 

먼저, 우리는 모든 부품의 위치, 회전, 크기를 공유된 모델 공간(model space)으로 구워 넣습니다(bake). 이는 원래의 객체 경계를 지워 버리고, 메시 심플리파이어(simplifier)가 전체 구성에 걸쳐 작동할 수 있게 해 줍니다.

그런 다음, 호환되는 재질과 셰이더 상태를 가진 지오메트리를 묶고 병합하며, 플랫폼의 한계로 인해 어쩔 수 없을 때에만 새로운 배치(batch) 시작합니다. 예를 들어, 16비트 인덱스 버퍼 한계에 도달하면 새로운 배치를 시작합니다.

또한, 집합화(aggregation)가 모든 것을 정적(static)으로 만드는 것은 아니라는 점에 유의하세요. 골격 메시(skeletal mesh)의 경우, 입력 골격들을 하나의 공유된 골격으로 병합하면서 스킨 가중치(skin weight)와 본(bone) 영향을 보존합니다. 정적 메시의 경우, 각 입력 메시마다 하나의 가상 본(virtual bone)을 추가합니다. 그래서 우리는 집합체를 사용하면서도 여전히 바퀴를 돌리거나 문을 열 수 있습니다.

핵심 원칙은 살아 있는 데이터 모델과의 매핑(mapping)을 유지하면서 구조를 최적화하는 것입니다.

 

다음으로, 우리는 이미지에 결코 기여하지 않는 지오메트리를 제거합니다.

 

트랜스코더는 전체 집합체에 대해 BVH(경계 볼륨 계층 구조) 구축합니다. 트랜스코딩을 하는 동안 많은 가시성(visibility) 테스트를 수행하기 때문입니다.

집합체 내의 각 삼각형에 대해, 우리는 그 면적에 걸쳐 여러 점(point)을 샘플링합니다. 더 큰 삼각형에는 더 많은 샘플 점이 주어집니다. 모든 샘플 점에서, 우리는 여러 방향으로 광선(ray)을 쏘고, 그중 하나라도 열린 공간(open space)으로 빠져나가는지 확인합니다.

이 규칙은 의도적으로 보수적입니다: 만약 광선이 하나라도 빠져나간다면, 그 삼각형은 잠재적으로 보이는 것으로 간주하여 그대로 유지합니다. 어떤 샘플도 빠져나갈 길을 찾지 못했을 때에만 삼각형을 제거합니다.

이는 모델에 눈에 띄는 구멍을 만들지 않으면서 내부의 삼각형과 겹치는 삼각형을 제거해 줍니다.

 

그다음, 우리는 남아 있는 것을 단순화(simplify)합니다.

여기 밴이 두 가지 디테일 단계로 제시되어 있습니다. 가장 디테일한 버전은 약 9,200개의 정점(vertex)과 8,400개의 삼각형을 가지고 있습니다. 4단계에서는, 이것이 약 2,600개의 정점과 1,600개의 삼각형으로 줄어듭니다.

 

메시 단순화에 대해 잠깐 말씀드리자면: 사용자 제작 콘텐츠는 깔끔한 매니폴드(manifold) 토폴로지를 갖는 경우가 드물기 때문에, 우리는 자체 알고리즘을 개발해야 했습니다.

이 단순화 알고리즘은 전통적인 하프 에지(half-edge) 데이터 구조를 사용하지 않고, 정점, 에지(edge), 면(face) 사이의 관계를 명시적으로 저장합니다. 인접한 면이 하나, 둘, 혹은 여럿인 에지를 모두 처리하며, 서로 분리된 부분은 분리된 상태로 유지합니다.

이차 오차 척도(quadric error metric)는 원래의 표면을 보존하며, 우리는 결과를 개선하기 위해 면적 손실(area loss) 대한 페널티를 추가했습니다. UV나 다른 정점 속성에서의 날카로운 주름(crease)과 왜곡은 에지 붕괴(edge-collapse) 비용을 높입니다. 각 단순화 단계는 가장 비용이 낮은 유효한 에지를 붕괴시키고, 새로운 정점 위치를 계산하며, 인근의 후보(candidate)들만 업데이트합니다. 속성 경계(attribute seam)에서는, 값들을 하나로 융합(weld)하는 대신 여러 개의 속성 값을 그대로 유지합니다.

그 결과는 깔끔한 토폴로지를 필요로 하지 않는 유용한 디테일 단계들의 집합입니다 — 우리는 그런 깔끔한 토폴로지를 좀처럼 얻지 못하기 때문입니다.

우리는 작년에 이 알고리즘에 관한 상세한 논문을 발표했습니다.

 

마지막 단계는 디테일 단계들을 여러 개의 SLIM ACR로 패키징하는 것입니다.

ACR은 렌더링 가능한 데이터 — 정점, 인덱스, 텍스처 ID, 그리고 재질 매핑 — 와 더불어, 런타임이 단계들 사이에서 선택하는 데 필요한 메타데이터를 담고 있습니다: 기하학적 오차, 삼각형 개수, 그리고 바이트 크기가 그것입니다.

 

이로써 세 번째 기둥, 런타임 오케스트레이션(runtime orchestration) 이르게 됩니다.

트랜스코더는 충실도(fidelity) 단계들을 만들었습니다. 이제 각 클라이언트가 자신의 예산(budget)을 그 단계들에 어떻게 쓸지 결정합니다.

 

런타임에, 클라이언트는 서로 연결된 두 가지 문제를 풉니다: 성능(performance)과 메모리(memory)입니다.

성능 예산은 보이는 각 인스턴스가 얼마만큼의 디테일을 받을지를 제어합니다. 가시성과 화면 점유율(screen coverage)이 이상적인 LOD를 정의하고, 장면이 렌더링하기에 너무 부담스러워지면 렌더링 예산이 그 목표치를 끌어내립니다. 이상적인 단계를 넘어서는 디테일은 우리에게 아무런 이득도 주지 않습니다 — 그 객체는 그것을 보여줄 만큼 충분한 픽셀을 차지하지 않기 때문입니다.

메모리 예산은 어떤 LOD가 GPU 메모리에 상주(resident)할지를 제어합니다. 모든 SLIM 메시에 걸쳐 하나의 고정된 바이트 예산이 있습니다. 공유되는 각 메시는 한 번만 저장되고, 필요 없는 단계는 축출(evict)되며, 아직 없지만 유용한 단계들은 사용 가능한 메모리를 두고 경쟁합니다.

성능은 어떤 디테일이 유용한지를 결정합니다.

메모리는 어떤 디테일이 상주할 수 있는지를 결정합니다.

이 두 한계가 함께, 우리가 장면 전체에 걸쳐 달성할 수 있는 최상의 가시적 품질을 결정합니다.

 

그 부분을 좀 더 정밀하게 설명해 보겠습니다.

기기의 성능과 품질 설정이 두 개의 고정된 예산을 설정합니다. 여기서 렌더링 예산은 삼각형 목표치입니다 — 데스크톱에서 470만 개, 태블릿에서 240만 개, 휴대폰에서 110만 개죠. 메모리 예산은 GPU 바이트의 고정된 풀(pool)입니다. 장면의 복잡도는 이 예산들 자체를 바꾸는 것이 아니라, 이 예산을 두고 벌어지는 경쟁을 바꿉니다.

매 프레임마다, 가시성과 화면 점유율이 각 인스턴스의 이상적인 LOD를 정하고, 렌더링 예산이 실제 목표치를 정합니다.

메모리의 경우, 우리는 먼저 새로운 목표치를 초과하는 단계들을 축출합니다. 그런 다음, 유용하지만 아직 없는 모든 단계를, 추가된 바이트당 제거하는 가시적 오차(visible error)를 기준으로 점수를 매기고, 사용 가능한 메모리에 들어맞는 가장 높은 점수의 업그레이드를 로드합니다.

이것이 익숙하게 들린다면, 그것은 바로 레이트-왜곡 최적화(rate-distortion optimization)이기 때문입니다. 우리는 가시적 오차를 가장 많이 제거하는 곳에 바이트를 씁니다.

 

다시 데모로 돌아가 보겠습니다. 이 표는 동일한 장면과 카메라를 여러 기기에 걸쳐 비교한 것입니다 — 열(column) 하나당 기기 하나씩이죠.

단순한(naive) 기준선과 비교했을 때, 우리는 클라이언트 인스턴스를 최대 97퍼센트, 삼각형을 98퍼센트, 드로우 콜을 74퍼센트까지 줄였고, 동시에 GPU 메모리를 33퍼센트 감소시켰습니다.

핵심 요점은, 이 표의 모든 수치가 동일한 소스 월드에서 나왔지만 서로 다른 기기와 기기별 예산을 반영한다는 것입니다. 데스크톱, 태블릿, 휴대폰을 위한 수작업 버전이란 없습니다. 모든 것이 필요할 때(on demand) 생성되며, 각 기기는 가능한 최상의 표현을 선택합니다.

 

이것이 단순한(naive) 기준선입니다 — 프레임당 5,940만 개의 삼각형이죠.

 

그리고 이것은 SLIM을 활성화한 데스크톱입니다 — 470만 개의 삼각형으로 거의 동일한 비주얼을 보여줍니다. 동일한 장면, 동일한 카메라입니다.

보시다시피, 차이는 눈에 띄기는 하지만 미미합니다. 우리가 제거한 작업의 대부분은 최종 이미지에 거의 영향을 주지 않았습니다.

디버그 뷰를 확인해서 런타임이 디테일을 어디에 썼는지 살펴보겠습니다.

 

색이 칠해지지 않은 객체들은 일반 렌더링 경로를 사용합니다. 색상은 각 객체에 대해 선택된 SLIM 디테일 단계를 나타냅니다.

선택된 디테일 단계들이 단순한 거리별 동심원(distance ring)을 이루지 않는다는 점에 주목하세요. 가까운 객체라도 화면의 작은 부분만 차지하거나 기하학적 오차가 작을 때는 더 낮은 디테일을 받습니다. 먼 객체는 더 높은 디테일을 받을 수 있는데, 특히 상주하는 하나의 메시가 동시에 여러 개의 보이는 인스턴스를 담당할 때 그렇습니다.

가시성, 화면 점유율, 재사용성, 삼각형 비용, 바이트 비용, 그리고 전역 예산 — 이 모든 것이 오케스트레이터(orchestrator) 입력됩니다.

 

이제 태블릿 목표치를 살펴보겠습니다 — 240만 개의 삼각형이죠.

런타임은 시각적 손실이 가장 적은 곳에서 디테일을 제거합니다. 그래서 이 장면은 데스크톱 예산의 대략 절반 수준에서도 그 실루엣과 밀도를 그대로 유지합니다.

 

디버그 뷰에서, 우리는 LOD 선택이 더 공격적으로 이루어지고 더 많은 객체가 SLIM을 통해 렌더링된다는 것을 볼 수 있습니다.

하지만 그 결정은 여전히 전역적이며 가치 기반(value-driven)입니다 — 우리는 하나의 커다란 품질 스위치를 끄고 켠 것이 아닙니다. 상주하는 모든 단계는 여전히 바이트당 가시적 이득(visible benefit)을 두고 경쟁합니다.

예산을 한 번 더 낮춰 보겠습니다.

 

그리고 이것이 휴대폰 목표치입니다 — 110만 개의 삼각형이죠.

저는 이 데모를 모바일 친화적으로 만들려고 제작한 적이 결코 없습니다. 솔직히, 이 스크린샷을 캡처하기 위해 딱 한 번 휴대폰에서 실행해 봤을 뿐입니다. 플랫폼이 최적화된 형태를 자동으로 파생시켰고, 휴대폰은 자신의 예산에 맞는 버전을 선택했습니다.

이것은 강연 시작 부분에서 보셨던 바로 그 장면입니다.

 

그리고 여기 그 휴대폰 이미지 뒤에 있는 할당(allocation)을 보여드립니다. 이제 거의 모든 것이 SLIM이며, LOD 선택은 한층 더 공격적입니다.

저는 어디를 봐야 할지 알기 때문에, 무엇이 제거되었는지 알아볼 수 있습니다. 하지만 여러분은 정말로 그 차이를 구분하실 수 있으신가요?

우리에게는 하나의 살아 있는 월드, 하나의 소스, 여러 개의 파생된 형태, 그리고 세 대의 서로 다른 기기가 있으며, 각 기기는 바로 그 프레임에 최적인 것을 선택합니다.

 

지금까지는 모든 것이 대체로 정적(static)으로 보였습니다. 하지만 로블록스 월드는 움직이고, 애니메이션되며, 변화합니다.

이것이 우리를 다이내믹 SLIM(dynamic SLIM)으로 이끕니다.

 

그리고 여기 그 역할 분담이 있습니다.

서버는 살아 있는 데이터 모델을 소유합니다 — 인스턴스, 속성, 트랜스폼, 색상, 애니메이션 상태, 그리고 그것들을 구동하는 스크립트까지요. 클라이언트는 변경 불가능한(immutable) SLIM 아티팩트(artifact)를 보유합니다 — 집합 지오메트리, 디테일 단계, 재질 매핑, 그리고 파라미터 슬롯(parameter slot)이죠.

우리가 집합체에 삽입했던 가상 본(virtual bone)과 데이터 모델로 되돌아가는 매핑을 기억하시나요? 바로 그것이 변경 불가능한 아티팩트가 트랜스폼과 색상 변화에 반응할 수 있게 해 주는 요소입니다.

서버는 그 파라미터들의 간결한 스냅샷을 취해 클라이언트로 복제(replicate)하고, 렌더러는 그것을 SLIM 모델에 적용합니다. 모든 지오메트리가 하나의 집합체로 구워져 단일 드로우 콜로 렌더링됨에도 불구하고, 색상은 계속 변하고 바퀴는 계속 돌아갑니다.

클라이언트는 언제나 동일한 아티팩트를 렌더링하되, 서버로부터 복제된 최신 상태로 파라미터화(parameterize)합니다. 클라이언트는 서버로부터의 실시간 스트림으로 구동되는 변경 불가능한 구조를 갖는 셈입니다.

 

SLIM 아티팩트가 이미 구조와 매핑을 담고 있기 때문에, 우리는 모든 속성 업데이트를 모든 클라이언트에 보내지는 않습니다.

만약 클라이언트가 이미 시뮬레이션 상태를 가지고 있다면, 우리는 그것을 직접 사용해 SLIM 모델을 파라미터화합니다 — 네트워크 비용은 전혀 들지 않죠.

클라이언트가 갖고 있지 않은 모델의 경우, 서버가 반드시 그 상태를 복제해야 합니다. 우리는 중요도 기반 우선순위(importance-based prioritization)를 사용하고, 고정된 네트워크 대역폭 예산 내에 머물도록 업데이트를 조절(throttle)합니다. 그리고 그렇습니다, 이 시스템의 다른 모든 것과 마찬가지로 네트워크 대역폭 역시 예산 기반입니다.

우리는 또한 마지막 업데이트 이후 경과한 시간을 추적하여, 멀리 있는 모델이 영원히 같은 자세로 얼어붙지 않도록 합니다. 가까운 모델은 더 자주 업데이트되지만, 결국에는 모든 SLIM 모델이 자신의 업데이트를 받게 됩니다.

네트워크 전달은 의도적으로 비신뢰성(unreliable) 방식이며, 매우 단순합니다: 재전송 큐도 없고 수신 확인(acknowledgment)도 없습니다. 어차피 더 새로운 스냅샷이 나오면 이전의 모든 것은 쓸모없어지기 때문입니다.

수신 측에서는, 스냅샷이 드문드문 도착하므로, 클라이언트는 5차 관성화(quintic inertialization)로 그것들을 부드럽게 다듬어, 드문드문한 네트워크 패킷을 다시 매끄러운, 렌더링 속도(render-rate)의 움직임으로 되돌립니다.

 

여기 또 다른 예시가 있습니다: 1,200개의 살아 있는, 애니메이션되는 로블록스 아바타입니다.

이 영상에서 SLIM이 어디에 있는지 구분하실 수 있으신가요?

서버는 처음부터 끝까지 권위 있는(authoritative) 원본으로 유지됩니다. 클라이언트는 경량 아티팩트를 렌더링하고, 화면상에서 중요한 움직임에 대해서만 간결한 스냅샷을 전달받습니다.

 

SLIM은 집합체를 최적화하는 데 대단히 효율적이지만, 어디를 개선할 수 있는지에 대해 솔직해져 봅시다.

SLIM은 구조가 충분히 오래 안정적으로 유지되거나, 트랜스코딩 및 저장 비용을 상쇄할 만큼 충분히 재사용될 때 그 값어치를 합니다. 잦은 구조적 변화는 새로운 디스크립터를 만들어 내고, 반복적인 작업(job)으로 시스템을 넘치게 만듭니다. 그런 경우, 우리는 전통적인 렌더링으로 되돌아가는(fall back) 편을 택합니다.

단 한 번만 사용되는 고유한 지오메트리도 LOD와 스트리밍의 혜택은 받지만, 중복 제거로는 거의 아무런 이득을 얻지 못하며, 메모리 비용은 빠르게 증가합니다. 이는 메모리 사용량을 예산 초과로 밀어붙일 수 있습니다. 한계 내에 머물기 위해, 오케스트레이터는 우리에게 필요한 순간에조차 고해상도 LOD를 축출해야 합니다. 이는 심각한 품질 저하를 초래합니다.

지연 시간(latency)은 또 다른 어려운 문제입니다. 올바른 원격 스냅샷이라 하더라도, 매우 동적인 시뮬레이션에서는 너무 늦게 도착할 수 있습니다. 극도로 지연에 민감한 경우에는, SLIM이 아직 최선의 선택이 아닐 수 있습니다.

그리고 집합화(aggregation) 자체에도 비용이 있습니다: 크지만 공간적으로 성긴(sparse) 모델은 절두체 컬링(frustum culling)과 오클루전 컬링(occlusion culling)의 효율을 떨어뜨립니다.

그래서 계약(contract)은 명시적입니다: 안정적이고 재사용 가능한 구조가 그 거래를 가치 있게 만드는 곳에서는 SLIM을 사용하고, 그렇지 않은 곳에서는 되돌아가라는 것이죠.

폴백(fallback)은 실패 모드가 아닙니다. 그것은 최적화가 올바르게 유지되도록 지켜 주는 것입니다.

 

SLIM을 구축하면서 우리가 배운 것들을 정리해 보겠습니다.

첫째, 진실의 원천(source of truth)을 보존하고, 표현(representation)은 파생시켜라. 소스는 권위 있는 원본으로 유지되는 반면, 최적화된 형태는 폐기 가능(disposable)합니다. 우리는 크리에이터에게 20년치의 콘텐츠를 다시 제작해 달라고 요청하는 대신, 알고리즘을 개선하고 나중에 콘텐츠를 다시 트랜스코딩할 수 있습니다.

둘째, 맥락(context)과 명시적인 계약을 가지고 최적화하라. 정규화하고, 재사용을 자동으로 발견하며, 전제(assumption)가 성립하는 동안에만 집합화하고, 폴백을 계약의 일부로 취급하라는 것입니다.

셋째, 런타임 예산이 디테일 수준을 결정하게 하라. 트랜스코더는 선택지를 만들고; 각 클라이언트는 자신의 메모리, 렌더링, 대역폭 예산을 가장 많은 가시적 품질을 사들이는 곳에만 씁니다.

그리고 마지막으로, 변경 불가능한 구조와 살아 있는 상태를 분리하라. 지오메트리와 파라미터 슬롯은 변경 불가능한 아티팩트 안에 존재합니다. 간결하고 동적인 서버 스냅샷이 그 모든 것을 계속 움직이게 합니다.

이 개념들이 한데 모여, 사용자 제작(user-generated) 플랫폼이 크리에이터의 추가적인 노력 없이도 데스크톱에서 휴대폰까지 확장될 수 있게 해 줍니다.

 

앞으로의 연구 과제(future work) 몇 가지도 짚어 드리고 싶습니다.

첫째, 우리는 더 나은 SLIM을 원합니다: 더 풍부한 파라미터화와 부분 무효화(partial invalidation) 통해 작은 구조적 변화가 있어도 재트랜스코딩 없이 견뎌 낼 수 있게 하고; 먼 거리의 SLIM을 위해 삼각형을 넘어서는 표현 방식 — 포인트 클라우드(point cloud), 가우시안 스플랫(Gaussian splat), 임포스터(impostor) 포함해 — 을 도입하며; 계층적이고 공간적인 SLIM — SLIM으로 이루어진 SLIM이라고 생각하시면 됩니다 — 을 통해 훨씬 더 큰 월드를 지원하고자 합니다.

둘째, 우리는 완전히 분산된(fully distributed) 렌더링 엔진을 원합니다. 우리는 렌더링 작업이 어디서든 — 클라이언트, 게임 서버, 서버 (rack), 혹은 클라우드에서 — 동일한 계약을 사용해 실행되고, 캐시되고 재사용될 수 있는 출력을 만들어 내기를 바랍니다. 조명(lighting), 오클루전, 가시성 — 무엇이든지요. 예를 들어, 우리는 전역 조명(global illumination)을 위한 ACR을 생성할 수도 있습니다.

셋째, 우리는 콘텐츠 업샘플링(upsampling)을 원합니다. 트랜스코더가 반드시 단순화만 해야 하는 것은 아닙니다; 그것은 향상시킬 수도 있습니다. 지오메트리, 텍스처, 재질, 조명, 애니메이션 — 이 모든 것이 생성형 AI로 개선될 수 있습니다.

그리고 마지막으로, 로블록스 리얼리티(Roblox Reality) — 하이브리드 렌더링 아키텍처입니다. AI 월드 모델은 지속적인 공간적·의미적(spatial and semantic) 메모리를 필요로 하는데, 우리에게는 이미 그것을 위한 데이터 모델이 있습니다. 비전 모델(vision model)의 출력을 저장하거나 월드를 잠재 공간(latent space)으로 표현하는 SLIM ACR을 상상해 보세요. 그것은 캐시 가능하고 재사용 가능하므로, 월드 모델이 그 작업을 반복하고 또 반복할 필요가 없어집니다.

분명히 말씀드리자면, 이것들은 연구 방향이지 곧 출시될 기능이 아닙니다. 하지만 이 모두는 동일한 토대를 공유합니다: 안정적인 소스, 이동 가능한(movable) 연산, 그리고 재사용 가능한 파생 결과물이죠.

 

이것으로 제 발표를 마치겠습니다.

대단히 감사합니다. 질문이 있으시면 기꺼이 답변드리겠습니다.


PPTX 구글 드라이브 링크: https://docs.google.com/presentation/d/1GUPHyL7VDREzBRtn86p1hBaoHLpT8Pfz/edit?usp=sharing&ouid=101375571592519364220&rtpof=true&sd=true

 

Google Slides 로드 중

 

docs.google.com