Введение
Инженеры по рендерингу подвержены двум проклятьям. Во-первых, мы теряем способность смотреть на реальность, не вспоминая постоянно, как удивительно сложно вычислять распространение света и моделировать материалы.
Во-вторых, когда мы начинаем играть в любую игру, мы не можем удержаться от того, чтобы выполнить реверс-инжиниринг её технологий рендеринга (что особенно бесит в многопользовательских играх — да хватит в меня стрелять, я сюда пришёл, чтобы просто посмотреть, как камни отбрасывают тени!).
Поэтому когда я купил Cyberpunk 2077, мне обязательно нужно было посмотреть, как он рендерит кадр. Выполнять захваты этой игры через RenderDoc очень легко, так что у меня не было отговорок.
Ниже представлены мои рассуждения о техниках рендеринга игры, наблюдения, сделанные при беглом анализе захватов и нескольких часах игрового процесса.
Эта статья ни в коем случае не является серьёзной попыткой реверс-инжиниринга.
Для этого мне не хватает ни времени, ни таланта. Кроме того, я оправдываю свою некачественную работу следующим: на самом деле так лучше.
Я думаю, лучше мечтать о том, каким может быть рендеринг (да и вообще что угодно), только слегка вдохновляясь от внешних источников (в данном случае это захваты RenderDoc), а не знать точно, что происходит внутри.
Если мы знаем, то мы знаем, больше не остаётся никакой тайны. Именно незнание заставляет нас мыслить, и иногда мы в точности угадываем то, что происходит, но в других случаях мы можем достичь чего-то лучшего — придумать нечто новое… Разве это не чудесно?
В статье по большей мере представлено изучение одного захвата. Я открывал второй, чтобы заполнить некоторые пробелы, но пока это всё.
Захваты выполнялись при высоких настройках графики, без RTX и DLSS, поскольку RenderDoc их не поддерживает (возможно, пока?). Я отключил Motion blur и другие неинтересные постэффекты и сделал так, чтобы игрок перемещался на всех захватах. Это даёт чуть больше понимания о передаче доступа к данным предыдущих кадров.
Кроме того, не использовалась никакая инсайдерская информация, так проще и веселее.
Основы
На первый взгляд, описать ядро рендеринга Cyberpunk 2077 можно очень кратко. Это классический отложенный рендерер (deferred renderer) с довольно «ванильной» схемой g-буфера. Мы не видим здесь безумного количества буферов, как, например, в Spiderman компании Suckerpunch, выпущенной на PS4. Нет здесь и сложной упаковки битов и реинтерпретации каналов.
- Нормали формата 10.10.10.2 с 2-битным альфа-каналом, зарезервированным для того, чтобы помечать волосы.
- Albedo в формате 10.10.10.2. Непонятно, что здесь делает альфа-канал, похоже для всего отрисовываемого он равен единице, но, возможно, так только в тех захватах, которые у меня есть.
- Metalness, Roughness, Translucency и Emissive в формате 8.8.8.8 (в порядке RGBA)
- Z-буфер и стенсил-буфер. Последний, похоже, используется для изоляции типов объектов/материалов. Движущиеся объекты помечены. Кожа, автомобили, растительность, волосы, дороги. Сложно понять значение каждой части, но общий смысл вам ясен…
Если взглянуть на кадр хронологически, то он начинается с множества отрисовок UI (которые я не исследовал), множества копий из буфера CPU в константы вершинных шейдеров, после чего идёт обновление карт теней (подробнее об этом позже) и, наконец, предварительный проход глубин.
Этот предварительный проход глубин частичен (не отрисовывает всю сцену) и используется только для снижения объёма перерисовки в последующем проходе g-буфера.
По сути, во всех отрисовках геометрии используется инстансинг и некая разновидность bindless-текстур. Могу предположить, что это была большая часть обновления движка из The Witcher 3 для адаптации под современное оборудование.
К сожалению, из-за bindless смотреть на захват renderDoc довольно напрягающе — при выборочной проверке я не могу найти особо много разных шейдеров в проходе g-буфера. Вероятно, это признак того, что художникам не разрешили создавать шейдеры при помощи визуальных графов?
Другие предположения: я не вижу в g-буфере никакой сортировки спереди назад, а предварительный проход глубин рендерит все типы геометрии, не только стены. Похоже, для этого нет никакой обработки (кистей, образующих BSP), а художники не размечали вручную объекты для предварительного прохода, поскольку в рендер попадают довольно «плохие» перекрывающие объекты. Предполагаю, что после усечения список объектов сортируется по шейдеру, а далее инстансы отрисовки динамически формируются в CPU.
Титры в начале игры не упоминают технологию Umbra (которая использовалась The Witcher 3), поэтому предположу, что CDPr реализовала собственную систему видимости. Её эффективность очень сложно оценить, поскольку видимость — это проблема балансировки GPU и CPU, однако, похоже, в захвате присутствует довольно много отрисовок, не вносящих вклад в изображение, хотя за это я не ручаюсь. Кроме того, похоже на то, что иногда рендеринг может отображать «скрытые» комнаты, поэтому, движок, кажется, не использует систему ячеек и порталов. Думаю, что для таких больших миров художникам непрактично выполнять большой объём ручной работы для вычисления видимости.
Наконец, я не вижу никакого усечения, выполняемого на стороне GPU, с использованием пирамид глубин и тому подобного; нет усечения на уровне треугольников или кластеров, как и нет прогнозируемых отрисовок (predicated draws), поэтому предполагаю, что всё усечение по пирамиде видимости и перекрытию выполняется на стороне CPU.
Двинемся дальше… После основного прохода g-буфера (который, похоже, всегда разделён на две части — не знаю, есть ли на то причина, или это два буфера команд, выполняемых в разных потоках) есть другие проходы для движущихся объектов (которые записывают векторы движения — буфер векторов движения сначала инициализируется со значениями движения камеры).
В этот проход включаются аватары, а шейдеры для этих текстур не используют bindless-текстуры (вероятно, они применяются только для геометрии мира), поэтому при желании гораздо проще разобраться, что здесь происходит.
Наконец, когда мы закончили с основными проходами g-буфера, операции записи глубин отключаются и начинается финальный проход для декалей. На удивление, они тоже довольно «ванильные»: большинство из них — это меш-декали.
Декали также используют в качестве входной информации стенсил глубин; сложно сказать, для чего — для чтения глубин, стенсила, или того и другого. Но поскольку большинство декалей создано на основе мешей, им должна требоваться глубина, поэтому предположим, что здесь используется стенсил…
Похоже, здесь рендерятся только треугольники с декалями при помощи специальных мешей декалей, но всё остальное удивительно просто. В конце прохода декалей мы иногда видим и проецируемые декали; я не изучал динамические декали, создаваемые оружием, но статические декали уровней просто наносятся при помощи плотно прилегающих к геометрии параллелепипедов (вероятно, созданных вручную), без каких-либо техник разметки стенсилов (которые наверно в данном случае не помогли бы), чтобы минимизировать количество пикселей с затенением.
Что касается отрисовок основного g-буфера, может оказаться, что многие декали совершенно не участвуют в создании изображения, а я не увидел никаких признаков усечения декалей, но это может зависеть от выбранных мной параметров графики.
Проход g-буфера довольно «тяжёл», а конечные результаты просто великолепны. Например, взгляните на нормали, у всего есть паттерны, а в целом детали имеют довольно высокую частоту.
Обратите также внимание на пятна в буфере нормалей. Вероятно, это артефакт упаковки нормалей, поскольку они, похоже, не соотносятся с пятнами на готовом изображении.
Освещение, часть 1: аналитические источники освещения
Очевидно, что анализ отложенного рендеринга не может закончиться на g-буфере; мы разделили затенение на две части, поэтому теперь нужно посмотреть, как реализовано освещение.
Здесь анализ становится немного рискованнее, поскольку в современную эпоху вычислительных шейдеров всё упаковывается в структуры, просматривать которые не так просто. Даже текстуры иногда сложно считать, потому что они не хранят в себе непрерывных данных, а упаковывают неизвестно что в целочисленные значения.
Тем не менее, достаточно очевидно, что после выполнения всей работы с буфером глубин/g-буфером, наступает черёд этапа суммирования всего, на котором выполняется множество операций, связанных с глубинами.
Сначала он упаковывает нормали и roughness в RGBA8 при помощи кодирования нормалей на основе таблиц поиска по принципу best-fit (эта техника создана компанией Crytek), затем создаёт mip-пирамиду min-max значений глубин.
Затем пирамида используется для создания чего-то, напоминающего объёмную (volumetric) текстуру для кластерного освещения.
То есть, исходя из увиденного, это похоже на кластерную систему отложенного освещения.
Кажется, кластеры — это фрагменты 32×32 пикселя в экранном пространстве (фрокселы, froxels) с 64 z-срезами. Однако похоже, что освещение выполняется с разрешением в 16×16 тайлов, и целиком реализовано при помощи косвенного выполнения вычислительных шейдеров.
Рискну предположить, что так получилось из-за того, что вычислительные шейдеры специализируются на материалах и свете, присутствующих в тайле, а затем выполняются в соответствующем порядке — такая схема часто используется в современных системах отложенного рендеринга (см., например, презентации Call of Duty Black Ops 3 и Uncharted 4 на эту тему).
На выходе прохода аналитического освещения получаются два буфера RGBA16; похоже, это вычисления diffuse и specular. Учитывая выбранные мной опции освещения сцены, не удивлюсь, что в ней присутствуют только прожекторные/точечные/сферические источники света и линейные/капсульные источники. Большинство источников освещения в Cyberpunk неоновые, поэтому поддержка линейных источников просто обязательна.
Также можно заметить, что большая часть освещения не подвергается затенению; кроме того, кажется, я ни разу не видел нескольких теней под одним объектом/аватаром. Я уверен, что движок не имеет ограничений в этом отношении, но все источники освещения тщательно прорабатывались авторами, аккуратно расставлявшими отбрасывающие тени источники. Не удивлюсь, если источникам освещения вручную задавали ограничивающие объёмы, чтобы избежать утечек освещения.
Освещение, часть 2: тени
Но увиденное нами выше не означает, что в Cyberpunk 2077 используются простые тени, всё совсем наоборот — определённо использовалось множество трюков, и большинство из них не так просто подвергнуть реверс-инжинирингу!
Во-первых, перед предварительным проходом глубин происходит серия отрисовок в нечто, напоминающее карту теней (shadowmap). Подозреваю, что это CSM (каскадные карты теней), но в изученном мной захвате они нигде не используются, в них только выполняется рендеринг. Это указывает на то, что система обновляет карты теней на протяжении нескольких кадров (вероятно, с учётом только статичных объектов?).
Эти эффекты, растянутые на несколько кадров, сложно передать в захвате, поэтому нельзя понять, существуют ли другие системы кэширования (например, см. тени Black Ops 3, сжатые в деревья квадрантов).
Интересным выглядит один аспект — если достаточно быстро перемещаться по уровню (например, на машине), то можно заметить, что теням нужно время, чтобы «догнать» вас, и они постепенно усиливаются довольно любопытным образом. Похоже на то, что применяется смещение глубины с точки зрения солнца, которое со временем ослабляется. Интересно!
Тени от солнца вычисляются заранее и записываются в буфер экранного пространства перед проходом вычисления освещения; думаю, это нужно для упрощения вычислительных шейдеров и более оптимальной нагрузки на GPU. Этот буфер генерируется в проходе, задействующем довольно много текстур, две из которых выглядят похожими на CSM. Одна из них — это точно CSM, в моём случае с пятью элементами в массиве текстур, где срезы с 0 по 3 являются разными каскадами, а последний срез выглядит таким же, как и срез 0, но немного с другой точки зрения.
Если кто-то возьмётся за эту работу, то ему хорошенько придётся потрудиться над реверс-инжинирингом!
Все остальные тени в сцене представлены в некой форме VSM (variance shadow maps), многократно вычисляемых инкрементно в течение времени. Я видел, что используются карты размером 512×512 и 256×256, а в моих захватах на кадр рендерились пять карт теней, но предполагаю, что это зависит от настроек. Большинство из них, похоже, используются только как render target, поэтому, опять же, может оказаться так, что для завершения их рендеринга требуется несколько кадров. Одна из них размывается (VSM) в срез массива текстур — я видел некоторые такие массивы с 10 срезами и с 20 срезами.
Наконец, в захватах есть то, что в настройках игры называется «контактными тенями» (contact shadows) — тени малой дальности в экранном пространстве, вычисленные при помощи raymarching-а. Похоже, что они вычисляются самими шейдерами вычисления освещения, что логично, ведь они знают об источниках освещения и их направлениях…
В целом, тени и просты, и сложны одновременно. Схема создания с CSM, VSM и опциональным raymarching-ом не особо удивляет, но уверен, что дьявол таится в деталях о том, как все они генерируются и подвергаются постепенному усилению. Очевидные артефакты в игре встречаются редко, за что стоит поблагодарить всю систему, которая хорошо справляется с работой даже в таком открытом мире!
Освещение, часть 3: всё остальное…
С самого первого запуска игры у меня сложилось чёткое ощущение, что бОльшая часть освещения на самом деле реализована не в виде аналического освещения. После изучения захватов оказалось, что мои подозрения были небезосновательными. В то же время, в игре нет карт освещения (lightmap) и я сомневаюсь, что в ней вообще есть предварительное запекание чего-либо. Наверно, это один из самых восхитительных аспектов рендеринга.
Во-первых, есть очень хороший проход SSAO половинного разрешения. Он вычисляется сразу после описанного выше общего прохода суммирования и использует упакованные в RGBA8 нормали и roughness, а не из g-буфера.
Похоже, что он вычисляет наклонные нормали и конусы апертур. Конкретную технику вычисления определить невозможно, но это определённо что-то типа HBAO-GTAO. Сначала глубина, нормали/roughness и векторы движения подвергаются даунсэмплингу до половинного разрешения. Затем проход вычисляет Ambient Occlusion текущего кадра, а последующие выполняют двунаправленную фильтрацию временное репроецирование. Паттерн дизеринга тоже довольно равномерный, предположу, что это градиентный шум Жорже.
Легко догадаться, что отдельные diffuse-specular, переданные из прохода освещения, используются здесь для упрощения более корректного их перекрытия с информацией конусов.
Во-вторых, нужно рассмотреть косвенное освещение. После прохода кластеризации освещения есть серия отрисовок, обновляющих массив текстур, похожих на развёрнутые сферически (или двойным параболоидом?) зонды. Всё это снова распределяется на несколько кадров, не все срезы этого массива обновляются в каждом кадре. В захватах легко заметить, что некоторые части массива зондов обновляются данными новых зондов, генерируя на лету mip-карты, предположительно, с предварительной фильтрацией GGX.
Сложнее найти источник данных зондов, но в основном захвате, который я использую, похоже, происходит повторное вычисление освещения кубической карты specular; для меня неочевидно, отличается ли этот зонд от находящихся в массиве, или он позже является источником данных массива.
Кроме того, сложно сказать, расставляются ли эти зонды по уровню вручную; если моё предположение о повторном вычислении освещения верно, то, наверно, их расположение фиксировано; вероятно, художник расставлял объёмы или плоскости, чтобы задать область действия каждого зонда и избежать утечек.
Здесь используется «стандартное» объёмное освещение, вычисленное в 3D-текстуре с временным репроецированием. Raymarching ограничивается глубиной сцены, вероятно, для экономии ресурсов, но это, в свою очередь, иногда может приводить к утечкам и артефактам репроецирования. Однако в большинстве случаев они малозаметны.
Здесь всё снова становится очень интересным. Сначала используется потрясающий проход отражений в экранном пространстве (Screen-Space Reflection), на котором опять используется буфер упакованных нормалей/roughness, а значит, поддерживаются размытые отражения; всё это выполняется в полном разрешении (по крайней мере, при моих настройках графики).
В нём применяются данные цвета предыдущего кадра до композитинга UI (для репроецирования используются векторы движения). И получается довольно много шума, даже если для дизеринга применяется текстура синего шума!
Далее идёт косвенное diffuse/ambient GI (Global Illumination). Используется g-буфер и серия объёмных текстур 64x64x64, которые сложно расшифровать. Исходя из входных и выходных данных, можно предположить, что объём центрирован относительно камеры и содержит индексы какого-то вычисленного свечения, возможно, сферических гармоник или чего-то подобного.
Освещение очень мягкое/низкочастотное и косвенные тени в этом проходе не особо заметны. Возможно, это даже динамическое GI!
Оно точно является объёмным, это даёт преимущество «однородности» освещения для всех объектов, как подвижных, так и статических, и эта согласованность видна в игре.
Наконец, выполняется общий композитинг всего: зондов specular, SSR, SSAO, diffuse GI, аналитического освещения. Этот проход снова создаёт два буфера, один из которых походит на окончательное освещение, а второй содержит только то, что похоже на части specular.
И здесь мы видим то, о чём я говорил в начале. Основная часть освещения берётся не из аналитических источников освещения! Здесь мы не видим привычных трюков со множеством «заполняющих» источников, добавленных художниками (хотя дизайн освещения определённо выполнен очень тщательно) — бОльшую часть сцены создаёт косвенное освещение. Это косвенное освещение не такое «точное», как в движках, более активно использующих запекания GI и сложное кодирование, но оно очень однородное и позволяет сохранить высокочастотные эффекты при помощи двух очень высококачественных проходов экранного пространства, проходов AO и отражений.
Проходы экранного пространства довольно шумные, что, в свою очередь, делает временное репроецирование очень важным аспектом, и это ещё одна чрезвычайно важная особенность игры. Привычный опыт говорит нам, что репроецирование не работает в играх с большим количеством прозрачных поверхностей. Научно-фантастические миры Cyberpunk определённо относятся к этой категории, но инженеры не стали прислушиваться к опыту, и всё равно смогли заставить систему работать!
Да, иногда можно заметить артефакты репроецирования, и всё затенение в движении может немного «плавать», но в целом оно качественное и целостное, а этими качествами не могут похвастаться даже многие движки, использующие карты освещения. Утечки освещения встречаются редко, силуэты обычно хорошо затенены и правильно перекрываются.
Всё остальное
В движке есть множество других эффектов, которые мы не будем рассматривать ради краткости статьи и чтобы я не сошёл с ума. Очень интересны волосы, похоже, что рендерится несколько срезов глубин и они частично вставляются в g-буфер с предварительно вычисленным освещением и странным эффектом нормалей (фальшивая анизотропия?). Ещё один важный эффект, который я не буду рассматривать — это затенение просвечивания/кожи.
Однако прежде чем закончился кадр, нужно упомянуть прозрачность — в этом аспекте определённо происходит ещё больше магии. Во-первых, существует проход, который, похоже, вычисляет график освещения (думаю, для всей прозрачности, не только для частиц).
Стекло может размывать всё, что за ним находится, и это реализовано при помощи специализированного прохода, который сначала рендерит прозрачную геометрию в буфер, накапливающий величину размытия, а затем серия вычислительных шейдеров создаёт три mip-уровня экрана, после чего всё это композитингом встраивается в сцену.
После «размытия за стеклом» снова рендерятся прозрачности вместе с частицами, используя при этом вычисленную в графике информацию освещения. Всё это выполняется в полном разрешении, по крайней мере, при моих настройках графики.
В конце выполняется всемогущее временное репроецирование. Мне бы очень хотелось посмотреть, как игра выглядит без него, разница до и после временного репроецирования просто потрясающа. Здесь происходит какая-то магия с расширенной маской, но, честно говоря, я не вижу ничего особо безумного, потрясает только качество реализации.
Вероятно, есть и другие очень сложные секретные рецепты, которые скрываются где-нибудь в шейдерах или за пределами моего понимания захватов.
Я написал «в конце», потому что дальше я не разбирался. Например, я не изучал подробности стека постэффектов, там нет ничего особо неожиданного. Его большой частью, разумеется, является Bloom, который почти добавляет ещё один слой косвенного освещения. Разумеется, он реализован качественно, стабилен и обширен.
Разумеется, там есть эффект глубины поля зрения, тональная коррекция и автоматическая выдержка… Также присутствуют все эффекты деградации изображения, которые можно ожидать от игр и которые вы скорее всего захотите отключить: зерно плёнки, lens flares, motion blur, хроматическая аберрация… Даже композитинг UI выполняется нетривиально, всё реализовано на вычислительных шейдерах, но у меня нет времени на анализ… Теперь, сняв этот груз с души, я могу, наконец, попробовать насладиться игрой! Пока!