Я сделал это! Это огромный повод для гордости. Теперь программа работает по тем же математическим принципам, что и самые передовым фрактальным в мире! Но в самый первый я скажу вот:
Благодарности
Этот проект использует передовые математические алгоритмы и идеи динамического управления фазой орбит, разработанные фрактальным сообществом. Особая благодарность авторам и исследователям с Fractal Forums, чей совместный труд лег в основу этого движка:
-
Kevin Martin — автор фундаментальных методов векторизации и оптимизации циклов возмущений.
-
Zhuoran Yu — разработчик концепции динамического сброса орбит.
-
Claude Heiland-Allen — исследователь экстремального фрактального приближения и создатель проекта MDZ.

Ключевые особенности:
-
Расчёт опорной траектории на 5000 бит всего один раз.
-
Реактивный расчёт миллионов пикселей на аппаратном double.
-
Революционный алгоритм Reference Reset to Zero.
-
Настоящий SSAA 8×8 для идеально сглаженного изображения без алиасинга.
-
Параллелизм OpenMP для высокоскоростного многопоточного рендеринга.
Безграничная точность (Arbitrary Precision Arithmetic)
Движок полностью избавлен от аппаратных ограничений 64-битных (double) и 128-битных (__float128) чисел, которые неизбежно слепнут и выдают пиксельные квадраты на глубинах более 10 − 15 и 10 − 34.
-
Интеграция MPFR/GMP: Вся высокоточная навигация, пересчёт масштаба при кликах мыши и движении стрелочками клавиатуры ведутся внутри сверхглубокой бинарной памяти с точностью 5000 бит!
-
1000 чистых знаков в текстовом кэше: Координаты кадра сохраняются и считываются из файла
Mandelbrot.txtв виде идеально точной текстовой строки из 1000 десятичных цифр после запятой, что позволяет исследовать глубокие структуры на масштабах до 10 − 1000 и глубже.
Реактивный метод возмущений (Perturbation Theory)
Рендеринг глубоких фракталов больше не требует тяжелых вычислений <в столбик> для каждого пикселя, что обычно замедляло программы в тысячи раз.
-
Однократный расчёт опоры: Сверхтяжелый BigFloat-радар MPFR вычисляет точную траекторию всего для одной-единственной центральной точки кадра и строго ОДИН раз в начале рендеринга.
-
Аппаратное ускорение на double: Весь остальной массив экрана (миллионы супер-пикселей) рассчитывается параллельно на бешеной скорости чистых, аппаратных регистров
doubleпроцессора, вычисляя лишь микроскопические отклонения (дельты) от центральной оси. Скорость генерации взлетела в 1000 раз!

