Да, вы уже поняли, наконец-то расскажу про реализацию воды в проекте. Приступим? Часть первая. Свет.
Часть вторая. Структура.
Часть третья. Глобальное освещение.
Часть четвёртая. Вода Для начала подсмотрим, как делают воду "взрослые ребята" из всяких крупных 3D проектов.
Вообще, перетаскивать идеи из 3D — отличный план, в 2D классных алгоритмов ощутимо меньше. Итак, от самого простого к более сложному: Плоская вода без физики. Бросили полигон на уровень и радуемся. Потому что fps не проседает и выглядит неплохо (художники постарались и собрали красивый шейдер). Взаимодействовать нельзя, говорите? Так у нас гоночная игра, воду видно только на горизонте! Полигональная вода без физики. Планировали карту с плохой погодой, а вода плоская, как зеркало? Добавим полигонов, вспомним про синусы и вот, на горизонте громоздятся штормовые волны. Если в воду не заехать случайно — даже и не заметно, что вода ненастоящая. Плоская вода с физикой. С гонками закончили, делаем Skyrim. Плоская вода с красивыми эффектами, несколько проверок в физическом движке и по воде можно плавать. Никаких тебе бурь и волн, но кому они в rpg нужны, когда по небу летают драконы, а фус-ро-да сбивает с ног прохожих? Полигональная вода с физикой. Завязали с фэнтези и начали делать GTA. И тут уже реалистичные волны, да ещё и с физикой водного транспорта (как в этой великолепной статье). Хорошая вода, неужели можно ещё усложнить? Система частиц. Ещё как можно! Если вы парни из Dark Energy Digital, и когда-то сделали Hydrophobia с "честной" водой, которая умеет течь, брызгаться и затапливать. И если у вас мощная видеокарта (или две) и процессор, способный всё это рассчитать. Теперь, когда у нас есть референсы из крупных проектов, помечтаем, чего бы хотелось реализовать в нашем проекте.  А вот и список хотелок: Мы не пойдем путём команды, сделавшей Hydrophobia, "честно" моделировать жидкость — слишком уж требователен к ресурсам этот способ. Нужно упрощать. Раз уж в проекте пиксельарт — все плоскости либо горизонтальны, либо вертикальны. И пустое пространство на уровне представимо в виде набора прямоугольников. Вот с ними и будем работать.
В препроцессинге на начале уровня: В реальном времени: Граф водных объёмов Но, сказать по правде, ничего не получилось. Подводных камней столько, что можно построить парочку пятиэтажек. Например: нужно синхронизировать волны, разбираться с давлением в сообщающихся сосудах и т.д. Но нет худа без добра — именно для реализации этого метода когда-то была написана имплементация region tree, которая сейчас используется повсюду в проекте. Раз с совсем динамической водой не получилось, давайте упростим себе жизнь — будем настраивать воду в редакторе, а в для иллюзии динамики оставим волны. По сути, перейдем к варианту №3 из списка вариантов. Вода — очень противоречивая штука, потому что умеет течь: нельзя сказать "вот в этих точках будет вода", разместить там меши для жидкости и получить хорошую картинку. Ну и ладно, пусть левел дизайнеры (мы в будущем) разместят некие ключевые точки, где точно должна быть вода, а движок выльет её из своих запасов, причем ровно до уровня ключевых точек. Конечно, можно поместить один "якорь" под другим, но это уже проблема не движка, а тех самых неизвестных нам левел дизайнеров: наш код аккуратно заполнит водой уровень до самой верхней ключевой точки. Ещё одна фишка — отдельные якоря для создания "пузырей". Если алгоритм найдёт такой якорь, он постарается не залить его водой, оставив аккуратную каверну с воздухом. Теперь, когда есть понимание, какие результаты мы получим, пора разработать алгоритм, не так ли? В общем случае, вода может залить весь уровень. Так как стены состоят из прямоугольных кусочков и могут быть расположены где угодно, водный объём — это невыпуклый прямоугольный многоугольник с дырами. Гадость какая. Давайте разбивать на более простые фигуры. Разберёмся с пустым пространством, в котором может быть жидкость. Великолепное region tree, про которое я рассказывал в прошлых статьях, очень поможет. Получим с его помощью все прямоугольные объёмы, не занятые стенами: Вместо тысячи слов На самом деле, прямоугольники — не самый удачный выбор. Из-за большого количества деталей (например, зубцов на башнях) прямоугольных водных объёмов получится очень много. А это уменьшит производительность. Спокойная вода — плоская, а форма дна нам не важна, поэтому объединим смежные прямоугольники таким образом: Если у прямоугольника A только одна связь справа (с прямоугольником B), а у прямоугольника B - только одна связь слева (с прямоугольником A) - объединяем эти прямоугольники в одну структуру. После этого этапа останется небольшое количество водных объёмов с ребристым дном, на рисунке ниже это хорошо видно. Разметка пустого пространства на карте (со связями) Теперь нужно найти все метки, оставленные на карте левел дизайнерами и "обрезать" наши заготовки по их уровню. С этим справится примерно такой алгоритм: Как видите, отличие меток воды от меток воздушных пузырей в том, что первые проходят по всем соседям рекурсивно, а вторые останавливаются, когда сосед находится целиком под уровнем пузыря. А теперь в картинках: Добавили на карту метки уровня воды То же самое, но со связями Обрезали прямоугольники по уровню воды Удалили лишние связи и прямоугольники Дебажная визуализация полученных водных объёмов Несмотря на то, что мы обрезаем прямоугольники по уровню меток, в каждом из них сохраняем массив изначальных высот (y-координату потолка над водой). В дальнейшем эти высоты понадобятся для ограничения волн — в противном случае волны смогут проходить сквозь стены. Самое время посмотреть, как это выглядит в редакторе: Раз уж мы отказались от текущей в реальном времени воды, давайте хоть волны красивые сделаем. После генерации воды у нас получается примерно такой менеджер для воды: Пока что из всего этого кода нас (ну или меня, как рассказчика) интересует только WaterPolygon. Это те самые минимальные кусочки воды, для которых можно строить волны. А еще эти элементы связаны друг с другом (информация о графе осталась доступна в этих полигонах). Если упустить неважные детали, выглядит этот класс так: Но есть одно важное дополнение. В туториале всего один "водный полигон" в то время как у нас их целый граф. И нужно правильно синхронизировать волны. На примере понятнее: Допустим, у нас есть вот такой уровень Он будет состоять из трёх водных полигонов В какой то момент в синем полигоне возникает волна Алгоритм находит подходящего соседа, исходя из высоты левой колонки и синхронизирует волну При очень больших волнах алгоритм выберет другого соседа Минутка оптимизаций. Нет смысла рассчитывать волны для: При очередном расчете волн происходит проверка — не оказались ли все волны меньше порогового значения, и если да — bed time! Если же полигон целиком за пределами экрана — пороговое значение чуть выше. Соответственно, разбудить воду может связный полигон или влияние других модулей (физика, ветер и т.д.). Пока что у воды нет причин для колебаний. Ничто не может потревожить её покой, ни сражения боевых магов, ни банальный ураган. Холодна, как сердце Морры. Время растопить лёд. Несмотря на несколько маниакальную проработку никому не нужных мелочей, не очень хочется потратить на реалистичную термодинамику ещё полгода разработки. Так что упростим донельзя. И ещё чуть-чуть. Начнём с погодного менеджера. Когда-нибудь в нем будут и бури и штиль, но сейчас — только это: Концепция такова: Температуру для WaterPoligon.Line будем хранить только для верхнего и нижнего концов водного столба.  Крайне неточный способ — ведь у всех столбов разная высота, но нам подойдёт. Передача температуры осуществляется в трёх разных "направлениях". Самое первое и очевидное — между краями водного столба: Второе — более интересное. Теплообмен между воздухом и верхним краем столба. При генерации воды мы получали height, фактически, координату потолка. А значит, в любой момент времени мы можем получить высоту воздушной прослойки над водой. Чем больше воздуха над водой, тем быстрее происходит теплообмен. Спорное утверждение. Зато на открытом воздухе вода будет остывать/разогреваться быстрее, чем в невысоких пещерах: Остается теплообмен между соседними столбиками. Так как информация о температуре хранится только на краях столбов, линейная интерполяция выходит на сцену. Для любителей картинок Обмен температурой похож по структуре на обмен волнами, но волна передаётся только одному соседнему WaterPolygon'у с каждой стороны, а теплота — каждому из соседей. И последний штрих — кипение и замерзание воды. Если температура выше или равна 100 °C — добавляем случайную скорость пружины к столбу жидкости, причем чем выше температура — тем больше разброс скоростей (на самом деле, не ровно 100 °C, вода начинает пузыриться чуть раньше). Ну а если температура ниже или равна 0 °C — сохраняем скорость пружины равной нулю и перестаём реагировать на передачу скоростей от соседних столбиков-пружин. Или, другими словами: Постепенное закипание воды После всего вышеописанного в проекте появилась вода, которую можно "вылить" на уровень в редакторе. Волнующаяся, кипящая, твердеющая на морозе живительная влага! Было бы обидно получить некрасивую картинку, написав столько кода. Отражения, преломления, каустика — наше всё! А значит, снова толстые шейдеры, взаимодействие с системой освещения, рендер в текстуру и всё такое. Но об этом — в следующей статье. :) Программист