Pull to refresh

Как рендерит кадр движок Metal Gear Solid V: Phantom Pain

Reading time21 min
Views26K
Original author: Adrian Courrèges

Серия игр Metal Gear получила мировое признание после того, как почти два десятилетия назад Metal Gear Solid стала бестселлером на первой PlayStation.

Игра познакомила многих игроков с жанром «тактического шпионского экшена» (tactical espionage action), название которого придумал создатель франшизы Хидео Кодзима.

Но лично я впервые играл за Снейка не в этой части, а в Ghost Babel, спин-оффе для консоли GBC, менее известной, и тем не менее превосходной игре с впечатляющей глубиной.

Последняя часть франшизы, Metal Gear Solid V: The Phantom Pain, была выпущена в 2015. Благодаря движку Fox Engine, созданному Kojima Productions, она подняла всю серию на новый уровень визуального качества.

Представленный ниже анализ основан на PC-версии игры с максимальными настройками графики. Часть изложенной здесь информации уже стала достоянием публики после доклада «Photorealism Through the Eyes of a FOX» на GDC 2013.

Анализируем кадр


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

Снейк лежит на полу, стремясь слиться с окружающими его трупами (он находится в нижней части экрана, с голым плечом).

Это не самая красивая сцена, но она неплохо иллюстрирует различные эффекты, которые может создавать движок.


Прямо перед Снейком стоят двое солдат. Они смотрят на горящий силуэт в конце коридора.

Я буду называть эту загадочную личность «человеком в огне», чтобы не спойлерить сюжет игры.

Итак, давайте же посмотрим, как рендерится кадр!

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

Предварительный проход глубин


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

Ниже можно увидеть генерируемый из карты высот меш рельефа: это 16-битная текстура с плавающей точкой, содержащая значения высоты рельефа (из вида сверху). Движок разделяет карту высот на различные тайлы, и для каждого тайла выполняется вызов отрисовки с плоской сеткой 16x16 вершин. Вершинный шейдер считывает карту высот и на лету изменяет положения вершин, чтобы они соответствовали значению высоты. Рельеф растеризируется примерно за 150 вызовов отрисовки.


Высота рельефа


Карта глубин: 5%


Карта глубин: 10%


Карта глубин: 40%


Карта глубин: 100%

Генерирование G-буфера


В MGS V, как и во многих играх этого поколения, используется отложенный рендеринг. Если вы читали мой анализ GTA V (перевод на Хабре), то можете заметить похожие элементы. Итак, вместо непосредственного вычисления конечного значения освещения каждого пикселя в процессе рендеринга сцены, движок сначала сохраняет свойства каждого пикселя (типа цветов albedo, нормалей и т.д.) в нескольких целевых рендерах, называемых G-буфером, и позже комбинирует всю эту информацию.

Все нижеуказанные буферы генерируются одновременно:

Генерирование G-буфера: 25%


Albedo


Нормали


Зеркальность (specular)


Глубины

Генерирование G-буфера: 50%


Albedo


Нормали


Зеркальность


Глубины

Генерирование G-буфера: 75%


Albedo


Нормали


Зеркальность


Глубины

Генерирование G-буфера: 100%


Albedo


Нормали


Зеркальность


Глубины

Здесь у нас получается относительно лёгкий G-буфер с тремя целевыми рендерами в формате B8G8R8A8:

  • Карта Albedo: каналы RGB содержат диффузный цвет albedo мешей, то есть собственный цвет, к которому не применено никакое освещение. Альфа-канал содержит значение непрозрачности/«светопроницаемости» материала (обычно 1 для полностью непрозрачных объектов и 0 для травы или листвы).
  • Карта нормалей: вектор нормали (x, y, z) пикселя хранится в каналах RGB. В альфа-канале содержится коэффициент шероховатости, зависящей от угла обзора, для некоторых материалов.
  • Карта зеркальности (specular map):
    • Red: шероховатость (roughness)
    • Green: зеркальные отражения (specular)
    • Blue: идентификатор материала
    • Alpha: просвечиваемость для подповерхностного рассеивания (похоже, это относится только к материалам человеческой кожи и волос)
  • Карта глубин: 32-битное значение float, обозначающее глубину пикселя. Глубина перевёрнута (значение 1 имеют меши рядом с камерой), чтобы сохранить высокую точность чисел с плавающей точкой для далёких объектов и избежать Z-конфликтов. Это важно для игр с открытым миром, в которых расстояние прорисовки может быть очень большим.



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

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

