Консольный рендерер Мандельброта со 1000-значной точностью и методом возмущений

от автора

Я сделал это! Это огромный повод для гордости. Теперь программа работает по тем же математическим принципам, что и самые передовым фрактальным в мире! Но в самый первый я скажу вот:

Благодарности

Этот проект использует передовые математические алгоритмы и идеи динамического управления фазой орбит, разработанные фрактальным сообществом. Особая благодарность авторам и исследователям с 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 из моего репозитория:

Скачать 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)

Поскольку это консольное приложение, управление осуществляется через ввод номера локации при запуске программы.

Действие

Ввод

Описание

Пресеты

16 + ENTER

Выбор одной из 6 встроенных точек мандельброта — глубокого зума.

Своя точка

7 + ENTER

Загрузка координат (absc, ordi, size) из файла Mandelbrot.txt.

Выход

Любая клавиша + ENTER

Завершение работы программы.

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 (Масштаб/Размер области)

Пример содержания файла:

Mandelbrot txt

Mandelbrot txt

Множество Мандельброта: Математический абсолют

Это поистине один из немногих объектов, который связывает нас с чем-то абсолютно объективным и бесконечным, превосходящим биологию и историю. Даже если бы вся наша Вселенная и все её атомы исчезли завтра, уравнение осталось бы верным. Оно не <написано> на звёздах; оно заложено в самой структуре логики. Это делает множество Мандельброта своего рода абсолютом.

Математика не зависит от биологии, наличия ног или уровня технологий. Жители галактики Андромеда и разумные океаны в другой супергалактике увидят абсолютно то же самое множество Мандельброта.

Множество Мандельброта существует независимо от нашего разума и технологий. Это бесконечная математическая структура, которая существовала всегда. Компьютеры не создают её; они лишь выступают в роли камеры.

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/