Взгляд изнутри...
В процессе разработки Lost in Random наши художники часто публиковали концепт-арты и другие изображения, позволявшие взглянуть за кулисы создания игры. Кто-то из нас задался вопросом, можно ли сделать что-то подобное и для кода. Хотя в то время ситуация была довольно суматошной, мне понравилась эта идея, ведь я сам люблю изучать внутреннее устройство любимых игр. Я захотел написать техническую статью о том, над чем работал в то время. Год спустя игра была завершена и у меня появилось немного времени на написание краткой статьи об особенно интересном визуальном эффекте и о том, как он помогает в создании внешнего вида нашей игры.
Скриншот из Lost in Random
… на внешний вид Random
Lost in Random обладает собственным визуальным стилем. При его создании мы приложили много сил и умений. Когда я пришёл в Zoink на должность программиста графики, визуальная составляющая и окружения игры уже проделали довольно долгий путь. Моя задача заключалась в том, чтобы взять имеющееся и помочь превратить проект в игру, способную работать на целевых платформах. Быстрая разработка с учётом первоначального видения игры была довольно сложной задачей.
В этой статье я постараюсь рассказать о небольшой части, формирующей эстетику игры, и о том, какую работу нам пришлось проделать для её реализации. Хотя на уникальный внешний вид игры влияет множество аспектов, я хотел бы поговорить об использовании цветного тумана. Может показаться, что это простой эффект, но он оказывает огромное воздействие на общий вид. Для сравнения посмотрите на эти два скриншота, сделанные в движке со включенным и отключенным туманом.
Один и тот же вид с туманом и без него.
Изначально в проекте использовался High Definition Render Pipeline (HDRP) движка Unity и его встроенный объёмный туман (Volumetric Fog). Они выглядят абсолютно потрясающе, но эти технологии оказались слишком вычислительно затратными для нескольких целевых платформ. Как и ожидалось, самая серьёзная проблема возникла с Nintendo Switch. Switch — замечательная платформа, как геймеру она нравится мне больше всех. К сожалению, оборудование её не такое уж мощное.
Потратив пару месяцев на тестирование различных опций и исследование внутренностей рендереров Unity, мы решили отказаться от HDRP в пользу более простого, но и более производительного Universal Render Pipeline (URP) для всех платформ. Хоть нас и волновало снижение реалистичности освещения, руководству проекта понравился более мультяшный стиль, который создавался упрощённым освещением. Разумеется, это означало, что теперь мы не могли пользоваться туманом HDRP. Поначалу я пробовал портировать этот туман в URP, но это оказалось хлопотной задачей, не оправдывающей усилий.
В конечном итоге мы использовали две различные реализации тумана. На большинстве платформ использовался объёмный туман. Вторым решением стала система в экранном пространстве, спроектированная так, чтобы быть достаточно быстрой на Switch. Любопытно, что мы использовали это решение в экранном пространстве в сценах со снами на всех платформах.
Объёмный туман
Объёмный туман, использовавшийся на мощных платформах, был модифицированной версией Aura 2 — ассета, разработанного Oniric Studio. С технической точки зрения этот ассет очень похож на встроенный в HDRP, но нашим художникам оказалось проще его настраивать нужным им образом. Достаточно было только конвертировать его под URP и добавить функции, которых нам не хватало.
Чтобы конвертировать Aura 2 под URP, мы создали новый
ScriptableRendererFeature
, который вызывает код Aura и заносит результаты в буфер кадров. Обратные вызовы Camera.onPreCull
и Camera.onPreRender
просто преобразуются в обратные вызовы RenderPipelineManager.beginCameraRendering
. Также нам пришлось внести изменения в шейдеры, чтобы прозрачные объекты красивее взаимодействовали с туманом, потому что они не выполняют запись в буфер глубин.Самым масштабным изменением в Aura 2 стало добавление угасания прямого освещения, присутствующего в тумане HDRP. Оно приглушает солнечный свет, когда игрок углубляется в туман. На скриншотах ниже показан один и тот же вид со включенной и выключенной функцией.
Тума с затуханием прямого освещения и без него.
Этот эффект стал одним из множества изменений, внесённых нами в шейдер Lit. Мы скопировали его из угасания по высоте в LightEvaluation.hlsl конвейера HDRP, которое я перенёс в Lighting.hlsl конвейера URP. Необходимо было только добавить код для нашего менеджера объёмов тумана, чтобы он собирал и усреднял параметры угасания от всех текущих активных объёмов тумана.
Туман в экранном пространстве
Для сохранения единого визуального стиля на всех платформах нам нужно было, чтобы туман выглядел максимально похожим на задуманное, но одновременно оставался достаточно быстрым при работе на Switch. Протестировав множество готовых решений, я написал собственный экспоненциальный туман по высоте специально для Switch.
Это система в экранном пространстве, использующая координаты высоты для определения цвета тумана и экспоненциального угасания для его плотности. Пусть у нас есть плотность тумана
density
и расстояние видимости depth
, тогда цвет тумана умножается на 1.0 - exp(-depth * density)
, что даёт нам коэффициент, на расстоянии 0.0 равный 0.0 и увеличивающийся до 1.0 с увеличением расстояния видимости до бесконечности.Туман состоит из трёх слоёв: нижнего, верхнего и слоя скайбокса. Каждый слой имеет собственную высоту, цвет и плотность. Всё, что находится дальше заданного расстояния от камеры и выше высоты скайбокса, использует описанную выше экспоненциальную функцию, применяемую к цвету слоя скайбока. Для всего остального мы вычисляем часть глубины обзора, пересекающуюся между нижней и верхней высоты, и используем её для вычисления видимой высоты в тумане. Затем мы используем нечто напоминающее
smoothstep(LowerHeight, UpperHeight, viewHeight)
для вычисления обратной величины линейной интерполяции между нижней и верхней высотами, а затем применяем получившееся значение как параметр линейной интерполяции между нижними и верхними цветами. Так туман становится градиентом между этими двумя значениями. Существует два способа вычисления высоты обзора. Стандартный просто берёт высоту, которая достигнута следованием по лучу длины
depth
из позиции камеры и через центр пикселя. Это означает, что цветовой градиент между нижней и верхней высотами чётко заметен на небе при большинстве углов обзора. Ещё один способ — вычисление средней высоты, на которую переместился луч. Это придаёт более мягкий внешний вид. Я сложил эти два способа, потому что не был точно уверен, какой конкретно способ имел в виду наш художник, когда объяснял мне требуемый результат. Я подумал, что надо позволить ему протестировать оба способа и убрать «лишний». После тщательного тестирования и сравнения мне сообщили, что нужны оба способа. На самом деле, в некоторых сценах для более динамичного внешнего вида используются объёмы тумана, сочетающие в себе оба способа.Три режима
Одного экземпляра этого решения на основе высот было недостаточно для создания той версии объёмного тумана, который нам был нужен. Чтобы усовершенствовать результат, мы скомбинировали в каждой сцене несколько таких туманов. Наш туман поддерживает три различных режима, определяющих, когда и где они будут рендериться.
-
Global
— рендерится, пока пользователь не находится полностью в объёме с режимомRenderWhileInside
. Используется для конфигурирования общего внешнего вида сцены. -
RenderWhileInside
— невидимый снаружи, рендерится только тогда, когда позиция камеры находится в его границах. Используется для изменения общего внешнего вида в маленьких областях. -
PhysicalObject
— рендерится как параллелепипед и в основном используется для образования слоя тумана, стелящегося по земле.
Обычно в сцене используется один-два объёма тумана
Global
плюс около десятка объёмов, использующих режим RenderWhileInside
или PhysicalObject
.Наш туман в экранном пространстве также поддерживает упомянутое выше затухание освещения. Это означает, что менеджер подает в шейдер Lit те же данные, что и наша модифицированная версия Aura 2. На самом деле, он не влияет на сам шейдер тумана. Там есть и другие тонкости (например, способ смешения солнечного света с цветом тумана), но в целом это довольно простой шейдер, рассчитанный на создание хорошей картинки без необходимости любой иной информации, кроме как позиции камеры и глубины рендеринга.
Клочья тумана
Моя любимая «фишка» тумана стала счастливой случайностью, используемой только в версии для экранного пространства. Вдоль верхней части объёмов тумана, растянувшихся над землёй, есть анимированная текстура с проплывающими клочьями тумана. При первой реализации этих объёмов я добавил простой шум, чтобы край тумана выглядел слегка нечётким, а не как параллелепипед с ровными краями. Также я сделал его анимированным, чтобы паттерны шума были чуть менее заметными. Позже по просьбе нашего потрясающего художника по окружениям Лео Бриниелссона я заменил функцию шума на текстуру. Я решил, что он просто хочет подобрать собственную функцию шума, не заставляя меня вносить изменения в код. Однако он использовал её, чтобы создать вот это:
Клочья тумана в версии Lost in Random для Switch.
Да здравствует Random!
Надеюсь, статья была для вас интересной и вы оценили магию, происходящую за кулисами. Разумеется, это лишь краткий обзор того, как работает туман. Можно сказать гораздо больше, особенно о производительности. Однако тогда статья станет гораздо длиннее и чуть сложнее, чем я стремился её сделать.
Об авторе
Меня зовут Дэниел «Agentlien» Квик, я разработчик ПО, страстно влюблённый в игры. Сейчас я работаю программистом графики в Thunderful Development над Lost in Random.
Здесь можно найти некоторые из моих работ.