Middle Earth: Shadow of Mordor была выпущена в 2014 году. Сама игра стала большим сюрпризом, и то, что она была спин-оффом сюжетной линии вселенной «Властелина кольца» оказалось довольно неожиданным. Игра обрела большой успех, и на момент написания статьи компания Monolith уже выпустила сиквел — Shadow of War. Графика игры очень красива, особенно учитывая то, что она была выпущена для разных поколений консолей, в том числе Xbox 360 и PS3. Версия для PC довольно хорошо отполирована, содержит дополнительные графические опции и пакеты текстур высокого разрешения, полностью раскрывающие потенциал игры.
В игре используется относительно новый отложенный рендерер DX11. Я воспользовался Renderdoc, чтобы глубоко изучить приёмы рендеринга игры. При работе использовались максимально возможные параметры графики (ultra) и были включены все возможные «примочки», такие как независимая от порядка прозрачность, тесселяция, окклюзия в экранном пространстве и различные motion blur.
Кадр
Вот кадр, который мы будем анализировать. Игрок находится на деревянных подмостках в регионе Удун. В Shadow of Mordor используются механики, схожие с механиками таких игр, как Assassin’s Creed, где можно взбираться по зданиям и башням, а потом наслаждаться с крыш окружающим цифровым пейзажем.
Предварительный проход глубин
Приблизительно 140 первых вызовов отрисовки выполняют быстрый предварительный проход для рендеринга самых крупных элементов рельефа и зданий в буфер глубин. Большинство объектов не попадают в этот предварительный проход, но он помогает, когда в игре очень большое количество вызовов отрисовки и можно смотреть далеко вдаль. Интересно, что персонаж, который всегда находится в центре экрана и занимает приличную долю экранного пространства, в этот проход не попадает. Как и во многих других играх с открытым миром, движок использует обратные значения z. Такая техника привязывает ближнюю плоскость к значению 1.0, а дальнюю — к значению 0.0 для повышения точности на больших расстояниях и предотвращения z-конфликтов. Подробнее о точности z-буфера можно прочитать здесь.
G-буфер
Сразу после этого начинается проход G-буфера, выполняемый примерно за 2700 вызовов отрисовки. Если вы читали мой предыдущий анализ Castlevania: Lords of Shadow 2 или изучали другие подобные статьи, то этот проход должен быть вам знаком. Свойство поверхностей записываются в набор буферов, которые затем считываются в проходах расчёта освещения для вычисления реакций поверхностей на свет. В Shadow of Mordor используется классический отложенный рендерер, но для достижения этой цели применяется относительно малое количество render targets G-буфера (3). Для сравнения: Unreal Engine в этом проходе использует 5-6 буферов. G-буфер имеет следующую схему:
Буфер нормалей
R | G | B | A |
Normal.x | Normal.y | Normal.z | ID |
Буфер нормалей хранит нормали в мировом пространстве в формате «8 бит на канал». Этого едва хватает, а иногда не хватает вовсе для описания плавно варьирующихся плоских поверхностей. Если приглядеться внимательно, это можно заметить в некоторых лужах в игре. Альфа-канал используется в качестве ID, помечающего различные типы объектов. Например, я выяснил, что 255 относится к персонажу, 128 — к анимированной части флага, а небо помечено ID 1, потому что позже эти идентификаторы используются для его фильтрации на этапе добавления (небо получает собственный радиальный bloom).
В оригинале статьи эти и многие другие изображения анимированы для большей наглядности, так что рекомендую туда заглянуть.
Буфер Albedo
R | G | B | A |
Albedo.r | Albedo.g | Albedo.b | Cavity Occlusion |
Буфер albedo хранит все три компонента albedo и маломасштабный occlusion (иногда называемый cavity occlusion), который применяется для затенения небольших деталей, которого нельзя достичь картой теней или постобработкой в экранном пространстве. В основном он используется для декоративных целей, например, прорех и складок на одежде, небольших трещин в дереве, небольших узоров на одежде Талиона и т.д.
При обработке врагов в шейдере albedo учитывает текстуру крови (интересно, что Талион никогда не получает видимых ран). Текстура крови является входными данными для этапа рендеринга одежды и тела врагов, но она задаёт не цвет крови, который является входными данными в буфере констант, а определяет множители/уровни крови для управления отображаемого количества крови. Также для масштабирования эффекта используется ориентация нормалей, позволяя контролировать направление брызг крови. Затем albedo по сути оттеняется яркостью полученных врагом ран, взятой из соответствующего места карты крови, а также модифицирует другие свойства, такие как specular, для получения убедительного эффекта крови. Мне не удалось найти часть кадра, в которой происходит рендеринг карты, но я предполагаю, что они записываются прямо в начале кадра, когда происходит воздействие меча, а затем используется здесь.
Specular-буфер
R | G | B | A |
Roughness | Specular Intensity | Fresnel | Subsurface Scattering Factor |
Specular-буфер содержит другие свойства поверхностей, которые можно ожидать увидеть в играх, такие как roughness (это не совсем roughness (шероховатость), а отмасштабированная степень specular, но её можно интерпретировать таким образом), specular intensity (яркость зеркального отражения), которая масштабирует albedo для получения верного цвета specular, reflectivity factor (коэффициент отражаемости) (обычно называемый в литературе F0, потому что он является входными данными для френелевского зеркального реагирования) и компонент subsurface scattering (подповерхностного рассеяния). Последний компонент используется для освещения просвечивающих материалов, таких как тонкая ткань, растения и кожа. Если позже мы погрузимся в изучение шейдера освещения, то обнаружим, что здесь используется вариация нормализованная модель specular по Блинну-Фонгу.
Отложенные декали
Как мы увидели выше, Shadow of Mordor довольно подробно отображает следы крови на раненных персонажах. Когда Талион взмахивает мечом, окружение тоже получает свою долю тёмной орочьей крови. Однако для окружений используется другая техника — отложенные декали. Эта техника состоит из проецирования набора плоских текстур на поверхность того, что было отрендерено ранее. Таким образом содержимое G-буфера заменяется этим новым содержимым перед выполнением прохода освещения. В случае крови достаточно просто кровавых брызг, и при рендеринге множества декалей по очереди быстро создаётся довольно мрачный ландшафт.
Последнее, что рендерится в проходе G-буфера — это небо, текстура неба очень высокого разрешения (8192×2048) в формате HDR BC6H. Мне пришлось выполнить небольшую тональную коррекцию, потому что в HDR все цвета слишком тёмные.
Tessellation
Очень интересная «фишка» игры (если она включена) — это тесселяция. Она используется для множества разных вещей, от рельефа до рендеринга персонажа (а также пропсов и объектов персонажа). Здесь тесселяция не подразделяет низкополигональный меш, а создаёт полигоны из облака точек, применяя необходимую степень подразделения, зависящую от критериев уровня детализации, например, от расстояния до камеры. Интересным примером является плащ Талиона, который передаётся в GPU как облако точек (после симуляции физики), а шейдер тесселяции воссоздаёт полигоны.
Независящая от порядка прозрачность
Одна из первых вещей, поразивших меня своей странностью — это проход обработки волос, потому что он выполняет очень сложный специальный шейдер. В опциях графики упоминается опция OIT (Order-Independent Transparency) для волос, то есть это должна быть она. Сначала она выполняет вывод в отдельный буфер и подсчитывает общее количество накладывающихся друг на друга прозрачных пикселей, одновременно сохраняя свойства в «глубинную» структуру, похожую на G-буфер. Позже другой шейдер сортирует отдельные отдельные фрагменты согласно их глубине. Похоже, что стрелы тоже рендерятся таким образом (наверно, их оперению требуется правильная сортировка). Это очень малозаметный эффект, не добавляющий особых графических отличий, но всё равно это интересная деталь. Вот простой пример: изображение выше отображает количество накладывающихся друг на друга фрагментов (чем краснее, тем больше фрагментов). Обычная прозрачность по-прежнему сортируется в ЦП и рендерится как традиционная альфа. В проход OIT попадают только отдельные сущности.
Тени Мордора
В SoM есть множество источников теней. Кроме традиционных карт теней динамических источников освещения SoM использует двухканальное ambient occlusion в экранном пространстве, микромасштабное occlusion, создаваемое почти для всех объектов в игре, и текстура окклюзии, похожая на карту высот с видом сверху.
Окклюзия в экранном пространстве
Первый проход рендерит с помощью G-буфера ambient и specular occlusion в экранном пространстве. Сам шейдер — это огромный развёрнутый цикл, сэмплирующий и полноразмерную карту глубин, и предварительно уменьшенную усреднённую карту глубин, ища соседние сэмплы в заданном паттерне. Он использует квадратную текстуру 4×4 для выбора псевдослучайных векторов в поисках источников окклюзии. Он рендерит буфер шумной окклюзии, который затем сглаживается простым размытием в два прохода. Самая интересная особенность здесь в том, что есть два разных канала окклюзии: один из них применяется как specular occlusion, а другой как diffuse occlusion. В стандартных реализациях SSAO вычисляется один канал, который применяется ко всему запечённому освещению. Здесь также считывается карта SSAO для передачи в проход направленного освещения, где и применяется.
Карты теней
Следующее событие — рендеринг карты теней. Так как действие игры в основном происходит на открытых пространствах, бОльшая часть света и теней берётся от основного направленного света. Здесь используется техника каскадных карт теней (вариацией которого является параллельные разделённые карты теней), достаточно стандартная техника наложения теней на дальних расстояниях, которая состоит из рендеринга той же сцены с одной точки зрения источника освещения для разных областей пространства. Обычно карты теней вдали от области охвата камеры или находятся на больших расстояниях, или имеют меньшее разрешение, чем предыдущие, по сути снижая разрешение в областях, в которых детали всё равно не требуются из-за того, что геометрия расположена слишком далеко. В этой сцене игра рендерит три каскада теней 4096×4096 (на самом деле в игре есть место для четырёх). Верхний каскад очень близко к Талиону, а нижний включает в себя горы и объекты очень далеко от камеры. При работе с тенями игра использует тот же трюк с обратной координатой z, что и в карте глубин.
Буфер теней
Следующий этап — создание буфера теней. Это одноканальная текстура, на основании информации окклюзии от предыдущих карт теней кодирующая коэффициент затенения в интервале [0, 1]. Для создания плавности вокруг краёв карта теней сэмплируется 4 раза с помощью состояния специального билинейного сэмплера, который получает 4 сэмпла и сравнивает их с заданным значением (это называется Percentage Close Filtering). Получение нескольких сэмплов и усреднение их результатов часто называется Percentage Closer Soft Shadows. В дополнение к считыванию карты теней также сэмплируется последний компонент specular-буфера (то есть коэффициент подповерхностного рассеяния), который умножается на «коэффициент растекания света» (light bleed factor). Похоже, что это нужно, чтобы устранить затенение от этих объектов, чтобы через них проходило чуть больше света.
Текстура направленных проекций
Ещё одним источником света и теней является текстура с видом сверху, сэмплируемая направленным источником освещения. Это оттенок цвета, добавляемый к цвету источника направленного освещения, плюс влияние глобального затенения, которое применяется к направленному освещению. Кажется, некоторые из них созданы вручную поверх автоматически сгенерированной карты освещения уровня с видом сверху. Похоже, что края теней для статической геометрии подправлены вручную (возможно, чтобы избежать конфликтов с настоящей картой теней), а некоторые части кажутся тоже немного подкрашенными вручную. Вероятно, задача этой текстуры — малозатратное добавление крупномасштабного ambient occlusion и лёгкой имитаций global illumination в дополнение к направленному освещению. На изображениях ниже показаны оттенок, окклюзия и произведение обоих факторов, что даёт нам представление о том, как выглядит окончательная цветовая маска.
Результат всех проходов освещения сохраняются в render target формата R11G11B10F. Вот, как приблизительно выглядит результат. Я выполнил тональную коррекцию результатов, чтобы сделать нагляднее влияние направленного освещения на уровень.
Все далёкие горы (не показанные на изображении выше) тоже освещаются направленным светом, но они выделены как отдельный случай, чтобы можно было лучше контролировать освещение. Некоторые отмасштабированы, но находящиеся дальше на самом деле являются плоскими текстурами (impostors) с умно созданными картами нормалей и albedo. У них есть особые источники направленного освещения, влияющие только на горы.
Статическое освещение
Shadow of Mordor использует очень требовательную к памяти реализацию статического освещения, в которой применяются очень большие объёмные текстуры. На изображении ниже показаны три статические текстуры объёма освещения, использованные для диффузного освещения части этой области. Каждая из них — это огромная сжатая текстура 512x512x128 BC6H, то есть они занимают 32 МБ на текстуру или 96 МБ в целом (мы ведь играем с максимальными настройками качества). Текстура цвета обозначает входящую в воксель интенсивность. Две другие обозначают силу или величину этой интенсивности вдоль всех шести направлений xyz и -xyz, а нормаль используется для выбора трёх компонентов (положительных или отрицательных xyz, тех, которые наиболее совпадают с нормалью). Построив этот вектор, мы берём его векторное произведение на квадрат нормали, и это становится коэффициентом масштабирования для интенсивности. Формула выглядит так:
Объёмы статического освещения (Static Light Volumes) также рендерят кубическую карту для specular lighting, которое вероятно захватывается в центре SLV. Довольно интересно то, что текстуры объёма хранят значения HDR сжатыми в BC6H, а кубические карты хранятся в формате BC3 (DXT5), который не может хранить значения с плавающей запятой. Чтобы компенсировать это ограничение, альфа-канал сохраняет яркость, которая затем масштабируется от 1-10. Это немного странное решение и для меня оно скорее выглядит как легаси-реализация. Не забывайте, что игра была выпущена и для консолей предыдущего поколения, которые не поддерживают новые форматы текстур HDR.
На кадрах ниже показаны результаты «до и после» с учётом воздействия среднего изображения. Для визуализации я выполнил тональную коррекцию.
Атмосферный туман
В Shadow of Mordor есть система погоды и времени суток, благодаря которой при прохождении игры в Мордоре светит солнце или льёт дождь. Этой системой управляет сумма компонентов, и одним из самых важных является туман. Shadow of Mordor использует довольно простую, но физически обоснованную модель атмосферного тумана, в том числе и однократное рассеяние Рэлея, а также рассеяние сферической частицей (Mie scattering).
Оно начинается с вычисления позиции камеры относительно центра Земли. Несколько тригонометрических формул позволяют определить, где в атмосфере находится камера, где находится пиксель, и какое расстояние прошёл луч в атмосфере при заданной максимальной высоте атмосферы. В нашем случае для атмосферы задана высота в 65000 метров над поверхностью планеты. С учётом этой информации применяются коэффициенты Рэлея и сферической частицы для вычисления и типов плотностей частиц тумана, и его цветов. Эти плотности затеняют уже затенённые пиксели, рассеивая свет, попадающий в камеру от затенённой поверхности, и вносит свой вклад в туман. При симуляции такого рассеяния учитывается яркость и направление солнца.
Выдержка и тональная коррекция
При вычислении выдержки используется достаточно стандартный подход: последовательное снижение разрешения буфера яркости, вычисленного из основного буфера HDR-цвета, в цепочку текстур, каждая из которых в два раза меньше предыдущей текстуры, начиная с текстуры размером в 1/3 от основного буфера кадра. При этом снижении разрешения берутся 4 сэмпла, усредняющих значения соседних пикселей, то есть после сведения всех средних значений в единый тексел окончательный результат становится средней яркостью. После того, как текстура достигнет размера 16×9 текселов, запускается compute-шейдер, суммирующий все оставшиеся текселы. Это значение сразу же считывается в проходе тональной коррекции для изменения значений яркости.
При тональной коррекции используется вариант оператор Рейнхарда, оптимизированную формулу которого можно найти здесь и здесь. В коде на hlsl это будет выглядеть следующим образом:
float3 hdrColor = tex2D(HDRTexture, uv.xy);
hdrColor *= exposureValue; // This was calculated by the compute shader in the luminance downsampling pass
float3 x = max(0.0, hdrColor - 0.004);
float3 finalColor = (x * (6.2 * x + 0.5)) / (x * (6.2 * x + 1.7) + 0.06);
Если мы построим график этой кривой, то увидим, что этот оператор отбрасывает 10% белых значений даже при входном значении 2.0, в то же время принудительно оставляя небольшую часть нижнего интервала полностью чёрной. Это создаёт ненасыщенную, тёмную картинку.
Этап альфы
Этап альфы немного необычен, потому что он рендерит объекты непосредственно в LDR-буфер. Другие игры рендерят их и в HDR-буфер, чтобы они могли участвовать в проходе выдержки. Как бы то ни было, ранее вычисленная текстура яркости ограничивается всеми освещёнными альфой объектами (в некоторых случаях, например, у испускающих свет объектов, выдержка вычисляется с помощью констант шейдера, а не поиска по текстуре), а потому выдержка применяется при отрисовке автоматически, а не выполняется в постобработке. Очень специфический случай использования в игре альфы — это переход в режим призрака (в нём поверх персонажа игрока рендерится призрак Келебримбора, выковавшего во вселенной LOTR кольца всевластья; таким образом игра показывает, что он всегда рядом, хоть и невидим). Игра передаёт в меши обоих персонажей несколько параметров, которые управляют непрозрачностью и позволяют игре частично затенить Талиона и постепенно показать Келебримора. Другие объекты в игре в режиме призрака тоже рендерят призрачные версии поверх непрозрачных объектов, например врагов и башен. Вот другая сцена с переходом в призрачный мир.
Дождь
В основном кадре, который мы исследовали, дождя нет, но погода — это такая важная часть игры, что я хотел бы его здесь упомянуть. Он генерируется и симулируется в GPU, и рендерится прямо в конце этапа альфы. Запускается compute-шейдер, выполняющий симуляцию и записывающий позиции в буфер. Эти позиции берутся другим шейдером, который с помощью instanced indirect call рендерит столько экземпляров квадов, сколько позиций было вычислено в предыдущем проходе. Вершинный шейдер имеет простой квад, который при необходимости деформируется и поворачивается к камере. Чтобы дождь не проникал сквозь поверхности, вершинный шейдер также считывает карты высот из вида сверху, что позволяет отклонять все капли ниже перекрывающей поверхности. Эта карта высот рендерится прямо в начале кадра. Тот же вершинный шейдер сообщает пиксельному шейдеру, откуда брать сэмпл из текстуры капли; если капля близко к поверхности, он выбирает область текстуры, содержащую анимацию брызг. Кроме того, капли дождя выполняют в пиксельном шейдере вычисления тумана для безупречного смешения с остальной частью сцены. Вот скриншот с той же точки зрения в дождливый день.
Когда активирован эффект дождя, specular-буфер глобально модифицируется для создания мокрых поверхностей, а волны дождя рендерятся в буфер нормалей. Анимация тайлится, поэтому используется только один кадр зацикленной анимации. Показанный ниже буфер нормалей модифицирован, чтобы показать волны, отрендеренные в буфер.
Lens Flares и Bloom
После завершения рендеринга альфы поверх рендерятся блики объектива (lens flares). Серия смещённых квадов рендерится, начиная с точки, откуда поступает направленный свет (в нашем случае — солнца). Сразу после этого выполняется проход bloom. Это довольно стандартная техника, которая состоит из серии уменьшенных в размерах и размытых текстур, содержащих пиксели, яркость которых превышает определённый порог. Используется два прохода bloom, общий с гауссовым размытием для всей сцены и особое радиальное размытие, применяемое только к небу. Радиальное размытие — это одна из операций, в которых используется специальный ID из G-буфера карт нормалей, потому что учитываются только пиксели неба. В качестве бонуса это размытие сэмплирует карту глубин и может создавать малозатратные сумеречные лучи. Так как на этом этапе мы работаем с LDR-буфером, значение порога bloom отличается от значения из HDR-ковейера (значения выше порога, обычно равного 1.0, приводят к вычислению), и это означает, что величина получаемого из него bloom немного ограничена. В любом случае, это идёт игре на пользу и вот результаты. В показанных ниже изображениях цвета mip-текстуры bloom выглядят немного странно, потому что каждый пиксель масштабируется на яркость, содержащуюся в альфа-канале. Эта яркость была вычислена ранее, на этапе тональной коррекции. В финальном композитинге bloom вычисляется как bloom.rgb · bloom.a · bloomScale.
Antialiasing + Depth of Field
Об этих двух операциях особо сказать нечего, используются стандартные для отрасли подходы. Простой прохода антиалиасинга FXAA выполняется сразу после композитинга bloom с LDR-изображением, а глубина резкости выполняется непосредственно за ним. Для глубины резкости игра рендерит две уменьшенных размытых версии финального буфера. Затем используется глубина пикселя для смешения размытого и нормального изображений, что даёт эффект расфокусировки. Ради наглядности в этом захвате я преувеличил эффект глубины резкости. В игре есть встроенный режим скриншотов, позволяющий с лёгкостью настраивать эти условия.
Motion Blur
Motion blur состоит из двух проходов. Сначала в полноэкранный буфер скоростей передаются данные из предыдущей и текущей ориентации камеры. При этом два канала текстуры заполняются скоростью в экранном пространстве. Теперь в канале r содержится величина изменения пикселя в горизонтальном направлении экрана, а в канале g — в вертикальном. Именно так получаются радиальные полосы при перемещении камеры. Персонаж рендерится заново, на этот раз заполняя и синий канал на основе его текущей и предыдущей поз, как и в случае с камерой. Синий канал используется для разметки того, должен ли рендериться пероснаж. Альфа-канал тоже заполняется постоянным значением (0.0598), но я не исследовал ни его значения, ни его целей. Разрешение буфера скоростей снижается до очень маленькой текстуры усреднением относительно широким соседством скоростей в исходной текстуре. В последнем проходе это даёт каждому пикселю примерное представление о том, каким будет радиус размытия в настоящем проходе размытия.
Затем проход размытия считывает обе текстуры скоростей, карту глубин, исходный цветовой буфер и текстуру шума. Последняя используется для сокрытия эффекта зеркального изображения, которое может возникать при таком виде размытия с большим радиусом. Затем несколько раз выполняется сэмплирование буфера изображения в направлении, указываемом буфером скоростей, усредняются цвета, что приводит к размытию изображения в направлении векторов движения. Также этот эффект масштабируется в соответсвии с частотой кадров, с которой работает игра. Для этого захвата мне пришлось ограничить игру 30fps, потому что при 60fps и выше это едва заметно.
Цветокоррекция
Финальный проход цветокоррекции выполняется с помощью «цветовых кубов». Цветовой куб — это 3D-текстура, rgb-компоненты которой привязываются к координатам xyz текстуры. Эти координаты xyz содержат цвет, которым мы должны заменить исходный цвет. В нашем случае таблица поиска (LUT) является нейтральной (т.е. координаты и цвет содержат одинаковое значение), поэтому я модифицировал ту же сцену с помощью пресетов, которые игра предоставляет в редакторе камеры.
Финальный кадр
После завершения создания основного кадра в отдельном буфере рендерится UI. Это гарантирует, что вне зависимости от выбранного для заднего буфера размера UI всегда будет рендерится чётким и красивым в нативном размере окна, в то время как игра может изменять разрешение, если это нужно для обеспечения скорости. В конце обе текстуры смешиваются вместе на основании данных альфа-канала UI, а затем рендерятся в финальный буфер кадра, который готов к отображению на экране.
Надеюсь, вам понравился мой анализ. Хочу поблагодарить Адриана Корреже за потрясающую работу, которая вдохновила меня на изучение графики, а также коллектив студии Monolith за эту поистине незабываемую игру.
Автор: PatientZero