Советы по графике и рендерингу от Survival Kids

Этим летом Unity выпустила первую игру, разработанную полностью внутри компании, обновление кооперативной семейной игры Survival Kids, в партнерстве с KONAMI. Игра была создана небольшой внутренней командой из примерно 20 человек в максимальном составе, поэтому команде пришлось находить инновационные способы оставаться в рамках проекта и графика выпуска с ограниченными ресурсами, как и любой независимой студии. В этом посте мы углубимся в то, как мы создали визуальную рамку и рендеринг игры.
Мы хотели достичь чего-то визуально интересного. Наши цели были очень художественными, но мы также хотели сделать это очень дешевым с точки зрения производительности, так как изначально не знали, с какими возможностями устройств будем работать.
Первая часть проекта заключалась просто в визуальном исследовании – у нас была художественная диорама, которую мы использовали, чтобы показать, как мы представляли искусство. Часть этого – очень стилизованная настройка освещения, включая индивидуальные тени.
Мы выбрали Universal Render Pipeline (URP), так как у него отличная репутация по производительности на широком диапазоне устройств, и относительно легко создавать любые новые функции, которые нам нужны, чтобы достичь визуальных целей игры. Отрендеренная рамка очень близка к стандартному URP в режиме Forward, так как в игре в основном только один источник света – солнце. У нас есть несколько модификаций здесь и там, таких как индивидуальные тени, амбиентная окклюзия и пара других индивидуальных функций рендеринга, но в целом это стандартный URP на экране.

Самым большим дополнением были шейдеры, чтобы поддерживать очень специфический вид художественного направления, так как нам нужно было внести изменения в то, как рассчитывалось освещение. Создание индивидуальных шейдеров не является чем-то особенно новым, однако мы написали свои собственные целевые шейдеры Shader Graph, чтобы гарантировать, что любой мог внести свой вклад. Использование AssemblyDefinitionReference позволило нам добавить специфические для проекта целевые шейдеры Shader Graph без необходимости иметь полностью индивидуальную версию URP. Это позволило нам придерживаться стандартного URP с только нашими локальными целевыми шейдерами Shader Graph, что очень хорошо сработало для нашего проекта.
Одной из наших целей было иметь динамическое освещение – мы хотели иметь возможность изменять цвет освещения, интенсивность и т.д. Это означало, что мы не могли легко запекать информацию об освещении, используя световые карты, поэтому мы бы упустили некоторые детали освещения, которые вы получили бы от запекания в отраженном освещении / глобальной иллюминации. Нам нужно было подумать о различных способах балансировки высокого визуального качества и хорошей производительности с динамическим подходом к освещению, так как это обычно более затратно. Это привело нас к тому, чтобы изначально использовать LightProbes и также полагаться больше на Ambient Occlusion (AO), чтобы помочь закрепить объекты.

Поскольку мы знали, что глобальное освещение будет очень важным для этого проекта, мы изначально реализовали собственное решение, которое обновляло LightProbes во время выполнения. Но затем, когда мы перешли на Unity 6, команде действительно хотелось перейти на Adaptive Probe Volumes (APVs), потому что визуальное качество было значительно лучше, чем у системы, которую мы собрали, при этом с сопоставимым влиянием на производительность. Когда у вас есть возможность обновиться с чего-то хорошего на что-то действительно хорошее, которое высококачественное и производительное, вы просто переключаетесь.
Океан был сильно основан на демонстрационном проекте Unity URP Boat Attack, но с более стилизованным видом. Одной из вещей, которую мы действительно хотели сделать, было создание следа от острова и других элементов в воде. Это обычно реализуется с помощью буфера глубины для определения береговой линии по расстоянию – но у нас на самом деле нет береговой линии, у нас есть остров Ухуртл.

С островом Ухуртл у вас резкое падение, и недостаточно глубины для эффекта, особенно учитывая рельеф, погруженный под воду. Лучшая идея, которую мы придумали, заключалась в использовании подписанного поля расстояний, или SDF – это в основном текстура, которая кодирует подписанное расстояние объекта, или, в нашем случае, береговой линии. Таким образом, мы можем начать след на определенном расстоянии от береговой линии, а затем использовать синусоиду и некоторые текстуры искажений, чтобы придать ему интересный вид.
В конце концов, у нас был инструмент редактора, который запекает подписанное расстояние для береговой линии на основе четырех заданных уровней воды. Затем мы сделали некоторое смешивание и интерполяцию между ними для грубой оценки того, где на самом деле была береговая линия, поскольку уровень воды в большинстве уровней меняется в зависимости от прогресса игрока. Мы полагались на эту заранее запеченную информацию SDF для нескольких различных эффектов, от регулировки высоты океанских волн до добавления пены, следа и каустики.

