Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Генерация скайбокса
В игре используется встроенная система неба HDRP Unity, то есть она генерирует текстуру скайбокса (кубическую карту) в каждом кадре. Это занимает около 0,65 миллисекунды, что не очень много по сравнению со всем остальным, но если игра нацелена на генерацию 60 FPS, то это будет почти 4% от общего бюджета времени на кадр.
Предварительный проход
Теперь мы переходим к самому рендерингу. В C:S2 используется отложенный рендеринг: по сути, это означает, что рендеринг выполняется за несколько фаз и с использованием множества промежуточных render target. Первая фаза — это предварительный проход, создающий попиксельную информацию о глубине, нормалях и (преположительно) о гладкости, записывая их в две текстуры.
Этот проход на удивление затратен, он занимает примерно 8,2 миллисекунды, то есть слишком много, и именно здесь начинает проявляться одна из самых больших проблем рендеринга игры. Но для начала нам нужно поговорить о тех самых зубах.
История с зубами
Очень странная, но популярная тема обсуждения производительности Cities: Skylines 2: модели персонажей имеют полностью смоделированные зубы, хотя их в буквальном смысле невозможно увидеть в игре, если только не включить фоторежим и не засунуть камеру внутрь головы персонажа. Пользователь Reddit Hexcoder0 провёл исследования при помощи NVidia Nsight Graphics™️ и опубликовал свои находки в теме в официальном подреддите (это вдохновило меня провести собственное расследование и написать эту чрезмерно длинную статью). Выяснилось, что в игре не только есть полностью смоделированные зубы, они ещё и рендерятся буквально всегда и с максимальным качеством. Ещё важнее то, что это справедливо и для всего остального, что связано с персонажами: ни у одного из мешей персонажей нет вариантов LOD. Colossal Order практически сразу публично признала это, и даже упомянула более общие проблемы с обработкой LOD. Забудьте все эти странные жалобы на симуляцию зубов жителей и прочего; это не Dwarf Fortress, никто её не рассчитывает, а если бы и рассчитывали, то это, очевидно, не требовало бы рендеринга зубов.
Кроме того, Colossal Order рассказала, что для генерации моделей персонажей использует промежуточное ПО Didimo Popul8. Если я не ошибаюсь, споры о зубах начались ещё до выпуска игры, когда кто-то заметил, что в спецификацию персонажей Didimo включены отдельные меши для таких объектов, как зубы и ресницы. Изначально я предполагал, что в игре используются стандартные меши персонажей Didimo, потому что, откровенно говоря, они выглядят очень посредственно и бездушно, но теперь я уже не так уверен. На самом деле, в мешах игры полигонов больше, чем в стандартных моделях Didimo: например, печально известная модель рта/зубов состоит из 6108 вершин, что существенно больше, чем 1060 вершин стандартного меша. Отдельный персонаж даже до добавления волос, одежды и аксессуаров состоит примерно из 56 тысяч вершин, а это много. Например, в среднем среднестатистическое жилое здание низкой плотности состоит из менее чем десяти тысяч вершин до добавления реквизита двора и других деталей.
В этом примере кадра игра рендерит 13 наборов зубов, при том, что они совершенно никак не влияют на кадр: они не меняют ни единого пикселя. Даже сами персонажи практически ничего не вносят в кадр, за исключением шума и артефактов.
Продолжение предварительного прохода, высокополигональное позорище
Вопиюще неоптимизированные моделей персонажей — не единственная причина низкой производительности игры (потому что всё никогда не бывает так просто), но они стали показателем более обширных проблем с ассетами и рендерингом игры. Игра регулярно отрисовывает слишком много объектов со слишком большим количеством полигонов, которые буквально никак не влияют на готовое изображение. И это относится не только к предварительному проходу: похоже, те же проблемы влияют на все проходы рендеринга, которые растеризируют геометрию. Я думаю, причины этого следующие:
У некоторых моделей вообще нет вариантов LOD.
Система усечения геометрии игры не очень совершенна; собственный код рендеринга реализует только усечение по пирамиде видимости, но нет никаких признаков усечения невидимой геометрии. Присутствует усечение по расстоянию, но оно не особо агрессивно, что отлично решает проблему резко «выпрыгивающих» в кадр объектов, но плохо для производительности.
Кроме моделей персонажей есть ещё несколько примеров.
Вы можете сказать, что это специально подобранные примеры, и что современное оборудование вполне справляется с обработкой подобных моделей. В общем смысле вы будете правы, но проблема в том, что все эти относительно небольшие затраты начинают суммироваться, особенно в градостроительном симуляторе, где одна неоптимизированная модель в одном кадре может рендериться несколько сотен раз. Растеризация десятков тысяч полигонов на инстанс в каждом кадре, буквально никак не влияющая ни на один пиксель — это пустая трата ресурсов, пусть даже с ней справляется оборудование. К счастью, такие проблемы довольно легко решить как созданием большего количества вариантов LOD, так и совершенствованием системы усечения. Однако для этого нужно время, и остаётся лишь посмотреть, захотят ли CO и Paradox потратить это время, особенно если для этого понадобится исправить один за одним большинство ассетов игры.
Само по себе наличие высокодетализированных моделей — не проблема, особенно если вы намереваетесь делать градостроительный симулятор нового поколения. Проблема в том, что игра не справляется с таким уровнем детализации, и что полигоны используются неэффективно и несогласованно. На каждую модель персонажа с тщательно смоделированными волосами в носу приходятся стандартные модели реквизита с на удивление низким количеством полигонов. Думаю, если бы игра работала нормально, люди бы хвалили эти высокодетализированные модели, публиковали бы хвалебные посты в соцсетях и кликбейтные видео с заголовками «OMG, разработчики продумали ВСЁ», «Не верится, что они смоделировали кабели в будке охранника!» и «CITY SKYLINES 2 — САМАЯ ДЕТАЛИЗИРОВАННАЯ ИГРА В МИРЕ?». Но мы имеем, что имеем.
А, да, мы же, кажется, говорили о рендеринге? Давайте продолжим.
Векторы движения
Игра рендерит в отдельном проходе попиксельные векторы движения, которые можно использовать для сглаживания и motion blur. Мне кажется, пока векторы движения немного поломаны, из-за чего игра на момент написания не поддерживает DLSS или FSR2. В расширенном меню настроек есть опция временно́го сглаживания, которая в определённой степени улучшает качество рендеринга, однако объекты, анимированные при помощи вершинных шейдеров (например, деревья), покрыты артефактами и призрачными следами (ghosting).
Этот проход занимает примерно 0,6 миллисекунды.
Дороги и декали
Наконец-то мы рендерим что-то различимое: дороги! А ещё лужайки и другие элементы, совпадающие с поверхностью рельефа.
Этот проход занимает примерно 1 миллисекунду.
Основной проход
Это самое «мясо» процесса отложенного рендеринга. Этот проход получает все созданные ранее промежуточные render target, кэши виртуальных текстур и жёстко прописанные текстуры, создавая множество буферов, включая буфер альбедо, нормалей, различных свойств PBR и глубин. Также он создаёт упомянутую в первой части статьи информацию о видимости виртуальных текстур. Он рендерится с половинным горизонтальным разрешением, предположительно для оптимизации. В рельефе не используется виртуальное текстурирование, поэтому он рендерится в полном разрешении и с постоянным цветом, вне зависимости от истинной текстуры рельефа.
Этот проход занимает 16,7 миллисекунды, или примерно столько же, сколько должен занимать весь кадр, если мы стремимся к 60 кадрам в секунду. Проход снова растеризует всю геометрию, поэтому к нему применимы те же причины торможения, что и в предварительном проходе. Дополнительные затраты, вероятно, объясняются количеством дополнительных выходных данных, плюс затратами на поиск в кэше виртуальных текстур и на само наложение текстур.
Окружающее затенение
Далее игра создаёт буфер окружающего затенения (ambient occlusion) при помощи векторов движения, нормалей и буфера глубин плюс копий двух последних из предыдущего кадра. Судя по отладочным именам шейдеров, используется алгоритм GTAO. Это занимает около 1,6 миллисекунды.
Каскадные карты теней
C:S2 использует каскадные карты теней и, на мой взгляд, справляется с этим не очень хорошо. В тенях есть куча артефактов и они постоянно мерцают, особенно при перемещении солнца или растительности (а они движутся постоянно). Даже когда экран не полностью покрыт артефактами, разрешение теней довольно низкое, и скачки качества между разными каскадами теней очень заметны.
В игре используются четыре каскада с разрешением 2048x2048 пикселей на каскад. В расширенном меню графических настроек есть настройка разрешения направленных карт теней, но на момент написания она не связана ни с чем в коде; ни эта отдельная настройка, ни общая настройка качества теней не меняет разрешение карты теней. Именно поэтому пресеты настроек среднего и высокого качества теней в буквальном смысле идентичны. Не знаю, недосмотр ли это, или настройку отключили в последний момент, потому что она вызывала проблемы. Пресет низкого качества отличается от среднего и высокого тем, что отключает тени, отбрасываемые рельефом.
Несмотря на низкое качество, это с большим отрывом самый медленный проход рендеринга, он занимает примерно 40 миллисекунд или почти половину от общего времени кадра. Кроме того, он сильно обгоняет все остальные проходы по количеству вызовов отрисовки: в моём тестовом кадре 4828 из 6705 вызовов отрисовки выполнялись для карт теней, то есть целых 72%. Именно поэтому производительность так повышается при отключении теней.
Причины тормознутости этого прохода почти такие же, как в случае предварительного и основного проходов: в слишком большом количестве вызовов отрисовки рендерится слишком много ненужной геометрии. Счётчики производительности Renderdoc показывают, что многие из вызовов отрисовки меняют от нуля до менее чем сотни пикселей в карте теней, к тому же здесь снова участвуют зубы. Похоже, игра считает, что каждый отдельный 3D-объект потенциально может отбрасывать тень на всех настройках качества, вне зависимости от расстояния. Это можно сильно оптимизировать, и теоретически, общие улучшения LOD и усечения существенно повлияют и на производительность карт теней. Надеюсь, после улучшения производительности CO (или моддеры) снова смогут повысить настройки качества теней и увеличить разрешение карт теней до чего-то более подходящего для 2023 года.
Давайте закончим эту часть на позитивной ноте: при исследовании кода обработки теней я выяснил, что игра вычисляет положение солнца и луны на основании текущей даты, времени и координат города. Очень милая деталь!
Отражения экранного пространства и глобальное освещение
В игре используются встроенные реализации HDRP Unity для screen space reflections (SSR) и screen space global illumination (SSGI). Не буду рассматривать их в подробностях, поскольку документация Unity и так достаточно исчерпывающая, плюс не хочу притворяться, что полностью их понимаю. В глобальном освещении (global illumination) используется ray-marching, и по умолчанию оно вычисляется в половинном разрешении экрана. Для повышения качества используются устранение шума и временно́е накопление. Было бы здорово, если бы игра, заявлявшаяся как градостроительный симулятор нового поколения, поддерживала аппаратную трассировку лучей, но особо бы я на это не рассчитывал.
Вместе на эти два эффекта тратится примерно 3 миллисекунды.
Отложенное освещение
На этом проходе всё объединяется. Большинство ранее созданных промежуточных буферов объединяются для рендеринга почти окончательного изображения. Особо про этот проход сказать нечего, кроме того, что он занимает примерно 2,1 миллисекунды.
Странный проход одежды
В игре есть небольшой проход только для одежды персонажей Didimo, в данном случае это три платья, один комбинезон и одни брюки гидрокостюма. Остальные восемь персонажей или голые, или для их одежды используются другие шейдеры. При таком масштабе этот проход не влияет практически ни на какие пиксели. К счастью, он занимает всего 0,2 миллисекунды.
Рендеринг неба
Затем из ранее сгенерированной текстуры скайбокса рендерится небо, однако в примере кадра этого не видно. Этот проход занимает около 0,3 миллисекунды.
Предварительный проход прозрачных объектов
Традиционное отложенное освещение не работает с прозрачными объектами, поэтому они рендерятся отдельно. Прозрачные объекты рендерятся за две фазы, начиная с этого предварительного прохода, который обновляет только буферы нормалей и глубин. В этом кадре не так много уникальных прозрачных объектов, так что проход занимает около 0,12 миллисекунды.
Рендеринг воды
Игра выполняет предварительную обработку в вычислительных шейдерах для подготовки к рендерингу воды, а затем создаёт множество размытых версий почти готового изображения с уменьшенным масштабом. Эти входные данные передаются основному шейдеру рендеринга воды, который рендерит поверхность воды. Это занимает около 1 миллисекунды.
Частицы, дождь и прозрачные объекты
Этот проход обрабатывает большинство прозрачных элементов, включая частицы, погодные эффекты и 3D-объекты, сделанные из стекла и других прозрачных материалов. В примере кадра не видно частиц, но игра всё равно пытается отрендерить дым из труб промышленной зоны, а также пар от отходов, льющихся из канализационной трубы. Далее рендерится дождь с использованием двадцати инстансов по 12 тысяч вершин каждый. Любопытно, что остальные прозрачные объекты рендерятся после дождя, вызывая странные эффекты при пересечении прозрачных объектов (наподобие теплиц и линий электропередач) с дождём. Всё это занимает примерно 0,56 миллисекунды.
Обработка передаваемых обратно виртуальных текстур
Полученный ранее буфер видимости виртуальных текстур обрабатывается вычислительным шейдером, создающим выходную текстуру размером в 1/16 от исходного разрешения. Для визуализации я по алгоритму ближайших соседей отмасштабировал выходные данные в восемь раз, чтобы они были более читаемыми. В конечном итоге, эту информацию игра получает обратно от GPU, чтобы решать, какие тайлы текстур загружать и выгружать. По данным Renderdoc, на это тратится очень мало времени, сильно меньше 0,1 миллисекунды.
Постобработка
В игре используется множество встроенных в Unity эффектов постобработки, в том числе временно́е сглаживание (которое, как я говорил выше, немного поломано), bloom и тональную коррекцию, плюс DOF и motion blur, если они включены. Не хотел возиться с суммированием таймингов всего этого, но примерно всё это занимает около 1-2 миллисекунд.
Контуры, текст и другой UI
Последние оставшиеся вызовы отрисовки используются для рендеринга всех элементов UI, как отрисовываемых в мире игры, так и более традиционных элементов UI наподобие нижней панели и других элементов управления. Достаточно большое количество вызовов отрисовки используется для элементов UI, создаваемых Gameface, однако в конечном итоге эти вызовы очень быстры по сравнению с остальной частью процесса рендеринга. Названия дорог рендерятся в сцене при помощи двухмерных полей расстояний со знаком (SDF). Если текст находится за зданием или другим объектом, то для смешения текста со сценой используется буфер глубин, что выглядит красиво. Этот последний проход занимает несущественное количество времени.
И теперь всё готово!
Я пытался не превращать статью в подробное исследование графики, но, наверно, потерпел поражение. Надеюсь, вы узнали что-то новое.
Итоги и выводы
Так почему же Cities: Skylines 2 так невероятно сильно нагружает GPU? Если вкратце, то игра забрасывает графическую карту слишком большим количеством геометрии, поэтому она в основном ограничена производительностью растеризации. Причиной ненужной геометрии стали как отсутствие упрощённых вариантов LOD для многих мешей игры, так и слишком простая и плохо настроенная реализация усечения. А собственную реализацию усечения в игре вместо встроенного решения Unity (которое, по крайней мере, теоретически, должно быть более совершенным) сделали потому, что Colossal Order пришлось реализовывать довольно большую часть графики самостоятельно, ведь интеграция между DOTS и HDRP в Unity по-прежнему находится в процессе разработки и, вероятно, не подходит для большинства реальных игр. К тому же решение виртуального текстурирования Unity находится в бесконечном состоянии беты, поэтому CO пришлось реализовывать собственное решение и для него, что и вызвало вопиющие проблемы.
Вот, что произошло по моему мнению (то есть это лишь моя гипотеза): Colossal Order сделала ставку на новую привлекательную технологию Unity, и в каком-то смысле это существенно себя оправдало, но в других аспектах вызвало множество неприятностей. Это нередкая ситуация в разработке ПО и я сталкивался с таким в своей повседневной работе разработчика с уклоном в веб. Компания выбрала в качестве архитектуры DOTS, чтобы избавиться от узких мест CPU, от которых страдала предыдущая игра, и для увеличения масштаба и глубины симуляции; в этом она очень преуспела. CO начала разработку игры, когда DOTS всё ещё была экспериментальной, и, вероятно, для компании оказалось неожиданностью, как много ей пришлось реализовывать самостоятельно, несмотря на то, что официально DOTS позиционировалась как готовая к продакшену. Не удивлюсь, если компания начала разработку с Entities Graphics, но потом ей пришлось создавать собственные решения для усечения, скелетной анимации, потоковой передачи текстур и так далее, когда она осознала, что официальное решение Unity с этим не справится. В конечном итоге, пришлось выпускать игру слишком рано, когда эти системы всё ещё не были отточены, вероятно, из-за финансовых аспектов и/или давления издателя. Ни одна из этих технических проблем не была чем-то неожиданным для разработчиков в день релиза, и я не верю их утверждениям о том, что изначально они нацеливались на 30 FPS — ни одна игра для PC не ставила такой цели с начала 2000-х, и качество графики этого не оправдывает.
Хотя можно жаловаться на многое в технологиях игры, это небольшое расследование, на которое я потратил большую долю своего свободного времени за последние полторы недели, заставило меня отдать должное завышенным целям игры и начать больше симпатизировать разработчикам этой технически амбициозной, но проблемной игры. Я многое узнал о внутреннем устройстве Cities: Skylines 2 и Unity HDRP, а также хорошо попрактиковался в работе с Renderdoc.
Ответы на вопросы и другие дополнения
Моя статья обрела популярность, мне стало поступать множество вопросов и комментариев. Я пытался отвечать на них, но они стали повторяться, поэтому я добавил раздел FAQ, чтобы подробнее объяснить некоторые аспекты.
Вы говорили о 7 FPS в главном меню. Почему оно работало так медленно?
Хотя именно это тормозное меню мотивировало меня исследовать производительность игры, в конечном итоге мне не удалось найти причин торможения. Точнее, не полностью. Ещё до того, как впервые запустить Renderdoc, я заметил, что при выходе из игры через главное меню мы на мгновение видим небо и воду. Renderdoc подтвердил мои подозрения: даже в главном меню всегда есть 3D-сцена с рельефом, водой и скайбоксом. Эта сцена рендерится относительно целиком, но потом полностью перекрывается пользовательским интерфейсом. Полный конвейер рендеринга используется даже для этой невидимой сцены, что объясняет мгновенное влияние графических настроек на главное меню. Если учесть, что как минимум при запуске игра устанавливает почти все настройки на максимум, в том числе те эффекты, которые разработчики не рекомендуют использовать, о вы получите причину этого «слайдшоу».
Однако одно это не объясняет такую низкую частоту кадров: скучный ответ заключается в том, что мне потом так и не удалось воссоздать подобный уровень производительности. Но дело не только во мне: почти у всех моих друзей, игравших в игру после релиза, при первом запуске возникала одна и та же проблема. При первом запуске и, вероятно, при смене ассетов игра выполняет обработку кэша виртуальных текстур, но я не проверял, использует ли она вообще для этого GPU. Поэтому пока это остаётся тайной.
Краткая информация о сцене в главном меню (в том числе о фоне и меню поверх него):
Примерно 400 вызовов отрисовки
563 тысячи входных вершин
745 тысяч растеризированных треугольников
Сколько в сцене суммарно вершин и треугольников?
Согласно счётчикам производительности Renderdoc, при рендеринге кадра задействуется 121 миллион входных вершин и примерно 36 миллионов растеризированных треугольников. Это не общее количество видимых на экране треугольников, а объём геометрии, обрабатываемый на всех проходах рендеринга. Для подобной игры это безумное количество геометрии, и неудивительно, что игра с ним не справляется. Я видел отчёты на Reddit о том, что в больших городах игра может достигать сотен миллионов вершин, а в некоторых ситуациях и до миллиарда на кадр.
Считаете ли вы, что игру стоило делать на Unreal Engine 5 или на каком-то другом движке?
Скучный ответ консультанта: это зависит от многого. В идеальном мире, где нет бюджетов и дедлайнов, Cities: Skylines 2, вероятно, следовало бы сделать на полностью собственном движке (или, по крайней мере, с полностью собственным рендерером), потому что ни один из этих крупных движков не предназначен для подобной игры. В Unreal Engine 5 есть решения многих проблем, от которых сейчас страдает C:S2; существует Nanite для LOD и Lumen с Virtual Shadow Maps для освещения и теней. Однако у UE5 есть и собственные ограничения: в нём нет ничего подобного ECS Unity для геймплейной логики и крупномасштабных симуляций (за исключением Mass, которая, вероятно, далека от готовности к продакшену), а в качестве основного языка программирования движка используется C++, который гораздо менее гибок и доступен с точки зрения моддинга, ставшего важным аспектом успеха первой части игры. Имея достаточно времени и денег, обе эти проблемы можно решить, но стоит помнить, что CO всё ещё относительно мелкая компания и ей нужно аккуратно выбирать, к чему прилагать свои усилия.
Renderdoc ненадёжен в бенчмаркинге, вам следовало использовать [вставьте название другого инструмента].
Вероятно, следовало, если бы мне удалось заставить работать альтернативы. Из полезных комментариев я узнал, что Nvidia, очевидно, в своей безграничной мудрости какое-то время назад отказалась от поддержки профилирования D3D11 в Nsight (™️), поэтому если бы я захотел правильно профилировать производительность рендеринга и получать больше информации, то мне бы следовало перейти на более старую версию этого ПО.
В конечном итоге целью был не бенчмаркинг производительности, а исследование общей картины и поиск ответа на вопрос «из-за чего же игра так тормозит?». Я считаю, что нашёл достаточно доказательств, чтобы ответить на этот вопрос, даже если отдельные тайминги в какой-то степени оказались неточными.
Не могу поверить, что в этой игре LOD используются не везде
Это не вопрос.
Поясню: в игре есть LOD некоторых/многих объектов. В целом я бы сказал, что у большинства, если не у всех зданий есть LOD, но у многих декораций и реквизита наподобие труб и реквизита дворов их нет. Более того, я даже не знаю, проблема ли в том, что у этого реквизита буквально нет LOD, или в том, что эти LOD по какой-то причине не загружаются. Возможно, разработчики создали автоматически сгенерированные LOD, но результат оказался настолько плохим, что его отключили? Понятия не имею.
Вы сказали, что в игре есть InstaLOD. Какова его роль?
Из декомпилированного кода видно, что InstaLOD включён для конвейера ассетов игры (по сути, для инструментов моддинга), и похоже, больше нигде не используется. То есть InstaLOD применяется только при импорте новых ассетов в игру.
Ненавижу JavaScript. Ненависть к JavaScript и/или к современному вебу — основная черта моей личности. В игре для UI используется JavaScript, и вероятно поэтому она такая медленная и уродливая.
Это снова не вопрос и это не совсем связано с рассматриваемой темой. Изученные мной данные показывают, что UI не стал существенным узким местом, и в ближайшее время этого не предвидится. Лично я никогда не работал с Gameface, но он, похоже, популярен в современных крупных играх, в том числе и тех, которые большинство считает хорошо оптимизированными. Важнее всего здесь знать то, что он не основан на полностью браузерном движке наподобие Electron; это собственный фреймворк пользовательского интерфейса, реализующий подмножество современных веб-технологий специально для применения в игровом UI. Это должно означать, что он занимает существенно меньше памяти и имеет более высокую производительность, чем нечто, основанное на Chromium/Blink или WebKit.