- Как мне работать с водой?
- Настройка воды
- Создание воды с нуля (для продвинутых пользователей)
- Свойства в материалах воды
- Руководство по моделированию 2D водной поверхности
- Авторизуйтесь
- Руководство по моделированию 2D водной поверхности
- Итак, приступим
- Создаём узлы
- Создаём саму воду
- Настройка детекторов столкновений
- Добавим немного физики
- Добавим столкновения
- Обнаружение столкновений
- В этой статье слишком много воды
- Разработка воды на Unity.
- Геометрия
- Генерация формы волны
- Физическая модель плавучести
- Шейдинг
Как мне работать с водой?
Важно: содержимое этой страницы применимо только к настольному режиму редактора.
В Unity включены некоторые префабы с водой (включая необходимые шейдеры, скрипты и ассеты с графикой) в пакетах Standard Assets и Pro Standard Assets. Unity содержит обычную простую воду, в то время как Unity Pro содержит воду с отражениями в реальном времени и преломлениями. В обоих случаях вода поставляется в качестве отдельных дневных и ночных префабов.
Настройка воды
В большинстве случаев вам достаточно просто разместить один из существующих префабов в вашей сцене (убедитесь, что у вас установлен набор стандартных ассетов):
- В Unity есть Daylight Simple Water и Nighttime Simple Water в Standard Assets->Water .
- В Unity Pro есть Daylight Water и Nighttime Water в Pro Standard Assets->Water (но также требуются некоторые ассеты из Standard Assets->Water ). Режим воды (Simple (простая), Reflective (отражающая), Refractive (преломляющая)) можно изменить в инспекторе.
Для воды префаб использует меш в форме овала. Если вы желаете использовать другой меш, то проще всего просто поменять его в Mesh Filter у объекта воды:
Создание воды с нуля (для продвинутых пользователей)
Простая вода в Unity требует добавления скрипта на плоский меш и использования шейдера воды:
- Подберите меш для воды. Это должен быть горизонтально ориентированный плоский меш. UV координаты не требуются. GameObject с водой должен использовать слой ( layer ) Water, который вы можете установить в инспекторе .
- Добавьте на объект скрипт WaterSimple (из Standard Assets/Water/Sources ).
- Используйте шейдер FX/Water (simple) в материале, или подстройте один из предоставленных материалов воды ( Daylight Simple Water или Nighttime Simple Water ).
Отражающая/преломляющая вода в Unity Pro требует аналогичных шагов для настройки с нуля:
- Подберите меш для воды. Это должен быть горизонтально ориентированный плоский меш. UV координаты не требуются. GameObject с водой должен использовать слой ( layer ) Water, который вы можете установить в инспекторе .
- Добавьте на объект скрипт Water (из Pro Standard Assets/Water/Sources ).
- Используйте шейдер FX/Water в материале, или подстройте один из предоставленных материалов воды ( Daylight Water или Nighttime Water ).
Свойства в материалах воды
Эти свойства используются в шейдере отражающей и преломляющей воды. Большинство из них также используется и в шейдере простой воды.
| Свойство: | Функция: |
|---|---|
| Wave scale | Изменение масштаба карты нормалей волн. Чем меньше значение, тем больше будут волны у воды. |
| Reflection/refraction distort | Как сильно отражение/преломление будет искажаться картой нормалей волн. |
| Refraction color | Дополнительный оттенок отражения. |
| Environment reflection/refraction | Render текстуры для отражения и преломления в реальном времени. |
| Normalmap | Определяет форму волн. Итоговые волны производятся путём совмещения этих двух карт нормалей, каждая из которых смещается в разном направлении, с разным масштабом и скоростью. Вторая карта нормалей в два раза меньше первой. |
| Wave speed | Скорость смещения для первой (1е и 2е числа) и второй карты нормалей (3е и 4е числа). |
| Fresnel | Текстура с альфа-каналом, управляющим эффектом Fresnel — количество видимого отражения против количества видимого преломления, в зависимости от угла зрения. |
Остальные свойства не используются отражающим и преломляющим шейдером, но должны быть настроены в случае, если графический ускоритель пользователя не поддерживает его и вынужден откатиться на более простой шейдер.
Источник
Руководство по моделированию 2D водной поверхности
Авторизуйтесь
Руководство по моделированию 2D водной поверхности
В этой статье мы рассмотрим создание динамической 2D воды с простейшей физикой. Мы будем использовать рендер линий, мешей, триггеры и частицы. Конечный результат с волнами и брызгами вы сможете добавить в какую-нибудь свою игру. Я выложил то, что получилось у меня на Unity (Unity 3D), но, используя принципы из данной статьи, вы сможете сделать то же самое на любом движке.
Итак, приступим
Мы будем создавать верхний контур воды, используя рендер линий и много узлов, чтобы это выглядело, как волны.
Нам нужно будет хранить позиции, скорости и ускорения для каждого узла, так что мы будем использовать массивы:
С помощью LineRenderer мы создадим границу нашей воды, но нам всё ещё нужна сама вода. Для этого мы будем использовать меши. Для их хранения нам также нужен будет массив GameObject :
Ещё нам нужно будет отслеживать столкновения с поверхностью воды:
Теперь несколько констант:
Здесь springconstant — «коэффициент жёсткости» наших волн (они будут двигаться подобно пружинному маятнику); damping — коэффициент гашения (иначе волна, единожды начав качаться, не остановится никогда); spread — коэффициент, отвечающий за скорость распространения волн. Вы можете играть с этими значениями, стараясь достичь свойств, наиболее напоминающих настоящую воду. Значение z (координату воды по оси Z) вы можете менять в зависимости от того, что должно стоять ближе к переднему плану в вашем приложении.
Далее, нам нужно хранить местоположение нашей воды:
…И её внешний вид:
Объект, в котором все эти данные будут храниться — что-то вроде менеджера, он же будет создавать воду. Создадим для этого функцию — SpawnWater() . В неё мы будем передавать координату левого края воды, ширину водного пространства, а также его верхнюю и нижнюю границы:
Создаём узлы
Посчитаем количество узлов, которое нам потребуется:
Мы будем использовать пять узлов на единицу ширины, но вы можете менять это значение в поисках своего баланса между производительностью и гладкостью воды.
Теперь самое время настроить наш LineRenderer :
После инициализируем переменные, которые мы создавали выше:
…И заполним их реальными значениями:
В конце этого участка кода, как вы видите, мы помещаем каждый узел из LineRenderer на его место.
Создаём саму воду
Здесь начинается самое интересное. Сейчас у нас есть граница воды, но самой воды нет. Поэтому сейчас мы создадим для неё меши:
Теперь в меш нужно передать несколько значений. Для начала — координаты его углов.
На диаграмме отмечены нужные нам углы первого меша. В общем виде это будет выглядеть так:
Как вы могли заметить, в нулевой ячейке хранится верхняя левая точка, в первой — верхняя правая, во второй — нижняя левая, а в третьей — нижняя правая. Теперь нам нужно выбрать, какую часть текстуры мы хотим использовать для этого меша. Мы хотим использовать всю, а значит нам нужно просто написать:
Меши должны состоять из треугольников. К счастью, любой четырёхугольник можно разделить на два треугольника.
Как вы можете видеть, треугольник A состоит из вершин 0 , 1 , 3 , а треугольник B — из вершин 3 , 2 , 0 . Эти шесть чисел нам нужно записать в отдельный массив, в этом же порядке:
Теперь три созданных нами массива нужно передать в меш:
Теперь у нас есть меши, но нет GameObject -ов, чтоб их рендерить. Исправим это:
Все эти меши теперь являются потомками менеджера.
Настройка детекторов столкновений
Здесь мы создаём несколько BoxCollider -ов, назначаем им имена и делаем их потомками менеджера. Также мы назначаем им позиции — посередине между узлами, определяем их размер и добавляем им WaterDetector .
Ну и следующая функция будет обновлять позиции наших узлов:
Добавим немного физики
Чтобы находить ускорения, скорости и новые позиции узлов, нам нам понадобятся закон Гука и метод Эйлера.
Итак, закон Гука гласит, что F=-k*x, где F — сила, с которой пружина стремится вернуться в начальное состояние (не забывайте, что наши волны будут состоять из множества маленьких пружин); k — коэффициент жёсткости (помните, мы записывали его в константах?); x — растяжение. По этой формуле мы будем высчитывать ускорение для узлов, значение растяжения будет высчитываться как разница между текущей позицией воды и её базовым уровнем. Ещё к силе мы будем прибавлять скорость, умноженную на коэффициент гашения — чтобы волны не были бесконечными.
Как видите, метод Эйлера очень прост — мы просто каждый раз прибавляем к позиции узла его скорость, а к скорости прибавляем ускорение. Разумеется, для точных вычислений больше бы подошло интегрирование Верье, но из-за использования гашения это с трудом представляется возможным. К тому же метод Эйлера на порядок проще.
Теперь добавим немного распространения волн. Создадим два массива, в которых мы будем хранить разницу между высотами соседних узлов, умноженную на коэффициент распространения:
…И заполним их, попутно меняя скорости и высоты:
Как вы заметили, выше мы совершаем действия 8 раз. Это сделано для большей плавности.
Добавим столкновения
Теперь у нас есть вода, которая может не просто отображаться, но и переливаться. Теперь нам нужна возможность её всколыхнуть!
Для этого создадим метод Splash() , который будет проверять место и скорость удара по воде:
Для начала проверим корректность переданной координаты по иксу:
Теперь сделаем так, чтоб хранилась не абсолютная координата, а расстояние от первого узла:
Теперь нужно вычислить, на который узел приходится удар:
Здесь мы делаем буквально следующее:
- Мы берём координату относительно первого узла — xpos .
- Делим её на разницу между позицей последнего и первого узла.
- Таким образом мы получаем дробное число, которое говорит нам, куда именно пришёлся удар.
- Умножаем полученное число на количество углов и округляем до целого.
Теперь приравниваем скорость найденного узла к скорости удара:
Вы можете поступить иначе, например, прибавить скорость удара к текущей скорости узла или дейсвовать согласно закону сохранения импульса.
Теперь добавим брызги:
Вы можете удивиться тому, что я назначил стартовую скорость дважды. Причина во встроенной системе частиц, которая назначает начальную скорость как случайное число между двумя константами. К сожалению, у нас нет особого доступа к ней через скрипты, поэтому приходится действовать так.
Дальше я добавил одну линию, но вы можете её пропустить:
Брызги не уничтожатся при столкновении с другими объектами, поэтому вам стоит либо назначить им Z координату фона (у меня — 5), либо сделать так, чтобы частицы всегда попадали обратно на воду. Я выбрал нечто среднее:
Вроде бы всё, так?
Обнаружение столкновений
Нет, не так! Нам нужно отслеживать столкновения объектов с водой, иначе всё это было написано зря. Помните класс WaterDetector , который мы упоминали выше? Сейчас мы займёмся именно им. Всё, что нам нужно от него — лишь один метод:
Теперь вам осталось лишь вызвать SpawnWater() где-либо в вашем коде и наслаждаться великолепным волнами и брызгами!
Хинт для программистов: если зарегистрируетесь на соревнования Huawei Cup, то бесплатно получите доступ к онлайн-школе для участников. Можно прокачаться по разным навыкам и выиграть призы в самом соревновании.
Перейти к регистрации
Источник
В этой статье слишком много воды
Разработка воды на Unity.
«Мы начинаем разработку новой игры, и нам нужна классная вода. Такую сможешь?»
, — cпросили меня. «Да не вопрос! Конечно, смогу», — ответил я, но голос предательски задрожал. «А, еще и на Unity?», — и мне стало понятно, что впереди очень много работы.
Итак, водичка. Unity до того момента я в глаза не видел, ровно как и C#, так что решил, что буду делать прототип на знакомых мне инструментах: С++ и DX9. Что я знал и умел на практике на тот момент так это скроллящиеся текстурки нормалей для формирования поверхности, и примитивный дисплейсмент маппинг на их основе. Тут же надо было менять абсолютно все. Реалистичная анимированная форма водной поверхности. Усложненый (сильно) шейдинг. Генерация пены. LOD система, привязанная к камере. Начал я выискивать информацию в интернете как же все это сделать то.
Академические пейперы с кучей заумных формул никогда мне особо не давались, так что после пары прочтений я мало что понял. Общие принципы были понятны: каждый кадр генерируется карта высот с помощью Fast Fourier Transform, которая, как функция от времени, плавно меняет свою форму формируя реалистичную водную поверхность. Но как и что считать я не знал. Я потихоньку вникал в премудрости просчета FFT на шейдерах в D3D9, и мне в этом очень помог исходник со статьей где-то в дебрях интернета, который я битый час пытался отыскать, но безуспешно (к сожалению). Первый результат был получен (страшный как ядерная война):
Стартовые успехи порадовали, и начался перенос воды на Unity с его доработкой.
К воде в игре про морские битвы выдвигались несколько требований:
- Реалистичный внешний вид. Красивые как близкие так и дальние ракурсы, динамическая пена, скаттеринг и т. д.
- Поддержка различных погодных условий: штиль, шторм и промежуточные состояния. Смена времени суток.
- Физика плавучести кораблей по симулированной поверхности, плавучие объекты.
- Так как игра мультиплеерная, вода должна быть у всех участников боя одинаковая.
- Рисование по поверхности: нарисованные зоны полета ядер залпа, пена от попаданий ядер в воду.
Геометрия
Решено было построить quadtree-like структуру, с центром вокруг камеры, которая дискретно перестраивается при движении наблюдателя. Почему дискретно? Если двигать меш плавно вместе с камерой или использовать screen space reprojection как в статье Real-time water rendering — introducing the projected grid concept, то на дальних планах из-за недостаточного разрешения геометрической сетки при выборке карты высот волны будут «скакать» полигоны вверх и вниз. Это очень сильно бросается в глаза. Картинка «рябит». Чтобы это побороть, надо либо сильно увеличивать разрешение полигональной сетки water mesh’a, либо «уплощать» геометрию на дальних дистанциях, либо так строить и двигать полигоны, чтобы эти сдвиги не было видно. Вода у нас прогрессивная (хехе) и выбрал я третий путь. Как и в любой подобной технике (особенно знакомой всем, кто создавал terrain в играх), необходимо избавиться от T-junctions на границах переходов уровней детализации. Для решения этой задачи на старте предрасчитывается 3 вида квадов с заданными параметрами тесселяции:
Первый тип для тех квадов, которые не являются переходными на более низкую детализацию. Ни одна из сторон не имеет уменьшенное в 2 раза количество вершин. Второй тип для граничных, но не угловых квадов. Третий тип — угловые граничные квады. Финальный меш для воды строится поворотом и масштабированием этих трех видов мешей.
Вот так выглядит рендер с подсветкой разным цветом LOD уровней воды.
На первых кадрах видно соединение двух различных уровней детализации.
Видео как кадр заполняется водяными квадами:
Напомню, это все было давно (и неправда). Сейчас более оптимально и гибко можно сделать сразу на GPU (GPU Pro 5. Quadtrees on the GPU). И рисовать будет в один draw call, и тесселяцией можно поднять детализацию.
Позднее проект переехал на D3D11, но до апгрейда этой части рендера океана руки так и не дошли.
Генерация формы волны
Вот для этого нам понадобится Fast Fourier Transform. Для выбранного (нужного) разрешения текстуры волны (пока назовем ее так, далее я объясню, какие данные там хранятся) подготавливаем начальные данные, используя параметры, заданные художниками (сила, направление ветра, зависимость волны от направления ветра и другие). Все это необходимо скормить в формулы т.н. Phillips spectrum’а. Полученные начальные данные модифицируем каждый кадр с учетом времени и выполняем FFT над ними. На выходе получаем тайлящуюся по всем направлениям текстуру которая содержит смещение вершин плоского меша. Почему не просто heightmap? Если хранить только оффсет по высоте, то результатом будет нереалистичная «бурлящая» масса, лишь отдаленно напоминающая море:
Если считать смещения для всех трех координат, то будут генерироваться красивые «острые» реалистичные волны:
Одной анимированной текстурки мало. Виден тайлинг, недостаточно деталей на ближних планах. Берем описанный алгоритм и делаем не одну, а 3 fft-generated текстуры. Первая — крупные волны. Она задает основную форму волны и используется для физики. Вторая — средние волны. Ну и напоследок самые мелкие. 3 FFT генератора (4-й вариант это финальный микс):
Параметры слоев задаются независимо друг от друга, и полученные текстуры смешиваются в шейдере воды в финальную форму волны. Параллельно со смещениями генерируются и карты нормалей каждого слоя.
«Одинаковость» воды у всех участников боя обеспечивается синхронизацией параметров океана на старте боя. Эту информацию передает сервер каждому клиенту.
Физическая модель плавучести
Так как необходимо было сделать не только красивую картинку, но и реалистичное поведение кораблей. А также учитывая то, что в игре должно присутствовать штормовое море (крупные волны), то еще одной задачей, которую требовалось решить, являлось обеспечение плавучести объектов на поверхности сгенерированного моря. Сперва я попытался сделать GPU readback текстуры волны. Но, так как быстро выяснилось, что всю физику морского боя необходимо делать на сервере, то и море, а точнее первый его слой который задает форму волны, необходимо считать также и на сервере (а на нем, скорее всего, нет быстрого и/или совместимого GPU), то было принято решение написать полную функциональную копию GPU FFT генератора, на CPU в виде native C++ плагина к Unity. Сам FFT алгоритм я не реализовывал и использовал готовый в библиотеке Intel Performance Primitives (IPP). А вот всю обвязку и постпроцессинг результатов был выполнен мной, с последующей оптимизацией на SSE и распараллеливанием по потокам. Сюда входила и подготовка массива данных для FFT каждый кадр, и финальное преобразование посчитанных значений в wave offset map.
Была еще одна интересная особенность алгоритма, которая исходила из требований к физике воды. Нужна была функция быстрого получения высоты волны в данной точке мира. Логично, ведь это и есть основа построения плавучести любого объекта. Но, так как на выходе FFT процессора у нас получалается offsetmap, а не heightmap, то обычная выборка из текстуры не давала нам высоту волны там где было необходимо. Для простоты рассмотрим 2D вариант:
Для формирования волны, тексели (текстурные элементы, показанные вертикальными линиями) содержат вектор (стрелки) который задает смещение вертекса плоского меша (синие точки) в направлении его финальной позиции (острие стрелки). Предположим мы возьмем эти данные и попробуем извлечь из нее высоту воды в интересующей нас точке. Например, нам надо узнать высоту в точке hB. Если мы возьмем вектор в текселе tB, то мы получим смещение в точку около hC, что может сильно отличаться от того что нам нужно. Вариантов решения этой проблемы два: при каждом запросе высоты проверять множество соседних текселей, пока не найдем тот, который имеет смещение в интересующую нас позицию. В нашем примере мы найдем тексель tA как содержащий наиболее близкое смещение. Но такой подход не назовешь быстрым. Сканирование радиуса текселей непонятно какого размера (а от того, штормовое море или спокойное, смещения могут сильно варьироваться) может занять продолжительное время.
Второй вариант — после просчета offset map конвертировать ее в height map, используя scattering подход. Это означает, что для каждого offset vector’а мы запишем высоту волны, которую он задает, в ту точку, куда он смещается. Это будет отдельный массив данных, который и будет использоваться для получения высоты в интересующей точке. Используя нашу иллюстрацию, ячейка tB будет содержать высоту hB полученную из вектора tA→hB. Есть еще одна особенность. Ячейка tA не будет содержать валидного значения, так как нет вектора, смещающегося в него. Для заполнения таких «дырок» выполняется проход заполнения их соседними значениями.
Вот так это выглядит, если сделать визуализацию смещений с помощью векторов (красные — большое смещение, зеленый — малое):
Далее все просто. Для корабля задается плоскость условной ватерлинии. На ней определяется прямоугольная сетка точек-проб, которая задает места приложения выталкивающих из воды сил для корабля. Затем для каждой точки проверяем, под водой она или нет, используя water heightmap, описанную выше. Если точка под водой, то прикладываем вертикальную силу вверх к physics hull корпуса в этой точке, масштабированной расстоянием от точки до водной поверхности. Если над водой, то ничего не делаем, гравитация сделает все для нас. На самом деле там формулы немного сложнее (вся для тонкого тюнига поведения корабля), но основной принцип такой. На видео визуализации плавучести ниже, синие кубы — это места расположения проб, а линии от них вниз — это величина выталкивающей из воды силы.
В реализации сервера есть еще один интересный оптимизационный момент. Нет никакой надобности симулировать разную воду для разных боевых инстансов, если они проходят в одинаковых погодных условиях (одинаковые параметры FFT симулятора). Так что логичным решением было сделать пул симуляторов, к которым боевые инстансы выполняют запросы на получение симулированной воды с заданными параметрами. Если параметры одинаковые от нескольких инстансов, то им вернется одна и та же вода. Реализовано это с помощью Memorу Mapped File API. Когда FFT симулятор создается, он дает доступ к своим данным, экспортируя дескрипторы нужных блоков. Серверный инстанс вместо того, чтобы запускать реальный симулятор, запускает «пустышку» которая просто отдает данные, открытые по этим дескрипторам. Было несколько веселых багов, связанных с этим функционалом. Из-за ошибок подсчета ссылок симулятор уничтожался, но memory mapped file жив пока открыт хоть один дескриптор на него. Данные переставали обновляться (симулятора-то нет) и вода «останавливалась».
На клиентской стороне нам необходима информация о форме волны для просчета попаданий ядер в волну и проигрывания систем частиц и пены. Просчет повреждений происходит на сервере и там также необходимо корректно определять, попало ли ядро в воду (волна может закрывать корабль, особенно в штормах). Тут уже необходимо делать heightmap tracing по аналогии как это делается в parallax mapping либо SSAO эффектах.
Шейдинг
В принципе как и везде. Отражения, преломления, subsurface scattering хитро замешиваем, учитывая глубину дна, учитываем fresnel effect, считаем спекуляр. Скаттеринг считаем для гребней в зависимости от позиции солнышка. Пена генерируется следующим образом: создаем «пятно пены» на гребнях волн (используем высоту как метрику), затем накладываем новосозданные пятна на пятна с предыдущих кадров одновременно уменьшая их интенсивность. Таким образом получаем размазывание пятен пены в виде хвоста от идущего гребня волны.
Используем полученную текстуру «пятен» как маску к которой примешиваем текстуры пузырьков, разводов и т. д. Получаем довольно реалистичный динамический рисунок пены на поверхности волн. Данная маска создается для каждого FFT слоя (напомню, у нас их 3), и в финальном миксе они все смешиваются.
На видео выше визуализация маски пены. Первый и второй слои. Я модифицирую параметры генератора и результат виден на текстуре.
И видео немножко коряво настроенного штормового моря. Тут хорошо видна форма волны, возможности генератора и пена:
Источник



