Как придать огню жизнь: Моделирование течения жидкости в реальном времени в Ignitement

В сегодняшнем гостевом посте независимый разработчик Sørb рассказывает о технических тонкостях создания впечатляющих визуальных эффектов огня и лавы в своей готовящейся к выпуску рогалайт-игре Ignitement.
Первое, что бросается в глаза при взгляде на «Ignitement», — это визуальные эффекты. В них сразу же чувствуется нечто особенное, особенно в этом огне. Это не просто выглядит как анимация — это выглядит живым, реагирует на действия и органично вписывается в окружающий мир.
Так что же происходит «под капотом»?
Почему вам стоит обратить внимание на визуальные эффекты, связанные с огнем
Огонь и, в целом, эффекты, имитирующие жидкость, как известно, очень сложно правильно воссоздать в играх. Традиционные системы частиц могут выглядеть великолепно, но им часто не хватает true взаимодействия с окружающим миром. С другой стороны, полноценные 3D-симуляции, как правило, слишком дорогостоящи для игрового процесса в режиме реального времени.

В нескольких играх симуляция жидкости стала одной из основных игровых механик. В игре «Little Inferno»от Tomorrow Corporation (вверху) поведение огня занимает центральное место в игровом процессе, тогда как в игре «Plasma Pong» от Стива Мейсона (внизу) весь игровой процесс построен на реактивных, плавных движениях. Эти примеры демонстрируют, насколько эффективны могут быть системы, основанные на использовании жидкостей, когда они напрямую влияют на игровой процесс.

Основная идея
Вместо использования систем частиц в Ignitement применяется полностью динамическое моделирование жидкости в режиме реального времени.
На первый взгляд это может показаться дорого.
«Но разве это не приведет к перегреву PC и снижению производительности?»
По крайней мере, не аппаратное обеспечение.
Симуляция полностью выполнена в 2D и работает через Graphics.Blit, обновляя небольшой набор текстур (в основном размером 1024×1024 и 512×512). На практике это делает его стоимость сопоставимой со стоимостью нескольких эффектов Post Processing.
Еще одним сознательным решением было остаться при использовании фрагментных шейдеров вместо вычислительных шейдеров. Это позволяет системе использовать встроенные функции фильтрации и интерполяции текстур, сохраняя при этом высокую совместимость даже на устаревшем оборудовании или потенциальных консольных платформах.
Чтобы было проще следить за ходом изложения, мы можем разделить систему на три части:
- Симуляция
- Рендеринг
- Освещение
Разбор моделирования течения жидкости
По сути, эта система представляет собой стандартное моделирование жидкости, полностью реализованное с помощью операций Graphics.Blit. Моделирование осуществляется с использованием нескольких текстур, каждая из которых отражает отдельное физическое свойство:
- Плотность (1024×1024, RGBA, половина) — это ваш дым, всё то, что придаёт воздуху густоту и делает его видимым.
- Скорость (512×512, RG half) Этот параметр определяет, как объекты движутся. Если что-то течет, дрейфует или кружится, вот почему.
- Температура (1024×1024, одноканальная половина) Определяет степень нагрева различных областей.
- Реакция (1024×1024, RGBA, половина) Именно здесь происходит само горение: его интенсивность, распространение и поведение.
С учетом этой структуры алгоритм расчета динамики жидкости можно описать с помощью следующего псевдокода:
void Simulate(float dt)
{
//feed data to simulation
RenderEmittersAndObstaclesToTexture();
ApplyDiffusion(dt); //fluid spreading
ApplyAdvection(dt); //moving data along the velocity field
ApplyExtinguishmentImpulse(dt); //fire producing smoke (reaction>density)
ComputeVorticityConfinement(dt); //increase swirlyness of fluid
//calculate divergence free velocity field
ComputeDivergence();
ComputePressure();
ComputeProjection();
AdvectParticles(dt); //move particles along velocity field
}
Данные о реакции время от времени даже ускользают из графического процессора. Он подвергается понижающему дискретизированию и считывается обратно на процессоре для управления игровыми эффектами, такими как нанесение урона. Так что да, огонь не только выглядит опасным, он действительно опасен!
Сама область моделирования не является фиксированной в мире. Вместо этого она следует за камерой и перемещается вместе с игроком. Это создает иллюзию бесконечной, непрерывной симуляции, хотя на самом деле в любой момент времени рассчитывается лишь относительно небольшая область. Дым повсюду, а денег не нужно.
Рендеринг
Как только все данные будут собраны, следующим шагом станет придание им такого вида, на который действительно захочется смотреть.
Пожар
Огонь визуализируется на основе текстуры реакции, которая используется примерно так же, как карта высот. Использование эффекта параллакса в шейдере фрагментов придает изображению псевдо-3D-эффект, создавая ощущение глубины без необходимости использования true объемной графики.
Дым
Дым и туман в основном определяются на основе данных о температуре. В шейдере эти данные обрабатываются таким образом, что получаются плавные, меняющиеся формы, которые выглядят удивительно объемными, учитывая, что с технической точки зрения это всего лишь несколько текстур, перемещаемых по поверхности.
Угли
И, конечно же, никакой костёр не обходится без углей.
Это частицы, управляемые графическим процессором, которые отслеживают поле скоростей, то есть естественным образом следуют за потоком в моделировании. Здесь не требуется никакой дополнительной логики, они просто плывут по течению (в буквальном смысле).
Эти частицы углей обновляются и перемещаются с помощью собственной реализации на GPU (без использования Shuriken и VFX Graph). Итак, всего лишь ComputeBuffer для всех данных о частицах, вызов ComputeShader.Dispatch для их обновления и вызов Graphics.DrawProcedural для их вывода на экран.
Освещение
Расчет карты освещения
Освещение реализовано с помощью простого приёма, который, в конечном итоге, берёт на себя большую часть работы.
Текстура реакции подвергается уменьшению разрешения и размытию, в результате чего она превращается в динамическую карту освещения. Это не соответствует законам физики, но в этом и нет необходимости. Главное, чтобы это выглядело красиво!
Освещение помещения
При рендеринге объектов освещение сводится к одному обращению к текстуре в пользовательском шейдере.
Вместо того чтобы производить выборку непосредственно на поверхности, точка отсчета слегка смещается вдоль нормали к поверхности:
worldPosition + worldNormal * c