Карта скоростей


Для применения на этапе постобработки эффекта размытия в движении необходимо знать скорость каждого пикселя на экране.

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

Здесь в дело вступает карта скоростей: она хранит векторы движения (скорости) каждого пикселя текущего кадра.


Карта скоростей (динамические меши)

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

Можно заметить, что в нашей сцене динамическим мешем считается только человек в огне. Даже если Снейк и солдаты технически не являются статичными мешами, движок обрабатывает их таким образом, и в нашем случае это вполне приемлемо, потому что они едва подвижны. Благодаря этому движок может избежать части вычислений: для вычисления скоростей анимированных персонажей вершинный скиннинг нужно выполнять дважды (для предыдущей и текущей позы), что может быть довольно затратно.

Красный канал используется в качестве маски (имеет значение 1 там, где отрисовывается персонаж), а сам вектор скорости записывается в синий и альфа-канал. Человек в огне не движется, поэтому его динамическая скорость равна (0, 0).

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


Карта скоростей (статичная + динамическая)

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

Преграждение окружающего света в экранном пространстве (Screen Space Ambient Occlusion)


Эффект SSAO должен добавлять немного затемнения в областях, в которых меньше окружающего освещения, например, в узких отверстиях или в складках. Интересно, что Fox Engine выполняет два отдельных прохода SSAO, используя разные алгоритмы и комбинируя результаты в последнем проходе.

SSAO на основе линейных интегралов


SSAO на основе линейных интегралов (Line Integral SSAO) — это техника вычисления ambient occlusion, которую Avalanche Software использовала в диснеевской игре Toy Story 3.

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

Здесь Fox Engine использует две пары симметричных сэмплов, то есть всего в оригинальный сэмпл включается пять значений буфера глубин на пиксель.


RGB: линейная глубина


Альфа: LISSAO

Вычисление выполняется при половинном разрешении, а данные сохраняются в текстуре RGBA8, где альфа-канал содержит действительный результат ambient occlusion, а в RGB хранится значение линейной глубины (используется кодирование Float-to-RGB, похожее на эту технику).

Результат в альфа-канале из-за малого количества сэмплов на самом деле оказывается шумным; эту карту SSAO можно сгладить позже с помощью фильтра размытия, учитывающего глубину: линейная глубина хранится в каналах RGB, то есть все необходимые данные можно считать за один раз.

Масштабируемое затемнение окружающего освещения (Scalable Ambient Obscurance)



SAO

Во втором проходе вычисления SSAO используется вариация техники Scalable Ambient Obscurance.

Она отличается от «официального» SAO в том, что не использует никакие mip-уровни глубин и не реконструирует нормали; она считывает непосредственно саму карту высот и работает на половинном разрешении, выполняя 11 считываний на пиксель (однако применяя другую стратегию выбора мест сэмплов).

В ней используется точно такая же фильтрация контраста средних тонов и двусторонний прямоугольный фильтр, что и в исходной реализации SAO.

Заметьте, что параметры SAO изменены таким образом, чтобы высокочастотные вариации (например, на ногах солдата) сильно выделялись по сравнению с версией для LISSAO.

Так же, как и в LISSAO, карта SAO размывается двумя побочными проходами с учётом глубин.

После этого compute shader комбинирует изображения LISSAO и SAO, получая конечный результат SSAO:


Готовое SSAO

Сферические карты освещённости


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


Сферические карты освещённости

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

Как же генерируются эти сферические карты? Они вычисляются из сферических гармоник. Лежащая в основе вычислений математика довольно пугающа, но, в сущности, сферические гармоники являются способом кодирования значения 360-градусного сигнала во множество коэффициентов (обычно их девять), создающих достаточно хорошую точность (SH второго порядка).

