Демо: плазма
Эффект плазмы (или попросту плазма) - эффект часто использующийся в демках, чтобы создать переливающийся эффект. Есть много способов создать анимацию плазмы, но я рассмотрю только один: сумма синусов и некоторых других функций. Экспериментируя с параметрами и пробуя различные функции, а так же цвета, можно найти очень круто выглядящие комбинации. Оригинал этой статьи можно найти здесь. В ней примеры указаны с использованием С, я же делал все на HTML5 (JavaScript + canvas). Примеры довольно просты и перенести на любой другой язык будет несложно. Внутри около 5 мегабайт картинок, если кого-то это беспокоит.
Перед тем, как мы приступим к собственно созданию плазмы, давайте рассмотрим функцию, которая нам в этом поможет. Значения функции синус лежат в промежутке от -1 до +1, выглядит вот так:
Для наших целей мы отобразим ее немного иначе, а именно будем устанавливать цвет пикселя равным синусу его x-координаты:
Белые полосы это места, где синус близок или равен единице, черные, соответственно, близко к -1. Такого эффекта можно добиться следующим кодом:
for (y = 0; y < canvas.width; y++) {
for (x = 0; x < canvas.height; x++) {
color = 128 + Math.floor(128 * Math.sin(x / 4));
context.fillStyle = rgbString(color, color, color);
context.fillRect(x, y, 1, 1);
}
}
Мы проходим через каждый пиксель, расчитываем его цвет на основе синуса, и выводим. Значение цвета должно быть в пределах от 0 до 256 (0x00 - 0xFF), а так как синус всегда имеет значение между -1 и 1, мы преобразуем его в 0-256, умножив на 128 и прибавив 128. X делим на 4 чтобы значение функции менялось медленнее. Если разделить на меньшее значение, то полосы станут уже:
Если использовать синус от суммы sin((x + y) / 8.0), то линии будут скошенными:
Другая интересная функция для плазмы это синус расстояния от пикселя до определенной точки. Расстояние от пикселя до точки отсчета (0, 0) равняется sqrt(x * x + y * y). А расстояние от пикселя до центра экрана (холста): sqrt((x - w / 2) * (x - w / 2) + (y - h / 2) * (y - h / 2)). Если взять от этого синус, то получится:
Вот код, который генерирует такую картинку (в этом примере, я перенес точку координат в центр изображения, для простоты [charnad]):
for (y = -cY; y < cY; y++) {
for (x = -cX; x < cX; x++) {
color = 128 + Math.floor(128 * Math.sin(Math.sqrt(x*x + y*y) / 8));
context.fillStyle = rgbString(color, color, color);
context.fillRect(x, y, 1, 1);
}
}
Складывая эти функции (с различными коэффициентами, точками отсчета, и т.д.), и используя лучшую цветовую палитру, чем оттенки серого, которые мы использовали до сих пор, мы можем создавать крутую плазму. Для разогрева, вот плазма сгенерированная суммой двух функций:
Несмотря на то, что выглядит не очень круто, это уже настоящая плазма. Она получается из суммы синусов x и y. Код ничуть не сложнее предыдущих примеров, но для ясности я расписал формулу на несколько строк. Также, в конце результат делится на два, таким образом мы остаемся в пределах 0-255:
for (y = 0; y < canvas.height; y++) {
for (x = 0; x < canvas.width; x++) {
color = 128 + Math.floor(128 * Math.sin(x / 8));
color += 128 + Math.floor(128 * Math.sin(y / 8));
color = Math.floor(color/2);
context.fillStyle = rgbString(color, color, color);
context.fillRect(x, y, 1, 1);
}
}
Чтобы получить и цвета и анимацию, мы будем использовать цветовую палитру (параметр "цвет" будет использоваться как индекс для выбора цвета из палитры), и каждый фрейм будем сдвигать палитру на небольшое значение, так что все цвета палитры будут проходить по кругу. В таком режиме нам нужно будет нарисовать плазму только один раз и дальше только сдвигать палитру. Так как цвета меняются по кругу, палитра не должна быть "прерывистой", и заканчиваться должна тем же цветом, что и начинаться. Следующие две "прерываются" и не могут быть использованы для плазмы, потому что тогда анимация будет рваной или перескакивать:
А вот эта палитра может быть использована:
Теперь чтобы у нас получился эффект плазмы: создаем палитру, создаем буфер, где будут хранится значения вычисленной функции синусов для разных пикселей, рисуем пиксели с цветом из палитры, смещаем палитру каждый фрейм. Плазма сама по себе такая же, как и в предыдущем примере, но использование палитры делает ее гораздо интереснее. Вот упрощенная часть кода, которая реализует этот алгоритм:
// Просчитываем значения цвета, как и в предыдущих примерах
for (y = 0; y < canvas.height; y++) {
data[y] = [];
for (x = 0; x < canvas.width; x++) {
color = 128 + Math.floor(128 * Math.sin(x / 8));
color += 128 + Math.floor(128 * Math.sin(y / 8));
color = Math.floor(color/2);
data[y][x] = color;
}
}
// Далее в цикле отрисовываем
// n - сдвиг палитры
for (y = 0; y < data.length; y++) {
for (x = 0; x < data[y].length; x++) {
color = palette[Math.floor(data[y][x] + n) % 360];
context.fillStyle = color;
context.fillRect(x, y, 1, 1);
}
}
и это дает нам примерно следующую картину:
Вторая картинка показывает ту же плазму, но с упрощенной палитрой состоящей всего из 2х цветов, чтобы можно было лучше проследить движение цвета.
Теперь, если в коде генерации плазмы мы заменим функцию на сумму 4 синусов, один из которых синус расстояния до верхнего левого угла картинки, то получим:
color = 128 + (128 * Math.sin(x / 16));
color += 128 + (128 * Math.sin(y / 8));
color += 128 + (128 * Math.sin((x+y) / 16));
color += 128 + (128 * Math.sin(Math.sqrt(x*x+y*y) / 8));
color = Math.floor(color/4);
И вот, что еще можно попробовать:
color = 128 + (128 * Math.sin(x / 16));
color += 128 + (128 * Math.sin(y / 32));
color += 128 + (128 * Math.sin(Math.sqrt(Math.pow(cX-x, 2)+Math.pow(cY-y, 2)) / 8));
color += 128 + (128 * Math.sin(Math.sqrt(x*x+y*y) / 8));
color = Math.floor(color/4);
Как видите, можно менять параметры и функции, пока не будет достигнут желаемый результат. Давайте теперь попробуем другие палитры.
for (i = 0; i < 256; i++) {
r = Math.floor(128.0 + 128 * Math.sin(3.1415 * i / 32.0));
g = Math.floor(128.0 + 128 * Math.sin(3.1415 * i / 64.0));
b = Math.floor(128.0 + 128 * Math.sin(3.1415 * i / 128.0));
palette[i] = rgbString(r, g, b);
}
Это все та же плазма, но уже с новой палитрой. Чтобы цвета не "перескакивали", палитра должна начинаться и заканчиваться одним и тем же цветом.
Следующая палитра дает довольно крутой эффект (хотя это дело вкуса):
for (i = 0; i < 256; i++) {
r = Math.floor(128.0 + 128 * Math.sin(3.1415 * i / 16.0));
g = Math.floor(128.0 + 128 * Math.sin(3.1415 * i / 128.0));
b = 0;
palette[i] = rgbString(r, g, b);
}
Наконец, вместо использования палитры и ее смещения, можно так же пересчитывать синусы каждый фрейм, каждый раз меняя значения, используя время в функции. Можно генерировать RBG-составляющие отдельно. Это все в сумме довольно много вычислений, так что плазма может тормозить. Но упомянуть об этом стоит. Каждый фрейм, для каждого пикселя расчитывается сумма 4х расстояний. Затем, из полученного значения вычисляется цвет, и он же используется чтобы создать R, G и B составляющие цвета этого пикселя. Из-за такого способа подсчета цвета картинка получится немного пикселизированной.
for (y = 0; y < canvas.height; y++) {
for (x = 0; x < canvas.width; x++) {
color = Math.sin(dist(x + time, y, 128.0, 128.0) / 8.0)
color += Math.sin(dist(x, y, 64.0, 64.0) / 8.0)
color += Math.sin(dist(x, y + time / 7, 192.0, 64) / 7.0)
color += Math.sin(dist(x, y, 192.0, 100.0) / 8.0);
color = Math.floor((4 + color)) * 32;
color = rgbString(color, color * 2, 255 - color);
context.fillStyle = color;
context.fillRect(x, y, 1, 1);
}
}
Можно так же расчитывать отдельную плазму для каждого цветового канала и т.д. Возможности безграничны, но это все будет медленнее, чем смещение палитры, особенно с неоптимизированными алгоритмами, которые приведены тут.