Это небольшое смещение дает значительный эффект. Создается впечатление, что свет исходит из окружающей среды, придавая поверхностям убедительное ощущение глубины и направленности.
И всё это — из одного образца текстуры. Неплохо.
Если вам интересуют подробности, вот функция шейдера, которую я использую:
void Simulate(float dt)
{
//feed data to simulation
RenderEmittersAndObstaclesToTexture();
ApplyDiffusion(dt); //fluid spreading
ApplyAdvection(dt); //moving data along the velocity field
ApplyExtinguishmentImpulse(dt); //fire producing smoke (reaction>density)
ComputeVorticityConfinement(dt); //increase swirlyness of fluid
//calculate divergence free velocity field
ComputeDivergence();
ComputePressure();
ComputeProjection();
AdvectParticles(dt); //move particles along velocity field
}
Я просто поместил эту функцию и все необходимые универсальные переменные в файл .cginc и теперь с удобством использую их в любом шейдере, которому требуется считывать данные из карты освещения.
Расширение возможностей карты освещения за пределы освещения
Один из самых приятных побочных эффектов этой настройки заключается в том, что карта освещения используется не только для освещения.
В Ignitement некоторые элементы пользовательского интерфейса также используют эту технологию. Элементы с картами нормалей используют карту освещения для имитации отражений. Например, стекло контейнера для здоровья отражает окружающий огонь, благодаря чему создается ощущение его связи с миром, а не простого нахождения над ним.
Это также открывает возможности для создания более необычных эффектов.
В одном из помещений стены выполнены из «живой плоти» (а почему бы и нет?). Они используют карту освещения для управления интенсивностью колебаний. Чем сильнее пожар поблизости, тем сильнее реагируют стены, создавая впечатление, что само помещение оживает и явно не в восторге от того, что горит.
Более того, всё это выполняется в шейдере вершин, благодаря чему такая динамичная визуализация обходится крайне недорого.
Как визуальные эффекты огня влияют на игровой процесс?
Графика — это хорошо, но одна только хорошая система визуальных эффектов не делает игру хорошей. В игре «Ignitement» огонь напрямую влияет на игровой процесс: любой враг, коснувшийся его, получает дебафф «горение», наносящий урон с течением времени.
Чтобы это работало, данные моделирования должны быть доступны в ЦП. В каждом кадре текстура реакции подвергается понижающему дискретизированию и считывается обратно с помощью AsyncGPUReadback.RequestIntoNativeArray. Вместо того чтобы выполнять дорогостоящие запросы по каждому объекту на графическом процессоре, система считывает текстуру один раз и для каждого врага выполняет недорогие поиски на центральном процессоре. Благодаря использованию простого порогового значения этот механизм фактически работает как единый, чрезвычайно динамичный коллайдер, который в любой момент времени идеально повторяет форму огня.
Ограничения и компромиссы
Конечно, этот подход не идеален.
Поскольку моделирование является 2D, все, что происходит по вертикали, является скорее приближением, чем физически верным решением. Кроме того, при смещении области моделирования следует проявлять осторожность, чтобы избежать появления заметных швов или скачков.
Тем не менее, эти компромиссы являются вполне осознанными. Они обеспечивают высокую скорость, масштабируемость и широкую совместимость системы, при этом гарантируя насыщенный и динамичный интерфейс.
Основные выводы
- 2D-моделирование течений может дать гораздо больше, чем вы могли бы предположить
- Именно в повторном использовании данных моделирования и заключается большая часть секрета успеха
- «Выглядит правильно» часто важнее, чем «физически верно»
- Обратный обмен данными между графическим процессором и центральным процессором вполне возможен, если объем данных небольшой
- Одна хорошо продуманная система может одновременно управлять графикой, игровым процессом и пользовательским интерфейсом
Благодаря тому, что все элементы построены на основе общей модели симуляции жидкости, в Ignitement удалось создать целостный визуальный стиль, в котором огонь, освещение, пользовательский интерфейс и даже отдельные элементы окружения «говорят» на одном языке.
Результатом стало не просто улучшение графики, а целый мир, который кажется более целостным, более динамичным и более живым.
А всё начинается с нескольких текстур, пары шейдеров… и того, что всё это загорается.
Если вам нравятся симуляторы жидкостей и игры в стиле «survival» или «roguelike», добавьте «Ignitement» в список желаний на Steam и присоединяйтесь к Discord. Познакомьтесь с другими играми, Made with Unity, на нашей странице куратора в Steam, а также прочитайте другие истории разработчиков Unity в блоге Unity и на порталересурсов.
