[Перевод] Как рендерится кадр DOOM Ethernal
Вступление
Doom Eternal не нуждается в отдельном представлении: это прямой преемник Doom 2016, разработанный благодаря седьмой итерации id Tech, внутреннего движка студии id Software. В свое время меня поразило и высокое качество визуальной составляющей Doom 2016, и простота и элегантность технических решений. В этом отношении Doom Eternal превосходит своего предшественника во многих областях, и некоторые из них достойны детального разбора. В этой аналитической статье я постараюсь обсудить их все.
Мой анализ вдохновлен трудом Adrian Courrèges про Doom 2016 (перевод). Я считаю, что подобные работы позволяют взглянуть на подходы к решению некоторых проблем рендеринга AAA-проектов и тем самым становятся превосходными обучающими материалами. В этом анализе я планирую обсудить общие особенности и не погружаться слишком глубоко в тонкости каждого способа и прохода рендеринга. Кроме того, некоторые проходы в Doom Eternal почти не отличаются от своих аналогов в Doom 2016 и уже были разобраны в труде Adrian Courrèges, поэтому я могу их пропустить.
Хочу особым образом отметить строго обучающий характер текущей статьи. Я никоим образом не поддерживаю реверс-инжиниринг продуктов с целью кражи интеллектуальной собственности или иного злого умысла. Если вы еще не успели сыграть в Doom Eternal, можете не беспокоиться: я разбирал лишь самое начало игры, поэтому спойлеры вам не грозят.
Итак, приступим.
С выходом id Tech 7 переход движка с OpenGL на Vulkan API позволил разработчикам эффективнее работать с особенностями текущего поколения графических процессоров, например несвязанными ресурсами (bindless resources).
Один кадр в Doom Eternal
Выше мы можем видеть близкую к началу секцию игры: интерьер с несколькими противниками и объемным освещением. По аналогии с его предшественником, процессом визуализации в Doom Eternal заведует forward rendering pipeline, однако если Doom 2016 вынужден проводить рендеринг совместно с G-буферизацией отражающих поверхностей, в нашем случае буфер не используется и рендеринг берет на себя все задачи.
Уход от мегатекстур
С выходом созданной на движке id Tech 5 игры Rage мир познакомился с концептом реализации текстур под названием «мегатекстуры». Этот метод применяется в Doom 2016 и на каждый кадр он рендерит так называемую «виртуальную текстуру» с информацией о видимых текстурах. Виртуальная текстура анализируется в следующем кадре, чтобы определить какие текстуры следует подгрузить с диска. Однако у мегатекстур есть очевидная проблема: как только текстура попадает поле зрения, подгружать ее уже поздновато, поэтому на первых нескольких кадрах после появления текстура выглядит размыто. С выходом id Tech 7 разработчики отказались от такого метода.
Скиннинг через графический процессор
Обычно еще до отрисовки каких-либо текстур и шейдинга оценку скиннинга проводит вершинный шейдер. Скиннинг id Tech 7 проводится заранее вычислительным шейдером с записью итоговых вершин в буфер. Благодаря такому подходу вершинному шейдеру больше не нужны данные скиннинга, и так как при каждом проходе геометрии он больше не проводится, в итоге перестановки шейдеров случаются реже.
Ключевой разницей между скиннингом в вычислительном шейдере и вершинном шейдере является запись результата в промежуточный буфер. Как и в вершинном шейдере, для каждой вершины поток вычислительного шейдера получает преобразование каждой влияющей на вершину кости. Затем он изменяет положение вершины каждым преобразованием кости и складывает все новые позиции в соответствии с хранимым в вершине весом скинов. В итоге вершинный шейдер может воспользоваться результатом из буфера для его интерпретации в виде статичной сетки.
По ссылке можно ознакомиться с отличной статьей о скиннинге через вычислительный шейдер за авторством János Turánszki.
Полезно также отметить, что в Doom Eternal используется интересный вид кеширования — Alembic Cache, сравнимый с сильно сжатым обратно воспроизводимым видео. В таких кешах хранится запеченная анимация для выдачи и разжатия в ходе исполнения программы. Цитируя технический анализ Digital Foundry, Alembic Cache применяется в широком диапазоне анимаций, начиная с масштабных кинематографичных сцен и заканчивая крохотными щупальцами на полу. Особенно удобен такой подход для анимаций со сложностями реализации через скиновую анимацию, например для органики и симуляции тканей. Если вас заинтересовала эта технология, рекомендую ознакомиться с презентацией Axel Gneiting на Siggraph 2014.
Карты теней
Следующим этапом является рендеринг теней, и подход к генерации их карт на первый взгляд у id Tech 7 и его предшественника не отличается.
Как можно видеть ниже, тени рендерятся в большую текстуру глубиной 24 бита и размером 4096 на 8196 пикселей, местами различаясь по уровню качества. Текстура не меняется между кадрами, и согласно презентации «Devil is in the Details» на Siggraph 2016, статичная геометрия кешируется в карте теней, чтобы не перерисовать ее для каждого кадра. Идея сама по себе проста: нам не нужно обновлять тени до тех пор, пока перед источником света не начнется движение, и таким образом мы можем объявить «кешированную» карту теней: обычную карту со статичной геометрией, так как мы полагаем, что геометрия не меняется. Если в конусе обзора движется динамический объект, «кешированная» карта теней копируется в основную, и поверх этого перерисовывается динамическая геометрия. Такой подход позволяет не перерисовывать всю сцену в конусе обзора при каждом ее обновлении. Естественно, при смещении света всю сцену придется перерисовать с нуля.
Для сглаживания краев теней при сэмплировании карты применяется 3×3 PCF сэмплинг. Поскольку солнечный свет обычно покрывает значительную часть окружения, для лучшего распределения качества используются каскадные теневые карты.
Для примера можно взглянуть на атлас теневых карт. Чем больше значимость света, крупнее область на экране или ближе объект к камере, тем крупнее будет выделенный сегмент атласа — это необходимо для повышенной детализации. Подобные эвристики оцениваются динамически.
Скорость и предварительный проход обработки глубины
Начиная с оружия игрока, в целевую глубину последовательно рендерятся непрозрачная, статическая и динамическая геометрии. Обычно, чтобы при потенциальном пересечении геометрий не делать лишних вычислений пиксельных шейдеров, проводится предварительный проход обработки глубины с добавлением результата в буфер. Поскольку перерисовка пикселей при их пересечении создает лишние перерасчеты и в итоге негативно сказывается на производительности, важность такого подхода становится неоценима. Благодаря предварительному проходу глубины пиксельный шейдер прямого освещения может исключить лишние пиксели, сравнив их с буфером глубины еще до собственно вычислений, и тем самым сэкономить ценные ресурсы.
Оружие игрока
Статические объекты
Динамические объекты
В предварительном проходе рендерится не только глубина, но и целевой цвет. В динамической геометрии скорость рендерится через векторы движения, то есть положение текущей позиции, вычитаемое из положения пикселя на предыдущем кадре. Так как движение хранится в красном и зеленом каналах 16 битной цели рендеринга с плавающей точкой, нам достаточно знать движение по осям X и Y. В дальнейшем эта информация используется при постобработке для применения размытости и репроекции временного сглаживания. Статичной геометрии векторы движения не нужны, поскольку она «двигается» только относительно камеры и ее движение может быть высчитано из движения самой камеры. Как можно видеть на скриншоте ниже, движения в нашей сцене не так уж и много.
Z-иерархическая глубина
Следующий шаг это генерация иерархической mip-цепи буфера глубины: эта цепь похожа на mip карту, но вместо усреднения четырех соседних пикселей берет их максимальное значение. Такой подход часто используется в графике для множества задач, например для ускорения отражений и отбрасывания прегражденной геометрии. В нашем случае mip-цепь отбрасывает освещение и декальные текстуры, о которых мы поговорим позднее. В последнее время mip-генерацию проводят за один проход, с записью сразу в несколько mip-ов, но в Doom Eternal запись все еще ведется отдельно для каждого mip.
Декали сеток
Пока что мы не успели познакомиться с какими-либо серьезными отличиями процессов в Doom Eternal по сравнению с Doom 2016, но под эту категорию подходят декали сеток. Это небольшие декали (болты, решетки, выпуклости), которые, как и обычные декали, могут влиять на любые свойства поверхностей (нормаль, неровность, цвет). Однако типичная декаль сетки назначается художниками в ходе разработки сеток и, в отличие от стандартного размещения декалей в окружении, принадлежит своей сетке. Doom и раньше сильно полагался на декали, и текущий переход на декали сеток только повысил детализацию и гибкость графики.
Чтобы добиться такого положительного результата, следующий проход геометрии рендерит идентификаторы каждой декали в восьмибитную текстуру. В дальнейшем, при наложении теней, мы сэмплим текстуру и через идентификаторы получаем связанные с каждым вызовом отрисовки матрицы проекций. Матрица проецирует координаты пикселя из пространства мира в пространство текстур, а затем эти координаты используются для семплирования декали и ее слияния с материалом поверхности. Этот прием невероятно быстр в своем исполнении и открывает художникам широкий простор для работы с множеством декалей. Так как идентификаторы рендерятся в восьмибитную текстуру, потенциально в одной сетке может быть до 255 декалей.
Единственным условием для всего этого является привязка всех декальных текстур к процессам при отрисовке сеток. Благодаря полностью несвязанному процессу рендеринга разработчики могут связывать все декальные текстуры разом и динамически индексировать их в шейдере. Так как разработчики пользуются этим методом при реализации еще нескольких приемов в игре, подробнее о несвязанном процессе рендеринга мы поговорим позже.
Ниже мы можем видеть сетку декальных текстур. Для удобства визуализации идентификаторы покрашены в разные цвета.
Отбрасывание света и декалей
Свет в Doom Eternal полностью динамичен, и одновременно в область обзора может попадать до нескольких сотен источников. Кроме того, как мы уже отметили ранее, декали в игре имеют большое значение, например в том же Doom 2016 число декалей перевалило за тысячи. Все это требует особого подхода к отбрасыванию лишнего, иначе производительность не выдержит тяжести пиксельных шейдров.
В Doom 2016 использовался процессорный вариант кластерного отбрасываения света: свет и декали собирались в конусообрзаные «фроксели», которые затем считывались в ходе шейдинга через определения индекса кластера из позиции пикселя. Размер каждого кластера составлял 256 пикселей и для сохранения квадратной формы логарифмически делился на 24 сегмента. Такой прием вскоре переняли многие другие разработчики, и похожие методы встречаются, например, в Detroit: Become Human и Just Cause.
Учитывая рост числа источников динамического освещения (сотни) и декалей (тысячи), процессорной кластеризации отбрасывания освещения в Doom Eternal уже не хватало, так как воксели становились слишком грубыми. В итоге для id Tech 7 разработчики придуман иной подход, и через исполняемые на различных этапах вычислительные шейдеры создали программный растеризатор. Сначала декали и свет связываются в гексаэдр (шестигранник) и передаются в вычислительный растеризатор, откуда вершины проецируются в экранное пространство. Затем второй вычислительный шейдер обрезает треугольники по границам экрана и собирает их в тайлы размером 256 на 256 пикселей. Одновременно с этим по аналогии с кластерным отбрасыванием отдельные элементы источников света и декалей записываются во фроксели, после чего следующий вычислительный шейдер проводит похожую процедуру под тайлы 32 на 32 пикселя. В каждом тайле прошедшие тест на глубину элементы помечаются в битовое поле. Последний вычислительный шейдер переводит битовые поля в список источников света, которые в итоге используются при проходе освещения. Что характерно, индексы элементов все еще записываются в трехмерные фроксели размером 256 на 256 пикселей по аналогии с кластерным подходом. В местах со значительным прерыванием глубины, для определения числа источников света в каждом тайле сравнивается минимальное значение и нового списка источников света, и старого списка кластерных источников.
Если вы не имели дела с традиционной растеризацией, столь насыщенное описание может оказаться для вас непонятным. При желании подробнее погрузиться в вопрос я рекомендую поисследовать общие принципы работы таких процессов, например на Scratchapixel есть очень хороший разбор темы.
Используемые для запросов игровой видимости так называемые «рамки видимости» тоже отбрасываются этой системой. Поскольку программная растеризация для вычислительных потоков это процесс долгий, занятость, очень вероятно, низка, и потому добавление нескольких дополнительных рамок почти не сказывается на производительности. С учетом этого, отбрасывание света, вероятно, ведется асинхронно, и таким образом итоговое влияние на производительность оказывается минимальным.
Преграждение окружающего света в экранном пространстве
Преграждение окружающего света вычисляется в половинном разрешении вполне стандартным путем: сначала 16 случайных лучей исходят из позиции каждого пикселя в полусфере, а затем при помощи буфера глубины определяются пересекающиеся с геометрией лучи. Чем больше лучей пересекает геометрии, тем больше будет преграждение. Эта техника называется «преграждение направленного света в экранном пространстве» (Screen Space Directional Occlusion), или SSDO, и подробное его описание за авторством Yuriy O`Donnell можно прочитать по ссылке. Вместо традиционного хранения значений преграждения в одноканальной текстуре, направленное преграждение хранится в трехкомпонентной текстуре, а итоговое преграждение определяется через скалярное произведение над нормалью пикселя.
Поскольку вычисление ведется в половинном разрешении, результат получается довольно шумным. Для повышения качества с буфером глубины применяется двустороннее размытие. Преграждение окружающего света обычно происходит на низких частотах, так что размытие обычно незаметно.
Непрозрачный прямой проход
В этом проходе многие элементы наконец встают на свои места. В отличие от Doom 2016, здесь все рендерится напрямую через несколько массивных мегашейдеров. Во всей игре предположительно около 500 процессорных состояний и дюжина макетов дескрипторов. Сначала рендерится оружие игрока, потом динамические объекты, и затем статические. Обратите внимание, что порядок не особо-то и важен, ведь благодаря препроходу глубины мы уже получили буфер глубины, и он может заранее исключать не соответствующие глубине пиксели.
Оружие игрока
Динамические объекты
Первый набор статических объектов
Второй набор статических объектов
У большинства движков AAA-игр графы шейдеров и особенности статических шейдеров позвляют разработчикам возможность креативно подходить к работе со всевозможными материалами и поверхностями, и каждый материал, каждая поверхность ведет к созданию своего собственного уникального шейдера. В результате мы сталкиваемся с невероятным многообразием перестановок шейдеров для всех возможных комбинаций особенностей движка. Однако id Tech сильно отличается от других AAA-проектов: он комбинирует почти все материалы и особенности во всего лишь несколько массивных мегашейдеров. Такой подход позволяет графическим процессам жестче объединять геометрию, что в свою очередь положительно сказывается на производительности. Позже мы еще это обсудим.
Несвязанные ресурсы
Стоит обратить внимание, что весь процесс формирования графики содержит в себе идею «несвязанных ресурсов». Это значит, что вместо привязки размытия, отражений, неровности текстуры перед каждым вызовом отрисовки, весь список текстур в сцене привязывается разом. Доступ к текстурам из списка осуществляется в шейдере динамически через переданные шейдеру константами индексы. Таким образом, через любой вызов отрисовки можно получить любую текстуру, что открывает путь множеству оптимизаций, об одной из которых мы сейчас и поговорим.
Динамическое слияние вызовов отрисовки
Поверх архитектуры полностью несвязанных ресурсов все данные геометрии выделяются из одного большого буфера. В этом буфере попросту хранится смещение всей геометрии.
Здесь в игру вступает самая интересная технология idTech 7: динамическое слияние вызовов отрисовки. Она полагается на архитектуру несвязанных ресурсов и обобщенную память вершин, и в итоге значительно уменьшает число вызовов отрисовки и время работы процессора. Еще до начала какого-либо рендеринга вычислительный шейдер динамически создает «непрямой» буфер индексов для эффективного слияния геометрий из невзаимосвязанных сеток в единый непрямой вызов отрисовки. Без несвязанных ресурсов добиться слияния вызовов не получилось бы, поскольку оно работает с геометриями с несовпадающими свойствами материалов. В дальнейшем воспользоваться динамическим буфером индекса можно будет вновь, как для препрохода глубины, так и для препрохода освещения.
Отражения
Чаще всего для создания отражений экранного пространства вычислительный шейдер использует алгоритм raymarching. Алгоритм испускает из пикселя луч в мировое пространство в сторону отражения, которое зависит от неровности отражающей поверхности. Точно так же дело обстояло в Doom 2016, там как часть прямого прохода записывался небольшой G-буфер. Однако в Doom Eternal уже никакого G-буфера нет, и даже отражения экранного пространства вычисляются не в вычислительном шейдере по отдельности, а сразу в прямом шейдере. Интересно узнать, насколько такое отклонение в пиксельном шейдере влияет на производительность, поскольку создается ощущение, что ценой повышенной нагрузки на регистр разработчики пытались снизить количество целей рендера и как следствие сократить нагрузку на пропускную способность памяти.
Зачастую когда в текстуре экранного пространства нет необходимой информации, у соответствующих эффектов возникают артефакты визуализации. Чаще всего это заметно при отражениях экранного пространства в случаях, когда невидимые отражаемые объекты не могут отражаться. Проблема обычно решается традиционным подходом, через статичные кубические карты отражений в качестве резерва.
Но так как мегатекстуры в Doom Eternal больше не используются, в резервных текстурах тоже нет нужды.
Частицы
Симуляция
В Doom Eternal часть процессорной симуляции частиц ложится на плечи вычислительных шейдерв, поскольку у некоторых систем частиц есть зависимости от информации экранного пространства, например буфера глубины для симуляции столкновений. Тогда как другие системы частиц могут прогоняться в кадре сразу и вычисляться асинхронно, таким симуляциям предварительно необходимы данные препрохода глубины. Что характерно, в отличие от традиционной шейдерной симуляции частиц, здесь симуляция ведется через выполнение последовательности «команд» из хранящегося в вычислительном шейдере буфера. Каждый поток шейдера прогоняет все команды, среди которых может быть по несколько kill, emit или модификаций параметра частицы. Все это похоже на записанную в шейдере виртуальную машину. Я многое не понимаю в тонкостях работы такой симуляции, но основан подход на презентации Brandon Whitney «The Destiny Particle Architecture» на Siggraph 2017. Метод в презентации очень похож на описанный мною выше и используется во множестве других игр. Например, я уверен, что похожим образом в Unreal Engine 4 работает система симуляции частиц Niagara.
Освещение
По аналогии с Doom 2016 и описанному на Siggraph 2016 методу, разрешение частиц освещения отделено от собственно разрешения экрана, что дает разработчикам управлять разрешением каждой системы частиц в зависимости от качества, размера экрана и прямого управления. Для низкочастотных эффектов освещение можно предоставить в значительно более низком разрешении почти без потери в качестве по сравнению с, например, искрами, которым требуется высокое разрешение. Освещение и доминирующее направление света хранятся в двух атласах размером 2048 на 2048 пикселей, оба они доступны для каждого прохода благодаря несвязанным ресурсам, как и любая другая текстура. В дальнейшем для рендера частиц простая геометрия отрисовывается через сэмплинг этих атласов.
Увеличенный фрагмент атласа освещения.
Небо и рассеяние
Теперь мы поговорим про объемное освещение. Его генерация состоит из четырех проходов и начинается с создания 3D LUT текстуры для атмосферы неба через raymarching сквозь само небо в сторону источника света.
С первого раза можно не понять, что именно отображает текстура на картинке, но если мы повернем ее на 90 градусов и растянем по горизонтали, все станет ясно: перед нами рассеяние атмосферы. Поскольку оно более вариативно по вертикали чем по горизонтали, то и разрешение по вертикали больше. Атмосфера представлена сферой, поэтому горизонтальное вращение обычно называется долготой, а вертикальное — широтой. Атмосферное рассеяние вычисляется полусферой и покрывает 360 градусов долготы и 180 градусов широты верхней части сферы. Для покрытия различных расстояний до наблюдателя в LUT текстуре содержится 32 сегмента глубины, и вместо перевычисления данных неба в каждом кадре процесс распределяется на 32 кадра.
Благодаря LUT текстуре, следующий проход вычисляет рассеяние света на наблюдаемый «фроксель» по аналогии с кластерным преграждением света в меньшем масштабе. Пронаблюдать несколько сегментов, от ближнего к дальнему, можно ниже.
В третьем проходе данные рассеяния для каждой клетки множатся в каждую последующую клетку в сторону обзора и пишутся в новую 3D текстуру.
В итоге поверх рендеририуемого изображения через сэмплинг только что сгенерированной 3D текстуры на основе глубины пикселей ставится объемное освещение.
До
После
Итоговое «видимое» небо рендерится на полусфере если попадает в обзор. В этой сцене небо в обзор не попало, но ниже можно взглянуть на пример рендера неба в сцене на открытом воздухе.
Прозрачность
По аналогии с Doom 2016, прозрачность рендерится прямым проходом после непрозрачной геометрии при наличии данных о рассеянии света. Текстура сцены при этом теряет в разрешении (downsamples), а для «имитации» прозрачности на основе гладкости поверхности подбирается подходящий mip-уровень. Данные о рассеянии света помогают создать изнутри поверхности визуально хорошее рассеяние.
Ниже можно видеть пример mip-цепи текстуры из сцены, где в область обзора попадает больше прозрачных поверхностей.
Для прозрачности теряют в разрешении только имеющие к ней отношение пиксели.
Пользовательский интерфейс
Обычно последним проходом в кадре становится пользовательский интерфейс. Как это обычно и происходит, интерфейс рендерится во вторичную с полным разрешением LDR (восьмибитную) цель рендера, и цвет предварительно умножается на альфа-канал. В ходе тональной компрессии интерфейс накладывается на HDR текстуру. Обычно заставить интерфейс работать с остальным HDR контентом кадра не так-то просто, но в Doom Eternal при тональной компресии интерфейс волшебным образом скалируется и выглядит естественно на фоне прочего 3D контента.
Постобработка
Первым при постобработке выступает размытость: этот двухпроходный эффект считывает данные из текстуры цвета и кастомизированного буфера скорости. Первый проход набирает четыре образца вертикальной оси, второй — четыре по горизонтальной. Затем образцы цвета смешиваются в соответствии движением пикселя. Чтобы исключить смазывание, кастомизирвоанный буфер скорости должен убедиться в отсутствии ореолов, и что оружие игрока исключено из процесса.
Далее идет целевое воздействие: эта RG (двухцветная) текстура форматом 1 на 1 содержит в себе среднее значение освещения всей сцены и вычисляется путем последовательного уменьшения разрешения цветовой текстуры и получения средней освещенности группы пикселей. Чаще всего такой прием используется для имитации привыкания человеческого глаза к резкой смене окружающей яркости. Также средняя освещенность используется при вычислении воздействия в ходе тональной компрессии.
После всего этого вычисляется Bloom. Этого эффекта маловато в нашем примере и широко визуализировать его не получится, но достаточно знать, что вычисление ведется путем получения данных о цвете выше определенного предела и последовательного уменьшения разрешения текстуры для ее размытия.
Затем тональная компрессия объединяет все эффекты. Один-единственный вычислительный шейдер делает следующее:
- Применяет искажение
- Рендерит поверх Bloom текстуру
- Вычисляет виньетирование, грязь на камере, хроматическую аберрацию, отблеск от линз и множество других эффектов
- Получает значение воздействия на основе средней освещенности
- Дает тональной компрессии распределить HDR цвета по корректным диапазонам как для LDR, так и для HDR через кастомный оператор тональной компрессии.
И наконец, сверху накладывается интерфейс.
Рендер текстуры искажений проводится еще до прохода постобработки: геометрия типа марева огня от эффектов частиц рендерится в новую цель рендера форматом с четверть исходного разрешения. В этом рендере данные искажений хранятся в красном и зеленом каналах, а преграждение в синем. Полученные данные применяются при искажении изображения на шаге тональной компрессии.
Заключение
Наш поверхностный разбор одного кадра Doom Eternal подошел к концу, хоть я и уверен, что не затронул несколько влияющих на внешний вид игры моментов. На мой взгляд, Doom Eternal это невероятный успех в техническом плане, и в будущем id Software сможет еще больше повысить планку. Команда разработчиков успешно продемонстрировала нам, как разумный подход и эффективное планирование помогли создать высококачественную игру, и я верю, что это отличный пример для подражания, равно как и обучающий материал. С нетерпением жду будущих разработок id Software.
Rip and tear, until it is done.