[Перевод] Как рендерится кадр Elden Ring

https://habr.com/ru/post/668838/image

Введение

После выхода 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 относятся к третьей категории и создают микроспециализированные описания вершин. Ниже представлены самые широко используемые в движке описания (но не все, существующие в игровом движке/рендерере).

Описания вершин Elden Ring – большинство мешей (камни, скалы, здания и т. п.)

Описания вершин Elden Ring – меши со скиннингом (персонажи, враги, животные и т. п.)

Описания вершин Elden Ring – меши со скиннингом (ткань)

Описания вершин Elden Ring – меши со скиннингом (ткань под тканью)

Описание вершин Elden Ring – меши листвы

Описания вершин Elden Ring – некоторые камни

Описание вершин Elden Ring – некоторые виды инстансинга (SpeedTree)

Описания вершин Elden Ring – некоторые меши (используются только в течение прохода теней)


За многие годы я научился любить и практиковать специализированные вещи, например, описания вершин, но не настолько специализированные! К примеру, если вы думаете, что это все вариации описаний вершин ткани, то вы ошибаетесь — есть ещё пара вариаций для описаний мешей ткани; одна из них предназначена для ткани под тканью, уже находящейся под тканью! Вы легко можете увидеть в игре эти слои одежды и брони, особенно в кинематографических вставках — лучше всего это заметно на Мелине!

CSM прямого освещения

Несколько ранних проходов только глубин для направленного/непосредственного освещения (солнечного света), также называемых Shadow Map Cascades. Это может быть от 6 до 14 проходов глубин (возможно, и больше, но это максимум, который я обнаружил на своих захватах), всё зависит от количества детализации (количества объектов/мешей на кадр), то есть может быть 6 или 8 проходов для кадра в подземелье, но 14 (а может и больше) в открытом мире над землёй. В конечном итоге остаётся 5 каскадов, всегда сохраняемых размером 8192*4096 формата R16_TYPELESS. Всегда! Даже когда игрок находится в подземелье, они имеют тот же размер. Постоянство — это хорошо… иногда!

Ниже показано несколько примеров, в третьей строке увеличена яркость для демонстрации большего количества деталей.

Изображения в высоком разрешении


8192*4096 – R16_TYPELESS


8192*4096 – R16_TYPELESS


8192*4096 – R16_TYPELESS


8192*4096 – R16_TYPELESS


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 будет заметнее вариативность и растянутость в финальной спроецированной текстуре пятна крови.


В частности, на первом из двух изображений пятно совершенно непохоже на текстуру декали из-за большой доли растянутости текстуры по вертикальной оси.

Если отложенным декалям передаётся часть с ресурсами, то как насчёт параметра шейдеров? Вот список параметров декалей, передаваемый шейдерам

MTD Param

Непонятно, зачем использовать такие «несвязанные» параметры в одной структуре. Разве не было лучше использовать в этой структуре то, что связано с декалями, а всё остальное применять там, где нужно?

Instance Data

Обратите внимание, что этот массив структур InstanceData всегда имеет два элемента, вне зависимости от количества экземпляров отрисовываемых декалей и даже без учёта того, если ли декали. Всегда два элемента в массиве.

Model Param

Decal Param

Наконец-то! Хотя бы одна из четырёх передаваемых шейдеру структур полностью относится к делу.

А теперь о самом интересном аспекте декалей в 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 Accumulation RT.

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 предыдущего и текущего кадра.


Текущий кадр


Предыдущий кадр


Текущий кадр


Предыдущий кадр

3. Окончательный Render Target SSAO

Текущий упакованный SSAO по-прежнему имеет размер в 1/2 от целевого. Поэтому на этом этапе игра сгенерирует готовый render target SSAO в оттенках серого, который будет использоваться преобразованием формата и растягиванием нового render target до полноэкранного целевого разрешения.


960*540 – R8G8B8A8_UNORM


1920*1080 – R8_UNORM


960*540 – R8G8B8A8_UNORM


1920*1080 – R8_UNORM

Весь этот процесс выполняется во фрагментном шейдере (к сожалению) и использует следующий набор параметров:

SSAO Param

SSAO Param 2

Post Process Common

Композитная тень

Имея на руках SSAO и ранее полученное определение поверхностей GBuffer (напомню, в в его каналах RGB хранятся Metallic, roughness и тени), композитный шейдер даёт нам новый render target, используемый в качестве композитной маски теней.


SSAO на входе


GBuffer на входе


Маска теней на выходе


Канал A ShadowMask на выходе


Окончательная Swapchain

Я хотел показать канал A готовых выходных данных Shadow Composite, потому что именно в нём хранятся данные о тенях. Вы видите, что канал A полностью чёрный, и это логично: если подробно рассмотреть готовый кадр, то можно заметить, что в нём отсутствуют все виды теней от прямого солнечного света и используется только SSAO для затемнения и затенения областей. Если посмотреть на другой пример (ниже) кадра на открытом воздухе, находящегося под прямым солнечным светом, то можно обнаружить, что канал A выходного Shadow Composite содержит все детали теней, присутствующие в готовом кадре swapchain (под Годриком, на здании, под цветами, на самом полу арены и т. д.).


SSAO на входе


GBuffer на входе


Shadow Mask на выходе


Канал A ShadowMask на выходе


Окончательная Swapchain


Что касается входных параметров композитного шейдера, то они очень просты:

Composite Shadow Mask

Соединяем всё вместе

Используя render target Irradiance + Specular поверх GBuffer (4 Render Target + глубины) + Shadow Mask+ Light Lookup + Light Buffers (Spot + Point) с обычным PBR и математикой во фрагментном шейдере мы получаем нечто красочное и готовое для постобработки.


Irradiance


Specular Acc


Цвет


Normal


Поверхности


Emissive


Глубины


Shadow Mask


Финальный PBR, готовый для постобработки