И всего из девяти этих чисел можно приблизительно воссоздать значение сигнала в любом направлении.

Если вам знакома концепция того, как преобразование Фурье может разбивать сигнал на синусоидные компоненты, то здесь ситуация довольно похожая, за исключением того, что мы разлагаем сигнал на функции на поверхности сферы.

Откуда же берутся эти коэффициенты? Они вычисляются предварительно — я предполагаю, что окружение каждой области, размеченное дизайнерами уровней, записывается в кубическую карту. Затем она преобразуется в кубическую карту освещённости и кодируется в коэффициенты сферических гармоник, которые считывает движок во время выполнения игры.

Можно задаться вопросом: почему бы не использовать для определения освещённости сами кубические карты? Это может сработать, кубические карты освещённости применять возможно, но у них есть свои недостатки. Самый главный — лишняя трата памяти на хранение шести граней кубической карты, в то время как сферические гармоники снижают затраты всего до девяти значений RGB на карту. Этим экономится много места в памяти и пропускной способности GPU, что очень важно, когда приходится иметь дело с десятками карт в сцене.

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

Диффузное освещение (Global Illumination)


Настало время применить все эти сгенерированные карты освещённости! Каждая зона, на которую влияет карта освещённости, передаётся для растеризации видеопроцессору. Обычно каждый вызов отрисовки (по одному на карту освещённости) отправляет меш в форме параллелепипеда, представляющий собой объём влияния карты в мире. Идея заключается в том, что он должен касаться всех пикселей, на которые может повлиять конкретная карта освещённости.

Диффузная карта вычисляется в HDR-текстуре половинного разрешения считыванием из карт нормалей, глубин и освещённости.


Нормали


Глубины


Освещённость


Диффузное освещение (GI): 15%


Диффузное освещение (GI): 30%


Диффузное освещение (GI): 80%


Диффузное освещение (GI): 100%

Процесс повторяется для каждой карты освещённости с аддитивным смешением новых фрагментов поверх старых.

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


Увеличение масштаба 2x (без фильтрации)


Билинейное увеличение масштаба 2x


Двунаправленное увеличение масштаба 2x

Источники освещения, не отбрасывающие тень


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


Диффузное освещение: 5%


Диффузное освещение: 30%


Диффузное освещение: 60%


Диффузное освещение: 100%

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


Отражённое освещение: 5%


Отражённое освещение: 30%


Отражённое освещение: 60%


Отражённое освещение: 100%

Карты теней


Можно догадаться, чем мы займёмся после источников без теней: источниками освещения, отбрасывающими тени!

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

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


Две карты теней

Источники освещения, отбрасывающие тень


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


Диффузное освещение 0%


Отражённое освещение 0%


Диффузное освещение 30%


Отражённое освещение 30%


Диффузное освещение 70%


Отражённое освещение 70%


Диффузное освещение 100%


Отражённое освещение 100%

Комбинирование освещения и карты тональной компрессии


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


Глубина

Albedo

Диффузное освещение

Отражённое освещение

SSAO




Комбинирование освещения

Кстати, какая же карта тональной компрессии используется в MGS V? В интервале от 0 до определённого порога (0.6) она совершенно линейна и возвращает исходное значение канала, а выше порога значения медленно стремятся к горизонтальной асимптоте.

Вот функция, применяемая к каждому каналу RGB, где $A = 0.6$, а $B = 0.45333$:

$ToneMap(x) = \begin{cases} x & \text{if $x \le A$} \\[2ex] min\left( \text{1 , } A + B - \large{\frac{\text{$B$ ²}}{x - A + B}}\right) & \text{if $x \gt A$} \end{cases}$



Итак, для перехода от линейного пространства к пространству sRGB применяется тональная компрессия, а также гамма-коррекция. В других играх это часто означает, что мы достигли последних этапов рендеринга кадра.

Но действительно ли это так? Ни в коем случае, мы только начинаем! Интересно, что Fox Engine выполняет тональную компрессию довольно рано и продолжает работу в LDR-пространтсве, в том числе выполняет проходы для прозрачных объектов, отражений, глубины резкости и т.д.

