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

Ключевые особенности:
Расчёт опорной траектории на 5000 бит всего один раз.
Реактивный расчёт миллионов пикселей на аппаратном double.
Революционный алгоритм Reference Reset to Zero.
Настоящий SSAA 8x8 для идеально сглаженного изображения без алиасинга.
Параллелизм OpenMP для высокоскоростного многопоточного рендеринга.
Безграничная точность (Arbitrary Precision Arithmetic)
Движок полностью избавлен от аппаратных ограничений 64-битных (double) и 128-битных (__float128) чисел, которые неизбежно слепнут и выдают пиксельные квадраты на глубинах более 10 − 15 и 10 − 34.
Интеграция MPFR/GMP: Вся высокоточная навигация, пересчёт масштаба при кликах мыши и движении стрелочками клавиатуры ведутся внутри сверхглубокой бинарной памяти с точностью 5000 бит!
* 308 десятичных знаков в текстовом кэше (Hardware Double Wall): Координаты кадра сохраняются и считываются из файла
Mandelbrot.txt. Навигация и радар MPFR работают на глубине до 5000 бит, однако скоростной пиксельный дельта-движок ограничен аппаратной экспонентойdouble, что позволяет исследовать безупречно четкие структуры на запредельных масштабах вплоть до 10 − 308 знаков.
Реактивный метод возмущений (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 8x8): Программа использует колоссальный уровень сглаживания. Каждый пиксель финального изображения вычисляется на основе 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-ядерном сервере.
Суперсэмплинг 8x8 (64 прохода на один пиксель)
Суперсэмплинг (SSAA) - ресурсоемкий метод сглаживания, увеличивающий число выборок на пиксель для повышения качества изображения. При значении 8x (N=8) сцена рендерится в разрешении, в 8 раз превышающем целевое, по обеим осям, создавая 64 (или 8 х 8) выборки на пиксель. Изображение просчитывается в более высоком разрешении, а затем принудительно уменьшается до разрешения дисплея, устраняя лесенки и улучшая чёткость.
Я решил вывести качество изображения на совершенно новый уровень. Этот движок использует истинное сглаживание 8x8 Supersampling Anti-Aliasing (SSAA) с 64 независимыми сэмплами на каждый пиксель экрана, используя прямую интеграцию в RGB-пространство.
После вычисления всех 64 сэмплов для пикселя, они уменьшаются до одного. Ключевые технические преимущества:
64-точечное фрактальное сэмплирование: каждый конечный пиксель экрана вычисляется из шестидесяти четырех независимых фрактальных координатных точек.
Высокоточное накопление RGB-цвета по каналам: движок сначала вычисляет конкретный 24-битный цвет для каждого субпикселя, прежде чем выполнять какое-либо смешивание.
Устранение шума: Накапливая интенсивность цвета (R, G, B), а не просто подсчитывая количество итераций, мы полностью устраняем <хроматический шум>. В результате получается кристально чистое, резкое изображение, где каждая микронить идеально воссоздана.
Интеграция истинного цвета: Наше решение выполняет интеграцию непосредственно в цветовом пространстве RGB. Вычисляя точные компоненты красного, зеленого и синего цветов для каждого субпикселя перед понижением разрешения, мы достигаем кинематографического уровня плавности и структурной целостности, недостижимого для 8-битных или итерационных рендеров.

Генерация 255 кадров
Это отличная стратегия оптимизации! Вы хотите применить пререндер: сначала рассчитать тяжелую математику один раз, сохранить их, а затем быстро генерировать кадры, просто меняя цвета и уменьшая размер. Поскольку считать 255 раз - это безумие, мы разделим задачу на два этапа.
Этап 1: Генерация <карты итераций> (Raw Data)
Вместо BMP мы создадим один огромный файл, где для каждого пикселя запишем только число t (номер итерации).
Этап 2: Генерация 255 кадров (Цвет + Сглаживание)
Теперь мы читаем эту карту и для каждого кадра делаем: Берем блок 8x8 пикселей из большой карты. Красим каждый пиксель согласно сдвинутой палитре. Усредняем цвета (это и есть сглаживание) и записываем в файл.
Визуальная эстетика
Красный, зеленый и синий каналы рассчитываются с использованием синусоидальных и косинусоидальных волн для создания плавных цветовых переходов:
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

Управление и выбор локаций (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; }
