Фрактальное пламя (или фрактальные искры, англ. fractal flame) – алгоритм, предложенный Скоттом Дрейвсом (Scott Draves) и использующий для построения изображений системы итерируемых функций (СИФ). Благодаря разным значениям seed для генератора псевдослучайных чисел можно получить множество разнообразных «картин». Хотя фрактальность в них просматривается далеко не всегда, результаты получаются очень интересными.
Под катом – краткое описание основных моментов реализации алгоритма.
Для начала, как и в обычных СИФ, нам понадобится узнать коэффициенты для каждого аффинного преобразования плоскости (их может быть несколько; каждое следующее будет вносить свои «мазки» на картину, а также изменять вклад предыдущих). В матричной форме это преобразование выглядит следующим образом:
Необходимо подобрать такие коэффициенты, чтобы полученное преобразование было сжимающим, то есть таким, что его коэффициент масштабирования меньше единицы. Этих преобразований должно быть несколько, и если выбирать случайным образом одно из них, чтобы вычислить новые координаты точки и отобразить ее на экране, мы получим аттрактор — множество точек, из которых и будет состоять изображение.
Используя датчик псевдослучайных чисел, получить такие коэффициенты несложно. При этом надо проверить 3 условия:
Все это относится к коэффициентам, задающим линейное преобразование. Оставшиеся два, и , выполняют трансляцию – перемещение точки на некоторое расстояние. Желательно, чтобы , , и находились на отрезке или . Для и это необязательно, однако не стоит задавать слишком большой отрезок, иначе изображение получится разреженным.
Кроме того, вместе с каждым набором коэффициентов, нужно сохранить стартовые значения трех цветовых составляющих модели RGB, которые будут присваиваться пикселю, в который попали первый раз. Это также делается с помощью датчика случайных чисел и особых трудностей не представляет.
Теперь необходимо определиться, изображение какого разрешения хотим получить и подготовить массив пикселей, каждый элемент которого будет хранить:
- координаты x и y;
- значения R, G и B;
- число попаданий.
Далее приведены некоторые нелинейные преобразования, которые будут выполняться над значениями x и y, полученными после выполнения одного из аффинных преобразований:
- Синусоидальное: , ;
- Сферическое: , ;
- Полярное: , ;
- Сердце: , ;
- Диск: , ;
В конце статьи – примеры изображений, полученных с использованием этих и некоторых других преобразований, а далее – псевдокод процедуры рендеринга:
void render(int n, int eqCount, int it, int xRes, int yRes)
{
Генерируем eqCount аффинных преобразований со стартовыми цветами;
for(int num=0; num<n; num++)
{
//Для изображения размером 1920х1080 можно
//взять XMIN=-1.777,XMAX=1.777,YMIN=-1,YMAX=1
//В этом случае в большинстве нелинейных преобразований с боков не будет оставаться черных областей
newX=Rand(XMIN,XMAX);
newY=Rand(YMIN,YMAX);
//Первые 20 итераций точку не рисуем, т.к. сначала надо найти начальную
for(int step=-20; step<it; step++)
{
//Выбираем одно из аффинных преобразований
i=Rand(0,eqCount);
//и применяем его
x=coeff[i].a*newX+coeff[i].b*newY+coeff[i].c;
y=coeff[i].d*newX+coeff[i].e*newY+coeff[i].f;
Применяем нелинейное преобразование;
if(step>=0 && newX∈[XMIN,XMAX] && newY∈[YMIN,YMAX])
{
//Вычисляем координаты точки, а затем задаем цвет
x1=xRes-Trunc(((XMAX-newX)/(XMAX-XMIN))*xRes);
y1=yRes-Trunc(((YMAX-newY)/(YMAX-YMIN))*yRes);
//Если точка попала в область изображения
if(x1<xRes && y1<yRes)
{
//то проверяем, первый ли раз попали в нее
if(pixels[x1][y1].counter==0)
{
//Попали в первый раз, берем стартовый цвет у соответствующего аффинного преобразования
pixels[x1][y1].red=coeff[i].red;
pixels[x1][y1].green=coeff[i].green;
pixels[x1][y1].blue=coeffs[i].blue;
} else
{
//Попали не в первый раз, считаем так:
pixels[x1][y1].red=(pixels[x1][y1].red+coeff[i].red)/2;
pixels[x1][y1].green=(pixels[x1][y1].green+coeff[i].green)/2;
pixels[x1][y1].blue=(pixels[x1][y1].blue+coeff[i].blue)/2;
}
//Увеличиваем счетчик точки на единицу
pixels[x1][y1].counter++;
}
}
}
}
}
Итак, теперь уже можно вывести наше изображение, однако результат в этом случае получится слишком зашумленным, так как яркость всех пикселей одинакова. Чтобы это исправить, надо провести логарифмическую и гамма-коррекцию, отразив число попаданий в каждую точку с помощью яркости. Ниже приведен код, выполняющий коррекцию:
void correction(int xRes, int yRes)
{
max=0.0;
gamma=2.2;
for (int row=0; row<xRes; row++)
for (int col=0; col<yRes; col++)
if (pixels[row][col].counter != 0)
{
pixels[row][col].normal=log10(pixels[row][col].counter);
if (pixels[row][col].normal>max)
max = pixels[row][col].normal;
}
for (int row=0; row<xRes; row++)
for (int col=0; col<yRes; col++)
{
pixels[row][col].normal/=max;
pixels[row][col].red =pixels[row][col].red*pow(pixels[row][col].normal,(1.0 / gamma));
pixels[row][col].green =pixels[row][col].green*pow(pixels[row][col].normal,(1.0 / gamma));
pixels[row][col].blue =pixels[row][col].blue*pow(pixels[row][col].normal,(1.0 / gamma));
}
}
Разница между изображением без коррекции (выше) и с коррекцией (ниже)
Ну а теперь обещанные изображения с различными нелинейными преобразованиями
Синусоидальное
Сферическое
Полярное
Сердце
Диск
«Ракушка»
Гиперболическое
Водоворот
Волны
Сферическое
Полярное
Сердце
Диск
«Ракушка»
Гиперболическое
Водоворот
Волны
А что если применять не одно и то же нелинейное преобразование, а случайным образом выбирать из нескольких?
Исходный код
Реализация алгоритма от Джеймса Маккарти на языке Си находится здесь, а статья Скотта Дрейвса на основе которой и создавался алгоритм — здесь.