Излучающие и прозрачные объекты


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


Излучающие и прозрачные объекты: до прохода


Излучающие и прозрачные объекты: после прохода

На скриншоте выше не очень заметно, но в случае стекла также применяются отражения от окружения.

Все данные окружения получаются из кубической HDR-карты размером 256x256, которая показана ниже (также называемой зондом отражения).


Зонд отражения (Reflection Probe)

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

На GDC 2013 показывали короткий клип о том, как движок генерирует такие зонды освещения.

Отражения в экранном пространстве (Screen Space Reflections)


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


Цвет SSR


Альфа SSR

Очевидно, что нам не хватает «глобальной» информации: никакой объект за пределами экрана не может внести свой вклад в отражения. Чтобы сделать артефакты менее заметными, карта отражений использует альфа-маску для плавного затемнения непрозрачности при приближении к краям экрана.

Ценность SSR заключается в том, что они могут обеспечивать динамические отражения в реальном времени при достаточно низких затратах.

Шум карты SSR позже снижается с помощью размытия по Гауссу и смешивается поверх сцены.

Тепловые искажения, декали и частицы


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

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

После этого накладываются декали, например, жидкость на полу, и, наконец, отрисовываются частицы для рендеринга огня и дыма.


Основа


Искажение


Декали


Частицы 30%


Частицы 60%


Частицы 100%

Bloom



Проход светлоты

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

Как фильтр прохода светлоты разделяет «тёмные» и «светлые» пиксели? Мы уже находимся не в HDR-пространстве, тональная компрессия перенесла нас в LDR-пространство, в котором сложнее определить, какой цвет изначально был светлым.

Вспомните, что альфа-канал буфера сцены содержит исходную HDR-яркость каждого пикселя до тональной компрессии — эту информацию фильтр использует для определения «светлоты» пикселей.


Блики в объективе

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

Блики от объектива накладываются поверх фильтра прохода светлоты, а затем генерируется размытая версия буфера большего радиуса. Это выполняется четырьмя последовательными итерациями алгоритма размытия Масаки Кавасе.

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

Bloom

Глубина резкости


Игры серии Metal Gear известны своими длинными кинематографическими роликами, поэтому естественно, что движок стремится как можно точнее воссоздать поведение реальных камер с помощью эффекта глубины резкости (Depth of Field, DoF): резкой выглядит только определённая область, а другие области вне фокуса выглядят размытыми.

Масштаб сцены уменьшается до половинного разрешения и преобразуется обратно из пространства sRGB в линейное пространство.

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

Каждое поле обрабатывается отдельно, к нему применяется размытие. Кружок рассеяния каждого пикселя вычисляется только на основании глубины и конфигурации камеры (диафрагмы, фокусного расстояния...). Значение кружка рассеяния сообщает, насколько «вне фокуса» находится пиксель — чем больше кружок рассеяния, тем больше пиксель распространяется вокруг.

После выполнения размытия создаётся два поля:


DoF — ближнее поле

DoF — дальнее поле


Скажу пару слов об этой операции «размытия»: на самом деле это относительно затратная операция, при которой один спрайт создаётся и рендерится для каждого пикселя в сцене.

Сам спрайт содержит диск, который показан выше, и его можно заменить на любую фигуру, например, на шестиугольник, если вы предпочитаете шестиугольное боке.

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


Все спрайты отрисовываются поверх друг друга с аддитивным смешением.

Эта техника называется «рассеиванием спрайтов» (sprite scattering); она используется во многих играх, например, в сериях Lost Planet, The Witcher и в постобработке Bokeh-DoF UE4.

После генерирования размытых дальнего и ближнего полей мы просто смешиваем их поверх исходной сцены:


DoF: до


DoF: после

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

Как же движку Fox Engine удаётся ослабить влияние этого эффекта?