Революционный алгоритм Reference Reset to Zero
-
Динамический сброс на ноль: Теперь пиксель на каждом шаге проверяет соотношение своих полных координат и дельты. Если дельта становится слишком большой или кэш центра иссякает, поток прямо на лету сбрасывает индекс чтения на ноль, превращая накопленные координаты в новую автономную точку.
-
Хакерская оптимизация цикла (One-Step Beyond Escape): Чтобы выжать максимум скорости из процессора, радар MPFR записывает строго одну дополнительную точку в массив опорной орбиты сразу после того, как она превышает радиус ухода.
-
Уничтожение ветвлений (Branch Unrolling): Этот изящный трюк позволил полностью избавиться от громоздких
ifиOR-условий внутри самого глубокого цикла итераций. Процессор больше не тратит такты на предсказание переходов, а компилятор смог идеально векторизовать код.
О проекте
Данное приложение представляет собой консольную утилиту (CLI) для высокопроизводительного рендеринга последовательностей кадров множества Мандельброта. В отличие от интерактивных визуализаторов, эта программа ориентирована на создание высококачественных заготовок для видео и сверхчётких изображений.
Что она делает:
-
Генерация анимации (Frame Sequences): Программа автоматически создает 255 последовательных кадров (.bmp) с эффектом ротации палитры. Эти кадры можно легко объединить в плавное видео (например, через FFmpeg).
-
Экстремальный Суперсэмплинг (SSAA 8×8): Программа использует колоссальный уровень сглаживания. Каждый пиксель финального изображения вычисляется на основе 64 независимых выборок. Это обеспечивает идеальную чистоту картинки даже в самых зашумленных зонах фрактала.
-
Batch Processing: Работает полностью в автоматическом режиме через командную строку. Вы выбираете точку (1-6), и программа выполняет всю тяжелую работу по расчету и сохранению файлов.
Мгновенная вариативность:
-
Программа генерирует 255 различных вариантов раскраски для выбранной локации за один проход. Вам не нужно запускать рендер снова и снова, чтобы подобрать идеальный вид — просто откройте папку и выберите самый красивый кадр из готовой серии. Благодаря методу Palette Shifting, расчет математики происходит один раз, а все 255 изображений создаются практически мгновенно.
Лайфхак: <Живая> анимация в проводнике
Вы можете увидеть эффект Color Rotation без видеоплеера!
-
Откройте папку с готовыми кадрами (Mandelbrot000.bmp — Mandelbrot254.bmp).
-
Откройте первое изображение во встроенном просмотре Windows.
-
Просто зажмите стрелку Вправо на клавиатуре или быстро крутите колесико мыши.
-
Благодаря тому, что программа создала все 255 вариантов, фрактал <оживет> прямо у вас на глазах.

Дополнительно: Рендеринг видео
Если вы хотите увидеть, как эти цвета перетекают, вы можете скомпилировать все 255 кадров в видео (30 кадров в секунду) с помощью FFmpeg. Вы можете скачать предварительно скомпилированный бинарный файл FFmpeg из моего репозитория:
Используйте следующую команду для кодирования кадров в файл Mandelbrot.mp4:
ffmpeg -y -stream_loop 3 -framerate 30 -i Mandelbrot%%03d.bmp -bsf:v h264_metadata=video_full_range_flag=0 -c:v libx264 -refs 6 -me_method umh -partitions all -psy 0 -qp 18 -subq 9 -me_range 24 -deblock -6:-6 -bf 6 -i_qfactor 2 -trellis 0 -b_strategy 2 -color_range full -pix_fmt yuv420p Mandelbrot.mp4
Если у вас видеокарта NVIDIA, вы можете значительно ускорить процесс кодирования:
ffmpeg -y -stream_loop 3 -framerate 30 -i Mandelbrot%%03d.bmp -bsf:v h264_metadata=video_full_range_flag=0 -c:v h264_nvenc -b:v 50M -profile:v high -coder 1 -rc-lookahead 32 -color_range full -pix_fmt yuv420p Mandelbrot.mp4
OpenMP
OpenMP — это стандарт, который говорит компилятору: «Возьми этот цикл и сам раздай итерации разным ядрам процессора». Используя OpenMP, вы занимаетесь параллельным программированием на уровне многопоточности (Multithreading). OpenMP — масштабируемость: ваш код будет одинаково эффективно работать как на 4-ядерном ноутбуке, так и на 128-ядерном сервере.
Суперсэмплинг 8×8 (64 прохода на один пиксель)
Суперсэмплинг (SSAA) — ресурсоемкий метод сглаживания, увеличивающий число выборок на пиксель для повышения качества изображения. При значении 8x (N=8) сцена рендерится в разрешении, в 8 раз превышающем целевое, по обеим осям, создавая 64 (или 8 х 8) выборки на пиксель. Изображение просчитывается в более высоком разрешении, а затем принудительно уменьшается до разрешения дисплея, устраняя лесенки и улучшая чёткость.
Я решил вывести качество изображения на совершенно новый уровень. Этот движок использует истинное сглаживание 8×8 Supersampling Anti-Aliasing (SSAA) с 64 независимыми сэмплами на каждый пиксель экрана, используя прямую интеграцию в RGB-пространство.
После вычисления всех 64 сэмплов для пикселя, они уменьшаются до одного. Ключевые технические преимущества:
-
64-точечное фрактальное сэмплирование: каждый конечный пиксель экрана вычисляется из шестидесяти четырех независимых фрактальных координатных точек.
-
Высокоточное накопление RGB-цвета по каналам: движок сначала вычисляет конкретный 24-битный цвет для каждого субпикселя, прежде чем выполнять какое-либо смешивание.
-
Устранение шума: Накапливая интенсивность цвета (R, G, B), а не просто подсчитывая количество итераций, мы полностью устраняем <хроматический шум>. В результате получается кристально чистое, резкое изображение, где каждая микронить идеально воссоздана.
-
Интеграция истинного цвета: Наше решение выполняет интеграцию непосредственно в цветовом пространстве RGB. Вычисляя точные компоненты красного, зеленого и синего цветов для каждого субпикселя перед понижением разрешения, мы достигаем кинематографического уровня плавности и структурной целостности, недостижимого для 8-битных или итерационных рендеров.

