Другие материалы: симуляция огня, движение сквозь звёзды
Если вы играли в Quake, то видели, какая там вода: это просто текстура, которая волнообразно искажается, симулируя эффект движения частиц воды.
Именно анимация делает более убедительной и заметной воду, а также другие жидкости, которые можно найти в Quake: лаву, болотную жижу. Немного изменяя параметры анимации, можно имитировать вязкость жидкости, чтобы, скажем, лава была больше похожа на лаву.
Конечно, современные 3D-игры моделируют воду в разы реалистичнее, чем старый Quake. Однако эффект волнообразного искажения по-прежнему можно использовать в простых играх, и не обязательно в 3D, и не обязательно для воды.
Итак, что нужно сделать, чтобы получить эффект жидкости? Когда мы смотрим на анимированную воду, то видим, что каждый пиксел текстуры движется по определенной траектории. Значит, нужна некоторая функция (в математическом смысле), которая преобразует две координаты (x, y) в две другие координаты (x1, y1).
(x1, y1) = f(x, y)
Применив эту функцию для каждого пиксела в текстуре, мы заставим пикселы двигаться. Конечно, перемещать пикселы физически не нужно. Нужно только получить цвет пиксела по координатам (x1, y1) и записать этот цвет в пиксел (x, y). То есть вместо своего собственного цвета пиксел получает цвет какого-то другого пиксела, который как будто переместился на его место.
Я нашел одну не очень хорошую, зато свою текстуру воды размером 256*256 пикселов:
Теперь нужно написать цикл обхода каждого пиксела текстуры и присвоения ему цвета из вычисленных функцией координат:
Это пока чисто условный код. На что следует обратить внимание:
- Я читаю значение пиксела из текстуры, обозначенной как массив texture, и записываю его в массив buffer. Текстура является исходным множеством пикселов, над которым производится преобразование, но сама она не должна меняться.
- Я использую не одну, а две функции преобразования координат: distort_x() и distort_y(). Так проще, чем программировать функцию, которая возвращает сразу два значения. Кроме того, я могу сделать разные функции для x и y, и значит применить более хитрые эффекты.
Осталось только сделать функцию искажения. Какие идеи? Давайте начнём с самого простого искажения: движения по прямой. Если к каждой координате y просто прибавлять какое-то число, то получится, что текстура движется по вертикали. Теперь вспомним о том, что нужно волнообразное искажение. Само слово "волнообразное" подсказывает решение – нужно прибавлять к координате не постоянное смещение, а такое, которое волнообразно меняется. Для этого у нас есть очень подходящая функция, а именно синус!
График синуса (чёрная линия) можно рассматривать как хранилище смещений, то есть в каждой позиции x хранится смещение по y. Cдвинув каждый столбец текстуры в соответствии с этими смещениями, мы и получим волнообразное искажение. Обратите внимание, что график построен так, чтобы его конец соответствовал его началу. Таким образом он зацикливается и может повторяться сколько угодно раз.
Чтобы не считать синус каждый раз, ничто не мешает заранее сохранить смещения в массив длиной 256 байт и потом просто пользоваться им:
Остаётся ещё одна проблема: если посмотреть на волнообразно искажённую текстуру, то видно, что с одной стороны у неё выступают горбы, а с другой наоборот образуются впадины. Но текстура квадратная и не может иметь такую форму. Решение тут простое. Все пикселы, которые выходят за пределы текстуры, должны заходить в неё с противоположной стороны. Тогда не будет ни выступов, ни впадин.
Математически это делается так. Если координата выходит за пределы текстуры (в нашем случае 256), то нужно отнять от неё 256, и она станет новой актуальной координатой.
Чтобы не писать проверку на 256, я просто использую битовую операцию "и" с 255, то есть остаток от деления на 256:
var y1 = (y + WAVE_STRENGTH + sinus[x]) & 255;
Вода уже практически готова. Теперь то же самое надо сделать по второй оси, то есть сдвинуть ещё и горизонтальные линии по такому же волнообразному графику. Собственно, вот и функция для вычисления (x1, y1):
var y1 = (y + WAVE_STRENGTH + sinus[x]) & 255;
var x1 = (x + WAVE_STRENGTH + sinus[y]) & 255;
Осталось только анимировать текстуру. Для этого нужно в каждом следующем кадре анимации сдвигать график. Тогда для одних и тех же координат будут получаться разные значения графика. Но график записан в массив, поэтому чтобы не двигать его, я просто двигаю текущий адрес внутри массива. Адрес увеличивается в каждом кадре, а когда доходит до конца массива, то начинает опять с нуля. Я опять же использую для этого битовую маску:
sinus_pos = ++sinus_pos & 255;
Вы можете посмотреть работу программы прямо в браузере по этой ссылке.
Чтобы посмотреть исходный код, нажмите Ctrl-U.