Ну, на самом деле я чрезмерно упростил свои объяснения, когда писал, что одно поле представлено буфером накопления половинного разрешения: есть не только один буфер, присутствуют и ещё несколько, более мелкого размера: 1/4, 1/8, 1/16 от разрешения. В зависимости от кружка рассеяния пикселя создаваемый им спрайт в результате оказывается в одном из этих буферов: обычно большие спрайты записываются в буферы низкого разрешения, чтобы снизить общее количество затронутых ими пикселей.

Чтобы реализовать это, буфер обрабатывает уровень каждого буфера один за другим, создавая 100% спрайтов и позволяя вершинному шейдеру «убивать» спрайты, не принадлежащие к текущему уровню. Вершинный шейдер по значению кружка рассеяния исходного пикселя знает, какой размер будет иметь спрайт, и если этот размер не подходит к текущему уровню, то шейдер просто выкидывает его за пределы пирамиды видимости. присваивая ему отрицательное значение глубины.

Геометрия спрайта никогда не добирается до этапа растеризации, выполняемого пиксельным шейдером.

Затем все эти буферы комбинируются для создания единого поля половинного разрешения.

Грязь на объективе и новые блики объектива



Грязь и блики 30%


Грязь и блики 60%


Грязь и блики 100%

Снейк находится в сложной ситуации и во враждебном окружении, вокруг раздаются взрывы, некоторые проекции пересекают объектив камеры и загрязняют его.

Чтобы отразить этот эффект, поверх изображения искусственно добавляется немного грязи на объективе. Грязь генерируется из спрайтов.

Затем нам нужно ещё больше бликов на объективе! Да, мы уже их добавляли, но ведь бликов мало не бывает, правда? На этот раз мы добавляем артефакты анаморфированных объективов: длинные вертикальные полосы света посередине экрана, возникающие от яркого пламени. Они тоже генерируются только из спрайтов.

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


Грязь и блики: до


Грязь и блики: после

Размытие в движении


Помните, что мы генерировали буфер скоростей в самом начале кадра? Наконец настало время применить к сцене размытие в движении (motion blur). Техника, используемая движком Fox Engine, вдохновлялась статьёй с MHBO 2012.

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

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

Цветокоррекция


Цветокоррекция выполняется для настройки конечного цвета сцены. Художники должны сбалансировать цвета, применить фильтры и т.д. Всё это выполняется операцией, берущей исходное RGB-значение пикселя и сопоставляющей его с новым RGB-значением; работа происходит в LDR-пространстве.

В некоторых случаях можно прийти к какой-то математической функции, выполняющей такое преобразование (именно это делает оператор тональной компрессии, производящий преобразование из HDR в LDR), но обычно художникам требуется более полный контроль над преобразованием цветов и никакая математическая функция с этим не справится.

В этом случае нам приходится смириться и применить способ грубого перебора: использовать таблицу поиска (look-up table, LUT) сопоставляя каждое возможное RGB-значение с другим RGB-значением.

Звучит безумно? Давайте прикинем: существует 256 x 256 x 256 возможных RGB-значений, то есть нам придётся хранить более 16 миллионов сопоставлений!

Сложно будет эффективно скармливать их пиксельному шейдеру… если мы не прибегнем к какому-нибудь трюку.

И трюк заключается в том, чтобы рассматривать пространство RGB как трёхмерный куб, заданный в трёх осях: красной, зелёной и синей.


Куб RGB
Мы берём этот куб слева, разрезаем его на 16 «срезов» и храним каждый срез в текстуре 16 x 16.

В результате мы получаем 16 срезов, которые показаны справа.

LUT

Итак, мы «дискретизировали» наш куб до 16 x 16 x 16 вокселей, то есть всего 4096 сопоставлений, что составляет всего малую долю от 16 миллионов элементов. Как же нам воссоздать промежуточные элементы? С помощью линейной интерполяции: для нужного цвета RGB мы просто смотрим на 8 его ближайших соседей в кубе, точное сопоставление которых знаем.

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

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

Можно сохранить 16 слоёв в 3D-текстуру внутри видеопроцессора, при этом код шейдера становится очень простым: просто запрашиваем поиск определённых 3D-координат, оборудование выполняет трилинейную фильтрацию восьми ближайших точек и возвращает верное значение. Быстро и просто.