Генерация 255 кадров
Это отличная стратегия оптимизации! Вы хотите применить пререндер: сначала рассчитать тяжелую математику один раз, сохранить их, а затем быстро генерировать кадры, просто меняя цвета и уменьшая размер. Поскольку считать 255 раз — это безумие, мы разделим задачу на два этапа.
Этап 1: Генерация <карты итераций> (Raw Data)
Вместо BMP мы создадим один огромный файл, где для каждого пикселя запишем только число t (номер итерации).
Этап 2: Генерация 255 кадров (Цвет + Сглаживание)
Теперь мы читаем эту карту и для каждого кадра делаем: Берем блок 8×8 пикселей из большой карты. Красим каждый пиксель согласно сдвинутой палитре. Усредняем цвета (это и есть сглаживание) и записываем в файл.
Визуальная эстетика
Красный, зеленый и синий каналы рассчитываются с использованием синусоидальных и косинусоидальных волн для создания плавных цветовых переходов:
pal[a][0] = (uint8_t)round(127.0 + 127.0 * cos(2.0 * PI * a / 255.0)); // Bluepal[a][1] = (uint8_t)round(127.0 + 127.0 * sin(2.0 * PI * a / 255.0)); // Greenpal[a][2] = (uint8_t)round(127.0 + 127.0 * sin(2.0 * PI * a / 255.0)); // Red

