Предисловие
После выхода последней игры из серии «Метро» я потратил несколько часов на изучение её внутренней работы и решил поделиться тем, что может показаться интересным с технологической точки зрения. Я не буду проводить подробный анализ или изучать дизассемблированный код шейдеров, а покажу высокоуровневые решения, принятые разработчиками в процессе создания игры.
На данный момент разработчики ещё не рассказывали об использованных в игре техниках рендеринга. Единственным официальным источником информации является доклад с GDC, который нельзя больше найти нигде в Интернете. И это досадно, ведь игра работает на очень интересном собственном движке, эволюционировавшем из предыдущих игр серии «Метро». Это одна из первых игр, в которых используется DXR.
Примечание: эта статья не является полным описанием и я буду возвращаться к ней, если найду что-то стоящее добавления. Возможно, я что-то упустил, потому что какие-то аспекты проявляются только на следующих этапах игры, или я просто просмотрел детали.
Первые шаги
На поиск среды, способной работать с этой игрой, у меня ушло несколько дней. Протестировав несколько версий RenderDoc и PIX, я остановился на изучении результатов трассировки лучей с помощью Nvidia NSight. Я хотел изучать рендеринг без функций raytracing, но NSight позволял исследовать подробности и этой функции, поэтому я решил оставить её включённой. Для всего остального рендеринга вполне подошёл PIX. Скриншоты сделаны с помощью обоих приложений.
У NSight есть один недостаток — он не поддерживает сохранение захвата в файл, поэтому я не мог возвращаться к кадрам, которые изучал.
В самом начале работы я также столкнулся с ещё одной проблемой, которая была никак не связана с приложениями отладки кадров: функции трассировки лучей требовали установки последнего обновления Windows, но игра позволяла включать их в опциях без установки обновления. В этом случае включение функций приводило к сбою игры при запуске. GeForce Experience тоже ничего не сообщало об необходимости правильной версии Windows для включения этих функций. Эту проблему нужно решать с обеих сторон.
Ради полноты исследования я делал захваты из игры, запущенной с максимально возможными параметрами, но без DLSS.
Анализ кадра
Готовый кадр
Краткий анализ рендеринга демонстрирует довольно стандартный набор функций, за исключением глобального освещения, выполняемого трассировкой лучей (raytraced GI).
Перед рендерингом картинки масштаб предыдущего кадра уменьшается в очереди compute и вычисляется средняя яркость.
Графическая очередь начинается с рендеринга частиц искажения (капель на камере), которые применяются на этапе постобработки. Затем быстрый предварительный проход глубин создаёт часть глубин перед Gbuffer; похоже, что он рендерит только рельеф.
Проход GBuffer заполняет 4 render target по показанной ниже схеме, а также завершает заполнение буфера глубин.
1. Target в формате RGBA8 с albedo и, возможно, Ambient Occlusion в альфа-канале; на некоторых поверхностях выглядит очень тёмным.
2. Target в формате RGB10A2 с нормалями и, возможно, маской подповерхностного рассеяния (subsurface scattering) в альфа-канале.
3. Target в формате RGBA8 с другими параметрами материалов, вероятно, metalness и roughness в альфа-канале. Любопытно, что каналы RGB в этом случае содержат точно такие же данные.
4. Target в формате RG16F с 2D-векторами движения.
После полного заполнения глубин строится линейный буфер глубин и уменьшается его масштаб. Всё это выполняется в очереди compute. В той же самой очереди буфер заполняется чем-то, похожим на направленное освещение без использования теней.
В графической очереди GPU занимается трассировкой лучей глобального освещения, но подробнее я расскажу об этом ниже.
В очереди compute вычисляются ambient occlusion, отражения и что-то, похожее на распознавание рёбер.
В графической очереди четырёхкаскадная карта теней рендерится в 32-битную карту глубин размером 6k * 6k. Подробнее об этом ниже. После завершения карты направленных теней разрешение третьего каскада по непонятным причинам снижается до 768 * 768.
Посередине процесса рендеринга теней есть любопытный момент: impostor atlas дополняется какими-то объектам до рендеринга локальных теней от освещения (о том, что такое impostors, можно прочитать здесь). И буферы impostor-ов, и буферы теней локального освещения тоже являются текстурами размером 6k * 6k.
После завершения всех теней начинается вычисление освещения. Эта часть рендеринга довольно непонятна, потому что здесь присутствует множество отрисовок, которые выполняют какие-то загадочные действия, а потому требуют дополнительного изучения.
Рендеринг сцены завершается фронтально освещёнными объектами (глаза, частицы). Визуальные эффекты рендерятся в буфер половинного разрешения, после чего выполняется их композитинг с непрозрачными объектами с помощью увеличения масштаба.
Окончательная картинка достигается тональной коррекцией и вычислением bloom (уменьшением, а затем увеличением разрешения кадра с тональной коррекцией). Наконец, в отдельный буфер рендерится UI и вместе с bloom композитингом накладывается поверх сцены.
Я не нашёл части, в которой выполняется сглаживание, поэтому оставлю это на потом.
Трассировка лучей глобального освещения
Некоторая информация о глобальном освещении, выполняемом трассировкой лучей (raytraced GI). Эта ускоряющая структура покрывает большую область игрового мира, вероятно, несколько сотен метров с сохранением везде очень высокой детализации. Похоже, она каким-то образом передаётся потоково. Сцена ускоряющей структуры не совпадает с растеризируемой сценой, например, здания на показанном ниже изображении в растеризированном виде не видны.
Вид сверху
Здесь мы можем видеть четыре тайла, окружающие позицию игрока. Также очевидно отсутствие геометрии, тестируемой по альфа-каналу. У деревьев есть стволы, но нет листвы, нет ни травы, ни кустов.
Вид вблизи
На виде вблизи лучше видно детализацию и плотность расположения объектов. Каждый объект разного цвета имеет свою ускоряющую структуру низового уровня. Только на этой картинке их несколько сотен.
Предметы игрока под ногами
Интересно, что предметы игрока тоже являются частью ускоряющей структуры, но по какой-то причине расположены у него под ногами.
Поломанный скиннинг?
Снова поломанный скиннинг?
Некоторые из объектов со скиннингом выглядят в ускоряющей структуре сломанными. Одна из наблюдаемых проблем вызывает растягивание меша (на ногах ребёнка). Другая проблема приводит к тому, что разные части персонажа со скиннингом оказываются в разных позициях. Растяжения нет, но части отделены друг от друга. Похоже, ничто из этого не видно в глобальном освещении с трассировкой лучей, или, по крайней мере, мне пока не удалось заметить этого в игре.
Огромное количество объектов
На более общем плане видно, как много разных объектов в ускоряющей структуре. Большинство из них на самом деле не внесёт никакого вклада в результаты вычислений глобального освщенеия. Также здесь видно, что нет никакой схемы LOD. Все объекты добавляются с полной детализацией. Было бы интересно узнать, оказывает ли это какое-то влияние на трассировку лучей (я бы предположил, что да).
Сверхвысокий LOD, полностью смоделирована каждая шкала и переключатель
Ещё один скриншот показывает огромную детализацию объектов даже вдалеке от игрока. Каждый переключатель и каждая шкала на этом снимке чётко читаются даже без текстур. Место, в которое я переместил камеру, чтобы сделать этот скриншот, находится в десятках метров от игрока и устранение этих деталей никак бы не ухудшило качество картинки. Возможно обновление ускоряющей структуры при использовании LOD было бы слишком затратным, но существует большая вероятность того, что это обновление можно выполнять асинхронно. Этот момент определённо стоит исследовать подробнее.
Рендеринг направленных теней
Основная часть рендеринга теней проста и не требует особого упоминания, но и здесь есть интересные моменты.
Меши, для которых мала вероятность отбрасывания теней
Огромная детализация в картах теней
Меши, для которых, похоже, используется неправильный буфер индексов
Похоже, что аналогично ускоряющим структурам, рендеринг теней включает в себя абсолютно всё. Есть объекты, которые почти не вносят вклада в карту теней, но они всё равно рендерятся. Интересно, так происходит из-за разрешения, или в движке нет простого способа исключить их?
Есть и объекты, которые трудно заметить даже с тенями в экранном пространстве. На их ренедринг уходит не так много времени, но интересно было бы посмотреть, можно ли их удалить, чтобы сэкономить немного времени.
При изучении меша кажется, что некоторые из мешей, отрендеренные в карте теней, имеют поломанные буферы индексов, но после вершинного шейдера они выглядят правильными (результаты одинаковы и в PIX, и в NSight). Это самый лучший пример, который мне удалось найти, но он далеко не единственный. Может быть, это какая-то специальная упаковка позиции?
Похоже, что у мешей плохой скиннинг
Кажется, скиннинг вызывает проблемы не только в ускоряющих структурах. Интересно, что не приводит к видимым артефактам на экране.
Часть 2
Небольшая поправка
В предыдущей части я писал, что в третьем render target буфера GBuffer скорее всего содержится metalness, но кажется, что на самом деле там содержится specular color. Сначала я не видел никаких цветов и не понимал, почему во всех трёх каналах RGB содержатся одинаковые данные, но наверно так было потому, что в сцене не было цветных отражений. Для этого оружия в буфере содержится гораздо больше разных цветов.
Ещё я забыл добавить свою любимую текстуру, которую нашёл в процессе исследования рендеринга игры. Её определённо стоит упомянуть, потому что она демонстрирует хаотичную природу разработки игр, когда не всегда удаётся всё подчистить.
«Улучши меня!»
Композитинг прозрачности и сглаживание
Пытаясь разобраться, как увеличивается разрешение буфера прозрачности половинного размера, и как игра выполняет сглаживание (antialiasing), я заметил нечто любопытное. Мне нужна была сцена, где гораздо больше контраста, чтобы было чётко видно, что происходит. К счастью, мне удалось захватить кадр, в котором оружие игрока между кадрами немного сдвигается.
До рендеринга прозрачности
Похоже, что до того, как выполняется композитинг буфера прозрачности, в буфере уже содержится полностью отрендеренное изображение, и поскольку в этом кадре нет никаких резких рёбер, логично предполжить, что это данные предыдущего кадра.
После композитинга прозрачности текущего кадра
При добавлении прозрачности текущего кадра мы можем заметить отдельные сломанные рёбра. Так получилось потому, что оружие немного сместилось вправо. Некоторые облака отрендерены прозрачными, но они отсекаются на горизонте (который непрозрачен), поэтому композитинг не меняет нижней части, но уже выполняет рендеринг поверх меша оружия из предыдущего кадра, используя буфер глубин текущего кадра.
После добавления непрозрачности текущего кадра
Спустя несколько вызовов отрисовки выполняется композитинг и непрозрачных мешей. Похоже, нет никаких особых причин выполнять это в таком порядке. Логично выполнять композитинг буфера прозрачности в данные непрозрачных объектов текущего кадра, но этого не происходит, и было бы интересно знать, почему.
После TAA
После завершения полного кадра проход TAA (временнОго сглаживания) сглаживает рёбра. Меня уже интересовало это раньше, потому что я не видел, где происходит сглаживание. Но я пропустил это потому, что сразу после этого вызова отрисовки начинается снижение разрешения (downsampling) для прохода bloom и я упустил этот один вызов отрисовки.
Блик объектива (Lens flare)
Обычно я не стремлюсь анализировать отдельные эффекты, но существует множество способов реализации lens flare, поэтому мне стало любопытно, какой выбрали разработчики.
Lens flare в готовом композитинге
В большинстве случаев блик объектива малозаметен, но это красивый эффект. Его сложно показать на скриншоте, поэтому я не буду прикладывать к этому много усилий.
Lens flare в буфере bloom
Поискав, я нашёл вызов отрисовки, добавляющий этот эффект, и оказалось, что это вызов после последнего этапа повышения разрешения bloom. В этом буфере эффект гораздо более заметен.
Геометрия Lens flare
Если посмотреть на геометрию, то lens flare довольно прост. В создании готового результата на экране участвуют не менее 6 четырёхугольников, но здесь нет серии более мелких четырёхугольников, становящихся всё ближе к позиции солнца. Можно прийти к выводу, что это довольно стандартное решение, хоть некоторые разработчики и рендерят lens flare непосредственно в rendertarget сцены, а другие вычисляют эффект как постобработку.
Рендеринг рельефа
Во всех играх с открытым миром одной из самых интересных трудностей является рендеринг рельефа. Я решил, что может показаться интересным изучить и этот аспект, но, честно говоря, немного разочаровался.
На первый взгляд фрагмент рельефа выглядит так, как будто выполняется какая-то тесселяция. То, как деформируется рельеф при движении делает логичным предположение, что есть какое-то дополнительное смещение. Кроме того, на PC игра достаточно активно использует тесселяцию, поэтому логично было бы использовать её и в рельефе.
Возможно, у меня были заданы неверные параметры, но игра рендерит все фрагменты рельефа без тесселяции. Для каждого фрагмента рельефа она использует эту равномерную сетку 32*32. Также нет никакого LOD.
Взглянув на фрагмент рельефа после вершинного шейдера, можно увидеть, что большинство пар вершин слились, образовав практически идеальную сетку 16*16, за исключением некоторых мест, в которых требуется бОльшая точность (вероятно, из-за кривизны рельефа). Упомянутая выше деформация вероятно возникает благодаря чтению mip-текстур карты высот рельефа, когда рельеф находится далеко от камеры.
Хитрости трассировки лучей
А теперь о том, чего все ждали.
Потоковая передача данных
Один из самых интересных аспектов любой реализации DXR на текущий момент — это способ работы с данными. Самое важное — это как данные загружаются в ускоряющие структуры и как они обновляются. Чтобы проверить это, я сделал два захвата и сравнил ускоряющие структуры в NSight.
Игрок находится внутри судна
В первом захвате я стоял внутри поломанного судна, которое видно посередине этого изображения. Загружены только ближайшие объекты, если не считать больших скал на краю карты.
Игрок переместился в левый верхний угол этого изображения
Во втором захвате я отдалился от края карты и оказался ближе к верхнему левому краю изображения. Судно и всё вокруг него по-прежнему загружено, но также загрузились и новые объекты. Интересно то, что я не могу определить никакой тайловой структуры. Объекты могут загружаться/удаляться из ускоряющей структуры на основании расстояния и видимости (возможно, ограничивающего параллелограмма?). К тому же верхний правый край выглядит более детализированным, хотя и отошёл от него. Интересно было бы больше узнать об этом.
Рельеф и то, что под ним
Можно упомянуть несколько аспектов реализации DXR в Metro: Exodus, касающихся рельефа.
Во-первых, любопытно то, что ускоряющие структуры не содержат никаких мешей рельефа (за исключением особых случаев). Эти монстры на самом деле в игре бегают по земле, но судя по данным в NSight можно подумать, что они летают. Это ставит перед нами интересный вопрос: может ли реализация глобального освещения каким-то образом учитывать рельеф (возможно, с помощью карты высот и материала рельефа), или нет.
Следующего момента я бы ни за что не заметил, если бы рельеф был на месте. Взглянув в начале уровня на ускоряющую структуру в NSight, я заметил под рельефом какие-то меши.
Художники довольно часто по разным причинам располагают под уровнем отладочные меши, но перед выпуском игры их обычно удаляют. В данном случае эти меши не только дожили до релиза, но и стали частью ускоряющей структуры.
Кроме упомянутых выше, я обнаружил другие меши, разбросанные под рельефом. В основном они не стоят особого упоминания, но этот был очень интересным — это персонаж, стоящий прямо под начальной точкой уровня. У него даже есть собственный бассейн.
Наконец, последний любопытный элемент ускоряющей структуры — односторонние меши, смотрящие наружу уровня. Если только они не считаются двухсторонними, очень мала вероятность того, что они вносят какой-то вклад в картинку игры. Даже если меши двусторонние, они находятся так далеко от играбельной области, что, вероятно, просто растягивают ускоряющую структуру. Интересно видеть, что они не отфильтрованы. На этом изображении также заметен один из особых случаев «меша рельефа» в нижнем правом углу, между поездом и зданием.
Безголовость со скиннингом
Я уже говорил о проблемах мешей со скиннингом, но на этом уровне я заметил ещё кое-что.
Во-первых, этот монстр демонстрирует в одном изображении обе ошибки, которые я заметил выше. Мне всё ещё интересно, чем они вызваны.
Также я заметил, что эти мелкие существа, похожие на летучих мышей, в ускоряющей структуре не имеют голов.
Ещё один пример. Заметьте отверстие в том месте, где должна быть голова. Я не видел ни одного случая, когда бы голова была видна.
Тот же вид существ в режиме растеризации. Заметьте, что голова чётко видна.
А вот каркасное отображение головы.
В заключение
На сегодня это всё. Надеюсь, вам понравился этот взгляд на внутренности Metro:Exodus.
Я продолжу исследовать рендеринг игры, но не буду публиковать новых частей статьи, если не найду какие-то особенные части, которые были бы интересны людям, или не обнаружу что-то, чем стоит поделиться.