역자의 말: 언리얼페스트2024에서 발표 된 놀라운 아이디어들이 담긴 발표네요. 특히 바인드리스 텍스처에 대한 부분은 내부에서 연구해서 꼭 적용해 보고자 합니다. 프리젠테이션 내용만 가지고는 보통의 테크아티스트는 이해하기 힘든 부분이지만 그래도 고통? 스럽지만 꼼꼼하게 읽고 되씹어볼만 한 발표라고 생각됩니다.
환영합니다. 언리얼 엔진에 Rendering Fast path를 추가한 방법과 그 과정에서 모든 것을 망치지 않는 방법에 대해 말씀드리고자 합니다.
저는 크리에이티브 어셈블리에서 선임 그래픽 프로그래머로 일하고 있습니다. 이 회사에서 일한 지 8년 정도 됐어요. 헤일로 워즈 2에서 시작해서 토탈 워에서 잠시 일한 후 하이에나에서 5년 정도 일했습니다.
하이에나가 무엇인지 모르시는 분들을 위해 설명하자면, 우주를 배경으로 한 멀티플레이어 슈팅 게임입니다. 언리얼 엔진 4를 사용하여 8세대 및 9세대 콘솔과 PC를 대상으로 개발했습니다.
먼저 맵의 성능 문제를 살펴본 다음, 초콜릿 렌더링이라는 솔루션을 살펴보고 마지막으로 이를 최신 렌더러로 전환하는 방법을 살펴보겠습니다.
하이에나에서 맵은 플런더십이라고 불리며, 대략 500m×500m의 매우 큰 크기입니다. 맵은 방과 허브 공간으로 구성되며, 방과 방 사이에는 계단이나 복도 같은 짧은 연결 경로가 있습니다. 일부 맵은 수직 레이어가 여러 개 있고 전환이 비슷하게 짧으며 플레이어는 대부분의 영역에서 제로 G에 도달할 수 있으므로 거의 모든 곳에서 동일한 디테일이 필요합니다.
이는 맵당 약 60,000개의 고유 인스턴스 또는 메시 컴포넌트에 해당합니다. 그리고 플레이어가 이동할 때는 로딩 화면이나 퍼널링 없이 항상 준비가 되어 있어야 하는 매우 밀집된 실내 공간일 뿐입니다.
다음은 허브 공간의 예시입니다. 제가 선택한 파사드를 볼 수 있는데, 내부를 포함하지 않는 하나의 맞춤형 메시입니다.
그리고 이것이 방의 모습입니다. 모듈형 키트 조각의 세분성을 확인할 수 있습니다. 여기서 20개를 선택했는데, 각각 기본 메시와 애디티브 메시가 하나씩 있습니다. 따라서 이 두 벽에만 40개의 메시 컴포넌트가 있습니다. 따라서 정말 빠르게 합산되는 것을 볼 수 있습니다.
이걸 보고 "도대체 왜 이런 식으로 레벨을 만드는 거지?"라고 생각할 수도 있습니다. 그냥 모든 수고를 덜고 단일 룸 메시를 만들면 되지 않을까요? 하지만 이런 접근 방식에는 많은 문제가 있습니다. 우주선에는 수백 개의 방이 있는데 일일이 손으로 만들면 시간이 너무 오래 걸립니다. 디자인에서 수정 사항이 생기면 다시 만들어야 하죠.
따라서 디자이너는 아티스트 없이는 반복 작업을 할 수 없습니다. 또한 이러한 에셋을 재사용할 수 없기 때문에 스트리밍에 큰 부담이 됩니다.
이 방식을 사용하면 메시의 빈 공간이 많아져 레이트레이싱에 매우 좋지 않습니다.
그리고 저희는 루멘을 사용하지 않지만, 원한다면 문서에 명시적으로 전체 방을 메시로 임포트하지 말라고 나와 있습니다.
그래서 대신 스플라인을 기반으로 하는 커스텀 룸 툴을 만들었습니다. 디자이너가 스플라인을 움직이면 툴이 자동으로 키트 조각을 배치하여 빠르게 반복 작업을 수행할 수 있습니다. 그런 다음 방을 완성하기 위해 Houdini를 사용하여 바닥과 천장을 순차적으로 생성합니다. 이를 통해 맵을 항상 플레이 테스트할 수 있는 상태로 만들 수 있으며, 이는 저희 개발에서 매우 중요한 요소였습니다. 기술적인 측면에서도 훨씬 더 미래지향적입니다. 이와 같은 메시를 사용하면 레이트레이싱 모범 사례를 준수할 수 있고 여러 위치에서 재사용할 수도 있습니다.
저희는 레벨 스트리밍에 월드 구성을 사용합니다. 맵은 각각 수천 개의 인스턴스로 구성된 논리적 하위 레벨로 나뉩니다. 모든 볼륨이 겹치기 때문에 기본 가시성 알고리즘을 교체하여 반경 기반이 아닌 타일 기반의 사전 계산된 가시성을 사용했습니다.
탐색할 때 스트리밍으로 인해 해당 액터와 컴포넌트가 생성 및 소멸됩니다. 그러면 해당 참조가 해결될 때 에셋의 로드 및 언로드가 트리거됩니다. 이렇게 많은 액터와 컴포넌트로 인해 스트리밍에 장애가 발생했습니다.
그렇다면 이 문제를 어떻게 해결할 수 있을까요?
생성하고 삭제하는 대신 표시하고 숨기면 됩니다. 이제 이러한 에셋 참조는 로딩 화면에서 해결됩니다. 이 방법은 분명히 도움이 되었지만 메모리라는 또 다른 문제가 발생했습니다.
이러한 레퍼런스를 게임 전체에 걸쳐 유지하면 에셋이 계속 살아 있기 때문에 맵의 모든 메시를 로드하는 데 1.5GB의 VRAM을 사용했습니다. 이로 인해 아트 팀에 많은 제약이 있었습니다.
사실 여기에는 예상치 못한 두 번째 숨겨진 비용이 있는데, 바로 6만 개의 액터와 컴포넌트를 C++로 할당해야 한다는 점입니다. 스트리밍이 없는 저희 서버에서는 이미 이 문제가 발생했습니다.
팀이 언리얼로 전환했을 때 가장 먼저 한 일은 액터와 컴포넌트의 크기를 확인하는 것이었습니다. 액터의 경우 약 500바이트, 스태틱 메시 컴포넌트의 경우 약 1200바이트였습니다. 약 100MB의 RAM이 사라진 셈입니다. 여기에는 메시 에셋 메모리, 렌더링 메모리 또는 우리가 많이 사용했던 머티리얼 오버라이드 배열과 같은 보조 할당은 포함되지 않았습니다.
메모리 문제로 돌아와서 먼저 해결해야 할 더 큰 문제가 있습니다. 이를 위해 컴포넌트가 어떻게 렌더링되는지 살펴볼 필요가 있습니다. 먼저 장면 프록시를 생성하고 렌더링 관련 데이터를 복제하는 정적 메시 컴포넌트로 시작합니다. 그런 다음 다시 돌아올 몇 가지 마법을 거칩니다. 그런 다음 RHI에서 드로우 호출로 끝납니다.
스태틱 메시 씬 프록시는 거의 2000바이트에 달하며, 이는 액터와 컴포넌트를 합친 것보다 더 큽니다. 이는 컴포넌트를 표시하거나 숨길 때 생성 및 소멸됩니다. 다음은 PIX에서 본 레벨 스트리밍 급증의 일부입니다. 게임 스레드에서 70ms. 처음에 노란색은 레벨 로딩 및 언로딩, 회색은 프록시 생성 및 소멸입니다. 그 다음에는 렌더링 스레드에 폭포수처럼 흘러내리며, 여기서 프록시를 마무리하는 130ms의 스파이크를 볼 수 있습니다.
커스텀 벤치마크 플라이스루 툴을 만들었습니다. 레벨을 통해 카메라를 날아다니는 스플라인입니다. 처음부터 끝까지 약 30초가 걸리며 이런 그래프 중 하나를 생성합니다. 캐릭터나 VFX는 없고 환경만 볼 수 있습니다. 테스트 환경의 PC에서 캡처한 것으로, Y축은 게임 스레드에서 밀리초입니다. 스트리밍에서 20밀리초의 주기적인 스파이크가 발생하고 가끔 40~80밀리초의 스파이크가 발생하는 것을 볼 수 있습니다.
그리고 이것이 렌더링 스레드입니다. 30~50밀리초의 주기적인 스파이크가 발생하고 가끔 80~200밀리초가 발생하기도 합니다. 가장 큰 스파이크는 그래프의 맨 위에 있습니다. 앞서 말했듯이 이것은 30초에 불과하고 세션은 20분 동안 지속되므로 전반적인 사용자 경험은 꽤 끔찍했습니다.
이 문제를 해결하기 위해 여러 가지 내장 솔루션을 살펴봤습니다. 인스턴스화된 정적 메시 컴포넌트는 프록시를 덜 사용하지만 단일 메시 에셋만 지원한다는 단점이 있었습니다. 포레스트와 같이 일반적으로 사용하는 환경이 아니기 때문에 저희에게는 적합하지 않았습니다.
그래서 키트 조각을 HLOD와 병합하는 방법을 검토했습니다. 다시 말하지만 프록시를 덜 사용하지만 최상위 LOD용으로 설계되지 않았기 때문에 타일링 텍스처 품질이 떨어지고 에셋 재사용이 많이 줄어들며 약간의 CI 오버헤드도 발생했습니다.
커스텀 나이아가라 모듈을 만들자는 아이디어가 있었습니다. 인스턴스화된 스태틱 메시 컴포넌트와 비슷하지만 여러 메시 에셋을 지원하는 모듈을 만들자는 아이디어였습니다. 파티클은 경량 인스턴스처럼 작동할 것입니다. 하지만 수많은 커스텀 툴을 사용해야 했기 때문에 많은 작업이 필요했습니다. 당시 나이아가라는 아직 실험 단계였기 때문에 의도한 사용 사례가 아니었습니다. 그래서 알려지지 않은 부분이 많았죠.
한동안은 공통 패턴을 검색하고 해당 에셋을 병합하는 오프라인 프로세스를 사용했습니다. 여전히 일부 재사용을 유지할 수는 있었지만, VRAM 비용이 증가하고 CI 오버헤드가 발생했습니다.
이러한 솔루션 중 일부는 효과가 있었지만, 출시하기에 충분하지 않았습니다.
결국 커스텀 솔루션으로 전환할 수밖에 없었습니다.
이것이 바로 초콜릿 렌더링입니다. 언리얼에 내장된 것은 바닐라, 커스텀은 초콜릿이라고 부르는데, 바닐라가 아니기 때문입니다. 여기서는 이 두 가지 색을 사용해 어떤 것이 어떤 것인지 구분하도록 하겠습니다.
다른 모든 솔루션을 먼저 시도해보는 것의 좋은 점은 이제 데이터를 파악할 수 있다는 것입니다. 병목 지점이 어디인지 정확히 알 수 있고, 수많은 실패 사례를 통해 배울 수 있습니다. 또한 이 프로젝트가 팀의 첫 언리얼 프로젝트였기 때문에 엔진 내부에 대해서도 더 잘 알고 있습니다. 그리고 이미 프로덕션 맵을 제작하고 있었기 때문에 솔루션의 지침이 될 실제 사용 사례도 있습니다.
하지만 렌더러와 모든 기능의 조합을 완전히 교체하는 것은 무리라는 것을 알았기 때문에 대신 지원하고자 하는 공통된 기능의 하위 집합을 찾았습니다. 초콜릿은 애니메이션 빠른 경로와 유사한 메시 컴포넌트의 빠른 경로라고 생각하면 됩니다. 지원되지 않는 컴포넌트는 그냥 바닐라를 통해 렌더링됩니다. 이렇게 하면 새로운 기능을 지원하고 빠른 경로를 통해 전송함으로써 천천히 안전하게 최적화할 수 있다는 두 가지 큰 이점이 있습니다. 또한 패키지를 포함해 언제든지 필터를 비활성화할 수 있습니다. 예를 들어 플레이 테스트에서 문제가 발생하면 기능 플래그를 비활성화하고 세션을 계속 진행할 수 있습니다.
그렇다면 Fast path에서 원하는 것은 무엇일까요? 프록시는 필요 없습니다. 최소한의 예측 가능한 메모리 오버헤드를 원합니다. 아티스트와 디자이너가 작업 방식을 변경할 필요가 없기를 원합니다. 에디터는 환상적인데 이를 망치고 싶지 않아요.
바닐라와 완전히 똑같아 보여야 하고 바닐라와 나란히 사용할 수 있어야 합니다.
어떤 작업을 하든 안전해야 하며 빌드를 깨뜨려서는 안 됩니다. 현재 활발하게 개발 중이고 메인 언리얼을 따라잡을 수 있어야 합니다. 이 문제를 해결하기 위해 쇠톱을 들고 들어가서는 안 됩니다.
그렇다면 프록시를 사용하지 않고 어떻게 렌더링할 수 있을까요?
지금 이 슬라이드로 돌아가면 그 마법을 확인할 수 있습니다.
실제로는 다음과 같이 보입니다. 메시 컴포넌트가 프록시를 만들고, 프록시는 LOD와 섹션에 대한 메시 배치를 생성합니다. 그런 다음 메시 그리기 명령으로 전환되고, 컬링 후 매 프레임마다 보이는 모든 것에 대한 가시 메시 그리기 명령이 생성됩니다. 그런 다음 RHI에서 드로 콜로 전환됩니다.
More detail: https://docs.unrealengine.com/4.27/en-US/ProgrammingAndScripting/Rendering/MeshDrawingPipeline/
초콜릿에 대해 하고 싶은 것은 이와 같습니다. 프록시를 만들지 않고 바로 메시 그리기 명령 캐싱으로 넘어간 다음 매 프레임마다 자체적으로 보이는 메시 그리기 명령을 제출하겠습니다. 그 다음부터는 바닐라 RHI가 나머지를 처리하도록 합니다.
컴포넌트를 중성화하는 것은 정말 쉽습니다. 씬 프록시 생성 함수에서 null을 반환하기만 하면 됩니다. 기억하시겠지만, 메시 컴포넌트를 숨기면 프록시가 파괴됩니다. 즉, 모든 컴포넌트가 이미 널 검사를 수행하고 있으므로 이 방법은 완전히 안전합니다.
하지만 프록시가 없으면 렌더러에서 컴포넌트를 표현할 방법이 필요합니다. 이를 초콜릿 인스턴스라고 부릅니다. 이는 여러 개의 평범한 데이터 구조로 구성되며, 몇 가지 헬퍼 함수가 있는 배열의 튜플인 배열 컨테이너라는 사용자 정의 구조에 저장됩니다. 각 배열은 65,000개의 인스턴스로 고정 할당되므로 16비트 핸들을 사용합니다.
각 인스턴스의 총합은 143바이트에 불과합니다. 따라서 전체 컨테이너의 용량은 9MB입니다. 프록시에서는 약 112MB가 되지만, 공정하게 말하자면 한 번에 그렇게 많은 용량이 할당되는 일은 없을 것입니다.
바닐라 프록시처럼 FScene에 이러한 인스턴스를 저장할 수도 있었지만, 그렇게 하면 통합이 악몽이 되었을 것입니다. 대신 초콜릿 렌더러를 만들었습니다. 새 파일에 새 클래스를 만들었기 때문에 통합 충돌이 발생하지 않을 것입니다. 렌더러 모듈에 위치하므로 엔진에 대한 액세스 권한이 모두 동일합니다.
그런 다음 자체 기능을 삽입할 수 있도록 몇 가지 후크를 추가합니다. 엔진 시작 및 종료, 레벨 스트리밍, 프러스텀 컬링, 그리기 명령 디스패치 등이 여기에 해당합니다. 오른쪽에서 몇 가지 예를 볼 수 있는데, 코드가 한 줄에 불과하기 때문에 통합하는 것은 간단합니다.
다음으로 자체 메쉬 그리기 명령을 캐시하려고 합니다. 메쉬 그리기 명령은 그리기를 영구적으로 표현한 것입니다. PSO, 버텍스 버퍼, 인덱스 버퍼와 같은 것들입니다. 이것은 메시 패스에 연결됩니다. 따라서 각 메시 섹션, 각 LOD, 각 유효한 메시 패스에 대해 이 중 하나씩이 있습니다. 메시 패스가 유효한지 여부는 머티리얼과 컴포넌트 설정에 따라 결정됩니다. 예를 들어 실제로 데칼 머티리얼인 경우 데칼 패스에 대해서만 하나만 만들려고 할 수 있습니다.
이를 위한 코드는 모든 다른 메시 패스에 대해 여러 파일에 분산되어 있으며, .cpp 파일과 가상 함수 오버로드에 많은 세부 사항이 숨겨져 있습니다. 또한 뷰 상수 버퍼와 같은 어색한 종속성도 있습니다. 다행히 UE5에서는 이런 부분이 조금 개선되어 다행입니다. 거기에서 변경 사항을 다시 적용하려고 할 수도 있었지만, 대신 필요한 부분을 복제하고 드로 빌더라는 통합 솔루션으로 약간 풀어냈습니다. 이 방법은 모든 메시와 모든 메시 패스에서 작동합니다.
제 일반적인 조언은 항상 복제하고 수정하지 말라는 것입니다. 물론 예외적인 경우도 있겠지만, 저희는 이 방법이 정말 잘 작동하고 있습니다. 엔진 업데이트를 통합하는 것이 훨씬 쉬워졌습니다. 원본에서 변경된 사항이 있는지 주의 깊게 살펴야 하므로 종이 흔적을 남기는 것이 정말 도움이 됩니다. 이 함수는 이 파일에서 복제된 것입니다.
메시 그리기 명령을 캐싱하는 것은 프록시 생성을 느리게 만드는 큰 원인 중 하나입니다. 렌더 스레드에서 본 56ms의 급증과 같은 현상입니다. 통합 드로 빌더가 이 문제를 해결하지는 못하지만, 이 문제가 발생하는 시점을 완전히 제어할 수 있습니다. 그리고 레벨 시작 시 모든 조합을 알고 있기 때문에 로딩 화면에서 캐시를 채우기만 하면 됩니다. 이 그리기 캐시의 핵심은 포인터 몇 개와 메시 패스 열거형, 그리고 플래그 몇 개뿐입니다. 이제 컴포넌트가 스트리밍되면 키로 룩업을 수행하면 렌더링할 준비가 완료됩니다.
마지막 단계는 보이는 메쉬 그리기 명령을 생성하고 제출하는 것입니다. 메시 그리기 명령이 그리는 방법을 알려주는 것이라면, 보이는 메시 그리기 명령은 언제 그릴지 알려주는 것입니다. 각 메시 패스마다 이러한 목록이 있으며 컬링 후 매 프레임마다 생성됩니다. 이 목록에는 정렬 키가 주어져 패스 내에서 순서를 결정합니다.
뷰 컬링에는 활성 인스턴스를 512개 그룹으로 나누는 후크가 있습니다. 각 그룹에 대한 작업 그래프에 작업을 스폰하여 프러스텀 및 엄브라 오클루전 컬링을 수행합니다. 표시되면 바닐라 메시 그리기 명령을 생성합니다. 이 명령은 작업 로컬 패킷에 저장됩니다. 그런 다음 모든 작업이 완료되면 이를 병합합니다.
그런 다음 프레임 후반부에 후크 중 하나에 의해 수집되어 바닐라 배열의 끝에 멤피싱됩니다.
이제 프록시가 없으니 고쳐야 할 것이 한 가지 더 있습니다. GPU 씬은 월드 트랜스폼과 같은 것에 대한 원시 데이터를 할당합니다. 이제 이 모든 것을 직접 제공해야 합니다. 바닐라 버퍼 끝에 초콜릿 프리미티브 데이터를 추가하기만 하면 됩니다. 바닐라 프록시에는 이 버퍼에 대한 인덱스인 프리미티브 ID가 부여됩니다. 바닐라 프리미티브 수에 초콜릿 인스턴스 인덱스를 더한 가짜 ID가 필요합니다. 이는 바닐라 가시 메시 그리기 명령을 통해 전달되므로 이제 모든 기존 셰이더가 초콜릿 프리미티브 데이터에 액세스할 수 있습니다.
여기서 한 가지 문제가 더 있는데, 바로 프리미티브 데이터가 매우 부풀려져 있다는 점입니다. 인스턴스당 576바이트이고 레이아웃이 고정되어 있기 때문에 특히 정적 레벨 지오메트리의 경우 낭비가 엄청나게 많습니다. 60,000개의 인스턴스를 준비하면 약 33MB의 VRAM이 사라집니다. 이 문제는 나중에 다시 다루겠지만 지금은 괜찮습니다.
사실 여기서 멈출 수도 있습니다. 프록시 없이 렌더링하는 데 필요한 것은 이것뿐입니다. 하지만 사용하지도 않는 기능을 위해 액터와 컴포넌트에 메모리를 낭비하고 있습니다.
이 그래프로 돌아가서 위로 확장하면 컴포넌트가 액터에 속하며, 이 경우 레벨에 속한다는 것을 알 수 있습니다. 하지만 이 두 가지는 실제로 필요하지 않습니다. 따라서 그냥 제거해 보겠습니다.
대신 각 레벨에 인스턴스 디스크립터 목록을 저장하려고 했습니다. 이는 컴포넌트가 프록시를 초기화하는 것과 같은 방식으로 초콜릿 인스턴스를 초기화하는 데 사용됩니다.
각 레벨은 유효한 스태틱 메시 컴포넌트에서 필요한 모든 것을 추출하여 이러한 디스크립터 목록을 작성합니다. 그런 다음 레벨과 함께 직렬화됩니다. 120바이트에 불과한 매우 가벼운 디스크립터입니다. 기본적으로 메시, 트랜스폼, 몇 가지 피처 프로퍼티로 구성되어 있습니다. 참고로 액터는 500바이트, 메시 컴포넌트는 1200바이트입니다.
레벨에는 수천 개의 인스턴스가 있을 수 있으며 표시, 숨기기 또는 이동과 같은 작업을 효율적으로 대량으로 수행할 수 있기를 원합니다. 따라서 렌더러는 초콜릿 배치에서 작동하며 실제로 인스턴스를 직접 생성하지 않습니다. 배치는 배열 컨테이너 구조에서 인접한 범위의 인스턴스를 할당하고 이전 슬라이드에서 직렬화된 인스턴스 설명자 배열과 함께 모두 초기화합니다. 이제 레벨 스트리밍은 배치에서 표시하거나 숨기는 호출에 불과합니다.
액터와 컴포넌트는 더 이상 에디터 외부에서 사용하지 않지만 패키지에는 여전히 존재하므로 쿠킹 중에 제거해야 합니다. 레벨을 쿠킹할 때 컴포넌트를 소멸시키면 됩니다. 액터에 유효한 컴포넌트가 없는 경우 해당 액터도 소멸시킬 수 있습니다. 인스턴스 디스크립터 60,000개는 약 7MB이므로 해당 액터와 컴포넌트를 제거하여 95MB의 RAM을 절약했습니다. 하지만 여기에는 단점이 있는데, 더 이상 패키지에서 바닐라로 돌아갈 수 없다는 것입니다. 해당 기능을 비활성화한 상태로 패키지를 새로 만들어야 합니다. 실제로는 괜찮았는데, 이 개발 시점까지 초콜릿은 충분히 안정적이었기 때문에 실제로는 그럴 필요가 없었습니다.
스태틱 메시 컴포넌트도 바디 인스턴스를 통해 콜리전을 제공하므로 이를 대체해야 합니다. 레벨에서 인스턴스 디스크립터를 추출할 때 바디 인스턴스도 추출합니다. 각 컴포넌트마다 하나씩 있습니다. 그런 다음 각 레벨에 새로운 멀티 바디 콜리전 액터를 삽입합니다. 그러면 해당 레벨의 모든 바디 인스턴스가 다시 생성됩니다. 그런 다음 멀티 바디 오버랩 부울을 true로 설정하고 인스턴스 바디 인덱스를 설정하여 콜백에서 이를 식별할 수 있도록 합니다.
텍스처 스트리밍도 깨졌습니다.
이는 화면 공간에서 컴포넌트 바운딩 박스의 크기를 계산하고 적절한 밉 레벨을 요청하는 방식으로 효과적으로 작동합니다. 이렇게 하면 컴포넌트 배열에 일치하는 프리미티브 컴포넌트가 있을 것으로 예상하지만 더 이상 존재하지 않습니다. 하지만 몇 가지 폴백이 있으므로 바로 깨지는 것을 눈치채지 못할 것입니다. 인스턴스에 두 번째 배열을 추가하면 엔진 편집이 너무 많아질 수 있습니다. 대신 태그가 지정된 포인터를 사용합니다. 포인터의 처음 몇 비트는 정렬 때문에 사용할 수 있습니다. 첫 번째 비트를 사용하겠습니다.
원시 컴포넌트를 초콜릿 태그 포인터로 대체하면 컴포넌트의 주소 또는 초콜릿 인스턴스 인덱스인 uint64가 1비트씩 위로 이동하여 저장됩니다. 이제 첫 번째 비트는 초콜릿인지 바닐라인지를 나타냅니다. C++ 캐스팅 연산자와 암시적 생성자를 활용하므로 바닐라에 관한 한 여전히 컴포넌트이므로 수정할 필요가 없습니다. 이제 최소한의 엔진 편집만으로 초콜릿 인스턴스와 바운딩 박스를 삽입할 수 있습니다.
저희는 시간을 절약하기 위해 초기에 초콜릿이 활성화된 상태에서 편집을 허용하지 않기로 결정했습니다. 편집기에서는 기본적으로 꺼져 있으며 바로 가기를 통해 활성화할 수 있습니다. 100% 패리티에 도달하면 성능만 차이가 나기 때문에 두 모드를 구분하기 위해 화면에 아이스크림 콘을 추가했습니다. 이 기능은 개발 패키지에도 포함되어 있어 버그 보고에 매우 유용합니다. (그리고 네, 제가 직접 만들었습니다).
다음은 신호등 시스템입니다. 액터나 컴포넌트를 선택하면 필터를 통과하면 초록불이, 실패하면 빨간불이 켜지고 그 이유도 함께 표시됩니다. 또한 새로운 기능을 지원할지 여부를 평가할 때 매우 유용한 전체 상태 보고서를 작성하는 맵 검사 기능도 있습니다. 저희는 90%의 커버리지를 목표로 하지만 실제로는 75% 정도에 그치고 있습니다.
초콜릿은 정적인 환경을 위해 설계되었지만, 움직이는 소품은 커버리지를 개선하기 위해 새로운 기능을 지원할 가치가 있는 경우였습니다. 위아래로 움직이는 대형 0g 소품과 유리병이나 커피잔과 같은 일반적인 작은 물리 소품이 있습니다. 지원을 추가하기 위해 각 게임 스레드 프레임은 이동 요청을 인스턴스 인덱스 배열과 새로운 트랜스폼에 저장합니다. 렌더 스레드가 해당 업데이트를 소비하기 때문에 이는 세 번 버퍼링됩니다.
그런 다음 바닐라 메시를 상속하는 새로운 무버블 스태틱 메시 컴포넌트를 추가했습니다. 변경 사항을 자동으로 전송할 수 있도록 온업데이트 트랜스폼에 과부하를 주기만 하면 됩니다. 하지만 콜백에 의존하고 있기 때문에 액터와 컴포넌트를 제거할 수는 없습니다.
다음은 게임 스레드의 원래 벤치마크이며 노란색으로 겹쳐진 부분이 초콜릿입니다. 여전히 약간의 스파이크가 있지만 훨씬 더 안정적입니다. 이러한 스파이크를 더 줄일 수 있는 방법은 커버리지를 개선하는 것입니다.
그리고 이것은 렌더 스레드입니다. 다시 말하지만 노란색은 초콜릿이며 스파이크가 많이 줄었고 훨씬 더 안정적이라는 것을 알 수 있습니다. 아직 해야 할 일이 남아있지만 훨씬 더 나은 경험입니다.
이 모든 과정을 통해 목표를 달성했나요? 프록시를 제거하고 로딩 중에 메시 그리기 명령을 캐시하여 스트리밍 장애를 크게 줄였습니다.
액터와 컴포넌트를 제거하여 표시/숨기기 스트리밍의 메모리 오버헤드를 줄였습니다. 그리고 서버에서도 이 문제를 해결했는데, 양쪽에서 모두 제거되었기 때문입니다.
디자이너와 아티스트는 여전히 에디터에서 액터와 컴포넌트를 사용하므로 변경된 사항은 없습니다. 저희는 액터와 컴포넌트를 필터링하여 패리티를 달성할 수 있는 것만 초콜릿을 맛볼 수 있도록 했습니다.
바닐라를 깨지 않고 보강한 것이기 때문에 나란히 작업하고, 기능 플래그를 사용하여 빌드를 깨지 않도록 했습니다.
그리고 엔진을 외과적으로 수정했기 때문에 통합이 여전히 간단합니다.
초기 문제를 해결하고 출시할 수 있었지만 아직 시간이 좀 더 필요하고 해결해야 할 몇 가지 미해결 문제가 남아 있습니다.
작은 드로우 콜이 많고, 프리미티브 데이터가 여전히 부풀어 있으며, 메시 에셋이 항상 로드되고 있습니다.
작은 드로우 콜부터 시작하겠습니다. 렌더링 및 RHI 스레드에 많은 오버헤드를 유발합니다.
먼저 눈에 보이는 메시 그리기 명령을 생성하고 정렬한 다음 이를 RHI 명령 목록에 기록한 다음 이를 그래픽 API 전용 명령 목록으로 변환해야 합니다.
이러한 인스턴스는 고유한 인스턴스이므로 자동 인스턴싱과 같은 기본 제공 기술에 의존할 수 없다는 점을 기억하세요.
그렇다면 이 모든 작업을 Chocolate 내부의 최신 렌더러를 사용하여 GPU로 옮기면 어떨까요?
GPU 기반 렌더링 파이프라인 컴퓨팅으로 그래픽 파이프라인 최적화하기, 둠 이터널의 지옥 풍경 렌더링하기 등 세 가지 강연을 통해 영감을 얻을 수 있었습니다.
아직 읽어보지 않으셨다면 꼭 읽어보시기 바랍니다.
따라서 초콜릿을 현대화하려면 드로우 콜의 컴퓨팅 컬링과 병합을 추가해야 합니다. 메시렛 또는 클러스터로 전환해야 합니다. 스트리밍하는 것이 좋습니다. 병합된 인스턴스를 올바르게 그리려면 바인드리스 지원을 추가해야 합니다. 동적 분기를 사용하여 재질 순열의 수를 최소화해야 합니다. 그리고 메모리 사용량에서 놀랄 만한 일이 일어나지 않기를 원합니다.
용어를 조금 더 설명하겠습니다. 전반부의 모든 렌더링을 전통적인 렌더링이라고 하고 이제부터 논의할 내용을 현대적인 렌더링이라고 하겠습니다.
모든 것이 이 현대적인 경로를 따르는 것은 아니므로 여전히 두 가지를 모두 지원해야 합니다. 이를 위한 또 다른 필터가 있는데, "자료가 현대적인 경로를 지원합니까?"라는 훨씬 간단한 필터입니다.
파란색은 기존 전통 경로를, 녹색은 새로운 현대 경로를 나타냅니다. 따라서 프러스텀 컬링 후 렌더링 항목을 생성한 다음 RHI 호출을 실행하여 계산 컬링을 수행하고 결과를 그릴 것입니다.
다음은 필터링의 예시이며, 왼쪽에 엔진 내 디버그 보기가 있습니다. 빨간색은 투명 및 애니메이션 이미시브 스트립과 같은 전통적인 필터링입니다. 녹색은 현대적인 것으로, 대부분의 환경이 이 경로를 거치고 있습니다.
초콜릿 렌더링 항목은 기본적으로 보이는 메쉬 그리기 명령과 동일합니다. 가볍고 정렬 가능한 그리기 표현입니다. 하지만 바닐라 그리기 명령에는 불필요한 요소가 많기 때문에 렌더링 항목은 5배 더 작습니다. 정렬 키, 정렬 키의 출처인 인스턴스, 렌더링할 LOD, 해당 LOD의 일부 그리기 정보에 대한 인덱스만 있으면 됩니다.
이제 프러스텀 컬링 작업은 렌더링 항목 또는 보이는 메시 그리기 명령을 생성할 것입니다.
그런 다음 메시 패스와 머티리얼에 따라 렌더링 항목을 그룹화합니다. 각 그룹은 계산 컬링과 간접 그리기 쌍을 생성합니다. 프레임당 최대 20쌍까지만 가능하므로 이 작업을 수행하려면 많은 양의 머티리얼을 사용할 수 없습니다.
머티리얼에 대한 저희의 접근 방식을 간략히 설명해드리겠습니다. 저희는 수작업 셰이더를 사용하는 커스텀 기술 출신이라 머티리얼 그래프가 다소 두려웠습니다. 그래서 저희 아티스트는 마스터 머티리얼의 핵심 세트만 사용했고, 머티리얼 인스턴스만 만들었습니다. 하지만 이렇게 하면 훨씬 더 일관된 룩을 얻을 수 있고, 모든 텍스처가 똑같은 방식으로 패킹되며, PSO(셰이더) 컴파일이 훨씬 적고, 기존 방식은 PSO(셰이더) 변경이 적어서 훨씬 빠릅니다. 그리고 이 방식이 없었다면 현대적인 방식은 불가능했을 것입니다.
선택할 수 있는 최신 머티리얼은 환경과 메시 데칼 두 가지뿐입니다. 둘 다 스태틱 스위치를 사용하지 않습니다.
애니메이션 이미시브 스트립, 마스크된 표면, 증강 현실 광고판과 같은 맞춤형 머티리얼도 있지만, 이는 전통적인 경로를 거쳤을 뿐입니다. 메시 몇 개를 위해 컴퓨팅 셰이더를 돌리고 싶지 않습니다.
다시 계산 컬링으로 돌아가 보겠습니다. 셰이더는 매우 간단합니다. 256개의 스레드로 구성된 1차원 작업 그룹이며 각 스레드는 하나의 삼각형을 처리합니다. 항상 같은 메시의 트라이앵글이므로 메시가 256으로 완벽하게 나뉘지 않을 때마다 퇴화된 트라이앵글이 처리될 가능성이 있습니다. 작업 그룹당 256개의 트라이앵글은 상당히 많은 크기이며, 더 작은 크기를 시도했지만 결국 여기에 만족하게 되었는데, 그 이유는 컬링 후 정렬 패스가 없기 때문입니다. 크기가 작을수록 출력의 무작위성이 높아져 렌더링 성능이 저하됩니다.
셰이더는 먼저 삼각형 버텍스 위치를 읽은 다음 백페이스 컬링을 수행한 다음 엄브라 깊이에서 빌드한 고차 피라미드 텍스처에 대해 오클루전 검사를 하고 작은 삼각형 컬링을 수행한 다음 프러스텀 컬링을 수행합니다. 그런 다음 보이는 모든 트라이앵글은 출력 버퍼에 간접 인덱스라고 부르는 것을 추가합니다. 12비트 그리기 인덱스와 20비트 버텍스 인덱스가 바로 그것입니다. 각 트라이앵글은 이 중 3개를 쓰게 됩니다.
그런 다음 일치하는 드로우 간접 호출에 의해 소비됩니다. 컬링 출력 버퍼는 드로우를 위한 인덱스 버퍼로 바인딩되므로 SV_VertexId 시맨틱을 통해 패킹된 간접 인덱스를 받습니다.
그런 다음 이를 언패킹하여 드로우 및 버텍스 인덱스를 가져옵니다.
그런 다음 이를 사용해 내부 드로우라고 하는 드로우 특정 오프셋의 작은 구조를 로드합니다. 병합된 그리기 호출 내의 그리기라고 생각하면 됩니다. 이를 통해 프리미티브 데이터 및 버텍스 가져오기 테이블과 같은 항목에 액세스할 수 있습니다.
버텍스 스트림에 오프셋이 있는 버텍스 불러오기 테이블을 로드합니다.
그런 다음 버텍스 스트림을 로드하고 이를 바닐라 셰이더에 패치합니다.
이 과정에서 몇 가지 간접적인 방법이 있지만 그만한 가치가 있습니다. 이 모든 작업은 커스텀 버텍스 팩토리 내에서 이루어지므로 여전히 머티리얼 그래프를 사용하고 있습니다.
다음은 컬링 단계의 몇 가지 예시입니다. 먼저 백페이스 컬링, 렌더링을 고정하면 뒤에 있는 삼각형이 제거되는 것을 볼 수 있습니다.
이번에도 프러스텀 컬링에서는 프러스텀 외부의 모든 것이 제거되었습니다.
오클루전 컬링을 보면 롤랜드 808이 로봇 몸체의 트라이앵글을 가리는 것을 볼 수 있습니다.
그런 다음 작은 트라이앵글 컬링을 통해 카메라를 멀리 이동합니다. 원래 뷰에서 서브픽셀이었던 모든 삼각형이 제거된 것을 볼 수 있습니다.
실제 장면입니다. 778,000개의 트라이앵글로 시작하여 백페이스 컬링을 활성화하여 340,000개의 트라이앵글을 제거합니다. 프러스텀 컬링으로 93,000개를 제거하고 오클루전 컬링으로 추가로 120,000개를 제거한 다음 작은 트라이앵글 컬링으로 65,000개를 제거합니다.
이 모든 기능을 활성화하면 778,000개에서 160,000개로 줄어들어 약 80%의 트라이앵글이 컬링됩니다.
원래는 트라이앵글 컬링 전에 메시렛 컬링 단계가 있었습니다. 즉, 메시 옵티마이저 라이브러리로 생성한 적절한 메시렛이 필요했습니다. 이 단계에서는 공간적으로 일관성이 있고 서로 마주보는 방향이 비슷한 트라이앵글을 그룹화하여 함께 컬링할 수 있도록 합니다. 하지만 결국 이 방법을 버리고 256개의 트라이앵글을 실행하는 정말 멍청한 클러스터링을 사용했습니다. 여기에는 몇 가지 이유가 있습니다. 우리 씬에는 트라이앵글이 충분하지 않습니다. 두 컴퓨트 패스 사이의 배리어로 인해 점유 밸리가 발생하므로 하나의 컴퓨트 셰이더가 더 많은 트라이앵글을 처리하도록 하는 것이 실제로 더 빠릅니다.
우리의 메시는 유기적이지 않고 모두 단단한 표면 모델입니다. 따라서 공간 일관성과 유사한 법선을 모두 갖는 것은 흔한 일이 아닙니다. 결국 수많은 퇴화된 트라이앵글이 생겨서 더 작은 메시렛 크기가 필요했고, 이는 이전의 무작위화 문제로 돌아가는 것을 의미했습니다.
이제 모든 데이터 액세스는 바인딩이 없어야 합니다. 바인드리스 텍스처에 RHI 지원을 추가하는 것도 좋지만, 모든 버퍼에 액세스할 수 있어야 합니다. 버텍스 데이터, 프리미티브 데이터, 셰이더 상수 같은 것들 말이죠. 저희는 RHI에서 하나의 큰 버퍼를 할당하고 이를 다시 하위 할당하는 통합 버퍼 접근 방식을 선택했습니다. 셰이더는 이 하나의 버퍼에 오프셋을 갖기만 하면 됩니다.
이 버퍼를 디바이스 메모리라고 부릅니다. 이 버퍼는 고정된 크기의 256MB 버퍼이며 간단한 자유 목록 할당 전략을 사용합니다. 사용자는 바이트 크기, 오프셋 및 생성이 포함된 핸들을 반환받습니다. 이는 뷰 셰이더 매개변수에서 액세스할 수 있으므로 거의 모든 셰이더에서 액세스할 수 있습니다.
하지만 디바이스 메모리는 CPU에서 액세스할 수 없으므로 데이터를 가져올 수 있는 방법이 필요합니다. 이를 위해 32MB의 작은 스테이징 링 버퍼를 작성했습니다. 디바이스와 마찬가지로 사용자에게 바이트 크기, 오프셋 및 생성이 포함된 핸들을 반환합니다. 그런 다음 컴퓨팅 셰이더를 사용하여 스테이징에서 디바이스 메모리로 복사합니다.
바인드리스 렌더링에서는 오프셋과 인덱스가 많기 때문에 이를 두 개의 테이블로 그룹화했습니다. 첫 번째는 버텍스 페치 테이블로, 여기에는 스트림, 압축 데이터 및 일부 플래그에 대한 디바이스 메모리에 대한 오프셋이 있습니다. 이 테이블은 LOD 레벨의 각 스트리밍에 대해 하나씩 있습니다.
그리고 텍스처 인덱스가 있는 머티리얼 페치 테이블이 있습니다. 16비트이며 최대 8개입니다. 몇 가지 동적 분기 스위치와 몇 가지 공통 파라미터가 있습니다. 이 역시 고정된 레이아웃이며, 모든 머티리얼 인스턴스마다 하나씩 있습니다. 바닐라 머티리얼 인스턴스에서 이러한 파라미터를 추출했기 때문에 아티스트는 여전히 일반 편집 워크플로를 사용하고 있습니다.
이제 돌아가서 원시 데이터를 수정할 수 있습니다. 기억하시겠지만 바닐라 구조는 인스턴스당 576바이트이고 이것이 모든 프로퍼티의 목록입니다. 이를 112바이트로 줄였습니다. 녹색으로 표시된 것은 우리가 유지한 것이고, 몇 가지 자체 매개변수도 추가했습니다. 표준 환경 에셋에 미리 스킨된 로컬 바운딩 박스 같은 것은 필요하지 않습니다. 그 결과 33MB에서 6.4MB로 줄었습니다.
초콜릿 배치는 기본 데이터를 위해 디바이스 메모리 블록을 할당하고 스테이징 메모리를 사용하여 전체 또는 부분 업데이트를 수행합니다.
모든 바닐라 머티리얼 셰이더는 이 하나의 GetPrimitiveData 호출로 내려갑니다. 기억하시겠지만, 보이는 메시 그리기 명령에 32비트 프리미티브 ID를 전달합니다. 여기서 끝납니다. 이를 가로채서 프리미티브 데이터가 있는 위치에 대한 바이트 오프셋을 디바이스 메모리로 전달합니다. 거기에서 자체 데이터를 로드하고 패치한 다음 반환 값에 넣으면 됩니다. 우리가 지원하지 않는 속성은 그냥 정상적인 기본값으로 설정하면 됩니다.
바인드리스 텍스처링은 바닐라에서 지원하지 않기 때문에 저희가 직접 추가해야 했습니다. 구현이 어색하게 두 개로 나뉘어 있지만 여러 플랫폼을 지원하기 위한 최선의 방법이었습니다. 이 전반부는 우리가 지원하는 각 RHI마다 중복됩니다. D3D12의 경우 4096개의 디스크립터 핸들을 디바이스에 배열로 저장합니다. 이는 어떤 텍스처가 어떤 슬롯에 있는지에 대한 기준이 됩니다. 그런 다음 각 프레임에 대한 사본을 글로벌 뷰 힙의 끝에 추가합니다. 이것은 바닐라와 공유되므로 힙 스와핑이 필요하지 않으므로 바닐라 및 초콜릿 드로우 호출을 효율적으로 혼합할 수 있습니다.
그런 다음 기준 버전 카운터와 각 프레임에 대한 복사본이 있습니다. 텍스처 슬롯이 변경될 때마다 해당 숫자를 변경하면 됩니다. 그런 다음 각 프레임이 시작될 때 버전을 비교하고 필요한 경우 기준 실측에서 글로벌 뷰 힙으로 복사합니다.
셰이더에서 이를 액세스하려면 루트 시그니처를 수정해야 했으므로 새 레지스터 공간을 추가하고 SRV 배열을 넣었습니다. 또한 일부 드로 오프셋에 사용하는 루트 상수 2개를 추가했습니다.
그런 다음 후반부는 렌더러에 배치됩니다. 이 바인드리스 API를 추가하여 UTexture를 등록하면 초콜릿 텍스처 핸들을 반환받습니다. 해당 핸들의 인덱스는 디스크립터 배열에 직접 매핑되므로 이를 사용하여 가져오기 테이블을 빌드할 수 있습니다. 이 두 레이어 간의 모든 통신은 RHI 명령을 통해 이루어집니다.
텍스처 스트리밍 지원을 추가하기 위해 RHI 텍스처 참조 오브젝트에 초콜릿 텍스처 핸들을 추가했습니다. 이는 참조하는 텍스처를 교체할 수 있는 안정적인 오브젝트입니다. 따라서 텍스처를 참조할 때 바인딩 없는 기준점 배열을 업데이트하면 이제 두 텍스처가 동기화된 상태로 유지됩니다.
바인딩 없는 머티리얼 지원은 솔직히 꽤 힘들지만, 현대 머티리얼은 두 가지뿐이고 아티스트 저작물이 없었기 때문에 이 방법을 선택해야 했습니다. 전통과 현대에 각각 하나씩 두 개의 새로운 머티리얼 사용법을 추가했습니다. 이 머티리얼은 컴파일할 때 전처리기 정의로 사용할 수 있습니다. 컴파일 시간 정의를 사용하여 스위치를 구현하는 사용자 정의 코드 노드가 있습니다. 따라서 최신 순열인 경우 머티리얼 불러오기 테이블을 로드하고 텍스처 인덱스를 가져와 바인딩 없는 텍스처 배열에서 샘플을 가져옵니다. 다른 경우에는 폴백을 반환하는데, 이 경우에는 텍스처 샘플 노드입니다. 즉, 바인드리스 및 기존 드로 콜 모두에서 작동하는 단일 머티리얼을 갖게 됩니다.
머티리얼 그래프에서는 동적 브랜칭도 필요합니다. 우리가 이상적으로 원하는 것은 브랜치 내부에 코드가 범위 지정되어 있는 것입니다. 하지만 머티리얼 컴파일러는 계층 구조를 구축하는 대신 노드를 효과적으로 평평하게 만듭니다. 따라서 내장된 "if" 노드는 A와 B를 모두 평가한 다음 출력만 마스킹하여 분기를 생성하지 않습니다. 사용자 정의 노드에서 직접 분기를 만들 수도 있지만, 결국 A와 B가 먼저 평가되는 동일한 문제가 발생합니다. 하지만 컴파일러가 브랜치를 생성하면 해당 표현식의 범위가 줄어든다는 사실을 발견했습니다(보너스 슬라이드 참조). 그래서 이를 방지하기 위해 브랜치 힌트를 추가했습니다. 머티리얼 컴파일러에 적절한 동적 분기를 추가하는 방법에 대한 아이디어가 있지만 현재로서는 이 정도면 충분합니다.
저희는 여전히 바닐라에서 메시를 공유하고 있으며, 메시렛이나 클러스터를 지원하지 않습니다. 추가할 수도 있었지만 추가하지 않기로 했습니다. 그 이유 중 하나는 관련 없는 최적화를 하고 싶었기 때문입니다. 대신 초콜릿 메시를 만들었습니다. 원래는 바닐라 에셋에서 생성된 독립형 에셋이었지만 동기화 상태를 유지해야 했기 때문에 아티스트는 물론 CI에도 골칫거리였습니다. 대신 바닐라 메시 안에 커스텀 렌더링 데이터를 저장했습니다. 실제로 나나이트도 이 방식을 사용한다는 사실을 알게 되었고, 이는 항상 좋은 신호입니다.
이제 메시에는 원하지 않는 중복 데이터가 많이 있습니다. 요리할 때 메쉬가 어떻게 사용되는지 분석하고 필요한 것만 요리합니다. 이것은 또한 초콜릿 메시를 사용하는 기존의 경로를 전환하는 것을 의미하기도 했습니다.
스트리밍을 추가하기 위해 디바이스 메모리 내부의 풀을 사용합니다. 이는 거의 모든 디바이스 메모리에 해당하는 고정된 230MB 블록입니다. 이 블록은 모든 메시가 공유하는데, 이제 셰이더가 오프셋만으로 모든 메시를 액세스할 수 있기 때문에 매우 중요합니다.
이제 이 모든 것이 어떻게 결합되어 서비스 코어를 렌더링하는지 GPU 캡처를 살펴보겠습니다.
이것은 첫 번째 컴퓨팅/드로우 쌍의 와이어프레임으로, 거의 모든 환경을 포괄합니다. 1800개의 내부 드로우와 5600개의 클러스터입니다.
그런 다음 약 4000개의 내부 드로우와 4000개의 클러스터로 구성된 제로 G 파편을 렌더링합니다.
계산/그리기 쌍이 처리할 수 있는 트라이앵글 수에는 제한이 있으므로 첫 번째 쌍에서 오버플로우가 발생합니다.
또한 960개의 메시 데칼과 1500개의 클러스터를 한 쌍으로 렌더링합니다. 여기에 표시하지 않은 몇 가지 사소한 베이스 패스와 메시 데칼 쌍이 더 있지만, 대부분의 버퍼는 4번의 컴퓨팅 디스패치와 4번의 드로우 콜로 채워졌습니다. 기존 방식대로라면 거의 7,000번의 드로우 콜이 필요했을 것이므로 CPU를 제거한 작업량은 엄청납니다.
GPU의 이득이 그리 크지 않다는 점도 주목할 가치가 있습니다. GPU에서 가장 큰 이점은 드로 콜 병합이 아닌 마스터 머티리얼에서 나왔습니다. 그리기만 놓고 보면 기존보다 빠르지만 계산 컬링 비용을 더하면 거의 비슷한 수준입니다. 특히 기존 경로가 GPU에 가장 적합한 경우라면, 자체 게임에서 GPU를 이기는 것은 정말 어렵습니다. PSO를 거의 변경하지 않고도 모든 드로우 콜을 처리할 수 있습니다. 그렇다고 해서 최적화를 위한 활주로가 부족하다는 뜻이 아니라, GPU가 병목 현상이 없었기 때문에 동등한 수준으로 충분했다는 뜻입니다.
여기서 가장 큰 차이점은 CPU 설정 비용입니다. 이것이 바로 우리가 해결하고자 했던 병목 현상입니다.
미해결 문제는 해결되었나요? 이제 GPU에서 드로우 호출을 병합하여 CPU 설정이 크게 줄었습니다. 프리미티브 데이터는 5배 더 작아졌고 메시를 스트리밍할 수 있으므로 더 이상 항상 로드하지 않아도 됩니다. 1.5GB 고정 비용에서 230MB 풀로 줄었습니다.
이 현대적인 경로를 통해 수많은 기능을 사용할 수 있게 되었습니다.
새로운 바인드리스 타일 기반 조명 경로를 도입하여 수많은 기능을 사용할 수 있게 되었습니다.
프로브 아틀라스 샘플링에 DDGI를 추가하고 바인드리스를 사용했습니다. 초콜릿에만 있는 유일한 기능인 제로 G 디브리스 볼륨이 있으며, 바닐라 패리티는 없습니다. 커스텀 피직스는 CPU에서 시뮬레이션한 다음 거의 모든 최신 기능을 사용하여 효율적으로 렌더링합니다.
저희 아트 스타일은 볼트나 판넬과 같은 데칼에 크게 의존합니다. 보시다시피 이 모든 것이 현대적인 경로를 통해 병합되고 그려집니다.
친애하는? ImGUI를 초콜릿 렌더러에 직접 통합했습니다. 텍스처 스트리머 RHI 바인딩 없는 기저 진실 초콜릿 버텍스 스트리머 그리고 드로우 캐시 이 작업을 더 일찍 했으면 정말 좋았을 텐데, 일단 확보하고 나니 정말 유용했습니다.
나나이트가 발표되었을 때 "우리가 만드는 게 그거 아니냐?"는 어색한 질문을 몇 번 받았지만, 디버그 모드가 비슷해 보이지만 같은 문제를 해결하려고 하는 것은 아니라는 것을 이제 알 수 있기를 바랍니다. 적어도 제 생각에 나나이트는 시네마틱 트라이앵글 수에 초점을 맞추고 있는 반면, 초콜릿은 많은 수의 고유 인스턴스에 초점을 맞추고 있습니다.
나나이트는 여전히 씬 프록시를 사용하므로 여기서 논의한 모든 CPU 문제를 여전히 겪을 것입니다. 하지만 초콜릿 맛 나나이트는 어떨까요? 안 될 이유가 없죠.
저희는 미래를 위한 수많은 아이디어를 가지고 있습니다. 저는 스킨 메시를 지원하고 싶습니다. 컬링 전에 트라이앵글에 스킨을 입혀야 하기 때문에 조금 까다롭기도 하지만, 레벨과는 다른 요구 사항인 실제 메시가 정말 잘 작동하기 때문이기도 합니다.
훨씬 더 많은 인스턴스를 지원하고 싶습니다. 65,000개라는 제한에 금방 도달했습니다. 레벨에는 60,000개의 인스턴스를, 파편에는 10,000개 이상의 인스턴스를 추가했습니다. 이는 모든 것이 우리가 원하는 만큼 모듈화되어 있지 않다는 것을 의미합니다. 하지만 많은 부분이 16비트에 의존하는 인덱스에 의존하고 있기 때문에 사소한 변화는 아니며, 일부는 다시 생각해야 합니다.
편집기 지원도 추가하고 싶습니다. 에디터나 스탠드얼론에 액터 제거 기능이 없는 등 여러 경로를 사용하는 것이 불편했습니다. 모든 경로를 통합하고 초콜릿을 항상 켜놓고 싶어요.
그리고 GPU로 더 많은 것을 옮기고 싶습니다. 퍼시스턴트 스레딩과 멀티 인, 멀티 아웃 대기열 같은 것을 통해 전체 컬링 계층구조를 갖추고 싶어요. 인스턴스 컬링부터 정렬, 트라이앵글 그리기까지 모든 작업을 수행할 수 있습니다. 이렇게 하면 CPU가 훨씬 더 단순해집니다.
이 강연을 통해 몇 가지 알아두셨으면 하는 점이 있습니다.
언리얼 엔진에 새로운 기술을 추가하는 가장 안전하고 강력한 방법은 느린 인수입니다. 특히 한창 제작 중일 때는 가능하면 엔진 코드를 수정하지 말고 복제하거나 추가하는 것이 좋습니다. 업데이트할 때 변경 사항을 찾기 위해 주의를 기울여야 합니다.
마지막은 다소 뻔한 이야기지만 실제 문제에 대한 맞춤형 솔루션이 일반 솔루션보다 훨씬 뛰어난 성능을 발휘합니다. 이러한 결과를 내부적으로 발표하면 보통 어떤 식으로든 에픽에 피드백을 제공해야 하지 않느냐는 질문을 받곤 합니다. 하지만 저희가 빠르게 작업할 수 있는 가장 큰 이유는 바닐라가 지원하는 기능의 1% 정도만 지원하지만 실제로는 그 정도면 충분하기 때문입니다.
이 작업은 저와 호세 산체스의 공동 작업이었습니다. 물론 사이먼 무스, 알렉스 찰우드, 알레산드로 모노폴리의 공헌이 없었다면 불가능했을 것입니다.
YOUTUBE
'TECH.ART.FLOW.IO' 카테고리의 다른 글
[번역] 언리얼 렌더링 시스템 해부하기 (12) - 모바일 파트 2 (UE 모바일 렌더링 분석) (0) | 2024.08.02 |
---|---|
[번역] Bindless Resources Notes (0) | 2024.07.30 |
[번역] UE5.4 커스텀 렌더패스로 인터랙티브 워터 퍼포먼스 최적화 (0) | 2024.07.25 |
[번역] UnrealBuildAccelerator(UBA)를 활용한 분산형 빌드 (1) | 2024.07.25 |
[YOUTUBE] Smart Enemy AI Tutorial in Unreal Engine 5 (1) | 2024.07.23 |