Введение
После выхода 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