Появилась возможность рассказать о том как мы создавали жидкость для TReload. Нам всего лишь нужно было залить уровни кислотой. Кислоты должно быть много, площади затопления огромные :) Один из финальных результатов:
Визуально кислота должна была представлять из себя грязную воду с желтым оттенком. Вот референсы:
Кислота должна поддерживать физическое взаимодействие с объектами, которые в нее брошены: рисовать волны, пену. Так же должна быть возможность видеть сквозь грязь. Возможно в уровнях будет небольшой ветер, но это неточно.
Разработка кислоты проводилась в несколько этапов:
- разрабатывались инструменты для работы с кислотой (в основном это инструмент рисования текстурных масок пены)
- разрабатывалась кислота (работали над шейдерами, материалами, логикой взаимодействия, звуковыми эффектами)
Инструмент рисования текстурных масок пены
Механизм рисования достаточно прост.
Условно есть 2 текстуры:
- текстура маски пены (далее маска)
- текстура кисти (далее кисть)
Задача состоит в том чтобы правильно произвести операцию Blit кисти с маской (использовать для кисти соответствующие “scale” и “offset”, чтобы корректно ее спроецировать в нужную область маски).
Чтобы можно было водить кистью по модели и рисовать, нужно чтобы координаты точки пересечения модели и кисти переводились в пространство UV.
Здесь есть 2 решения по части перевода координат:
- использовать “MeshColluder” и из него получать “texcoord.xy” области пересечения луча “Raycast”. В этом случае координаты будут уже приведены к “UV” виду, нам только останется проецировать текстуру кисти в нужную область маски.
- использовать “BoxCollider” и самостоятельно переводить “worldSpace” координаты кисти к UV координатам маски.
Мы использовали второй вариант:
- к модели кислоты добавляется “BoxColider”
- делается RayCast
- worldSpace точка пересечения луча кисти и кислоты переводится в “acidLocalSpace”
- далее эта точка переводится в “UV-space”. Для этого мы делим координаты точки пересечения на размеры кислоты:
Доработчки: механизм отмены (ctrl+z)
Для ввода механизма отмены пришлось изменить подход: была создана ортографическая камера, которая рендерит только слой кистей. Размеры камеры соответствовали размерам кислоты. В области пересечения кисти и маски создавался меш кисти, который рендерился камерой, а далее делался "Blit" с маской. Таким образом появилась возможность отменять действия.
Небольшая демонстрация работы системы рисования масок:
Волны
Нами предпринимались разные попытки создания волн:
- рисования волн на тектуре кислоты
- волны созданные геометрическим шейдером поверх кислоты
- тесселяция + GPU Instancing и волны
Справа на рисунке представлена волна, которая создана геометрическим шейдером. Волна слева - плоская.
Кстати, про кубики я писал здесь: Немного о дематериализации в нашей игре
В конечном счете мы отказались от таких волн и решили сделать реалистичные волны, которые учитывают интерференцию и генерируют пену в области распространения волны.
Как работает генерация волн
Опишу это простыми словами: есть 2-х мерное уравнение “колебаний”, которое нужно решать каждый кадр. Это уравнение позволяет генерировать распространение волн. С материалом по теме вы можете ознакомиться здесь: ссылка 1
А здесь еще один отличный материал: ссылка 2
Здесь крутой пример исходного кода для Unity: ссылка 3
Мой результат генерации волны (используется стандартная тесселяция от Unity и стандартный шейдер):
Но генерация волн это еще не все. Если у вас маленький бассейн, то примера с Github должно хватить. А если нужно рендерить море или океан, то возникает масса проблем оптимизации:
- оказывается Unity не поддерживает “Tessellation + GPU Instancing of Standard shaders”
- ближние участки кислоты должны быть высокополигональными (для этого нужно использовать систему “LOD”)
- дальние волны, пену можно не рендерить
- артефакты распространения волн
Самое важное я узнал в самом конце. Unity, почему “Tessellation + GPU Instancing” не не работают со стандартными шейдерами? Для решения этой проблемы пришлось посмотреть сгенерированный код Standard-шейдера, вытащить из него то что вам нужно и вставить это в “Fragment shader”.
Структура водной поверхности, распространение воды на соседние сегменты
Водная поверхность представляет из себя NxN объектов с “LOD”. По мере удаления, объекты с LOD подменяют друг друга так, что на расстоянии X вместо 4-х различных объектов с LOD, рисуется один:
То есть водная поверхность - это “умная” сетка из разных участков воды. Допустим, вода имеет размеры 8х8 и пусть источник волн возник в ячейке [2,4]:
Тогда мы резервируем соседние N ячеек (в моем примере по одной с каждой стороны) и проецируем текстуру распространения волн на этот участок общей водной поверхности. Проекция текстуры распространения волн показана красным. То есть мы растягиваем и смещаем текстуру для каждого участка воды. Поиск зоны отрисовки волн в Unity выглядит следующим образом:
Кстати, если источник волн на к краю воды, то мы располагаем текстуру с волнами так, чтобы она не уходила за границы воды (на видео этого нет).
А здесь мы спроецировали текстуру на которой должны рисоваться волны (настроили “tilling & offset”):
Таким образом распространение волны происходит на прилегающие соседние объекты, то есть за пределы одного участка воды.
Вот результаты работы симуляции воды и тесселяции:
Генерация волн от объектов сложной формы
До этого момента я упрощенно рассказал и сослался на литературу, описывающую то как генерировать волны в виде кругов. А что если в воду упадет параллелепипед, капсула или еще какой-то объект (в том числе и невыпуклый). В этом случае форма волн должна быть соответствующей.
Чтобы добиться “реалистичной” формы волн, мы поступили следующим образом:
- падающие в воду объекты рендерятся в текстуру _FallTex. (Ортографическая камера рендерит значения глубины упавших объектов умноженные на скорость падения обьекта)
- далее текстура _FallTex размывается и результат размытия передается в текстуру волн
То есть мы вмешиваемся в процесс симуляции воды, добавляем в симуляцию новые значения (новые источники волн).
Здесь показан результат симуляции волн от объектов сложной формы:
Распространение волн на дальние сегменты
Это одна из проблемных задач. Распространение волн осуществляется за счет использования дополнительных текстур. Игрок не способен летать над водной гладью со скоростью пули и присутствовать то в одном месте, то в другом. Поэтому есть возможность переставать генерировать те волны, которые “далеко”. А распространение тех волн, которые близко, нужно плавно переносить из одного водного сегмента на другой. Здесь видно как ведет себя вода при переходе между разными участками симуляции жидкости:
Чтобы передергов с водой не было, нужно иметь 2 текстуры симулирующие жидкость. Одна из них должна симулировать жидкость, а другая должна перекопировать на себя участок с волнами при перемещении игрока (то есть при смене центрального участка воды). Если этого не делать, то возможны такие баги:
Допустим упавший в воду объект поплыл из [2,4] в [3,4] :
Тогда, чтобы сымитировать цельность водной поверхности, мы копируем часть текстуры текущей симуляции жидкости и вставляем ее во вторую текстуру. Теперь делаем вторую текстуру основной и продолжаем распространение волн на этой текстуре.
Для чего мы проводим операцию копирования? Дело в том, что нам нужно начать новую симуляцию и отобразить в ней результат старой симуляции (на рисунке выше этот результат находится в желтой зоне).
Артефакты
Если объект - источник волн расположен на границе разных водных сегментов, то при копировании текстуры распространения волны могут возникнуть артефакты:
Эти артефакты связаны с тем, что текстура волн является “Clamp”. Для устранения данных артефактов, необходимо учитывать расположение объектов (проверять расположение относительно стыков) и, в случае необходимости, исключать часть объектов из процесса симуляции волн.
Возможны ситуации, когда возникают щели. Они решаются в тесселляцинном шейдере путем уменьшения высоты отклонения волн. То есть высота волн меняется в зависимости от расстояния до камеры игрока.
Вот мои тесты тесселяции и попытки объединения Tesselation + GPU Instancing в Standard shader:
Волны от объектов разной формы:
На этом пока все!
Надеюсь статья была полезна и позволила рядовому читателю понять часть трудностей с которыми сталкиваются разработчики в процессе работы над играми :)
Ссылки на нас:
VK // Twitter // Instagram // DTF // Pikabu // Habr