Управление и выбор локаций (CLI Controls)
Поскольку это консольное приложение, управление осуществляется через ввод номера локации при запуске программы.
|
Действие |
Ввод |
Описание |
|---|---|---|
|
Пресеты |
|
Выбор одной из 6 встроенных точек мандельброта — глубокого зума. |
|
Своя точка |
|
Загрузка координат ( |
|
Выход |
|
Завершение работы программы. |
case 1: absc_str = "-1.7491976289657893741942376816272921165326158557416159"; ordi_str = "-0.00000042530777152440422725855012159249401150956515248"; size_str = "0.0000000000000000000000000000000000000000000000000043"; break;case 2: absc_str = "-1.7490781615052017316791245451566330412"; ordi_str = "0.0000055099190662909660251309856720635"; size_str = "0.000000000000000000000000000000000215"; break;case 3: absc_str = "-1.748943661768663337207355215321150725806353337382441467976"; ordi_str = "-0.0000073748967541889836640985849393311615399776865199722998"; size_str = "0.0000000000000000000000000000000000000000000000000000001"; break;case 4: absc_str = "-1.7489740586384718864866264297253934254"; ordi_str = "-0.0002265965897111407857153825623868331"; size_str = "0.00000000000000000000000000000000007"; break;case 5: absc_str = "-1.7499458649755745940752606707005571"; ordi_str = "-0.0000000852088539604644334731909824511"; size_str = "0.00000000000000000000000000000000001"; break;case 6: absc_str = "-1.267078059171397835210199054200436920994876769284288837862647"; ordi_str = "-0.123788215196292957558264285607075473360968832625384429809391"; size_str = "0.0000000000000000000000000000000000000000000000000000000023"; break;
Структура файла Mandelbrot.txt
Для загрузки пользовательских координат (пункт 7 в меню), создайте текстовый файл Mandelbrot.txt в папке с программой. Файл должен содержать три числа, разделенных переносом строки:
-
Abscissa (Координата X центра)
-
Ordinate (Координата Y центра)
-
Size (Масштаб/Размер области)
Пример содержания файла:
Множество Мандельброта: Математический абсолют
Это поистине один из немногих объектов, который связывает нас с чем-то абсолютно объективным и бесконечным, превосходящим биологию и историю. Даже если бы вся наша Вселенная и все её атомы исчезли завтра, уравнение осталось бы верным. Оно не <написано> на звёздах; оно заложено в самой структуре логики. Это делает множество Мандельброта своего рода абсолютом.
Математика не зависит от биологии, наличия ног или уровня технологий. Жители галактики Андромеда и разумные океаны в другой супергалактике увидят абсолютно то же самое множество Мандельброта.
Множество Мандельброта существует независимо от нашего разума и технологий. Это бесконечная математическая структура, которая существовала всегда. Компьютеры не создают её; они лишь выступают в роли камеры.

https://github.com/Divetoxx/Mandelbrot#russian
Это Гитхаб с версии с виндовс Mandelbrot_windows_msse3.exe и Mandelbrot_windows_mavx2 и для Линукс Mandelbrot_linux_msse3 -msse3 и Mandelbrot_linux_mavx2
А вот я как делаю дома: g++ -O3 main.cpp -o Mandelbrot.exe -fopenmp -lmpfr -lgmp -static -march=native
А тут полный код на языке С++ — main.cpp
#include <iostream>#include <fstream>#include <vector>#include <cmath>#include <cstdint>#include <string>#include <atomic>#include <omp.h>#include <cstdio>#include <iomanip>#include <gmp.h>#include <mpfr.h>using namespace std;const double PI = 3.14159265358979323846;const mpfr_prec_t MPFR_BITS = 5000;#pragma pack(push, 1)struct BMPHeader { uint16_t type{0x4D42}; uint32_t size{0}; uint16_t reserved1{0}; uint16_t reserved2{0}; uint32_t offBits{54}; uint32_t structSize{40}; int32_t width{0}; int32_t height{0}; uint16_t planes{1}; uint16_t bitCount{24}; uint32_t compression{0}; uint32_t sizeImage{0}; int32_t xpelsPerMeter{2834}; int32_t ypelsPerMeter{2834}; uint32_t clrUsed{0}; uint32_t clrImportant{0};};#pragma pack(pop)struct ComplexDouble { double re; double im;};void save_bmp(const string& filename, const vector<uint8_t>& data, int w, int h) { int rowSize = (w * 3 + 3) & ~3; BMPHeader header; header.width = w; header.height = h; header.sizeImage = rowSize * h; header.size = header.sizeImage + 54; ofstream f(filename, ios::binary); f.write(reinterpret_cast<char*>(&header), 54); f.write(reinterpret_cast<const char*>(data.data()), data.size()); f.close();}int main() { cout << "Cleaning old frames..." << endl; for (int i = 0; i < 255; ++i) { string filename = "Mandelbrot" + to_string(1000 + i).substr(1) + ".bmp"; std::remove(filename.c_str()); } string absc_str, ordi_str, size_str; int choice; std::cout << "Select point (1-7): "; if (!(std::cin >> choice)) choice = 1; switch (choice) { case 1: absc_str = "-1.7491976289657893741942376816272921165326158557416159"; ordi_str = "-0.00000042530777152440422725855012159249401150956515248"; size_str = "0.0000000000000000000000000000000000000000000000000043"; break; case 2: absc_str = "-1.7490781615052017316791245451566330412"; ordi_str = "0.0000055099190662909660251309856720635"; size_str = "0.000000000000000000000000000000000215"; break; case 3: absc_str = "-1.748943661768663337207355215321150725806353337382441467976"; ordi_str = "-0.0000073748967541889836640985849393311615399776865199722998"; size_str = "0.0000000000000000000000000000000000000000000000000000001"; break; case 4: absc_str = "-1.7489740586384718864866264297253934254"; ordi_str = "-0.0002265965897111407857153825623868331"; size_str = "0.00000000000000000000000000000000007"; break; case 5: absc_str = "-1.7499458649755745940752606707005571"; ordi_str = "-0.0000000852088539604644334731909824511"; size_str = "0.00000000000000000000000000000000001"; break; case 6: absc_str = "-1.267078059171397835210199054200436920994876769284288837862647"; ordi_str = "-0.123788215196292957558264285607075473360968832625384429809391"; size_str = "0.0000000000000000000000000000000000000000000000000000000023"; break; case 7: { std::ifstream ff("Mandelbrot.txt"); if (!ff.is_open()) { std::cerr << "Error: Mandelbrot.txt not found!" << std::endl; return 1; } std::vector<std::string> lines; std::string line; while (std::getline(ff, line)) { if (!line.empty()) lines.push_back(line); if (lines.size() == 3) break; } ff.close(); if (lines.size() == 3) { absc_str = lines[0]; ordi_str = lines[1]; size_str = lines[2]; } else { std::cerr << "Error: Mandelbrot.txt has invalid format!" << std::endl; return 1; } break; } } const int targetW = 2160; const int targetH = 2160; const int scale = 8; const int rawW = targetW * scale; const int rawH = targetH * scale; cout << "Step 1: Calculating Raw Map (" << rawW << "x" << rawH << ") using Perturbation..." << endl; vector<uint8_t> iterMap((size_t)rawW * rawH); mpfr_t rx, ry, zr, zi, zr2, zi2, tmp, sz, st; mpfr_inits2(MPFR_BITS, rx, ry, zr, zi, zr2, zi2, tmp, sz, st, NULL); mpfr_set_str(rx, absc_str.c_str(), 10, MPFR_RNDN); mpfr_set_str(ry, ordi_str.c_str(), 10, MPFR_RNDN); mpfr_set_str(sz, size_str.c_str(), 10, MPFR_RNDN); mpfr_div_ui(st, sz, rawW, MPFR_RNDN); double step_d = mpfr_get_d(st, MPFR_RNDN); double ref_rec_d = mpfr_get_d(rx, MPFR_RNDN); double ref_imc_d = mpfr_get_d(ry, MPFR_RNDN); vector<ComplexDouble> ref_orbit_double(50005); mpfr_set_ui(zr, 0, MPFR_RNDN); mpfr_set_ui(zi, 0, MPFR_RNDN); mpfr_set_ui(zr2, 0, MPFR_RNDN); mpfr_set_ui(zi2, 0, MPFR_RNDN); uint32_t ref_i = 0; bool escaped = false; while (ref_i < 50000) { ref_orbit_double[ref_i].re = mpfr_get_d(zr, MPFR_RNDN); ref_orbit_double[ref_i].im = mpfr_get_d(zi, MPFR_RNDN); mpfr_mul(tmp, zr, zi, MPFR_RNDN); mpfr_mul_ui(zi, tmp, 2, MPFR_RNDN); mpfr_add(zi, zi, ry, MPFR_RNDN); mpfr_sub(zr, zr2, zi2, MPFR_RNDN); mpfr_add(zr, zr, rx, MPFR_RNDN); mpfr_mul(zr2, zr, zr, MPFR_RNDN); mpfr_mul(zi2, zi, zi, MPFR_RNDN); if (escaped) { ref_i++; break; } mpfr_add(tmp, zr2, zi2, MPFR_RNDN); if (mpfr_cmp_d(tmp, 4.0) >= 0) { escaped = true; } ref_i++; } ref_orbit_double[ref_i].re = mpfr_get_d(zr, MPFR_RNDN); ref_orbit_double[ref_i].im = mpfr_get_d(zi, MPFR_RNDN); uint32_t max_valid_ref_iter = ref_i; mpfr_clears(rx, ry, zr, zi, zr2, zi2, tmp, sz, st, NULL); atomic<int> linesDone{0}; #pragma omp parallel for schedule(dynamic) for (size_t b = 0; b < (size_t)rawH; ++b) { for (size_t a = 0; a < (size_t)rawW; ++a) { double delta_rec = (double)((long long)a - (rawW / 2)) * step_d; double delta_imc = (double)((long long)b - (rawH / 2)) * step_d; uint32_t index = 0; double delta_re = 0.0; double delta_im = 0.0; double z_re = 0.0; double z_im = 0.0; uint32_t i = 0; const ComplexDouble* ref_ptr = ref_orbit_double.data(); while (i < max_valid_ref_iter) { if ((z_re * z_re + z_im * z_im) >= 40000.0) { break; } if ((z_re * z_re + z_im * z_im) < (delta_re * delta_re + delta_im * delta_im)) { index = 0; delta_re = z_re; delta_im = z_im; } for (int step = 0; step < 2; ++step) { double Ur = ref_ptr[index].re; double Ui = ref_ptr[index].im; double next_delta_im = 2.0 * Ur * delta_im + 2.0 * Ui * delta_re + 2.0 * delta_re * delta_im + delta_imc; delta_re = 2.0 * Ur * delta_re - 2.0 * Ui * delta_im + delta_re * delta_re - delta_im * delta_im + delta_rec; delta_im = next_delta_im; index++; } z_re = ref_ptr[index].re + delta_re; z_im = ref_ptr[index].im + delta_im; i += 2; } int final_t = 50000 - i; if (final_t == 0) { iterMap[b * (size_t)rawW + a] = 255; } else { iterMap[b * (size_t)rawW + a] = (uint8_t)(final_t % 254); } } if (++linesDone % 100 == 0) cout << "Progress: " << linesDone << "/" << rawH << "\r" << flush; } uint8_t pal[256][3]; for (int a = 0; a < 255; ++a) { pal[a][0] = (uint8_t)round(127.0 + 127.0 * cos(2.0 * PI * a / 255.0)); // Blue pal[a][1] = (uint8_t)round(127.0 + 127.0 * sin(2.0 * PI * a / 255.0)); // Green pal[a][2] = (uint8_t)round(127.0 + 127.0 * sin(2.0 * PI * a / 255.0)); // Red } pal[255][0] = 255; pal[255][1] = 255; pal[255][2] = 255; cout << "\nStep 2: Rendering frames..." << endl; int rowSize = (targetW * 3 + 3) & ~3; for (int frame = 0; frame < 255; ++frame) { vector<uint8_t> frameData(rowSize * targetH); #pragma omp parallel for schedule(static) for (int y = 0; y < targetH; ++y) { for (int x = 0; x < targetW; ++x) { uint32_t rSum = 0, gSum = 0, bSum = 0; for (int j = 0; j < scale; ++j) { size_t mapRowIdx = (size_t)(y * scale + j) * rawW; for (int i = 0; i < scale; ++i) { uint8_t t = iterMap[mapRowIdx + (x * scale + i)]; int colorIdx; if (t == 255) { colorIdx = 255; } else { colorIdx = (t - frame + 255) % 255; } bSum += pal[colorIdx][0]; gSum += pal[colorIdx][1]; rSum += pal[colorIdx][2]; } } int outIdx = y * rowSize + x * 3; frameData[outIdx + 0] = (uint8_t)(bSum >> 6); frameData[outIdx + 1] = (uint8_t)(gSum >> 6); frameData[outIdx + 2] = (uint8_t)(rSum >> 6); } } string filename = "Mandelbrot" + to_string(1000 + frame).substr(1) + ".bmp"; save_bmp(filename, frameData, targetW, targetH); cout << "Frame " << frame << "/254 saved. \r" << flush; } return 0;}
ссылка на оригинал статьи https://habr.com/ru/articles/1045660/