Меш карты окружений

Генерация кубической карты (многократная)

Здесь не происходит ничего важного, подробнее см. в подразделе «Генерация кубической карты» раздела Compute.

Атмосфера

Но как вы видите, несмотря на то, что кадр готов перейти к постобработке, на нём по-прежнему отсутствует небо! И тут настаёт время отрисовать и выполнить его композитинг с текущим кадром в отдельном проходе цвета.

Атмосфера и небо проходят несколько этапов, сначала отрисовываются меш купола неба с голубой текстурой неба, затем меш облаков и, наконец, рельеф (terrain) — не знаю, почему это часть неба/атмосферы, возможно это связано с приданием «атмосферы» и туманом.

Итак, когда все эти три меша объединяются (учитывая то, что они центрированы на мире), они выглядят примерно так. Но я всё равно не понимаю, почему terrain почти всегда невидим!


Купол неба


Меш облаков


Атмосферный рельеф


Все атмосферные меши вместе

Купол/полусфера неба

На этом первом этапе отрисовываются такие элементы атмосферы, как луна и звёзды (при необходимости). Звёзды имеют две основные текстуры (пока).


Паттерн звёзд 1 — 4096*4096 – BC1_TYPELESS – RGBA


Паттерн звёзд 2 — 1024*1024 – BC7_TYPELESS – RGB


Паттерн звёзд 2 — A

Луна имеет текстуру, использующую каналы RGB для различных целей.


1024*1024 – 11 Mips – BC7_UNORM


R — цвет


G — Luminance


B — Mask

После этого первого этапа мы переходим от пустого пространства за кадром (обычно после PBR заливаемого серым) к тому, что будет походить на чистое небо (при необходимости со звёздами)


На входе в этап атмосферы 1


На выходе из этапа атмосферы 1

Облака

Затем частично деформированный вторичный купол (не полный купол, он уплощённый, как показано выше) отрисовывается внутри первого купола (большего размера); он используется для облаков с группой текстур облаков. Сложно сказать, какие конкретно из них используются, но стоит упомянуть. что каждая отрисовка облаков получает весь набор текстур; вне зависимости от того, используются ли одна или две, шейдеру передаются они все.


2048*512


2048*2048


4096*1024


2048*256


4096*1024


2048*2048


4096*1024


2048*256


Все текстуры имеют формат BC7_TYPELESS. На изображениях в градациях серого представлен канал A каждой текстуры, он очень полезен для симуляции анимации «течения». Если вы думаете, что некоторые текстуры я ошибочно повторил, то нет, игра передаёт некоторые текстуры дважды тому же шейдеру как разные входные данные!


На входе этапа атмосферы 2


На выходе этапа атмосферы 2

