Алгоритми множини Мандельброта: час виходу, оцінка відстані та GPU-рендеринг
Один із найвідоміших об'єктів у всій математиці можна намалювати циклом, зрозумілим будь-якому програмісту-початківцю — проте під цією простотою криються шари алгоритмічної витонченості, які переводять рендеринг із повільного в реальний час: гладке розфарбування, виявлення внутрішньої області, теорія збурень та масово-паралельні GPU-шейдери.
1. Означення: одне рівняння, нескінченна складність
Множина Мандельброта M — це множина всіх комплексних чисел c, для яких орбіта 0 при повторному застосуванні f(z) = z² + c назавжди лишається обмеженою:
Межа M — нескінченно заплутана берегова лінія, видима у відео фрактального зуму, — це місце, де живуть цікаві алгоритмічні виклики. Точки строго всередині M ітерують вічно; точки ззовні виходять на якомусь скінченному кроці. Сама межа має розмірність Гаусдорфа 2 (вона має нульову площу, але настільки складна, що в топологічному сенсі заповнює площину).
Множина лежить у комплексній площині з дійсною частиною (Re c) на осі x та уявною частиною (Im c) на осі y. Уся множина вміщується в колі радіуса 2 із центром у початку координат, тож нам потрібно вибирати лише значення c з |c| ≤ 2.
2. Алгоритм часу виходу
Класичний алгоритм ітерує відповідне значення c кожного пікселя,
доки |z|² > 4 (еквівалентно |z| > 2, але без обчислення квадратного кореня)
або доки не досягнуто максимальної кількості ітерацій MAX_ITER:
Повернене значення n потім відображається в колір.
Найпростіше відображення призначає відтінок, пропорційний до
n / max_iter. Але це дає різкі видимі смуги на
кожній цілій кількості ітерацій — що приводить до прийому гладкого
розфарбування, описаного в наступному розділі.
Вибір MAX_ITER
За типового вигляду (зум ≈ 1×) 256–512 ітерацій достатньо для впізнаваної деталізації. Зображення глибокого зуму вимагають мільйонів ітерацій для точок поблизу межі. Емпіричне правило: MAX_ITER ≈ 200 × (рівень зуму)^0.4. Замало ітерацій роблять множину «товстою» — зовнішні точки поблизу межі хибно класифікуються як внутрішні.
Оптимізація: пропустити квадратний корінь
Перевірка zx*zx + zy*zy > 4.0 уникає дорогого
виклику sqrt(). Інша рання оптимізація —
періодично перевіряти, чи z збігся до циклу — що вказує, що
точка всередині M — заощаджуючи час на «точно внутрішніх» точках.
3. Гладке розфарбування та нормалізована кількість ітерацій
Цілочислові лічильники виходу породжують кольорові смуги — концентричні кільця, чітко
розділені на кожному цілому n.
Нормалізована кількість ітерацій (також звана формулою
Büldt або Linas) інтерполює між цілими, використовуючи логарифм
модуля в момент виходу:
Це усуває всі видимі смуги, даючи гладке градієнтне розфарбування, яке видно в професійних фрактальних рендерах. Дизайн палітри потім визначає кінцевий вигляд: циклічна зміна відтінку, використання синусоїдальних RGB-функцій або відображення в ручно створені точки градієнта.
Орбітальні пастки
Більш художній метод розфарбування записує мінімальну відстань від орбіти до геометричної фігури ( точки, лінії, кола чи хреста). Колір залежить від цієї мінімальної відстані. Орбітальні пастки дають разюче різні зображення — повторювані візерунки форми пастки вздовж фрактальної межі — й можуть генерувати текстури, що нагадують реальні об'єкти.
4. Виявлення внутрішньої області: перевірка періоду та тест кардіоїди
Точки всередині M ітерують аж до MAX_ITER, що робить
внутрішні пікселі найдорожчим попіксельним обчисленням. Два
прийоми разюче зменшують вартість обробки внутрішньої області.
Тест кардіоїди та бульбашки періоду 2
Головна кардіоїда та найбільша кругла бульбашка (з центром у c = −1) разом містять велику частку всіх внутрішніх точок. Їх можна перевірити аналітично перед ітеруванням:
Перевірка періодичності / циклів
Внутрішні орбіти зрештою входять у періодичний цикл — орбіта повторюється
з певним періодом p. Виявлення циклів за Брентом або Флойдом може це
ідентифікувати: зберігайте «повільну» копію z кожні k ітерацій, порівнюйте з
поточним z. Якщо |z_current - z_saved| < ε, точка
в M, і ітерація припиняється достроково.
У поєднанні тест кардіоїди + перевірка періоду зменшують внутрішні обчислення на 40–80% за типових рівнів зуму, приблизно подвоюючи середню швидкість рендерингу.
5. Оцінка відстані для деталізації межі
Обчислення зовнішнього оцінювача відстані (DEM) дає приблизну відстань від точки c до найближчої межі M, не знаючи точно, де ця межа. Це досягається одночасним відстеженням похідної функції ітерації:
Застосування DEM:
- Згладжування (anti-aliasing): пікселі, ближчі до межі, ніж ширина одного пікселя, отримують субпіксельну вибірку (суперсемплінг або Монте- Карло).
- Рендеринг країв: призначити окремий колір усім пікселям, де DEM < поріг — малює контур множини без ітерування до великих глибин.
- 3D-рельєфний рендеринг: використовувати DEM як поле висот; затінювати віртуальним спрямованим світлом, щоб отримати «тиснену» фрактальну рельєфну карту.
6. Глибокий зум та теорія збурень
Зум до глибин понад ~10¹⁴ вичерпує точність 64-бітних чисел подвійної точності (які мають ~15–16 десяткових цифр мантиси). На таких глибинах помилки округлення накопичуються, і зображення вироджується в неправильні однокольорові області, звані глітчами.
Арифметика довільної точності
Прямолінійне рішення замінює double-числа на числа довільної точності (наприклад, 128-бітні, 256-бітні чи навіть бібліотеки на 1000+ цифр). Але вартість зростає приблизно квадратично з точністю — у 10× більше цифр → у 100× повільніше. Це робить інтерактивні рендери глибокого зуму неможливими без додаткової кмітливості.
Теорія збурень
Ключова ідея: більшість пікселів у вигляді глибокого зуму дуже близькі одне до одного в комплексній площині. Обчисліть одну опорну орбіту в центрі вікна перегляду за допомогою високоточної арифметики. Потім представте кожен інший піксель як мале збурення δ відносно опорної орбіти:
Це зводить високоточну арифметику до однієї орбіти на кадр (опорної), тоді як усі інші пікселі використовують дешеву double-арифметику. Глітчі через збій наближення виявляються й виправляються вибором додаткових опорних орбіт поблизу проблемних пікселів.
Найсучасніші рендерери Мандельброта (як-от Kalles Fraktaler та Mandel Machine) поєднують теорію збурень із наближенням рядами (Series Approximation, SA), яке аналітично пропускає цілі блоки ранніх ітерацій — уможливлюючи інтерактивний зум до глибин 10¹⁰⁰⁰ і далі.
7. GPU-рендеринг із шейдерами GLSL
Сучасні GPU виконують тисячі викликів шейдера паралельно. Фрагментний шейдер, що обробляє один піксель за виклик, ідеально лягає на цикл ітерації Мандельброта. Кожному пікселю не потрібні дані від жодного іншого пікселя — синхронізація не потрібна.
Головне обмеження GPU-шейдерів — у старіших версіях GLSL межа циклу має бути константою часу компіляції. Запихання надто багатьох ітерацій переповнює кеш інструкцій GPU або перевищує тайм-аути сторожового таймера драйвера, спричиняючи скидання екрана. Практичні межі GPU — 10 000–50 000 ітерацій на піксель за інтерактивної частоти кадрів.
Подвійна точність на GPU
Споживчі відеокарти пропонують апаратну подвійну точність (64-бітну) арифметику з пропускною здатністю приблизно 1/32 від одинарної точності. Для рендерингу глибокого зуму, де потрібні double-числа, перевага GPU зменшується. Поширений обхідний шлях — double-float емуляція — представлення кожного double як пари float за алгоритмом double-float Деккера–Кнута. Це повертає точність подвійної точності за ~3–4× вартості одинарної точності, усе ще набагато швидше, ніж довільна точність на боці CPU.
Особливості реалізації у WebGL
-
WebGL 1 потребує розширення
OES_element_index_uintдля 32-бітних індексів; немає вбудованої підтримки double. -
WebGL 2 підтримує
EXT_color_buffer_floatдля 32-бітних float-цілей рендерингу (корисно для проходів накопичення). - Для інтерактивних демо рендерте на половинній роздільності й масштабуйте вгору — це вдвічі зменшує загальну кількість викликів фрагментів за помірної втрати якості.
- Прогресивне уточнення: спершу відрендерте грубий прохід, потім уточніть. Користувачі сприймають грубий попередній перегляд як миттєвий відгук.
8. Множини Жюліа: сімейство родичів
Для кожного комплексного числа c відповідна множина Жюліа J(c) обчислюється тією самою ітерацією — але тепер c фіксоване, а z змінюється по всьому зображенню:
Множини Мандельброта та Жюліа тісно пов'язані: якщо c всередині M, J(c) — зв'язний фрактал; якщо c поза M, J(c) — незв'язний «пил» (топологія канторової множини). Множину Мандельброта буквально можна вважати картою всіх можливих форм Жюліа — кожна точка в M відповідає зв'язній множині Жюліа, а форма M у малих масштабах віддзеркалює сусідні множини Жюліа.
Цей зв'язок використовується в інтерактивних дослідниках: клацання по будь-якій точці c у переглядачі Мандельброта миттєво оновлює бічну панель, що показує J(c). Для естетичного дослідження значення c уздовж межі M — філаменти та антени — дають найзаплутаніші форми Жюліа.