Итак, у нас есть способ кодирования сопоставления цветов с помощью этой таблицы поиска на основе 16 срезов, но как же художник на самом деле создаёт такую таблицу?

Всё достаточно просто: достаточно разложить все срезы рядом друг с другом, чтобы получить подобное изображение:


LUT-текстура 256x16

Затем мы делаем скриншот игры в сцене, которой нужна цветовая коррекция. Вставляем изображение LUT в какой-нибудь угол скриншота, отдаём изображение художникам и позволяем им создать всю магию. Они используют графический редактор, вносят нужные изменения, после чего отправляют нам откорректированное изображение. Встроенная в угол картинки LUT будет отражать новое сопоставление цветов RGB.

Теперь можно просто извлечь изменённую LUT 256x16 и передать её непосредственно игровому движку.


Цветовая коррекция + Bloom: до (LUT: )


Цветовая коррекция + Bloom: после (LUT: )

На этом этапе до применения цветовой коррекции поверх сцены добавляется буфер bloom.

Антиалиасинг


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

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

Fox Engine исправляет алиасинг на рёбрах, выполняя этап постобработки FXAA: пиксельный шейдер стремится распознать и исправить искажённые рёбра на основании значений цвета соседних пикселей.

Заметьте, как «лесенка», хорошо заметная на границе перил, сглаживается в конечном результате.


FXAA: до


FXAA: после

Последние штрихи


Закончили ли мы с антиалиасингом? Почти, но не совсем! На последнем этапе художники имеют возможность нанести маски на определённые области изображения, чтобы затемнить или осветлить пиксели. Это просто серия спрайтов, отрисовываемых поверх сцены. Интересно видеть, насколько Fox Engine позволяет художникам управлять изображением даже на самом последнем этапе рендеринга.


Последние штрихи: 0%


Последние штрихи: 30%


Последние штрихи: 60%


Последние штрихи: 100%

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

Некоторые метрики этой сцены: 2331 вызов отрисовки, 623 текстур и 73 целевых рендеров.

Бонусные примечания


Посмотрим на буферы в действии


Вот короткий клип, показывающий разные буферы, о которых я рассказывал ранее (G-буфер, SSAO и т.д.).


Если вам интересно, как было записано видео: анализ этой игры по сравнению с предыдущими потребовал гораздо больше усилий. Здесь нельзя было применять ни один из графических отладчиков, потому что MGS V завершает работу, когда обнаруживает DLL-инъекторы, изменяющие определённые функции D3D. Мне пришлось закатать рукава и форкнуть старую версию ReShade, которую я расширил собственными перехватчиками. Благодаря им я мог сохранять буферы, текстуры, двоичные данные шейдеров (DXBC, содержащий все данные отладки)…

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

Истинное лицо Измаила


Измаил — это загадочный пациент, лежащий в койке рядом со Снейком, который помогает тому сбежать из больницы. Его голова замотана бинтами, скрывающими его истинную личность. Вам интересно было узнать, как же он выглядит на самом деле?

Ну так давайте же посмотрим! Вот буфер диффузного albedo сразу до и после рендеринга повязки.

Здесь нет спойлеров, но на всякий случай я спрятал второе изображение.



Измаил без повязки

Это… не совсем то лицо, которое должно бы быть

Но давайте сделаем ещё один шаг!

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

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

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

Вызовы отрисовки повязки
ID3D11DeviceContext::DrawIndexed( 0x591, 0xF009, 0x0 );
ID3D11DeviceContext::DrawIndexed( 0xB4F, 0xFA59, 0x0 );


Ниже показано видео игры с истинным лицом Измаила. Я использую горячую клавишу для переключения рендеринга повязки. Переход является прогрессивным и до полного затемнения уменьшает количество треугольников, использованных для отрисовки повязки.


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

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

Дополнительные ссылки


На этом анализ MGS V заканчивается. Я надеюсь, вы лучше стали разбираться в том, как движок Fox Engine рендерит кадр.

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

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+45
Comments10

Articles