Разбор рендеринга в Unity, часть вторая: посмотрим на Genshin Impact
Итак, Genshin Impact… В нем используется кастомный Scriptable Render Pipeline, причем и в мобильной и в десктопной версиях рендеринг видимо устроен похоже (в мобильной отключены некоторые эффекты, ограничено разрешение). Точно сказать не могу, так как десктопная версия игры защищена от анализа рендердоком и другими профайлерами — я так и не смог к ней подключится. Однако Ninja Ripper позволяет вытащить все текстуры, включая буферы кадра, и меши. Мобильная версия тоже не поддавалась анализу, однако поиском в китайском сегменте интернета удалось обнаружить небольшой трик: игра должна быть запущена в эмуляторе MuMu player (не проверял, работает ли это с другими эмуляторами), в корневую директорию которого добавлена d3dd11.dll, модифицированная для загрузки renderDoc. Такой MuMu player позволяет подключать к себе рендердок и профайлить запущенные на нем игры. Да, появляются некоторые артефакты, но сравнением с другими Unity проектами можно их исключить из анализа. Я тестировал игру только на самом максимальном качестве.
Рендеринг — Deferred, что довольно необычно как для мобильных игр, так и с точки зрения стилизации. Все же типичный случай Deferred рендеринга — когда все материалы PBR (с физически корректной моделью освещения).
Первый DrawCall — рендер квада в текстуру размером 128×128: из текущих параметров освещения и LUT текстуры рассчитывается что-то вроде цвета неба. Эта текстура позже используется при рендеринге отражений на воде и тумана.
Далее, также в текстуру 128×128, смешиванием текстур шума и маски подготавливается текстура, используемая при рендеринге неба для едва заметных облаков самого верхнего слоя.
Результаты рендеринга первого и второго вызовов. Первая текстура оранжевая, потому что кадр с закатным освещением — когда освещение дневное, она голубая
Третьим идет основной проход рендеринга обьектов в G-буфер. Это три RGBA текстуры (8 бит на канал) и Depth (24bit)Stencil буфер, причем для разных типов объектов назначения текстур несколько отличаются (достаточно нетипично для Deferred рендеринга). Для хранения информации о типе обьекта служит стенсил-буфер. Всего типов три:
трава, кроны деревьев и кусты;
анимированные персонажи;
остальная геометрия — скалы, здания, стволы деревьев, игровые предметы и т.д.
Небо, вода, облака и партиклы не пишутся в G-буфер и рендерятся позднее, о них — потом.
Первая RGBA текстура G-буфера — это нормали (каналы RGB) и glossiness для основной геометрии. Вторая текстура — диффузный цвет (каналы RGB), в альфа канал записывается интенсивность самосвечения (Emissive). RGB каналы третьей текстуры используются для хранения дополнительных нормалей травы или амбиентного освещения крон деревьев или Specular color основной геометрии. Breakdown заполнения G-буфера я сохранил в gif, для иллюстрации:
Альбедо
Нормали
Третья текстура G-буфера в финальном виде
Сначала рендерится трава, группы травинок (по 2 — 4 треугольника на травинку) по 48 — 114 треугольников (LODы, в зависимости от расстояния) инстансиируются до 32 групп на 1 DrawCall. Травинки, соответственно, не прозрачные (кроме цветочков), это хорошо — филлрейт от объектов с альфа-тестом может просто убивать производительность мобильных игр. Практически все вычисления выполняются в вертексном шейдере. Используются псевдорандомные значения для положения и изгиба — синус или косинус от позиции, текстура шума для дополнительной рандомизации изгиба, текстура положения персонажей (из предыдущего кадра) для еще большего изгиба для примятия травы. В текстуру нормалей G-буфера записывается нормаль террейна под травой, а в RGB каналы третей текстуры G-буфера — дополнительные нормали, рассчитываемые из полученного рандомного положения травинки.
Далее кусты и кроны деревьев. По 1300 — 3500 треугольников на объект. Активно используются LODы. Крупные лиственные деревья сделаны хитро — часть геометрии статичная, часть — спрайтами, нескольких типов. В итоге эти спрайты не бросаются в глаза, нет ощущения, что дерево следит за тобой. Дальние деревья — спрайтами, инстансиируются в 1 батч.
Скалы, здания, игровые обьекты (сундуки например) и террейн — это все объекты с не-стилизованным шейдингом, с картами нормалей и интенсивности спекуляра/glossiness. Везде LODы. Одинаковые объекты тоже инстансиируются.
Примеры моделей небольших деревьев, кустов и скалы
Анимированные персонажи рендерятся в 2 прохода — сначала основной цвет (и интенсивность Emissive в альфу буфера цвета) и нормали (третья карта G-буфера не используется). Потом — рендерится слегка смещенная по нормали («раздутая») геометрия для создания ink-обводки. В основных персонажах по 16 — 20 тысяч треугольников. В драконе — 42 тысячи. Текстуры человеческих персонажей весьма необычно объединены в атласы (и, соответственно, DrawCalls): 1) все тело и почти вся одежда , 2) оставшаяся часть одежды 3) нижняя часть лица и лоб, 3) верхняя часть лица, 4) волосы и глаза (!). Маленькая деталь: блики в глазах сделаны геометрией по 18 треугольников.
Модель главной героини
После того, как все объекты отрендерены в G-буфер, происходит копирование буфера глубины в текстуру R32 разрешением в 2 раза меньшим разрешения экрана (во всяком случае так в мобильной версии), последовательное копирование ее в текстуры 512×256, 256×128 и тд, и их обьединение в одну текстуру 512×512. Потом из этой текстуры сложным шейдером создается некая индексная текстура, которая в дальнейшем нигде не используется.
Далее следует проход рендеринга карты теней — похоже что обычные тени Unity, четыре каскада (в десктопной версии на макс качестве — 6). Во внутренних помещениях вместо теней от солнца рендерятся карты теней от точечных (Omni) источников света. Это кубмапы, и вид у них странноватый.
Вот такой кубмап. Карту каскадных теней приводить не буду — она совершенно обычная
Я даже сначала подумал что это какой-то хитрый способ расчёта Global Illumination. Но все оказалось проще — карта теней вместо 32-битного формата пакуется в четыре 8-битных канала (R8G8B8A8_UNORM). Зачем оно так сделано — не знаю. Лет 20 назад, когда поддержка HDR текстур еще не была общепринятой, такой способ упаковки теней использовался некоторыми движками. Какой сейчас от него выигрыш, сказать не могу. Это особенность именно Genshin, в URP и в Built-in рендерерах Unity кубмапы теней от Omni источников сохраняются в обычный R16 формат.
Следующим проходом рендерим вид сверху на вот такие расплющенные конусы, в тех местах, где находятся персонажи. Получаемая текстура применяется для приминания травы (в направлении от центра конуса) во время рендеринга травы в следующем кадре.
Текстура для приминания травы
Следующий проход — рендеринг собственно теней, по карте глубины и карте теней. Используется эффект Sun Shafts — получается такое «затекание» света на объекты.
Готовая текстура тени
Потом рендеринг screen-space reflections (вроде бы, применяются только на водных поверхностях), в отдельный буфер. Используются копия буфера предыдущего кадра и карта глубины.
Затем следует рендеринг полупрозрачных объектов в отдельный буфер — в основном это эффекты. Часть из них сделана партиклами, часть — модели.
Вихрь над городом — не партиклы, а моделька с анимированной текстурой. И лучи volume light — тоже. И авроры в подземельях — все вполне олдскульно на вид
Наконец, собственно deferred рендеринг основной картинки. Он происходит в три прохода, в соответствии со значениями в стенсил-буфере. Используются текстуры G-буфера и готовая текстура теней. Результат записывается в 2 буфера — один из них основной буфер кадра, другой — будет использован в следующем кадре для генерации Screen-Space Reflections. Первый проход — основная геометрия, там сложный длинный шейдер, используется по сути PBR рендеринг.
Второй проход — персонажи, тут шейдер Cell-шейдинга, он сильно проще. К персонажу тени не применяются — он целиком уменьшает свою яркость при попадании в тень, видимо значение карты теней проверяется в контроллере персонажа после чего значение яркости выставляется в рендер.
Третий проход — растительность — снова большой шейдер, подробно не смотрел, что там и как.
Следующий этап — применение spot и omni источников света, если где они есть. Эти источники рендерятся как геометрия — сферы для omni и пирамидки для spot, что типично для deferred рендеринга. В одной из сцен подземелий я насчитал 72 источника света (кроме основного directional света), по 3 вызова для каждого — также, с тремя стенсил-масками: для применения на основной геометрии, растительности и персонажах, но для персонажей используется «пустой» шейдер, который ничего не пишет в буфер, зачем он вообще вызывается — не знаю.
Breakdown применения источников света (кадр из подземелий)
Далее рендерится небо (сфера с процедурной моделью освещения неба, ~3k треугольников), солнце (спрайт с процедурно нарисованным кружочком), облака (несколько слоев с разными параметрами, сами облака сделаны спрайтами). Следующие шаги: применяется туман, по карте глубины с использованием рендертекстуры, отрендеренной самой первой; рендерятся водные поверхности (используется уже отрендеренный буфер SSR); применяется тоже уже отрендеренный буфер с полупрозрачными объектами; рендерится Lens Flare (спрайтами, стандартно).
Теперь рендерим анимированных персонажей и движущиеся объекты в буфер для создания motion blur эффекта. На движущейся растительности motion blur нет.
Потом идут 3 прохода применения motion blur эффекта. Далее — эффект Bloom. Тут вроде бы все как обычно: текстура уменьшенного разрешения размывается горизонтально, вертикально, потом еще уменьшается и так 3 раза. Запись уменьшенных текстур последовательно происходит в один и тот же рендертаргет — таким образом количество переключений рендертаргетов уменьшается, что хорошо, особенно для мобильных (в Bloom’е, встроенном в Unity, сделано не так, там много рендертаргетов). К финальной картинке в один пасс применяются Bloom и Color Correction.
Ну и наконец рендеринг UI: батчами, но все равно получается около 50 DrawCalls.
Финальная картинка
В типичном кадре всего, в сумме всех проходов (включая тени), рендерится 500 — 850 тысяч треугольников, что немало для мобильной игры. Персонажи рендерятся 3 раза (основной G-буфер, обводка, motion blur), плюс рендеринг в карту теней. Вызовов DrawCalls немного, все что можно инстансиируется, все красиво и чисто, но и особой заботы об отключении невидимых в данный момент стадий нет — например в подземельях и интерьерах рендерится текстура для приминания травы. В интерьерах, вместо динамических теней (те самые, запакованные в RGBA кубмапы) можно было бы использовать лайтмапы –, но лайтмапы в игре вообще не применяются. Шейдеры не производят впечатление оптимизированных, написанных руками. В мобильной версии для оптимизации отключены некоторые эффекты и ограничено разрешение рендеринга — перед рендерингом GUI фреймбуфер апскейлится до разрешения экрана (это часто применяется в мобильной графике — из-за высокой плотности точек на экранах телефонов такой апскейл не сильно заметно портит качество).
Но в целом Genshin Impact производит позитивное впечатление «как надо делать» О том, как делать не надо, мы поговорим в одной из следующих статей, благо примеры мобильных игр, поражающих воображение скрытыми фейлами рендеринга, тоже имеются в наличии