Water Simulation

Появилась возможность рассказать о том как мы создавали жидкость для TReload. Нам всего лишь нужно было залить уровни кислотой. Кислоты должно быть много, площади затопления огромные :) Один из финальных результатов:

Water Simulation

Визуально кислота должна была представлять из себя грязную воду с желтым оттенком. Вот референсы:

Water Simulation

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

Разработка кислоты проводилась в несколько этапов:

  • разрабатывались инструменты для работы с кислотой (в основном это инструмент рисования текстурных масок пены)
  • разрабатывалась кислота (работали над шейдерами, материалами, логикой взаимодействия, звуковыми эффектами)

Инструмент рисования текстурных масок пены

Механизм рисования достаточно прост.

Условно есть 2 текстуры:

  • текстура маски пены (далее маска)
  • текстура кисти (далее кисть)

Задача состоит в том чтобы правильно произвести операцию "Blit кисти с маской (использовать для кисти соответствующие “scale” и “offset”, чтобы корректно ее спроецировать в нужную область маски).

Чтобы можно было водить кистью по модели и рисовать, нужно чтобы координаты точки пересечения модели и кисти переводились в пространство UV.

Здесь есть 2 решения по части перевода координат:

  • использовать “MeshColluder” и из него получать “texcoord.xy” области пересечения луча “Raycast”. В этом случае координаты будут уже приведены к “UV” виду, нам только останется проецировать текстуру кисти в нужную область маски.
  • использовать “BoxCollider” и самостоятельно переводить “worldSpace” координаты кисти к UV координатам маски.

Мы использовали второй вариант:

  • к модели кислоты добавляется “BoxColider”
  • делается RayCast
  • worldSpace точка пересечения луча кисти и кислоты переводится в “acidLocalSpace”
  • далее эта точка переводится в “UV-space”. Для этого мы делим координаты точки пересечения на размеры кислоты:
Water Simulation

Доработчки: механизм отмены (ctrl+z)

Для ввода механизма отмены пришлось изменить подход: была создана ортографическая камера, которая рендерит только слой кистей. Размеры камеры соответствовали размерам кислоты. В области пересечения кисти и маски создавался меш кисти, который рендерился камерой, а далее делался “Blit” с маской. Таким образом появилась возможность отменять действия.

Небольшая демонстрация работы системы рисования масок:

Волны

Нами предпринимались разные попытки создания волн:

  • рисования волн на тектуре кислоты
  • волны созданные геометрическим шейдером поверх кислоты
  • тесселяция + GPU Instancing и волны
Water Simulation

Справа на рисунке представлена волна, которая создана геометрическим шейдером. Волна слева - плоская.

Кстати, про кубики я писал здесь: ссылка

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

Как работает генерация волн

Опишу это простыми словами: есть 2-х мерное уравнение “колебаний”, которое нужно решать каждый кадр. Это уравнение позволяет генерировать распространение волн. С материалом по теме вы можете ознакомиться здесь: ссылка 1

А здесь еще один отличный материал: ссылка 2

Здесь крутой пример исходного кода для Unity: ссылка 3

Мой результат генерации волны (используется стандартная тесселяция от Unity и стандартный шейдер):

Water Simulation

Но генерация волн это еще не все. Если у вас маленький бассейн, то примера с Github должно хватить. А если нужно рендерить море или океан, то возникает масса проблем оптимизации:

  • оказывается Unity не поддерживает “Tessellation + GPU Instancing of Standard shaders”
  • ближние участки кислоты должны быть высокополигональными (для этого нужно использовать систему “LOD”)
  • дальние волны, пену можно не рендерить
  • артефакты распространения волн

Самое важное я узнал в самом конце. Unity, почему “Tessellation + GPU Instancing” не не работают со стандартными шейдерами? Для решения этой проблемы пришлось посмотреть сгенерированный код Standard-шейдера, вытащить из него то что вам нужно и вставить это в “Fragment shader”.

Структура водной поверхности, распространение воды на соседние сегменты