Для визуальных взаимодействий капсула отрисовывается сверху вокруг всего, что нам нужно было отслеживать, например, игроков, переносимые объекты, инструменты и т. д., в RenderTexture. Текстура основана в мировом пространстве с движущимся окном, когда камера игрока перемещается.
Мы генерируем смещение (красный, синий) от центра капсулы, а также информацию о высоте в мировом пространстве (зеленый). В альфа-канале мы храним значение затухания для силы. Это затем используется различными шейдерами для создания эффектов, таких как изгибание растительности, анимированные рябь на водной поверхности или немного затемнение местности для создания очень мягкого эффекта тени.

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

Мы обрабатывали объекты с дезертизацией отдельно в настраиваемом проходе, потому что нам нужно было отрисовывать их по-разному в зависимости от их состояния и того, какой игрок их видит. Они находятся в другом слое GameObject, который исключен из маски непрозрачного слоя в рендерере, поэтому они не отрисовываются автоматически, и это означает, что нам нужно отрисовывать их в настраиваемом проходе. Мы использовали MaterialPropertyBlocks для установки индивидуальных значений для объектов и применили шаблоны, чтобы отметить объекты, которые дезертизированы, чтобы мы могли размытить эти участки позже. Однако, поскольку это нарушает пакетирование SRP, нам нужно было ограничить его использование. Мы решили применять MaterialPropertyBlocks только по мере необходимости и удалять их после завершения, восстанавливая объекты в состояние, пригодное для пакетирования.
В конце концов, у нас есть целый проход, который просто занимается тем, как мы отрисовываем этот конкретный слой в буфер глубины. Затем мы применяем шаблон к буферу глубины, чтобы отметить, какие пиксели являются частью объектов, которые мы затухаем, и затем это используется позже, когда мы делаем сглаживание.
Часть нашего художественного стиля заключалась в том, чтобы иметь цветные тени с градиентом вдоль направления тени. Для достижения этого мы создали текстуру в пространстве экрана, сгенерированную из RenderFeature, которая бы выбирала карту теней в мировом пространстве, но также смотрела вперед в плоскости XZ, чтобы определить значение смешивания теней. Это похоже на фильтр PCF, используемый в мягких тенях, но в одном направлении. Это было отрендерено в уменьшенную текстуру примерно в четверть размера экрана, и затем мы смешали цвет тени между тремя цветами.

К сожалению для нас, SSAO, предоставленный с URP, не совсем подходил нашим нуждам. Хотя это мобильная реализация, для того вида, который мы хотели получить, нам нужно было установить значение радиуса довольно высоким, что заняло значительную часть нашего бюджета кадров (~4 мс). Вместо этого мы повторно использовали реализацию MSVAO из старого пакета PostProcessing Stack v2, с некоторыми незначительными изменениями, чтобы сделать его более эффективным и интегрировать наш цвет тени.

Survival Kids имеет стандартные проходы рендеринга, которые вы ожидаете в URP (Непрозрачный, Небесная коробка, Прозрачность), но у нас также есть дополнительный проход для обработки наших дотированных объектов, сразу после непрозрачного прохода. Здесь мы фактически будем рендерить нашу дотированную геометрию из-за того, что геометрия в этом слое не рендерится в непрозрачном проходе. Мы также проводим тест на равенство глубины в этом проходе, чтобы убедиться, что мы рендерим только там, где мы предварительно заполнили буфер глубины.

Для объектов, которые дотированы, нам нужно отключить окружающее затенение из-за артефактов, которые возникнут из-за того, что MSVAO рассматривает "дыры" в буфере глубины как затенение.

После рендеринга сцены мы применяем антиалиасинг. К сожалению, области, которые дотированы, будут сбивать алгоритм (SMAA), вызывая визуальные артефакты. Чтобы избежать этого, нам нужно обрабатывать эти области отдельно. Области, которые дотированы (определяются трафаретом), размываются, создавая эффект альфа-смешивания на этих областях, а затем SMAA обрабатывается в областях, которые не дотированы. Это пропускается в определенных обстоятельствах, но в итоге мы получаем очищенное финальное изображение, готовое к постобработке.
Мы сделали наши эффекты постобработки как можно дешевле, используя немного тонемаппинга, блум и цветокоррекцию.
В какой-то момент мы использовали размытие URP в постобработке, чтобы смягчить игру за пользовательским интерфейсом, но позже мы заменили это на более дешевое размытие Kawase RenderFeature. Наша система пользовательского интерфейса построена на UGUI с небольшим количеством пользовательского рендеринга для затухания.

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