[Перевод] Как рендерится кадр Elden Ring
После выхода Elden Ring мне захотелось заглянуть за кулисы этой игры и узнать, что же там находится. Когда я смотрел анализ PC-версии игры Elden Ring в Digital foundry, то заметил, что MSI-Afterburner/Rivatuner сообщает, что эта игра основана на D3D12, и это меня восхитило. Потому что а) последняя изученная мной игра была старой и работала на D3D9 (подробнее об этом в будущем), а игра до неё была проектом 2022 года, работающим на D3D11, и это стало большим разочарованием; поэтому почти полгода я не изучал современные игровые технологии выпущенных игр для PC; б) это значит, что в игре будет много интересных функций D3D12, используемых хитрым способом. Что может быть лучше использования современного графического API для игры на PC в 2022 году?
На самом деле, всё оказалось иначе. Меня немного разочаровали решения разработчиков, но не очень сильно; подробнее об этом я напишу ниже. Однако любому геймеру очевидно, что в подобной игре главное — базовая боёвка, лор и геймплей, а графика уровня AAA — лишь дополнение, обеспечивающее более глубокий лор. Первые демонстрации игры выглядели потрясающе (ну, этим всегда славились E3 и все AAA!), но визуальное качество и уровень детализации готовой игры не так хороши (на мой взгляд). На самом деле, в Bloodborne и Sekiro качество с точки зрения визуальной красоты было выше. Так что да, меня как исследователя графики немного разочаровала игра для PC на D3D12. Зато как геймер я очень доволен!
На этот раз захваты были выполнены только на одном из моих PC, я не хотел тратить больше времени, как при анализе предыдущей игры, когда я делал захваты конфигураций на двух машинах и копировал файлы сохранений по сети! На этот раз я выбрал PC с RTX 3080, Ryzen 5950x и 32 ГБ ОЗУ. Параметры качества игры (Quality Settings) установлены на максимум (Maximum), а разрешение (Resolution) выбрано равным 1080p и включено автоматическое распознавание наилучших параметров (Auto-Detect Best Rendering Settings) (этот параметр работает как динамическое разрешение, только для практически всех графических аспектов, а не только для разрешения). Но я всё равно думаю, что эта настройка никак не повлияет на игру и не будет ничего менять, так как тестовый PC достаточно хорош для такой игры.
Учитывая всё это, я удивлён «странным» выбором параметров графики: некоторые параметры, например, качество текстур (Texture Quality), имеют четыре конфигурации, а другие, например, качество сглаживания (Antialiasing Quality) — только три конфигурации. Всё можно было организовать гораздо лучше!
По старой привычке я сделал множество захватов различных областей игры, чтобы лучше разобраться в теме, потому что один захват может содержать больше информации о каком-то аспекте, чем другой. Кроме того, на всякий случай я делал несколько версий одного захвата в одной и той же области и с одинаковым поворотом камеры.
Я больше склонен ссылаться на захваты кинематографических вставок, чем на захваты самого геймплея: они используют один движок и имеют примерно одинаковые очереди рендеринга, но я заметил (и это достаточно распространено), что во вставках всё выкручено на максимум и есть некоторые любимые мной эффекты (например, DoF), которые совершенно отсутствуют в геймплее. С другой стороны, я заметил, что игре не удаётся работать со стабильными 60 fps во время геймплея, когда игрок находится под открытым небом, однако внутри помещений (пещер, подземелий, замка и т. п.) и во время кинематографических вставок выдаёт стабильные 60 fps. Поэтому в захватах из вставок гарантированно будут отсутствовать даунскейлинг/динамическое разрешение и тому подобное.
Примечание: если вы захотите выполнять захваты в этой игре, то это может стать настоящим кошмаром из-за ПО EasyAntiCheat, которое запускается для запуска самой игры, поэтому практически все привычные инструменты практически бесполезны против этой игры. Поэтому вам придётся найти способ это обойти. Однако в целом для меня это оказалось довольно проблемно! И даже после успешного захвата игра часто вылетала. Я больше не буду углубляться в эту тему, но могу намекнуть, что EasyAntiCheat, вероятно, назвали «easy», потому что этот античит легко обмануть!
Как это обычно бывает у меня, я сделал несколько захватов из различных областей игры. Поэтому если вы видите захват области X, это один захват этого места, но, возможно, я сделал ещё десять захватов того же места на протяжении нескольких секунд. Показанные ниже скриншоты могут различаться в пределах около пяти кадров, потому что я хотел рассмотреть некоторые аспекты, которые не всегда присутствуют в кадре одновременно. Более того, лично я обожаю пограничные случаи, поэтому если, например, я анализирую DOF, то выбираю сцену с очень различающимися фоном и передним планом, и, вероятно, кинематографический захват, потому что именно во вставках AAA-игр DOF обычно проявляется во всю силу! Если мы изучаем блум, то почему бы не найти сцену с огнём и освещением, чтобы захват был очень хорошим, и так далее…
D3D12
Как я говорил выше, ещё до того, как делать захваты, я знал, что в порте игры для Windows используется D3D12, и был очень рад этому. Но оказалось, что если разработчик создаёт качественные игры для PlayStation, ситуация с портом для PC необязательно будет такой же. На самом деле, я считаю, что игре вполне было бы достаточно D3D11 или даже более старой версии, поскольку она практически не использует возможности D3D12. По крайней мере, в ней, похоже, не используется технология VRS, которая для меня является одной из самых интересных функций D3D12. Mesh Shaders, Raytracing, Sampler Feedback, Direct Storage — всё это совершенно не используется в порте Elden Ring для PC с D3D12.
Draw
Копирование/создание ресурсов
Каждый кадр в Elden Ring начинается с приблизительно 25 тысяч команд CopyDescriptorsSimple и CreateConstantBufferView. Их назначение понятно из названия, они подготавливают данные ресурсов к доступу. Если вы чаще всего работали с OpenGL, а теперь с Vulkan (то есть слабо знакомы с DX), то можете сказать: «Ага, то есть так работают с DX12», но на самом деле это не так! Можете попробовать делать захваты из опенсорсных игр/движков на D3D12, можете сделать захваты из игр Unreal на D3D12, можете взять новый проект Unreal на основе RHI D3D12, упаковать его и сделать захваты в нём, даже использовать BaseMark GPU Benchmark для D3D12 — во всех этих играх/приложениях D3D12 вы заметите два основных аспекта:
1. Они редко вызывают эти функции (а то и вовсе их не вызывают)
2. Они не подготавливают «все» просмотры доступа к ресурсам до выполнения рендеринга кадра.
Чтобы чётче понять эту проблему (которая может быть вероятной причиной споров о торможениях Elden Ring), можете ниже посмотреть на последовательности первого захвата из BaseMarkGPU; как видите, с начала кадра и до конца, это вся работа над кадром, как и ожидается. С другой стороны, последовательность второго захвата — это кадр Elden Ring; вы видите, что сама работа над кадром (отрисовка/вычисления) происходит в последней четверти длительности кадра, и это очень странно!
Не обращайте внимания на количество событий, BaseMarkGPU предназначен для стресс-тестов GPU
Это интересно, сбивает с толку и наводит на множество вопросов. Самый простой из них: зачем? Второй по простоте вопрос в моём списке: Разве никто не заметил этого в процессе работы над игрой? Приходится довольно долго ждать ExecuteCommandLists. Это совершенно точно связано с простоями и торможением ЦП! Не любое «потерянное время» должно быть связано с Device Wait/Idle, он может относиться и ко многому другому. И чтобы подтвердить теорию простоев ЦП, можно сравнить длительность работы GPU и ЦП, долгий период ожидания заметен сразу.
Длительность работы GPU
Длительность работы ЦП
Показанный на изображениях маркер находится в «фиксированной» точке времени жизни кадра, но на двух «шкалах». Когда маркер кадра на GPU Duration Scale находится на 0.005632ms, эта точка соответствует 17.8682ms на CPU Duration Scale. Именно тогда начинается работа GPU!
Если вы ещё не поняли, ниже я объясняю шкалы времени работы ЦП-GPU: по сути, мы как бы «вырезали» это первое изображение (последовательность кадра GPU) и «вставили» его туда, где оно должно быть на втором изображении (последовательность кадра ЦП).
Я считаю, что при другой организации/архитектуре кадра Elden Ring могла бы работать гораздо быстрее и без торможений!
Copy и Clear
Ничего особенно важного, это просто три прохода копирования/очистки данных предыдущего кадра, последовательность CopyBufferRegion и ClearDepthStencilView. В основном эти операции связаны с GBuffer, например, добавление барьеров ресурсов, а затм копирование содержимого render target GBuffer из предыдущего кадра (Color, Surface, Normals, AO, Depth, SSS и т. п.), так как они могут понадобиться в текущем кадре.
Если бы я выбирал этой игре название, связанное с графикой, то она называлась бы «Временным кольцом», а не «Кольцом Элдена», потому что многие данные переносятся из предыдущего кадра (копируются), а многие вещи накапливаются между кадрами, становятся «временнЫми». Когда мы слышим это слово, то обычно знаем, что тема будет касаться сглаживания, однако во «Временном кольце» всё заходит гораздо дальше — AA является временным, SSAO, Glare, даже тени — тени из предыдущего кадра переносятся в текущий! А те аспекты, которые не используют RT предыдущего кадра, используют скорость или дельту движения из предыдущего кадра. Я заметил, что это применимо практически к любому важному этапу/эффекту в игре — они тем или иным образом используют данные предыдущего кадра. При глубоком изучении игры начинаешь привыкать к префиксу Prev!
Вершины Elden
Один из аспектов, сильно интересующих меня в разных играх — это описания вершин. Мне всегда любопытно, используется ли в движке ленивое описание вершин (как я делал когда-то давно) и одно и то же описание вершин применяется для всего (обычно со скиннингом даже для мешей без скиннинга), или же применяются продуманные разные описания, соответствующие различным ситуациям. Или же они крайне специализированы и используют множество описаний вершин для разных типов одного и того же объекта. И насколько я могу судить, разработчики Elden Ring относятся к третьей категории и создают микроспециализированные описания вершин. Ниже представлены самые широко используемые в движке описания (но не все, существующие в игровом движке/рендерере).
За многие годы я научился любить и практиковать специализированные вещи, например, описания вершин, но не настолько специализированные! К примеру, если вы думаете, что это все вариации описаний вершин ткани, то вы ошибаетесь — есть ещё пара вариаций для описаний мешей ткани; одна из них предназначена для ткани под тканью, уже находящейся под тканью! Вы легко можете увидеть в игре эти слои одежды и брони, особенно в кинематографических вставках — лучше всего это заметно на Мелине!
CSM прямого освещения
Несколько ранних проходов только глубин для направленного/непосредственного освещения (солнечного света), также называемых Shadow Map Cascades. Это может быть от 6 до 14 проходов глубин (возможно, и больше, но это максимум, который я обнаружил на своих захватах), всё зависит от количества детализации (количества объектов/мешей на кадр), то есть может быть 6 или 8 проходов для кадра в подземелье, но 14 (а может и больше) в открытом мире над землёй. В конечном итоге остаётся 5 каскадов, всегда сохраняемых размером 8192×4096 формата R16_TYPELESS. Всегда! Даже когда игрок находится в подземелье, они имеют тот же размер. Постоянство — это хорошо… иногда!
Ниже показано несколько примеров, в третьей строке увеличена яркость для демонстрации большего количества деталей.
Проходы цвета [GBuffer/отложенное затенение]
Так как игра рендерится с отложенным затенением, на этом этапе будет создаваться последовательный набор отложенного затенения, передаваемого для создания окончательных render target GBuffer. Количество (длительность) этих проходов может разниться в зависимости от количества отрисовываемых элементов. В пещере или киновставке может быть 8–12 проходов отрисовки, а на большом открытом пространстве — 20 или больше. В конечном итоге мы приходим к обычному GBuffer, здесь нет ничего непривычного.
Готовая Swapchain
Цвет — R8G8B8A8_TYPELESS
Глубина — D32S8_TYPELESS
Нормали — R10G10B10A2_UNORM
Поверхности — R8G8B8A8_UNORM
В render target определения поверхностей используются отдельные каналы. Один — это metallic, второй — roughness, а третий довольно «необычен». Он для теней (используется несколькими этапами далее).
R
G
B
[A] — это сплошной белый, не используется, поскольку позже создаются другие полные RT и используются только их [R] или [A]
А вот таймлапс render target GBuffer.
ЦветГлубинаНормалиПоверхностиR поверхностейG поверхностейB поверхностей
Стоит заметить, что обычно в GBuffer есть два render target, которые не делают ничего, они полностью чёрные — в одном проходе находятся все основные render target + эти два, в другом проходе эти два хранят некоторые из значений основных render target, при том что сами render target полностью чёрные. Это происходит совершенно случайно и, насколько я могу судить, какой-то предсказуемый паттерн отсутствует.
И, разумеется, в некоторых кадрах/областях будут существовать значения в отдельном Render Target для Emissive (свечения), при котором предыдущий кадр имеет почти 0 свечения, а RT чёрный, но в примере кадра ниже показан полезный render target свечения, сопровождающий GBuffer.
Окончательный кадр Swapchain — 1920×1080
Вспомогательный Render Target свечения — 1920×1080 — R16G16B16A16_FLOAT
Дизеринг
Многие объекты в Elden Ring поддерживают дизеринг-прозрачность и во время отрисовки им передаётся глобальная одноканальная текстура дизеринга размером 8×8. Вне зависимости от того, видите ли вы дизеринг этих объектов, при необходимости они отрисовываются готовыми для этого. Трава, деревья, все типы листвы, а также броня и одежда персонажа, флаги и знамёна, NPC и враги — все они изначально поддерживают дизеринг.
Это увеличенная версия глобальной текстуры дизеринга с линейной фильтрацией. А вот как она выглядит в реальном размере:
Декали
Отрисовка декалей поверх отложенного GBuffer происходит в последовательности DrawIndexedInstanced в зависимости от вариаций декалей и их количества. Здесь особо ничего интересного.
Итак, у нас есть отложенный GBuffer
1920×1080 — цвет
1920×1080 — поверхность (Metallic, Roughness…)
1920×1080 — нормали
1920×1080 — глубина
А закончить нам нужно кадром, показанным ниже, на котором рядом с двумя мёртвыми существами находятся два больших пятна крови.
Кадр swapchain в конце
Мы пройдём по двум операциям отрисовки, для каждой из них в качестве маски используется декаль, а также несколько дополнительных текстур (таких как diffuse, specular и normal) для проецирования декали в нужное положение.
Отрисовка 1
Кадр на входе (декалей пока нет)
512×512 BC4_UNORM
256×256 BC7_UNORM
256×256 BC1_TYPELESS
256×256 BC1_TYPELESS
Кадр на выходе
Свойства поверхностей GBuffer на выходе
Отрисовка 2
Кадр на входе (с одной отрисовкой декали)
512×512 BC4_UNORM
256×256 BC7_UNORM
256×256 BC1_TYPELESS
256×256 BC1_TYPELESS
Кадр на выходе
Свойства поверхностей GBuffer на выходе
Отрисовка N…
Небольшое примечание: маска крови на самом деле не красная, в ней используется BC4_UNORM, то есть это просто совпадение, что маска крови «выглядит» красной.
Также стоит заметить, что проецирование декалей не всегда использует стандартный кубический объём, обычно применяемый во многих других играх и движках, а иногда реализуется при помощи «усечённой призмы» или «срезанной сверху призмы», которая, по сути является кубом, только деформированным. Поэтому это можно (приблизительно) воспринимать вот так:
Это действительно прекрасное решение, потому что куб используется для проецирования объектов сверху вниз, например, следа от ноги, и работает хорошо. Но если для вещей наподобие большого пятна крови деформировать этот куб, чтобы он больше напоминал усечённую призму + повернуть его, то это придаст финальной декали немного деформации и растянутости, делая её как будто уникальной каждый раз, когда вы её видите, несмотря на то, что используется одна и та же маска декали. Допустим, у нас есть большое пятно крови:
Оно используется на двух показанных ниже кадрах, но проекция каждый раз выглядит иначе, потому что каждый раз куб деформируется на разную величину и имеет другое направление.
Возможно, при изучении определений поверхностей GBuffer будет заметнее вариативность и растянутость в финальной спроецированной текстуре пятна крови.
В частности, на первом из двух изображений пятно совершенно непохоже на текстуру декали из-за большой доли растянутости текстуры по вертикальной оси.
Если отложенным декалям передаётся часть с ресурсами, то как насчёт параметра шейдеров? Вот список параметров декалей, передаваемый шейдерам
Непонятно, зачем использовать такие «несвязанные» параметры в одной структуре. Разве не было лучше использовать в этой структуре то, что связано с декалями, а всё остальное применять там, где нужно?
Обратите внимание, что этот массив структур InstanceData всегда имеет два элемента, вне зависимости от количества экземпляров отрисовываемых декалей и даже без учёта того, если ли декали. Всегда два элемента в массиве.
Наконец-то! Хотя бы одна из четырёх передаваемых шейдеру структур полностью относится к делу.
А теперь о самом интересном аспекте декалей в Elden Ring. Декали используются (обычно) для детализации объектов сцены/мира, и один из них (как вы могли видеть в предыдущих разборах) — это следы подошв. В этом смысле Elden Ring не отличается от других игр. Посмотрите на показанный ниже готовый кадр: зная, как выглядят декали следов, сможете ли вы найти их в мире, и если они есть, то где эти следы проецируются? И самое главное — можете ли вы прикинуть по готовому кадру, сколько видов следов отрисовывается как декали? Считайте это викториной и при желании откройте ответ под спойлером!
Готовый кадр
Diffuse декали — 256×256 BC1_TYPELESS
Specular декали — 256×256 BC1_TYPELESS
Bump декали — 256×256 BC7_UNORM
Маска декали — 512×512 BC4_UNORM
За множество проведённых в игре часов я не нашёл ни одной вариации следов, кроме той. Она используется для всего!
Итак, вы ошибаетесь, если думаете, что следов нет — на самом деле в этом кадре спроецировано и отрисовано 60 различных следов, и ни один из них невидим ни игроку, ни на финальном кадре, они просто скрыты где-то за теми далёкими деревьями! Единственная декаль, которую вы видите из почти семидесяти команд отрисовки декалей — то пятно крови рядом с птицей. Декали занимают не так уж мало ресурсов, поэтому я думаю, что в Elden Ring можно было бы оптимизировать!
А поскольку я люблю декали и следы, в местах, где много грязи, есть много замечательных декалей. Ниже показан пример развития GBuffer декалей в области, где много грязи и декалей.
Готовый кадр
Цвет GBuffer декалей
Нормали GBuffer декалей
Поверхности GBuffer декалей
Обратите внимание, что эти изображения GBuffer анимированы!
Irradiance и Specular Accumulation
Имея полностью готовый GBuffer, его можно использовать множеством различных способов. Он в месте с кубической картой или текстурой IBL, а также запечённым IrradianceVolume (XYZW) передаются шейдеру, который выполняет все необходимые вычисления освещения, создавая Irradiance RT и Specular AccumulationRT.
Cubemap (IBL) + GBuffer«s (Normals + Depth + Surface) + Irradiance Volume XYZW = Irradiance RT + Specular Acc RT
Примеры кубических карт (IBL) Elden Ring, каждая из которых имеет по 6 слайсов (±X, ±Y, ±Z) с 5 mip-текстурами (от 128×128 до 8×8) в формате BC6_UFLOAT
На некоторые области влияет одна кубическая карта, а на другие — две или более.
Irradiance Volume X
Irradiance Volume Y
Irradiance Volume Z
Irradiance Volume W
На показанных выше анимациях демонстрируется запечённый Irradiance Volume для одной области; в данном случае объём имеет 72 слайса, но это число варьируется в зависимости от размера объёма, оно может быть больше (более 100 слайсов) или меньше (20 слайсов и менее). Эти запечённые данные Irradiance обычно имеют формат BC7_UNORM, однако размеры тоже меняются в зависимости от количества слайсов, в данном случае это 120×124.
И, разумеется, как и в случае с кубическими картами, у некоторых областей будет один объём, а у других несколько, это зависит от «пространства», на которое смотрит игрок.
X
Y
Z
W
Ещё один пример объёма/3d-текстуры размером 236×84 из 144 слайсов
Итак, эти два элемента (IBL + Irradiance Bakes) совместно с GBuffer приводят к созданию Irradiance RT, а также Specular Accumulation RT.
Irradiance RT — 1920×1080 — R11G11B10_FLOAT
Specular Accumulation RT — 1920×1080 — R11G11B10_FLOAT
И кадр киновставки в этом не отличается — все входные данные точно такие же; я надеялся, что во вставках используется кубическая карта большего разрешения или что-то подобное, но увы, они одинаковые…
Irradiance RT — 1920×1080 — R11G11B10_FLOAT
Specular Accumulation RT — 1920×1080 — R11G11B10_FLOAT
Даунсэмплинг глубин
Даунсэмплинг глубин выполняется дважды. Один раз здесь (Graphics Queue) и один раз в Compute Queue. Тот, который выполняется здесь (пока) не оправдан, его результаты не используются, но стоит упомянуть, что он существует! Другой этап под названием Depth Downsampling находится в compute и его результаты используются; об этом мы поговорим ниже.
1920×1080 — D32S8_TYPELESS
960×540 — D32S8_TYPELESS
480×270 — D32S8_TYPELESS
Даунсэмплированные до 960×540 глубины очень важны в Elden Ring, поскольку большинство эффектов, использующих глубины, пользуются этими данными, а не полнокадровым target глубины 1920×1080
SSAO
Генерация SSAO выполняется за три этапа. Вот примеры двух кадров
1. Генерация SSAO + вращения
При помощи имеющихся глубин половинного размера + render target нормалей, а также вспомогательной текстуры «Random Rotations» мы получаем новый render target SSAO.
in Depth — 960×540 — D32S8_TYPELESS
in Normal — 1920×1080 — R10G10B10A2_UNORM
in RandomRot — 64×64 — R8G8B8A8_UNORM
Out SSAO — 960×540 — R8G8B8A8_UNORM
in Depth — 960×540 — D32S8_TYPELESS
in Normal — 1920×1080 — R10G10B10A2_UNORM
in RandomRot — 64×64 — R8G8B8A8_UNORM
Out SSAO — 960×540 — R8G8B8A8_UNORM
Этот новый SSAO является упакованной текстурой. Реальный SSAO находится в канале R, а канал A — это маска для SSAO (на практике можно считать, что это маска скайбокса, поскольку это единственное, к чему мы не можем применить SSAO). Канал G хранит случайные повороты для ядра сэмпла в каждом пикселе. А канал B, насколько я понимаю — это «проверка расстояния», позволяющая избежать ошибочных перекрытий.
RGBA
R
G
B
A
RGBA
R
G
B
A
2. Предыдущий и текущий кадр
Выполняем своего рода интерполяцию (временнУю) между SSAO предыдущего и текущего кадра.
Текущий кадр