И, разумеется, есть группа вспомогательных текстур, используtvs[ для симуляции фальшивых облаков (flowmap)


1024*1024 – BC7_TYPELESS


1024*1024 – BC7_TYPELESS


256*256 – BC7_UNORM

Рельеф

Последним отрисовывается меш рельефа. Этот меш очень сложно увидеть; на самом деле, почти на всех моих захватах мне не удавалось найти ни одной части рельефа в окончательном рендере. Ниже показаны вход и выход этого этапа, заметите ли вы какую-нибудь разницу?


На входе этапа атмосферы 3


На выходе этапа атмосферы 3

Этот меш огромный и очень-очень плотный, но его всё равно не так легко заметить. Основная часть рельефа, который вы видите и по которому ходите — это отдельные от этого «атмосферного» рельефа меши рельефа/камней. Рельеф этого этапа проходит обычный процесс тайловой отрисовки рельефа, в котором много слоёв в виде текстур (травы, камня, грязи и т. п/) и масок. Вот список часто встречающихся слоёв и масок рельефа, используемых при этом типе отрисовки.


512*512 – BC1_UNORM


1024*1024 – BC1_TYPELESS


2048*2048 – BC1_UNORM


1024*1024 – BC1_TYPELESS


1024*1024 – BC1_TYPELESS


1024*1024 – BC1_UNORM


1024*1024 – BC1_UNORM


1024*1024 – BC1_TYPELESS


256*256 – BC4_UNORM


1024*1024 – BC1_UNORM


1024*1024 – BC1_UNORM


1024*1024 – BC1_UNORM

Вы ошибаетесь, если думаете, что вместо одиннадцатого изображения я случайно вставил повтор. Нет! Игра всегда передаёт её дважды как две разные текстуры!

И вы правы, если предположили, что первая текстура выше упакована. Я люблю упакованные текстуры, иногда они кажутся мне «инопланетными»!


512*512 RBG


R


G


B

Если объединить всё вместе, то для получения хорошего примера области, где видны небо, звёзды, луна, облака и очень атмосферные элементы, нужно отрисовать их в следующем порядке:


GBuffer на входе


Луна + звёзды


Базовый слой облаков


Передний слой облаков + рельеф


Окончательная Swapchain

Слои облаков нужны для своего рода «параллакса». Атмосферный рельеф по-прежнему сложно заметить!

Постобработка

Основная часть постобработки в Elden Ring выполняется во фрагментных шейдерах, и довольно редко можно встретить постобработчик на уровне compute. И это ещё один разочаровывающий момент игры под D3D12, выпущенной в 2022 году! Но в конечном итоге мне понравилось качество постобработчиков, а также то, насколько «лёгкими» казались данные с моей точки зрения, и общая упорядоченность потока постобработки.

Depth of Field

Depth of Field в Elden Ring используется только в кинематографических вставках и катсценах, в остальной части игры он отсутствует. Возможно, в некоторой степени логично никак не использовать DOF при геймплее, однако во многих играх его применяли в качестве геймплейной механики, помогающей направлять игрока или ограничивать его зрение из-за сюжетных элементов. Как бы то ни было, я не могу осуждать это решение и возможно, позже в игре DOF может меняться (хоть я и сомневаюсь в этом), но в целом он выглядит так:


Изображение Swapchain кадра катсцены


Изображение Swapchain кадра геймплея


Пятно рассеивания DOF кадра катсцены


Пятно рассеивания DOF кадра геймплея

Пятно рассеивания (CoC) для for DOF имеет формат R10G10B10A2_UNORM и рендерится в половинном разрешении; так как мои эксперименты проводятся в 1080p, эти голубоватые CoC рендерились в размере 540p (960*540).

Далее CoC в половинном разрешении уменьшается вдвое ещё раз, достигая размера 480*270, затем 270p много раз пропускается через одинаковый фрагментный шейдер, чтобы выполнить магию DOF в разных слоях размытия и аппроксимировать глубину относительно разделения фона, переднего плана и элементов в фокусе.

Стоит заметить, что количество исполнений фрагментного шейдера DOF ограничено пятью, после чего он достигает величины размытия, необходимой команде From Software. Общее размытие зависит от того, где находится элемент в фокусе и от того, какой процент кадра он занимает. Используются (передний план + элемент в фокусе + фон) или (только элемент в фокусе + фон). Например:


960*540


480*270


480*270


480*270


480*270

В предыдущем примере нет элементов переднего плана, но если бы они были, ситуация бы немного различалась, поскольку необходимо было бы чёткое разделение между тремя элементами. Давайте рассмотрим ещё один кадр:


Разделение


960*540


480*270


960*540


960*540


480*270


480*270


480*270

Затем Blur


960*540


480*270


480*270


480*270


480*270

Я хотел показать эти детали по одной причине: если DOF совершенно не используется во время геймплея, почему он всё равно включён в графический конвейер? Ресурс создаётся и заливется фрагментным шейдером в каждом кадре (сплошным синим), а также проходит весь конвейер DOF, где изображение рендерится в половинном размере в 540p, а затем снова уменьшается вдвое до 270p, а потом эти ресурсы перемещаются между входными и выходными данными шейдеров. Это можно было бы полностью обойти в вызовах CPU рендерера и сэкономить немного времени кадра, особенно если бы оно понадобилось нам в течение геймплея (вспомните, что в открытом мире игра никогда не достигает 60fps)!

Для любопытствующих ниже представлена структура со всеми параметрами DOF, передаваемыми фрагментному шейдеру.

DOF Params


Depth of Field — один из самых любимых мной эффектов в 3D-рендеринге реального времени, и несмотря на всё сказанное о DOF Elden Ring, мне нравится его качество в игре! Разработчики прекрасно с ним справились!

Цветокоррекция (подготовка)

См. этап [7] очереди Compute.

AA

Anti-Aliasing в Elden Ring является временнЫм. Здесь нет ничего сложного, игра просто генерирует render target скоростей пикселей (называемый в этой игре render target «move») и использует его между предыдущим и текущим кадром.


Кадр на входе — 1920*1080 – R11G11B10_FLOAT


Кадр на выходе — 1920*1080 – R11G11B10_FLOAT


Текущий кадр — 1920*1080 – R11G11B10_FLOAT


Текущие скорости/Move – 1920*1080 – R16G16_FLOAT


Текущий стенсил – 1920*1080 – R8_UINT


Предыдущий кадр – 1920*1080 – R11G11B10_FLOAT


Предыдущие скорости/Move – 1920*1080 – R16G16_FLOAT


Предыдущий стенсил – 1920*1080 – R8_UINT

И, разумеется, этому этапу передаётся набор параметров шейдеров

AA

Свечение (Bloom)

Выполняется последовательность масштабирования выходных данных кадра, шесть раз, каждый раз размер уменьшается вдвое; в результате мы получаем семь каскадов, пока не достигнем половины от исходного размера кадра (960*540).


15*9


30*17


60*34


120*68


240*135


480*270


960*540

Разумеется, как это всегда бывает с bloom, когда приходит его время, эти данные не используются в своём размере, а масштабируются обратно к полноэкранному разрешению, поэтому в момент использования они выглядят примерно так (это не истинное разрешение, а просто демонстрация).


15*9


30*17


60*34


120*68


240*135


480*270


960*540

Этому шейдеру передаётся очень простое единственное значение.

Glow Param

Даунскейлинг

На этом этапе текущее состояние кадра уменьшается в два раза, поскольку эта уменьшенная версия понадобится сейчас для всех последующих этапов постобработки. Если игра работает в 1920*1080, то на этом этапе мы получим 960*540 в формате R11G11B10_FLOAT. Здесь нет ничего сложного, никаких особых параметров шейдеров и тому подобного.


1920*1080 – R11G11B10_FLOAT


960*540 – R11G11B10_FLOAT

Помните, что представленные выше изображения немного изменены для понятности, поскольку на этом этапе кадр очень тёмный: нет тональной и гамма-коррекции, никакой другой обработки. Но если вам интересны кадры, которые поступают на вход этого этапа и выходящие из этапа даунскейлинга, то они представлены ниже:


1920*1080 – R11G11B10_FLOAT


960*540 – R11G11B10_FLOAT

LightShaft

Хотя постобработчик столбов света (Light Shaft) существует, мне не удалось найти ему хорошего применения. Даже в самых контрастных кадрах наподобие показанного здесь его результаты не полностью похожи на то, как должны выглядеть результаты постобработки light shaft. Но что есть, то есть!

Здесь стоит заметить, что эффект столбов света работает с кадром половинного разрешения, а не полного.

Также здесь стоит упомянуть, что несмотря на то, что я описал столбы света здесь, прямо перед постобработчиком Glare/Bloom, на самом деле они выполняются «посередине» Glare/Bloom. Как видно ниже, Glare/Bloom выполняется за три отдельных этапа, однако столбы света вычисляются непосредственно после первого этапа (этап генерации) и до завершения Glare/Bloom (второй и третий этапы). Возможно, это можно оптимизировать: может быть, это не приведёт к большому изменению производительности и качества изображения, однако улучшит качество кода и конвейера.


На входе — R11G11B10_FLOAT – 950*540


На выходе — R11G11B10_FLOAT – 950*540


Усиленный выход — R11G11B10_FLOAT – 540p


Кадр Swapchain в конце

Для демонстрации столбов света я хотел взять именно этот кадр, поскольку если в этой игре и есть столбы света, то нет лучшего места для них, чем красивое Древо Эрд. То есть я просто хотел сказать, что эти яркие столбы света (которые также называют God Rays) — это просто меш с прозрачной текстурой и, возможно, билбордингом, а не настоящий постобработчик.


Древо Эрд — меш столба света


Столб света — 4096*2048 – BC1_UNORM


R


G

Это меш и текстура, используемые вокруг ствола Древа Эрд для симуляции столбов света

Ниже можно посмотреть на усиленную версию отложенного кадра до всей постобработки — это просто меш! Но должен сказать, что окончательный результат выглядит хорошо.


Отложенный кадр до всей постобработки


Кадр Swapchain со всей постобработкой

Наличие этапа столбов света в постобработчике подразумевает, что в плане была эта функция, или её реализовали, а потом вырезали. Я говорю это, потому что во многих областях игры, где можно ожидать наличия God Rays, не видно ничего, реализованного как столбы света, а шейдер столбов света получает разрешение в 1/2 кадра и выводит чёрный цвет. То есть он выполняется и производит вычисления, но просто заполняет ненужную текстуру чёрным и передаёт эти ресурсы на последующие этапы.

Light Shaft

Glare(Bloom)

Стоит сказать, что я буду всегда называть этот эффект Glare/Bloom, потому что я видел его и обычно его называют Bloom, однако разработчики Elden Ring чаще всего в коде называют его Glare (в редких случаях они называют его и Bloom!), а в других случая они называют то же самое Glow. Поэтому я попытаюсь использовать оба термина.

Bloom выполняется за несколько этапов, не все они выполняются одновременно и не все используют один шейдер. К сожалению, разработчики Elden Ring могли реализовать его лучше, воспользовавшись compute-шейдерами для большинства из этих этапов, а то и для всех!

Генерация Glare/Bloom

Здесь генерируется так называемая «Accumulation Texture», которая позже используется для применения эффекта Glare/Bloom. Это очень типичное уменьшение размера кадра (6 раз, каждый раз вдвое), которое мы видели на предыдущем этапе.

Тональная коррекция Glare/Bloom

На текущий момент Accumulation Texture находится в непригодном для использования состоянии. Или, по крайней мере, так выглядит. Если наложить эту текстуру сейчас, она затемнит кадр, поэтому к Accumulation Texture Glare/Bloom применяется тональная коррекция для преобразования её в нужное цветовое пространство. Используемая здесь тональная коррекция (для регулировки Glare/Bloom) — это тот же алгоритм и те же параметры шейдеров, что используются позже для применения тональной коррекции ко всему кадру для заверешния прохода постобработки.


До тональной коррекции Accumulation Glare/Bloom – R11G11B10_FLOAT – 960*540


После тональной коррекции Accumulation Glare/Bloom – R11G11B10_FLOAT – 960*540

Формат текстур (R11G11B10_FLOAT), а также размер (1/2 кадра = 960*540) те же для Accumulation Texture, на этом этапе она никак не растягивается, не масштабируется и не преобразуется.

Применение Glare/Bloom

Теперь осталось лишь добавить красивый Glare/Bloom вокруг пикселей с luminance, для этого достаточно лишь применить композитингом Accumulation Texture к текущему кадру.


До Glare/Bloom – 1920*1080 – R11G11B10_FLOAT


Glare Accumulation – 960*540 – R11G11B10_FLOAT


После Glare/Bloom – 1920*1080 – R10G10B10A2_UNORM

Тональная коррекция

В Elden Ring используется простая техника тональной коррекции Рейнхарда, ничего особо сложного или инновационного, как может показаться, и я не думаю, что здесь нужны какие-то усложнения, игре это подходит. Мне кажется, техника Рейнхарда скорее художественна, чем реалистична. Ниже показаны входные и выходные данные этого постобработчика, а также полное описание входных параметров шейдеров.


До тональной коррекции – 1920*1080 – R10G10B10A2_UNORM


Таблица тональной коррекции – 1024*1 – R16_FLOAT


После тональной коррекции – 1920*1080 – R10G10B10A2_UNORM

Помните, что ради демонстрации я поместил здесь выходные данные Glare/Bloom в качестве входных данных для этапа тональной коррекции (R10G10B10A2_UNORM), но на самом деле, поскольку обе операции выполняются в одном шейдере и одновременно, сложно создать захват одного Glare/Bloom, поэтому при реальной реализации кадра обе операции (Glare/Boom и тональная коррекция) получают на входе один и тот же кадр (R11G11B10_FLOAT), показанный ниже:


1920*1080 – R11G11B10_FLOAT – без гамма-коррекции


1920*1080 – R11G11B10_FLOAT – с гамма-коррекцией

Tone Map

Post Process Common


Этот этап тональной коррекции в конвейере постобработки сбивает с толку своим названием. Тональная коррекция — это «отдельный» постобработчик, выполняемый на этом этапе из нескольких, но склеенных вместе постобработчиков; однако как вы могли заметить по структуре Tone Map (под спойлером выше), передаваемой шейдеру, это неправильное имя для структуры, поскольку это «глобальная» структура постобработчика для нескольких постобработчиков, выполняемых одновременно в одном шейдере. Поэтому да, она называется Tone Map, и мы выполняем тональную коррекцию, но одновременно по необходимости применяются и другие элементы, такие как Brightness, Contrast, Vignette, Chromatic Aberration, Glare/Bloom, Lens Distortion и Color Correction. Например, на предыдущем этапе до тональной коррекции мы говорили, что выполняется этап Bloom/Glare. Этот первый этап Glare выполняет только «подготовку», но само применение Glare выполняется в том же шейдере и в то же время, что и тональная коррекция (или, возможно, на этапе цветокоррекции, о котором говорится ниже).

Цветокоррекция (LUT)

Здесь не происходит ничего особо сложного, просто обычная единственная 3D-текстура 16*16*16 в формате R16G16B16A16_FLOAT для придания кадру нужного настроения. Она применяется как к геймплею, так и к вставкам. Помните, что в показанных ниже примерах вы можете заметить изменения в «тоне», а также немного glare между кадрами до и после этапа цветокоррекции, и это нормально: похоже, это один общий шейдер, используемый для выполнения тональной коррекции, Glare, а также цветокоррекции. Это общий шейдер с общим набором параметров, упомянутый выше в разделах о тональной коррекции и Glare. То есть показанные ниже результаты не являются полностью результатом применения только цветокоррекции.


Сверху вниз: LUT, кадр до, кадр после

UI

Финальный композитный результат

К этому времени у нас есть два финальных render target для всей этой работы. В движке Elden Ring они называются HDRScene и UIScene, каждый имеет размер 1920*1080 (или разрешение, выбранное вами в параметрах игры) и формат R8G8B8A8_UNORM . Назначение второго понятно из названия (это весь игровой UI на одном изображении, которое размещается поверх кадра), а первый, гипотетически, называется HDR, но это совсем необязательно должен быть HDR; например, я играл на своём мониторе без поддержки HDR и в этом случае изображение было просто результатом работы все очереди рендеринга (а также compute). Это готовый кадр со всеми эффектами, готовый к отображению.

И, разумеется, когда кадр взят из кинематографической вставки, render target UIScene полностью чёрный и с полной альфой; это ещё один странный выбор со стороны разработчиков, потому что изображение 1080p копируется в шейдер композитинга окончательного кадра. Не было ли лучше пропускать этап композитинга в случае киновставок? На стороне шейдера есть bool/int (как вы увидите ниже) для пропуска композитинга, но сам ресурс всё равно передаётся шейдеру, хоть его и не нужно использовать. Разве не было бы лучше в случае вставок передавать передавать текстуру 1*1? В конечном итоге все дескрипторы назначаются предварительно, но мы хотя бы могли обмануть их при помощи не требующей ресурсов текстуры 1*1.

Для фрагментного шейдера композитинга используются следующие параметры:

Display Mapping Data


То есть потом HDRScene + UIScene с использованием альфа-канала UIScene превратятся в готовый красивый кадр. Как в Photoshop, только в реальном времени и много раз в секунду!


HDRScene – R8G8B8A8_UNORM


UIScene – R8G8B8A8_UNORM


UIScene[A] – R8G8B8A8_UNORM


Финальный композитинг (Swapchain)

Все изображения имеют размер 1920*1080 или то целевое разрешение, которое вы задали в настройках игры

Лично я бы рисовал UI прямо на финальном HDRScene и сэкономил бы render target размером в целый экран; при этом сохранилась бы возможность полностью отказаться от прохода UI, когда он не нужен (например, во время катсцены). Именно так я поступил в своём движке, и, как вы можете помнить по предыдущей статье, в God of War сделано так же!

Compute

FromSoftware удивила меня тем, что несмотря на создание качественных игр, студия всегда отстаёт от технологий и новых функций, если считать Compute чем-то новым!

При создании кадров Elden Ring используются compute-шейдеры, но не очень активно. Бывает, что они достаточно часто используются в течение жизни кадра, но не на «максимум» или это использование не особо полезно по сравнению с другими играми, выпущенными в том же поколении или в тот же год. Честно говоря, местами в игре выполняются compute, иногда можно удивиться идеям разработчиков, в других же случаях они кажутся бесполезными или их можно реализовать иначе. Очередь compute в Elden Ring выполняется в следующем порядке:

Состояния Compute

[Всегда] Этот compute выполняется в каждом кадре на протяжении всей игры.
[Снаружи] Этот compute выполняется только когда игрок находится на открытом воздухе, однако в пещерах, подземельях и помещениях его не будет.
[Если существует] Этот compute выполняется только когда существует рендерящийся элемент. Лучший пример — это GPU-частицы

1. Даунсэмплинг глубин [Всегда]

Этот compute-шейдер выполняется сразу после даунсэмплинга глубин в очереди графики и перед SSAO Этот этап «гипотетически» называется даунсэмплингом глубин, но на самом деле здесь не происходит никакого даунсэмплинга, это просто этап продолжения даунсэмплинга глубин из очереди графики. Два результата даунсэмплинга (960*540 и 480*270 из очереди графики) передаются этому compute, чтобы использовать их для создания этого нового изображения маски 120*68 в формате R32G32_FLOAT, которое, похоже, используется позже для усечения источников освещения (точечных и прожекторных).


1920*1080 – D32S8_TYPELESS


120*68 – R32G32_FLOAT


1920*1080 – D32S8_TYPELESS


120*68 – R32G32_FLOAT

Примеры кадров внутри (пещеры) и снаружи (открытый мир)

Down Sample Depth

2. Генерация кубических карт [Снаружи]

Этот этап не совсем полностью является генерацией, похоже, это этап «копирования» для предварительно запечённых кубических карт. Здесь выполняется последовательный набор compute-команд, количество выполнений этого compute зависит от видимых объектов кубических карт/захватов отражений в текущей загруженной области. Во многих областях это будут 1-2 захвата, однако в более крупных областях (например, в показанной ниже) будет девять захватов с девятью последовательными compute-командами.


Каждая из этих команд сопровождается командой отрисовки кубической формы очереди графики, как будто это меш/заполнитель захвата в игровом мире. Каждая из этих команд отрисовки куба, по сути, является вызовом отрисовки Irradiance & Specular Accumulation в очереди графики (это объясняется ниже). Так что можно представить эти девять отрисовок (в других случаях их больше или меньше) как последовательность «пинг-понга» между очередями графики и compute, повторяемых девять раз COMPUTE-COPY, за которым следует GRAPHICS-DRAW.


Каждая из кубических карт — это 2DTextureArray с шестью гранями/слайсами и 8 mip-текстурами (от 128*128 до 1*1)

Каждая из этих команд получает в качестве входных параметров для compute-шейдера копирования следующую структуру:

Cube Copy Param

Любопытно то, что передаваемое шейдеру значение numMipLevels всегда равно 5, но в конце мы всегда получаем 8 mip-текстур.

3. Усечение освещения [Всегда]

Теперь желтоватая маска освещения, полученная ранее на этапе даунсэмплинга глубин, передаётся compute-шейдеру (этому) вместе с двумя Structured Buffer (один для точечных источников, второй для прожекторных), и мы получаем Light Lookup Structured Buffer (таблицу), а также несколько сопровождающих структурированных буферов (Light Indices и Work Data) в качестве результата того, что похоже на compute тайлового рендеринга.

Point Light – StructuredBuffer[1024]

Spot Light – StructuredBuffer[1024]


Параллельно с передачей шейдеру обычной структуры Scene Params (она передаётся большинству шейдеров в игре) этот compute получает дополнительные входные параметры буфера, содержащие данные о накопленном освещении (пирамиду, количество источников, ячейки и т. п.), имеющие следующий вид:

Light Acc Param


Анализ четвёртого, пятого и шестого этапов (Volumetrics, Shadows, GPU Particles) находится в процессе разработки.

7. Цветокоррекция [Всегда]

На этом раннем этапе постобработки игра подготавливает таблицу поиска цветокоррекции, то есть 3D-текстуру, которая будет использоваться в очереди графики для применения «истинной» цветокоррекции при помощи фрагментного шейдера.

Таблица имеет размер 16*256 и формат R8G8B8A8_UNORM


Этот compute приводит к созданию структуры 16*16*16 формата R16G16B16A16_FLOAT, которая используется на входе постобработчика цветокоррекции очереди графики.


Найти разницу в этих изображениях PNG может быть сложно, и разница заключается не в отличиях строк и столбцов, это я решил разделить второе изображение на две строки по 8 слайсов (всего 16), чтобы их было проще различать. Разница заключается в глубине текстур, а также в формате DXGI_FORMAT. Под глубиной я подразумеваю то, что первая передаваемая compute текстура — это обычный сэмплер 2D-текстуры, а на выходе compute получается 3D-текстура (слайсы по XYZ). То есть можно сказать, что этот compute является шейдером преобразования из 2DTexture в 3DTexture.

Я бы сказал, что это совершенно необязательный повторяющийся шаг, происходящий в каждом кадре (геймплея и вставок), а не просто вычисляемый один раз в начале уровня. Лучше бы было, если бы это был предварительно запечённый ресурс, созданный до подготовки игровых данных, как и во многих других играх. Крошечная compute-команда в очереди, выполняемая в каждом кадре с группами потоков (1, 1, 16) может и не вызывать проблем, однако если не отслеживать эти мелочи, «не влияющие» на затраты вычисления кадра, то внезапно у вас возникнет куча множества мелких проблем, которые вполне себе «влияют»!

Жизнь кадра [граф рендеринга]

Итак, узнав о том, что же скрывается в очереди графики и compute, мы можем воссоздать весь конвейер/граф рендеринга кадра (для типичного/золотого кадра, включающего в себя максимально большое количество функций) и получить псевдовизуальную диаграмму развития кадра.

И разумеется, мы не можем обойтись полноэкранного видео воссоздания. Я люблю наблюдать за такими видео и понимать, сколько времени мне с моей средней человеческой скоростью восприятия нужно, чтобы рассмотреть каждый этап. Для просмотра всех этапов создания кадра Elden Ring требуется около десяти минут наблюдений, а игра рендерит их по 60 раз в секунду, потрясающе!

Кадр геймплея
Кадр кинематографической вставки

Общие наблюдения о движке

Анимации окружения

Анимации окружения мира Elden Ring довольно красивы. Разнообразие вариаций медленно движущихся «объектов» мира придаёт ему большой реализм. Эти анимации можно заметить в таких объектах, как птицы, мелкие существа, ткань, флаги и знамёна. Все анимации окружений в игре используют технику VertexToTexture, при которой кадры анимации (ограниченные) запекаются в данные Positions и Normals в текстурах, а анимация воспроизводится при помощи считывания вершинным шейдером этих текстур. Например, ниже показаны два слоя ткани, образующих в игре один флаг:


Меш флага – 9202 вершины – 15048 треугольников (45144 индекса)


Позиции вершин – 7977*121 – R16G16B16A16_FLOAT


Нормали вершин – 7977*121 – R8G8B8A8_UNORM


Меш флага – 1188 вершин – 890 треугольников (2670 индексов)


Позиции вершин – 569*240 – R16G16B16A16_FLOAT


Нормали вершин – 269*240 – R8G8B8A8_UNORM

Для такого меша и подобных мелких текстур мы получаем красивый эпичный реальный флаг с циклической анимацией!


Кроме трюка с анимацией флаг использует группу текстур для задания своей поверхности в GBuffer, как любой другой геометрии в мире.


Цвет – 1024*1024 – BC1_TYPELESS


Нормали – 1024*1024 – BC7_UNORM


Bump – 256*256 – BC1_TYPELESS


Волоски ткани — 256*256 – BC7_UNORM

И, разумеется, существуют вариации наборов текстур для создания при необходимости разорванной версии или другого внешнего вида флага.


Цвет – 1024*1024 – BC1_TYPELESS


Нормали – 1024*1024 – BC7_UNORM


Грязь – 512*512 – BC4_UNORM


Flow – 512*512 – BC1_UNORM

Render Target

Общее замечание: в этой игре есть много render target, которые нужны просто для того, чтобы быть. Некоторые из них постоянно чёрные, можно представить это как будто у вас есть пять коробок, которые нужно переместить из комнаты А в комнату Б, можно перенести пять коробок (одну за другой или за раз в тележке, это не так важно), переместив их из комнаты А в комнату Б. Или у вас появляется еще две коробки (теперь их семь) и вы помещаете содержимое двух из пяти коробок, которые нам нужно переместить, в две новые коробки, оставляя две из исходных пяти коробок пустыми (чёрный цвет). Наконец, мы перемещаем новые две коробки (с содержимым исходных коробок) + ещё три коробки (с содержимым) + две исходные пустые коробки (мы используем их позже) в комнату Б! Бессмысленно использовать подобные коробки, если и так можно переместить пять коробок с их содержимым! Эти коробки — render target, создаваемые и перемещаемые только для хранения значений других render target, которые вполне можно было переместить и так.

Ещё один тип render target, который мне не понравился: игра хранит один канал в целом render target RGBA, а в другом render target используются только два или три канала. Упаковать каналы можно было и лучше.

Последний тип render target — это, по сути, вещи наподобие UIScene; некоторые спорят, стоит ли иметь отдельный RT только для содержимого UI. Лично я принадлежу к другой школе и считаю, что нужно максимально ужимать и упаковывать данные. Производительность в играх достигается непросто!

Аудиопоток

Аудио в этой игре выполняется в собственном потоке и это вполне нормально сегодня, но, похоже, аудиопоток не воспроизводится в гармонии с остальной частью игры и другими потоками (игровой поток, поток рендеринга и т. п.), так как чаще всего, когда я делал захваты, в игре возникают торможения при копировании содержимого кадра из GPU на HDD (и это вполне стандартно). Но в большинстве других игр (будь то на PC или на консоли) во время захвата GPU притормаживало абсолютно всё и игровой звук не воспроизводится. В Elden Ring аудио воспроизводится совершенно плавно, кроме того, если произносится строка диалога или воспроизводится музыка в киновставке, то игрок будет слышать музыку или диалог, пока вся игра притормаживает на десять или более секунд, а после разблокировки графического потока и продолжения игры вы увидите оставшуюся часть геймплейной анимации или оставшуюся часть вставки без аудио, поскольку аудиоинформация была воспроизведена заранее, во время процесса копирования данных кадра. Разумеется, это происходит только тогда, когда аудиопоток бsk запущен перед захватом, торможением потока рендеринга.

Вполне нормально иметь полностью отдельный аудиопоток и сегодня это совершенно нормально, но он должен синхронизироваться с остальной частью игры. Да, он будет выполняться независимо, но должен иметь больше зависимости от потоков игры и графики. Это усложняет архитектуру, увеличивает финансовые и временные затраты на продакшен, вероятно, вызывает больше багов и даже условий гонки (при определённом подходе), но во многих ситуациях это было бы логичнее. Лично я считаю, что многопоточность на самом деле не многопоточна, если это многопоточность на 100% .

Ассеты

Хотя движок кажется очень мощным (мы поверхностно рассмотрели только графику, но в движке есть другие великолепные аспекты, например, сетевая часть), я заметил часто встречающийся паттерн: игровым ассетам (я рассматривал графические ассеты, например, модели и текстуры) уделено недостаточно внимания. Например, почти всегда мы видим атласы UI, заполненные сплошным чёрным цветом или даже текстуры, которые никак не используются.

С другой стороны, если взглянуть на 3D-данные, то можно заметить несогласованность между типами, например, часть травы создана в полной детализации (стебли и тому подобное), а другая трава создаётся в виде карт (четырёхугольники с вырезанной альфой). И разница в типах мешей никак не связана с расстоянием до камеры, она кажется очень случайной и несогласованной. Более того, у меня сложилось ощущение, что в большинстве случаев в Elden Ring никогда не используются LOD (я могу ошибаться на 50%): 1. я никогда не видел никаких переходов между LOD. Как бы ни был великолепен движок игры, вы всегда можете заметить переходы между LOD, с дизерингом или без. 2. при изучении сцен в каркасном виде (wireframe) можно заметить небольшие части камней, деревьев и прочего, находящиеся очень далеко от окна обзора игрока, а количество вершин меша остаётся тем же, как и с приближением к этим ассетам.

Объяснение

«LOD никогда не используются» — это саркастическое преувеличение. В игре есть некая разновидность LOD, но не та, которую можно ожидать от AAA-проекта, и не та, которую можно ожидать в современной игре с открытым миром. Например, в этих руинах разрушенного здания меш выглядит следующим образом:


Это не единый меш, а структура состоит из слоёв мешей, и если не обращать внимания на вьюны и декали, то здание задаётся примерно тремя слоями геометрии.


Даже на расстоянии чуть менее полукилометра эти слои здания и слой маленьких кирпичей всё равно отрисовываются!


Если приблизить предыдущее изображение, то можно увидеть слои в подробностях:


То есть при очень близком расстоянии (почти внутри здания) и при расстоянии в полкилометра используются два меша:


Готовый кадр


Нормали GBuffer


Использованный меш (2693 вершины)


Готовый кадр


Нормали GBuffer


Использованный меш (2693 вершины)

В обоих случаях рендерится один и тот же меш с одинаковым количеством вершин!

При таком расстоянии модель в обычном открытом мире должна находиться на LOD2 (третий уровень, наверно) (например, в Assassin’s Creed Origins). Я оставил сигнатуру меша, а также «индекс кадра» в названии меша, чтобы не запутаться в разных захватах.

Даже мелкие отдельные кирпичи в обоих случаях рендерятся. Полигонов чуть меньше, ноих всё равно очень много для такого расстояния!


Близко, менее 1,5 километра — 17954 вершины


Далеко, больше 1,5 километра — 10025 вершин

Но если уйти дальше, например, на 1,5 километра, то отрендерится другой меш.


Готовый кадр


Нормали GBuffer


Использованный меш (893 вершины)

Дело в том, что на этом расстоянии (более 1,5 км) большое здание в типичном открытом мире было был заменено на импостор!

Пока мне не удалось найти более чем один уровень деталей в больших мешах (имеющих различные уровни, пока я не увижу меш на экране. В примере с руинами я видел упрощённую версию на всех захватах дальше 1,5 км вплоть до того момента, пока здания полностью стало невидимым.

Повторюсь, такое происходит только когда у мешей есть несколько разных версий (я не буду называть их LODs), а в Elden Ring у многих мешей нет версий (LOD)!

Подобные проблемы никогда не связаны с проблемами движка или рендеринга, их причиной является обычная неразбериха в конвейере производства ассетов; кто знает, возможно, это вызвано тем, что на некоторые должности технических художников не нашлось людей! Но если бы у Elden Ring была реальная надёжная команда технических художников (5-8 человек для такой большой игры), то ситуация была бы гораздо лучше и это бы сильно повысило качество и производительность игры.

Но это далеко не значит, что всё, что связано с ассетами и их организацией, сделано плохо! Есть и качественно реализованные аспекты, один из них — это атласы листвы, которые понравились мне больше всего. Можно считать их атласом каждой области или семейства элементов, хранящим в одном атласе все формы листьев деревьев в этой области. Это хорошая идея, они загружаются за раз и используются, пока игрок находится в этой области, от нескольких минут до часа! Ещё мне нравятся решения, связанные с размером «поддерживающих» текстур, например, текстур нормалей.


Цвет листьев – 4096*2048 – BC1_TYPELESS


Нормали листьев – 2048*1024 – BC1_UNORM


Цвет листьев – 2048*1024 – BC7_TYPELESS


Нормали листьев – 1024*512 – BC7_UNORM

Мне нравится, когда игра уменьшает текстуру нормалей до половины BaseColor, в длительной перспективе это приводит к большой экономии!

Сторонние фреймворки

После того, как я увидел когда-то первый экран заставки (splashscreen), это стало своего рода зависимостью, я изучаю начало каждой игры (а иногда и титры в конце) и замечаю, какие сторонние фреймворки разработчики решили выбрать, чтобы расширить возможности своего игрового движка. Это технологии, ставшие торговыми марками, которые мы видим в начальных заставках во всех играх. Однако при запуске Elden Ring я не увидел ни одной такой заставки. Игра начинается с логотипа BandaiNamco, за которым следует логотип FromSoftware, а затем главное меню игры. А при выходе игра просто показывает чёрный экран. В настройках нет никаких пунктов Credits, позволяющих запустить титры, как это бывает в других играх. Это подразумевает следующее: во-первых, в игре не используются сторонние технологии; во-вторых, движок полностью был создан From Software.

Но ситуация оказалась немного запутанной…

При создании захватов меню игры я заметил атласную текстуру размером 4k*2K, которая, по сути, была заполнена или текстами лицензий или логотипами сторонних компаний.


Мы видим Speedtree, Bink, Havok, Oodle, Wwise. Значит ли это, что эти сторонние технологии использовались в игре? Или это значит, что они использовались на каком-то этапе, возможно, это остатки от старого/предыдущего движка, но теперь они не применяются? Или, возможно, игра использует их на консолях, а здесь они оказались лишь из-за кроссплатформного кода или ошибочной загрузки изображения в 4k не на ту платформу. А может быть, всё гораздо проще — кто-то забыл показывать заставку в нужный момент, а отдел QA это упустил. Остаётся только гадать!

Лично я склоняюсь к последнему варианту, потому что мне удалось найти в установочных файлах игры несколько видеофайлов Bink (*.bk2).

Если вас интересует блок японского текста, то я его перевёл: это предупреждение о нелегальном распространении без разрешения (не знаю, относится ли оно к игре или к файлам сторонних технологий), но каким бы ни был его контекст, оно никогда не появляется на экране.

Эпилог

Помните, что всё сказанное выше — это лишь моя точка зрения после изучения множества захватов GPU, их анализа и реверс-инжиниринга кадров для лучшего их понимания. Я хотел лучше понять решения, принятые при их проектировании. Главная задача для меня — учиться на отличных идеях и ошибках других разработчиков, поэтому помните, что я никого не сужу, а изучаю, так что, как всегда, воспринимайте всё написанное скептически.

Я очень рад, что мне удалось опубликовать эту статью целиком, уже больше десятка лет я делаю захваты игр, в которые играю, но каждый раз, когда пытаюсь что-то написать, на меня наваливаются заботы и статья оказывается в длинном списке незаконченных черновиков. К счастью, на этот раз мне удалось ещё раз добраться до конца статьи, несмотря на то, что я начал анализировать игру спустя пару недель после релиза, то есть написание статьи заняло три месяца (если не больше) полностью занятых выходных (и нескольких бессонных ночей в будни), поэтому не критикуйте меня за опечатки или недосказанности. У меня осталось ещё много набросков, которые я не изложил в этой статье, а некоторые моменты опустил намеренно; другие части ускользнули от моего внимания, но всё, что я изложил в статье — это то, что мне важно, когда я копаюсь в чужих играх для приобретения знаний.

Пусть я и не большой фанат жанра Souls, но мне нравятся работа, мир, истории и лор Миядзаки, и если уж не они заставили меня сыграть в сточасовую игру и попытаться её пройти, так это Мелина, в которую я влюбился с первого взгляда!

Читайте также