Водная поверхность представляет из себя NxN объектов с “LOD”. По мере удаления, объекты с LOD подменяют друг друга так, что на расстоянии X вместо 4-х различных объектов с LOD, рисуется один:

Water Simulation

То есть водная поверхность - это “умная” сетка из разных участков воды. Допустим, вода имеет размеры 8х8 и пусть источник волн возник в ячейке [2,4]:

Water Simulation

Тогда мы резервируем соседние N ячеек (в моем примере по одной с каждой стороны) и проецируем текстуру распространения волн на этот участок общей водной поверхности. Проекция текстуры распространения волн показана красным. То есть мы растягиваем и смещаем текстуру для каждого участка воды. Поиск зоны отрисовки волн в Unity выглядит следующим образом:

Кстати, если источник волн на к краю воды, то мы располагаем текстуру с волнами так, чтобы она не уходила за границы воды (на видео этого нет).

А здесь мы спроецировали текстуру на которой должны рисоваться волны (настроили “tilling & offset”):

Таким образом распространение волны происходит на прилегающие соседние объекты, то есть за пределы одного участка воды.

Вот результаты работы симуляции воды и тесселяции:

Генерация волн от объектов сложной формы

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

Чтобы добиться “реалистичной” формы волн, мы поступили следующим образом:

  • падающие в воду объекты рендерятся в текстуру _FallTex. (Ортографическая камера рендерит значения глубины упавших объектов умноженные на скорость падения обьекта)
  • далее текстура _FallTex размывается и результат размытия передается в текстуру волн

То есть мы вмешиваемся в процесс симуляции воды, добавляем в симуляцию новые значения (новые источники волн).

Здесь показан результат симуляции волн от объектов сложной формы:

Распространение волн на дальние сегменты

Это одна из проблемных задач. Распространение волн осуществляется за счет использования дополнительных текстур. Игрок не способен летать над водной гладью со скоростью пули и присутствовать то в одном месте, то в другом. Поэтому есть возможность переставать генерировать те волны, которые “далеко”. А распространение тех волн, которые близко, нужно плавно переносить из одного водного сегмента на другой. Здесь видно как ведет себя вода при переходе между разными участками симуляции жидкости:

Чтобы передергов с водой не было, нужно иметь 2 текстуры симулирующие жидкость. Одна из них должна симулировать жидкость, а другая должна перекопировать на себя участок с волнами при перемещении игрока (то есть при смене центрального участка воды). Если этого не делать, то возможны такие баги:

Water Simulation

Допустим упавший в воду объект поплыл из [2,4] в [3,4] :

Water Simulation

Тогда, чтобы сымитировать цельность водной поверхности, мы копируем часть текстуры текущей симуляции жидкости и вставляем ее во вторую текстуру. Теперь делаем вторую текстуру основной и продолжаем распространение волн на этой текстуре.

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

Артефакты

Если объект - источник волн расположен на границе разных водных сегментов, то при копировании текстуры распространения волны могут возникнуть артефакты:

Water Simulation
Water Simulation
Water Simulation

Эти артефакты связаны с тем, что текстура волн является “Clamp”. Для устранения данных артефактов, необходимо учитывать расположение объектов (проверять расположение относительно стыков) и, в случае необходимости, исключать часть объектов из процесса симуляции волн.

Возможны ситуации, когда возникают щели. Они решаются в тесселляцинном шейдере путем уменьшения высоты отклонения волн. То есть высота волн меняется в зависимости от расстояния до камеры игрока.

Water Simulation

Вот мои тесты тесселяции и попытки объединения Tesselation + GPU Instancing в Standard shader:

Water Simulation
Water Simulation

Волны от объектов разной формы:

На этом все!

Надеюсь статья была полезна и позволила рядовому читателю понять часть трудностей с которыми сталкиваются разработчики в процессе работы над играми :)

Ссылки на нас:

4545
2 комментария

Здорово! Хорошая статья.
Не понятно только, зачем столько сил тратить на визуальный эффект, не влияющий на геймплей. Любой простенький эффект движения воды сошел бы, никто не заметит особой разницы.

1
Ответить