— Я тут воду для проекта запилил.
— О, круто! А почему она плоская? Даёшь волны!
…
— Слушай, ты тогда про волны говорил, помнишь? Зацени!
— Да, хорошие волны, а преломление и каустику ещё не делал?
…
— Привет, я тут игрался с Unity всю ночь, смотри какие отражения и каустику закодил!
— Дарова, и правда, хорошо! А когда у тебя вода кипит, отражения не глючат?
…
— Хай, реализовал наконец, кипение, вроде ничего?
— О, прямо как нужно! Слушай, прикинь как круто, если кипящую волну заморозить?
…
— Лови картинку, лёд вроде ничего придумал?
— Норм, слушай, а у тебя лёд замерзает, он в объёме увеличивается? И кстати, ты когда геймлей то делать начнёшь?
Вариации на тему лога с другом.
Да, вы уже поняли, наконец-то расскажу про реализацию воды в проекте. Приступим?
Предыдущие статьи
Часть первая. Свет.
Часть вторая. Структура.
Часть третья. Глобальное освещение.
Часть четвёртая. Вода
Оглавление
- Подглядывание за 3D
- Хотелки
- Первый блин комом
- Статика
- Генерация воды
- Динамика
- Температура
- Заключение и интрига
Подглядывание за 3D
Для начала подсмотрим, как делают воду "взрослые ребята" из всяких крупных 3D проектов.
Вообще, перетаскивать идеи из 3D — отличный план, в 2D классных алгоритмов ощутимо меньше.
Итак, от самого простого к более сложному:
Плоская вода без физики. Бросили полигон на уровень и радуемся. Потому что fps не проседает и выглядит неплохо (художники постарались и собрали красивый шейдер). Взаимодействовать нельзя, говорите? Так у нас гоночная игра, воду видно только на горизонте!
Полигональная вода без физики. Планировали карту с плохой погодой, а вода плоская, как зеркало? Добавим полигонов, вспомним про синусы и вот, на горизонте громоздятся штормовые волны. Если в воду не заехать случайно — даже и не заметно, что вода ненастоящая.
Плоская вода с физикой. С гонками закончили, делаем Skyrim. Плоская вода с красивыми эффектами, несколько проверок в физическом движке и по воде можно плавать. Никаких тебе бурь и волн, но кому они в rpg нужны, когда по небу летают драконы, а фус-ро-да сбивает с ног прохожих?
Полигональная вода с физикой. Завязали с фэнтези и начали делать GTA. И тут уже реалистичные волны, да ещё и с физикой водного транспорта (как в этой великолепной статье). Хорошая вода, неужели можно ещё усложнить?
- Система частиц. Ещё как можно! Если вы парни из Dark Energy Digital, и когда-то сделали Hydrophobia с "честной" водой, которая умеет течь, брызгаться и затапливать. И если у вас мощная видеокарта (или две) и процессор, способный всё это рассчитать.
Хотелки
Теперь, когда у нас есть референсы из крупных проектов, помечтаем, чего бы хотелось реализовать в нашем проекте.
Помните, шутливое вступление в начале статьи? Это совсем не шутка — каждый раз, когда я кидал другу скрины или видео из очередной демки, он предлагал какие-то совершенно безумные идеи. И это великолепно, ведь большую часть этих идей удалось воплотить, сделав проект ещё интереснее с точки зрения разработки. Поэтому список ниже составлялся итеративно, без мейлстоунов и плана.
А вот и список хотелок:
- Возможность в realtime добавлять новые объёмы воды или "испарять" существующие.
- Волны на поверхности воды.
- Температура, возможность замерзания и кипения воды.
- Взаимодействие с другими модулями: ветром, физическими телами, погодой.
- Тесное взаимодействие с системой освещения: каустика, рассеивание света, отражения.
- Возможность настройки в редакторе.
А про взаимодействия с модулями — тема практически бесконечная. Смотрите сами — в статье про свет я рассказывал, что в проекте есть возможность включить god rays — лучи света, видимые из-за пыли в воздухе. А теперь, когда я доделал кипящую воду, ничто не мешает сделать генерацию частиц пара, над которыми будут видны эти god rays! А ведь с частицами умеет взаимодействовать ветер, а значит, пар над горячей водой будет красиво рассеиваться. И таких взаимодействий столько, что не успеваешь записывать, не то что прототипировать.
Первый блин комом
Мы не пойдем путём команды, сделавшей Hydrophobia, "честно" моделировать жидкость — слишком уж требователен к ресурсам этот способ. Нужно упрощать. Раз уж в проекте пиксельарт — все плоскости либо горизонтальны, либо вертикальны. И пустое пространство на уровне представимо в виде набора прямоугольников. Вот с ними и будем работать.
В препроцессинге на начале уровня:
- Разбиваем всё пустое пространство на уровне на прямоугольные непересекающиеся объёмы.
- Находим связи объёмов друг с другом и строим граф "течений" воды.
В реальном времени:
- Для каждого объёма отдельно рассчитываем волны на поверхности (игнорируем тот факт, что с помощью волн вода может перетекать из объёма в объём).
- Рассчитываем перетекание воды с помощью графа.
Граф водных объёмов
Но, сказать по правде, ничего не получилось. Подводных камней столько, что можно построить парочку пятиэтажек. Например: нужно синхронизировать волны, разбираться с давлением в сообщающихся сосудах и т.д. Но нет худа без добра — именно для реализации этого метода когда-то была написана имплементация region tree, которая сейчас используется повсюду в проекте.
Статика
Раз с совсем динамической водой не получилось, давайте упростим себе жизнь — будем настраивать воду в редакторе, а в для иллюзии динамики оставим волны. По сути, перейдем к варианту №3 из списка вариантов.
Вода — очень противоречивая штука, потому что умеет течь: нельзя сказать "вот в этих точках будет вода", разместить там меши для жидкости и получить хорошую картинку. Ну и ладно, пусть левел дизайнеры (мы в будущем) разместят некие ключевые точки, где точно должна быть вода, а движок выльет её из своих запасов, причем ровно до уровня ключевых точек. Конечно, можно поместить один "якорь" под другим, но это уже проблема не движка, а тех самых неизвестных нам левел дизайнеров: наш код аккуратно заполнит водой уровень до самой верхней ключевой точки.
Ещё одна фишка — отдельные якоря для создания "пузырей". Если алгоритм найдёт такой якорь, он постарается не залить его водой, оставив аккуратную каверну с воздухом.
Теперь, когда есть понимание, какие результаты мы получим, пора разработать алгоритм, не так ли?
Генерация воды
В общем случае, вода может залить весь уровень. Так как стены состоят из прямоугольных кусочков и могут быть расположены где угодно, водный объём — это невыпуклый прямоугольный многоугольник с дырами. Гадость какая. Давайте разбивать на более простые фигуры.
Небольшой спойлер, чтобы было понятнее. В результате препроцессинга мы должны получить некие водные объёмы, в которых просто рассчитывать волны. По сути, один водный объём — это набор водяных столбиков толщиной в один пиксель.
Разберёмся с пустым пространством, в котором может быть жидкость. Великолепное region tree, про которое я рассказывал в прошлых статьях, очень поможет. Получим с его помощью все прямоугольные объёмы, не занятые стенами:
- Начиная с нижнего (изначально — левого) угла дерева ищем первую пустую область сверху.
- Начиная с найденной пустой области, ищем первую непустую область сверху.
- Добавляем полученный отрезок в список.
- Если достигли верхней (в геометрическом смысле) границы дерева, перемещаемся на 1 пиксель вправо.
- Если не достигли правой границы дерева, переходим к пт.1.
- Собираем из списка отрезков (а они довольно удачно отсортированы) прямоугольники.
- Строим связи между смежными прямоугольниками.
Вместо тысячи слов
Скорее всего, можно получить эти прямоугольники куда более красивым способом. Буду рад комментариям на эту тему.
На самом деле, прямоугольники — не самый удачный выбор. Из-за большого количества деталей (например, зубцов на башнях) прямоугольных водных объёмов получится очень много. А это уменьшит производительность. Спокойная вода — плоская, а форма дна нам неважна, поэтому объединим смежные прямоугольники таким образом:
Если у прямоугольника A только одна связь справа (с прямоугольником B), а у прямоугольника B - только одна связь слева (с прямоугольником A) - объединяем эти прямоугольники в одну структуру.
После этого этапа останется небольшое количество водных объёмов с ребристым дном, на рисунке ниже это хорошо видно.
Разметка пустого пространства на карте (со связями)
Теперь нужно найти все метки, оставленные на карте левел дизайнерами и "обрезать" наши заготовки по их уровню. С этим справится примерно такой алгоритм:
- Собираем все метки уровня воды (), сортируем их по y-координате по убыванию значения.
- Для каждой метки:
2.1. Находим прямоугольник, которому принадлежит метка.
2.2. Если прямоугольник не найден, метка — в стене, игнорируем её и переходим к пт. 2.
2.2. Уменьшаем высоту прямоугольника до уровня метки.
2.3. Если высота прямоугольника стала равна нулю — удаляем прямоугольник.
2.4. Находим все смежные прямоугольники, нижний край которых выше метки и удаляем их.
2.5. Находим все прочие смежные прямоугольники, переходим к пт. 2.3 (рекурсивно обрезаем все прямоугольники по уровню воды). - Собираем все метки воздушных пузырей (), сортируем их по y-координате по убыванию значения.
- Для каждой метки:
4.1. Находим прямоугольник, которому принадлежит метка.
4.2. Если прямоугольник не найден, метка — в стене, игнорируем её и переходим к пт. 4.
4.2. Уменьшаем высоту прямоугольника до уровня метки.
4.3. Если высота прямоугольника стала равна нулю — удаляем прямоугольник.
4.4. Находим все смежные прямоугольники, нижний край которых выше метки и удаляем их.
4.5. Находим все прочие смежные прямоугольники, у которых верхний край выше уровня метки, переходим к п 4.3 (рекурсивно обрезаем все прямоугольники по уровню воздушного пузыря).
Как видите, отличие меток воды от меток воздушных пузырей в том, что первые проходят по всем соседям рекурсивно, а вторые останавливаются, когда сосед находится целиком под уровнем пузыря.
А теперь в картинках:
Добавили на карту метки уровня воды
То же самое, но со связями
Обрезали прямоугольники по уровню воды
Удалили лишние связи и прямоугольники
Дебажная визуализация полученных водных объёмов
Несмотря на то, что мы обрезаем прямоугольники по уровню меток, в каждом из них сохраняем массив изначальных высот (y-координату потолка над водой). В дальнейшем эти высоты понадобятся для ограничения волн — в противном случае волны смогут проходить сквозь стены.
Самое время посмотреть, как это выглядит в редакторе:
Динамика
Раз уж мы отказались от текущей в реальном времени воды, давайте хоть волны красивые сделаем. После генерации воды у нас получается примерно такой менеджер для воды:
namespace NewEngine.Core.Water {
public class WaterManager : MonoBehaviour {
// куча настроек для жидкости
// ...
WaterPolygon[] waters;
CombineInstance[] combineInstances = null;
Mesh mesh;
public void Generate() {
if (tree == null)
return;
waters = WaterGenerator.Generate(tree, waveCeil);
Debug.Log("Regenerate water");
}
void FixedUpdate() {
if (waters == null)
return;
var viewportRect = cameraManager.ViewRect;
WaterPolygon.Update(waters, ref combineInstances, viewportRect, /*а тут куча настроек, в виде структуры или просто списком, кому как больше нравится*/);
mesh.Clear();
mesh.CombineMeshes(combineInstances);
}
}
}
Пока что из всего этого кода нас (ну или меня, как рассказчика) интересует только WaterPolygon. Это те самые минимальные кусочки воды, для которых можно строить волны. А еще эти элементы связаны друг с другом (информация о графе осталась доступна в этих полигонах). Если упустить неважные детали, выглядит этот класс так:
namespace NewEngine.Core.Water {
public class WaterPolygon {
// одна "линия" - колонка воды шириной в один пиксель
// хранит свою геометрию (минимальное значение по y, максимальное), свойства пружины (например, текущую скорость)
class Line {
public int min; // y-координата нижнего края воды
public int max; // y-координата верхнего края воды
public int target; // y-координата верхнего края воды, когда жидкость не колеблется
public int height; // y-координата максимального верхнего края воды (потолок, если есть или MAX_WAVE_HEIGHT, если нет)
public float speed;
float lastDelta;
public void Add(float additionalWater);
public void Sleep();
public void Update(float tension, float dampening, float foamThreshold, float foamForce, float foamDampening, float airTemperature, float airTranscalency, float verticalTranscalency, float minBoilTemperature, float minBoilBubble, float maxBoilBubble, float boilFrequency);
}
// координаты по y хранятся в Line, а все линии последовательны, так что нужно хранить только начальную позицию по x
int x;
// соседние водные полигоны; для синхронизации волн и температуры
WaterPolygon[] left;
WaterPolygon[] right;
// сами водные "колонки"
Line[] lines;
// мы не обрабатываяем спящую воду
bool sleeping;
int sleepingFrames;
// кешированные значения для проверки пересечения AABB
int minWaterY;
float maxWaterY;
// обычный меш из UnityEngine
Mesh mesh;
// когда я допишу эту статью, запихну все параметры в аккуратную структуру, честно :)
public static void Update(WaterPolygon[] water, ref CombineInstance[] combineInstances, Geom.IntRect viewportRect, int outsideCameraSleepOffset, float heightSleepThreshold, float speedSleepThreshold, float tension, float dampening, float spread, int steps, float foamThreshold, float foamForce, float foamDampening, float airTemperature, float airTranscalency, float verticalTranscalency, float horisontalTranscalency, float minBoilTemperature, float minBoilBubble, float maxBoilBubble, float boilFrequency, int sleepingUpdateFrames);
// проверка AABB с неким допуском offset
public bool IsOutside(Geom.IntRect viewportRect, int offset);
// первый шаг обновления - фактически, расчет новых скоростей и положений пружин в lines
void UpdateFirst(Geom.IntRect viewportRect, int outsideCameraSleepOffset, float tension, float dampening, int steps, float foamThreshold, float foamForce, float foamDampening, float airTemperature, float airTranscalency, float verticalTranscalency, float minBoilTemperature, float minBoilBubble, float maxBoilBubble, float boilFrequency, int sleepingUpdateFrames);
// второй шаг обновления - обмен скоростями между соседними пружинами, в том числе из полигонов соседей (left и right)
void UpdateSecond(float heightSleepThreshold, float speedSleepThreshold, float spread, int steps, float foamThreshold, float foamForce, float foamDampening, float horisontalTranscalency, float airTemperature, float minBoilTemperature);
// чистка кешей и ненужных данных перед сном
void Sleep(bool withClear);
// поиск ближайшей линии соседа полигона (в зависимости от высоты волны), об этом ниже
void FindLine(bool isRight, out Line line, out WaterPolygon water);
// тут всё говорит само за себя
public void CreateMesh(ref CombineInstance combineInstance);
}
}
Я не буду рассказывать про физику жидкости, потому что я делал волны по одному хорошему туториалу. Если в двух словах: представляем воду в виде связанных пружин, где у пружины известна текущая высота, скорость и оптимальная высота. Пружины колеблются и передают колебания соседям. Ширина пружины в проекте — один пиксель.
Но есть одно важное дополнение. В туториале всего один "водный полигон" в то время как у нас их целый граф. И нужно правильно синхронизировать волны. На примере понятнее:
Допустим, у нас есть вот такой уровень
Он будет состоять из трёх водных полигонов
В какой то момент в синем полигоне возникает волна
Алгоритм находит подходящего соседа, исходя из высоты левой колонки и синхронизирует волну
При очень больших волнах алгоритм выберет другого соседа
Минутка оптимизаций. Нет смысла рассчитывать волны для:
- Полигонов за пределами экрана.
- Полигонов, в которых нет волн.
При очередном расчете волн происходит проверка — не оказались ли все волны меньше порогового значения, и если да — bed time! Если же полигон целиком за пределами экрана — пороговое значение чуть выше. Соответственно, разбудить воду может связный полигон или влияние других модулей (физика, ветер и т.д.).
Пока что у воды нет причин для колебаний. Ничто не может потревожить её покой, ни сражения боевых магов, ни банальный ураган. Холодна, как сердце Морры. Время растопить лёд.
Температура
Несмотря на несколько маниакальную проработку никому не нужных мелочей, не очень хочется потратить на реалистичную термодинамику ещё полгода разработки. Так что упростим донельзя. И ещё чуть-чуть.
Начнём с погодного менеджера. Когда-нибудь в нем будут и бури и штиль, но сейчас — только это:
namespace NewEngine.Core.Weather {
public class WeatherManager : MonoBehaviour {
[SerializeField, Range(-100, 200)] float airTemperature;
public float AirTemperature {
get {
return airTemperature;
}
}
}
}
Концепция такова:
- Температура воды изначально равна температуре воздуха.
- При температуре ниже 0 °C вода замерзает.
- При температуре в 100 °C вода закипает.
- Водные объёмы обмениваются температурой друг с другом.
- Водные объёмы обмениваются температурой с атмосферой, при этом учитывается объём (в геометрическом смысле) атмосферы.
- Оптимизации и деоптимизации:
6.1 Кипящая вода никогда не спит.
6.2. При "пробуждении" воды происходит упрощенный расчёт изменений температуры жидкости за время сна.
Температуру для WaterPoligon.Line будем хранить только для верхнего и нижнего концов водного столба.
class Line {
public int min;
public int max;
...
public float minTemperature;
public float maxTemperature;
...
}
Крайне неточный способ — ведь у всех столбов разная высота, но нам подойдёт.
Сначала была идея разбивать столбы на некоторое количество кусочков и рассчитывать передачу тепла между этими кусочками (как в одном столбе, так и между соседними). В этом случае можно было бы делать "слоистую воду" — лёд/вода/лед, что невозможно в текущей реализации.
Передача температуры осуществляется в трёх разных "направлениях". Самое первое и очевидное — между краями водного столба:
if (length > 0) {
...
float avgTemperature = (maxTemperature + minTemperature) * 0.5f;
float ratioTranscalency = verticalTranscalency / length;
maxTemperature = maxTemperature + (avgTemperature - maxTemperature) * ratioTranscalency;
minTemperature = minTemperature + (avgTemperature - minTemperature) * ratioTranscalency;
}
Второе — более интересное. Теплообмен между воздухом и верхним краем столба. При генерации воды мы получали height, фактически, координату потолка. А значит, в любой момент времени мы можем получить высоту воздушной прослойки над водой. Чем больше воздуха над водой, тем быстрее происходит теплообмен. Спорное утверждение. Зато на открытом воздухе вода будет остывать/разогреваться быстрее, чем в невысоких пещерах:
float length = height - min;
if (length > 0) {
if (height < max) {
float airVolume = Mathf.Min(max - height, MAX_AIR_VOLUME);
maxTemperature = maxTemperature + (airTemperature - maxTemperature) * Mathf.Clamp01(airTranscalency * airVolume);
}
...
}
Остается теплообмен между соседними столбиками. Так как информация о температуре хранится только на краях столбов, линейная интерполяция выходит на сцену.
Для любителей картинок
static void UpdateTemperatureDelta(float horisontalTranscalency, float[] minTemperatureDelta, float[] maxTemperatureDelta, int i, Line line, Line other) {
if (line.height <= line.min || other.height <= other.min) {
minTemperatureDelta[i] = 0;
maxTemperatureDelta[i] = 0;
return;
}
float height = line.height - line.min;
float otherHeight = other.height - other.min;
if (Mathf.Max(line.height, other.height) - Mathf.Min(line.min, other.min) >= height + otherHeight) {
minTemperatureDelta[i] = 0;
maxTemperatureDelta[i] = 0;
return;
}
float minY = Mathf.Max(line.min, other.min);
float maxY = Mathf.Min(line.height, other.height);
float minT = (minY - line.min) / height;
float maxT = (maxY - line.min) / height;
float otherMinT = (minY - other.min) / otherHeight;
float otherMaxT = (maxY - other.min) / otherHeight;
float minTemperature = Mathf.Lerp(line.minTemperature, line.maxTemperature, minT);
float maxTemperature = Mathf.Lerp(line.minTemperature, line.maxTemperature, maxT);
float otherMinTemperature = Mathf.Lerp(other.minTemperature, other.maxTemperature, otherMinT);
float otherMaxTemperature = Mathf.Lerp(other.minTemperature, other.maxTemperature, otherMaxT);
float ratio = horisontalTranscalency * Mathf.Clamp01(height / otherHeight);
minTemperatureDelta[i] = ratio * (minTemperature - otherMinTemperature);
maxTemperatureDelta[i] = ratio * (maxTemperature - otherMaxTemperature);
}
Обмен температурой похож по структуре на обмен волнами, но волна передаётся только одному соседнему WaterPolygon'у с каждой стороны, а теплота — каждому из соседей.
И последний штрих — кипение и замерзание воды. Если температура выше или равна 100 °C — добавляем случайную скорость пружины к столбу жидкости, причем чем выше температура — тем больше разброс скоростей (на самом деле, не ровно 100 °C, вода начинает пузыриться чуть раньше).
Ну а если температура ниже или равна 0 °C — сохраняем скорость пружины равной нулю и перестаём реагировать на передачу скоростей от соседних столбиков-пружин.
Или, другими словами:
if (height < max) {
if (maxTemperature >= minBoilTemperature) {
float depth = target - min;
float ratio = Mathf.Clamp01((maxTemperature - minBoilTemperature) / (100 - minBoilTemperature));
float heightValue = Mathf.Min(depth * 2, Mathf.Max(minBoilBubble, ratio * maxBoilBubble));
float frequency = Mathf.Lerp(0, boilFrequency, ratio * ratio);
if (Random.value > 1 - frequency)
height += Random.Range(-heightValue, heightValue);
}
}
if (maxTemperature <= 0) {
speed = 0;
return;
}
Постепенное закипание воды
Заключение и интрига
После всего вышеописанного в проекте появилась вода, которую можно "вылить" на уровень в редакторе. Волнующаяся, кипящая, твердеющая на морозе живительная влага!
Было бы обидно получить некрасивую картинку, написав столько кода. Отражения, преломления, каустика — наше всё! А значит, снова толстые шейдеры, взаимодействие с системой освещения, рендер в текстуру и всё такое.
Но об этом — в следующей статье. :)
Комментарии (36)
maaGames
19.01.2017 14:24+2Просто безумие! Кипячение добавляет прикольные игровые возможности. Как в инкрадибл машинз, замутить сложное взаимодействие объектов для прохождения уровней в каком-нибудь рпг-платформере… Вообще крутотень, Жаль я такое запрограммировать не осилю.)
nightrain912
19.01.2017 14:31+1Да! Там действительно очень много забавных взаимодействий — и с кипением и с замерзанием. Из моих любимых идей:
- С другой стороны воды игрока обстреливают из луков.
- Стреляем в воду любой магией с большой ударной волной, например, файрболом (в воде поднимаются крупные волны).
- Стреляем в воду любой замораживающей магией.
- Прячемся за стеной льда.
И что самое приятное — каждый новый модуль добавляет (если это прорабатывать) не 1 взаимодействие, а N — 1 :)
maaGames
19.01.2017 14:35+1Найти «кипятильник», чтобы вскипятить воду и убить плавающего в ней чудовище. А горячими парами от кипящего озера воздушный шар поднимет игрока на следующий уровень. Конечно, в данной постановке это всё без проблем скриптуется, но «реалистичная физичность» будет круче.)
nightrain912
19.01.2017 14:37Да, вот только всему этому придется игроку учится самому — слишком много взаимодействий. Но для уровней песочниц — идеально.
AutumnMelancholy
19.01.2017 14:54Когда уже готово будет?)
nightrain912
19.01.2017 14:55+3Ох, если бы я знал. )
Очень много кода и материала, причем приходится постоянно проводить research и иногда переписывать нафиг куски кода. Тут, похоже, процесс важен не менее, чем результат.
xfishbonex
19.01.2017 15:48+1Что интересно, видео называется не “water 29 best”. Можно ждать часть про траву?
nightrain912
19.01.2017 15:49Вы еще на дату публикации видео посмотрите (
Да, по сути, планируется минимум про:
- Воду (графика)
- Траву и систему частиц
- Статья по оптимизации всего этого дела
Может, в процессе разработки еще что появится, например, ии.
norlin
19.01.2017 16:02Постепенное закипание воды
Наверняка вы в курсе, но там баг с закипанием правого колодца (на видео примерно с 0:40). По логике, он же должен снизу вверх нагреваться, а получается, что весь его объём слева направа греется.
А вообще очень круто это всё! Я вот только начинаю геймдев осваивать, и, представляя хотелки на свою игру, прихожу в ужас, когда вижу, сколько работы требуется для подобных деталей...
nightrain912
19.01.2017 16:20Да, выше в комментариях писали про похожую ситуацию — в идеале горизонтальный перенос температуры должен быть быстрее вертикального, да еще и конвекцию нужно учитывать. На самом деле, при прозрачной воде этого и не заметно, но если будет влиять на геймплей, придется доработать.
tzlom
19.01.2017 16:37Как раз такой вариант ближе к реальному — тёплый поток устремится вверх, правда отогреваться будет не стеной а скорее сверху вниз, но в вашей модели воды такое не реализовать. В местах где кипящая вода примыкает к потолку визуальный баг высокой плоской волны, неужели пружина отскакивает от потолка?
nightrain912
19.01.2017 17:21Скорее косяк суммирования скоростей, спасибо, я как-то не заметил это.
Думаю, довольно просто пофиксить будет.
uLow
20.01.2017 11:44+2А что это у Вас в заглавном видео дергается примерно между 1 и 2 секундой (там, где освещение на уровне земли, где какие-то две веточки прямо из фонарей на потолке выходят)?
При полной скорости просмотра даже и не заметно. Поставьте 0,25хuLow
20.01.2017 11:52А на последнем видео не понятно, откуда появляются прямоуголные впадины на стыках с углами? Лучше всего разгледеть в стковке второго водоёма со свисающим куском башни
nightrain912
20.01.2017 11:53Прямоугольные впадины в воде при колебаниях? Какая-то ошибка в коде :)
uLow
20.01.2017 11:59+2Очень похоже на отрицательную высоту волны (или даже скорее уровня воды в определенном месте) при нормализации.
nightrain912
20.01.2017 12:29Там немножко более дурацкая ситуация:
При расчете волн у каждой пружины есть текущая скорость. И каждое обновление на эту скорость влияют 2 фактора:
- Затухание колебаний (неинтересно нам)
- Добавление ускорения
А ускорение — рассчитывается как разница между y-координатой текущей пружины и соседних (+некие коэффициенты). Обычно все ок — т.к нужно выровнять воду к одному уровню.
Но тут у пары пружин максимальная y-координата ниже, чем у соседей (т.к в стену упирается). И поэтому кадра 3-4 при расчете ускорения получается так, что y-координата пружины по-прежнему ниже соседей и в сумму получается нехилое ускорение. Из-за него и происходит такое "проседание".
В общем, хороший вопрос — как это корректно исправлять :)genagen
21.01.2017 01:09Есть несколько вопросов по вашему проекту, но слишком много, чтобы обсуждать их здесь. Возможно ли с вами связаться? Мой вк: https://vk.com/nikgen
nightrain912
21.01.2017 01:09Если честно, я буду очень рад обсуждению именно здесь — это полезнее и для меня и для хабровчан :)
nightrain912
20.01.2017 11:52Хорого, что заметили: там все фонари болтаются на "цепях", которые всего-лишь перекрашенная трава, чересчур гибкая. Поэтому на пкрвых кадрах источники света резко сносит в сторону.
semenyakinVS
20.01.2017 15:38+1Напомнило powder toy. Не пробовали искать исходники или связываться с авторами сего чуда? Возможно, какими-то идеями взаимно обогатиться вышло бы.
nightrain912
20.01.2017 15:54+1Посмотрел (мельком, если честно) игрушку — всё-таки у нас очень разные подходы. У автора очень реалистичная и цельная штука, в то время как у меня модульная (т.е не один общий набор "законов", а много независимых взаимодействующих систем). Плюс у меня не так "честно" всё считается — хаки на каждом ходу, т.к. красивая картинка — одна из основных целей. :)
semenyakinVS
20.01.2017 20:04+1Ясно. Ну, тут вам виднее… Вообще, я вспомнил powder toy во многом потому, что подход этой штуки к расчёту теплообмена напомнил отдалённо ваш подход. Всегда казалось очень интересным используемое в powder toy совмещение клеточного автомата (для обработки фишек, связанных с давлением) и физики частиц (собственно, для частиц). Подумал что именно эта концепция могла бы вам пригодиться.
Помню, читал где-то (чуть ли не тут тоже) статью про расчёт Навье-Стокса со смешиванием цветом на сетчатой модели в реальном времени (всё это в толстом шейдере на видеокарте). Выглядело весьма впечатляюще.marsermd
20.01.2017 20:57+1Сейчас это обычно делают все же сглаженными частицами. Но да, такой подход тоже существует. Для 2d симуляция физики жидкости — давно довольно легкая(в вычислительном плане) задача.
DanielDan0
21.01.2017 19:38+1Можно сделать чтобы из кипящей воды вылетали капли. А еще можно сделать в ней пузыри. Можно сделать чтобы толщина льда зависела от температуры воздуха. И чтобы во льду замерзали пузыри.
nightrain912
21.01.2017 19:41+1Капель скорее всего, не будет, а вот пузырьки в кипящей воде — в разработке :)
Толщина льда не сможет зависеть от температуры воздуха, как в реальности: т.к. в реальности вода редко промерзает до дна, а у меня вполне может, за некоторое время (течений ведь нет).
Кстати, про пузыри во льду — шейдер для льда основная проблема, по которой задерживается написание следующей статьи — никак не могу придумать, как должен выглядеть прозрачный лёд так, чтобы при этом была видна граница вода/лёдDanielDan0
21.01.2017 20:20+2Можно сделать его как воду, но белее и матовым. Капли будут частицами, которые летят по параболической траектории обратно в воду. И как нельзя сделать толщину льда? От температуры, площади поверхности и высоты нахождения воды будет зависеть соотношение льда и воды. Например, в сильный мороз будет у какого-то озера 20% льда и 80% воды. А лужица замёрзнет полностью. Кстати, лаву собираетесь делать? Как может обойтись неприступная крепость без рва с лавой и карпами?
nightrain912
21.01.2017 22:52+2С каплями все просто — примерный размер игрока сейчас — 8 пикселей. По правде говоря, не хотел бы я находится рядом с кипящей водой, где брызги размером с мою голову. :)
А если серьёзно, после доработки системы частиц попробую, и если окажется классно — напишу вам в личку большое спасибо.
Про воду и лёд. Тут все достаточно просто — даже если исключить течения, конвекцию и т.д. в реальном мире возможны ситуации, в которых и вода не замерзает ещё и лёд уже толком не тает. Метастабильность.
А у меня эти фишки принесены в жертву геймплею: из-за большой теплопроводности жидкости лед всё-таки достаточно быстро достигает дна. А если уменьшать теплопроводность, будет неинтересно использовать огненную и ледяную магию.
Про лаву. Я уже пробовал колдовать с настройками жидкости, можно сделать довольно тягучую и медленно пузырящуюся субстанцию. Не могу сказать, что лава в приоритетных планах, но хочется. :)
sen77
Если добавить конвекцию, будет намного реалистичней. Уж очень бойко идет прогрев воды вниз.
nightrain912
Хм, можно подработать формулы так, чтобы передача теплоты вниз была медленнее, чем вверх.
sen77
… во много раз медленней (х50 — х100). Прогрев вверх идет за счет конвекции, а вниз только за счет теплообмена, а вода — плохой проводник тепла.
nightrain912
Тогда возникает вопрос — если сделать это физичным — не потеряет ли в фановости? )
Но в любом случае, можно запилить и потом править коэффициенты.
sen77
по моему это даже бОльшие возможности дает:
— хочешь закипятить все — прогреваешь с самого низу;
— хочешь, закипятить только одну комнату — делаешь источник тепла только в/под ней.
Также на одном уровне смогут ужиться и лед и кипящая вода.
Durimar123
Не думаю, что это важно — игроки не будут видеть температурное распределение в реалтайме, но будут видеть, что изменение температуры в точке А, изменит воду и в удаленной точке Б.
А этого, имхо, вполне хватит для игр не заточенных на реал физике.