Математика та фрактали
Квітень 2026 · 18 хв читання · GLSL · Ray Marching · Поля відстаней

3D-фрактали: Mandelbulb, губка Менгера та рендеринг через поля відстаней

Перехід від 2D- до 3D-фракталів не є простим — не існує коректного 3D- аналога комплексних чисел, який зберігав би всю алгебру. Та попри це надзвичайно багаті структури, як-от Mandelbulb та губка Менгера, виникають із простих правил. Ця стаття розкриває математику обох підходів, пояснює ray-marching полів знакових відстаней і наводить код шейдера GLSL для дослідження в реальному часі.

1. Чому 3D-фрактали складні

Множина Мандельброта живе на комплексній площині й визначається ітерацією z→ z² + c. У 2D піднесення до квадрата — це просто множення комплексних чисел: (a + bi)² = a² − b² + 2abi. Ключова властивість, завдяки якій це працює фрактально, — аналітичність: похідна існує й задовольняє рівняння Коші–Рімана, тобто відображення є конформним (зберігає кути) і породжує нескінченно складні самоподібні межі.

Природне розширення на три виміри потребувало б нормованої алгебри з діленням над ℝ³. By Hurwitz's Гурвіца така алгебра не існує — єдині коректні нормовані алгебри з діленням мають розмірність 1, 2, 4 (кватерніони) або 8 (октоніони). Кватерніони дають 4D-фрактал (кватерніонну множину Жюліа / Мандельброта), але проєкція його назад у 3D дає відносно гладкі форми без складної бульбоподібної структури, на яку ми очікуємо.

Існує два успішні підходи:

Рендеринг цих неявних 3D-об'єктів у реальному часі стає практичним завдяки ray-marching полів знакових відстаней, який використовує функції-оцінювачі відстані, виведені з ітерації фрактала за часом виходу (escape-time).

2. Mandelbulb: формула сферичного степеня

Mandelbulb запропонували Деніел Вайт і Пол Найландер у 2009 році. Ідея полягає в тому, щоб подати 3D-точку v = (x, y, z) у сферичних координатах (r, θ, φ), піднести її до n-го степеня, помноживши радіус n разів і помноживши кути, а потім перетворити назад:

r = |v| = sqrt(x² + y² + z²) θ = atan2(sqrt(x² + y²), z) (polar angle from +z) φ = atan2(y, x) (azimuthal angle in xy-plane) v^n = ( r^n · sin(nθ)·cos(nφ), r^n · sin(nθ)·sin(nφ), r^n · cos(nθ) )

Тоді ітерація Mandelbulb має вигляд z → z^n + c (у наведеному вище 3D-сенсі) починаючи з zâ‚€ = 0. Точка c належить множині, якщо |z| залишається обмеженим. Канонічний вибір — n = 8, що дає складні бульбоподібні структури з аналогами «слонячої долини» та «долини морського коника».

Чому n = 8?

Для n = 2 результат схожий на гладку кулю — нецікаво. Зі збільшенням n поверхня набуває дедалі більше самоподібних деталей. При n = 8 фрактал демонструє переконливий 3D-аналог множини Мандельброта з глибоко вкладеними спіральними елементами й багатою межею, чия гаусдорфова розмірність оцінюється приблизно в 2,97 — майже заповнює простір.

Обмеження

Оскільки сферичний степінь не підкоряється правилу добутку для похідних у справжньому алгебраїчному сенсі (це не голоморфне відображення), Mandelbulb не є справжньою 3D-множиною Мандельброта. Внутрішні області та межа не мають тих самих математичних гарантій, що й у 2D- випадку, та візуально й обчислювально це один із найбільш вдячних відомих 3D-фракталів.

Оцінювач відстані

Щоб виконувати ray-marching Mandelbulb, нам потрібен оцінювач відстані (DE) — нижня межа відстані від точки до поверхні фрактала. Виводиться з величини похідної вздовж ітерації:

// Running derivative magnitude (chain rule approximation) dr ← n · r^(n-1) · dr + 1 // After escape: DE = 0.5 · log(|z|) · |z| / |dz|

Це формула Іньїго Квілеза. Вона дає консервативну (із заниженням) межу відстані — завжди безпечно крокувати на частку (зазвичай 0,8–0,99) від DE, щоб запобігти проходженню крізь тонкі елементи.

3. Губка Менгера: система ітерованих функцій у 3D

Губку Менгера будують, починаючи з одиничного куба й рекурсивно ділячи його на сітку 3×3×3 з 27 підкубів, а потім вилучаючи центральний куб і 6 кубів у центрах граней (залишаючи 20 підкубів) і повторюючи процес для кожного підкуба, що залишився.

Після k ітерацій губка має 20^k кубів, кожен зі стороною (1/3)^k. Її гаусдорфова розмірність:

d_H = log(20) / log(3) ≈ 2.727

Губка має нульовий обʼєм (у границі), але нескінченну площу поверхні — характерна властивість фракталів із розмірністю між 2 і 3.

SDF для губки Менгера (підхід зі складанням)

Губку Менгера можна обчислити за допомогою SDF зі складанням, що відтворює рекурсивну побудову без розгалужень:

// Iterate the folding transform for (int i = 0; i < ITERATIONS; i++) { p = abs(p); if (p.x < p.y) p.xy = p.yx; if (p.x < p.z) p.xz = p.zx; if (p.y < p.z) p.yz = p.zy; p = p * 3.0 - vec3(2.0); // scale and shift if (p.z < -1.0) p.z += 2.0; } // SDF of the inner cross (6 infinite cylinders minus the box) float cross = max(max(abs(p.x), abs(p.y)), abs(p.z)) - 1.0; DE = cross / (pow(3.0, float(ITERATIONS)) * scale);

Сортування (обміни xy, xz, yz) складають точку в канонічний октант на кожному рівні, а масштабний коефіцієнт накопичується за правилом ланцюга, так само як і в DE для Mandelbulb.

Порівняння підходів

4. Поля знакових відстаней та ray marching

Поле знакових відстаней (SDF) — це скалярна функція f(p), що повертає знакову відстань від точки p до найближчої поверхні: відʼємну всередині, додатну ззовні, нуль на поверхні. Ray marching (sphere tracing) використовує SDF, щоб безпечно просувати промінь:

  1. Випустити промінь із початку камери o у напрямку d (нормованому).
  2. У поточній позиції p обчислити SDF: t = f(p).
  3. Стрибнути вперед на t уздовж променя: p += t · d.
  4. Повторювати, доки |t| < ε (влучання) або накопичена відстань > max_dist (промах).
// Sphere-tracing loop float totalDist = 0.0; for (int i = 0; i < MAX_STEPS; i++) { vec3 p = ro + totalDist * rd; float d = sceneSDF(p); if (d < 1e-4) { /* hit */ break; } totalDist += d; if (totalDist > 200.0) { /* miss */ break; } }

Оцінювання нормалі

Нормалі до поверхні обчислюють через градієнт SDF за допомогою скінченних різниць:

vec3 normal(vec3 p) { const float h = 1e-4; return normalize(vec3( sdf(p + vec3(h,0,0)) - sdf(p - vec3(h,0,0)), sdf(p + vec3(0,h,0)) - sdf(p - vec3(0,h,0)), sdf(p + vec3(0,0,h)) - sdf(p - vec3(0,0,h)) )); }

Це потребує 6 обчислень SDF на влучання. Скінченні різниці на основі тетраедра різниці на основі тетраедра (трюк Квілеза) скорочують це до 4 обчислень, зберігаючи точність.

Питання продуктивності

5. SDF-оцінювачі відстані для фракталів

Для escape-time фракталів DE виводять із модуля похідної вздовж орбіти. Загальна формула для відображення z → f(z), що починається в c:

// Accumulate derivative norm float dr = 1.0; float r = 0.0; for (int i = 0; i < ITER; i++) { r = length(z); if (r > BAILOUT) break; // Update derivative before updating z dr = pow(r, n - 1.0) * n * dr + 1.0; // Mandelbulb power step float theta = acos(z.z / r); float phi = atan(z.y, z.x); float zr = pow(r, float(n)); z = zr * vec3(sin(n*theta)*cos(n*phi), sin(n*theta)*sin(n*phi), cos(n*theta)) + c; } r = length(z); return 0.5 * log(r) * r / dr; // distance estimate

Ключова тонкість: DE потрібно розділити на накопичений масштабний коефіцієнт для фракталів на основі складання. Для губки Менгера кожна ітерація множить координати на 3, тож DE ділять на 3^ITERATIONS. Якщо про це забути, форма виглядатиме геометрично правильною, але з нормалями, спрямованими у випадкові напрямки, — поширений головний біль при налагодженні.

Ambient occlusion з кількості кроків

Дешева, але ефективна оцінка ambient occlusion — це кількість зроблених кроків ray-marching, поділена на MAX_STEPS. Області, що потребують багатьох дрібних кроків (поблизу глибоких щілин), темніші; відкриті пласкі області (мало кроків) світліші. Це коректно затемнює фрактальні фіорди та глибокі складки без додаткових променів.

6. Розфарбовування orbit-trap та згладжений лік ітерацій

Згладжений лік ітерацій

Сирий лік ітерацій escape-time є цілим числом, що породжує помітні смуги на відрендереному зображенні. Згладжене розфарбовування замінює індекс смуги n дійсним значенням:

// After escape at iteration i with |z| = r: float smooth_i = float(i) - log2(log2(r)) + 4.0; // (the 4.0 is an additive constant for a nice phase shift)

Ця формула походить із теорії потенціалу множини Мандельброта: функція Гріна G(c) = lim log|z_n| / 2^n неперервна через межі смуг. Поправка logâ‚‚(logâ‚‚(r)) усуває цілочислові сходинки.

Orbit traps

Orbit traps розфарбовують точку залежно від того, наскільки близько орбіта проходить до геометричної фігури:

Кілька пасток можна змішувати за зваженим мінімумом або відстежуючи, яка пастка спрацювала на найближчому наближенні. Результат значно багатший за просте градієнтне розфарбовування.

Освітлення на основі нормалей

Щойно відомі точка влучання та її нормаль, безпосередньо застосовується стандартне затінення Фонга або Блінна–Фонга. Додавання дзеркального відблиску з показником 64–128 значно посилює сприйняття складної поверхні. Мʼякі тіні можна апроксимувати, просуваючи вторинний промінь до джерела світла й відстежуючи, наскільки близько він підходить до поверхні (техніка напівтіні «м'якості тіні» від Квілеза).

7. Ray-marcher на GLSL: повний шейдер

Нижче наведено самодостатній фрагментний шейдер GLSL для Mandelbulb у восьмому степені. Він працює у WebGL 1.0 з відповідними підказками точності.

// ── Mandelbulb Ray-Marcher (GLSL ES 3.0) ──────────────────────────
precision highp float;
uniform vec2 u_resolution;
uniform float u_time;

const int ITER = 8;
const int STEPS = 120;
const float BAILOUT = 4.0;
const int N = 8;

// Distance estimator for Mandelbulb power-8
float mandelbulbDE(vec3 pos) {
  vec3 z = pos;
  float dr = 1.0, r = 0.0;
  for (int i = 0; i < ITER; i++) {
    r = length(z);
    if (r > BAILOUT) break;
    float theta = acos(clamp(z.z / r, -1.0, 1.0));
    float phi   = atan(z.y, z.x);
    dr = pow(r, float(N - 1)) * float(N) * dr + 1.0;
    float zr = pow(r, float(N));
    z = zr * vec3(
      sin(float(N)*theta) * cos(float(N)*phi),
      sin(float(N)*theta) * sin(float(N)*phi),
      cos(float(N)*theta)
    ) + pos;
  }
  return 0.5 * log(r) * r / dr;
}

// Finite-difference normal (tetrahedron method)
vec3 calcNormal(vec3 p) {
  const vec2 k = vec2(1.0, -1.0) * 5e-4;
  return normalize(
    k.xyy * mandelbulbDE(p + k.xyy) +
    k.yyx * mandelbulbDE(p + k.yyx) +
    k.yxy * mandelbulbDE(p + k.yxy) +
    k.xxx * mandelbulbDE(p + k.xxx)
  );
}

// Sphere-trace
float trace(vec3 ro, vec3 rd) {
  float t = 0.0;
  for (int i = 0; i < STEPS; i++) {
    float d = mandelbulbDE(ro + t * rd);
    if (d < 1e-4) return t;
    t += d * 0.8;
    if (t > 8.0) break;
  }
  return -1.0;
}

void main() {
  vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / u_resolution.y;

  // Orbiting camera
  float angle = u_time * 0.3;
  vec3 ro = vec3(2.5 * sin(angle), 0.5, 2.5 * cos(angle));
  vec3 ta = vec3(0.0);
  vec3 ww = normalize(ta - ro);
  vec3 uu = normalize(cross(ww, vec3(0, 1, 0)));
  vec3 vv = cross(uu, ww);
  vec3 rd = normalize(uv.x * uu + uv.y * vv + 1.5 * ww);

  float t = trace(ro, rd);
  vec3 col = vec3(0.05, 0.05, 0.08);  // background

  if (t > 0.0) {
    vec3 p    = ro + t * rd;
    vec3 n    = calcNormal(p);
    vec3 lig  = normalize(vec3(0.8, 0.6, 0.4));
    float diff = clamp(dot(n, lig), 0.0, 1.0);
    float spec = pow(clamp(dot(reflect(-lig, n), -rd), 0.0, 1.0), 64.0);
    float trap = clamp(0.1 / abs(p.x * p.y), 0.0, 1.0);
    vec3 baseCol = mix(vec3(0.1, 0.2, 0.8), vec3(0.9, 0.4, 0.1), trap);
    col = baseCol * diff + vec3(0.5) * spec;
    col *= 0.8 + 0.2 * (1.0 - t / 8.0);
  }

  gl_FragColor = vec4(pow(col, vec3(0.4545)), 1.0);
}
Спробуйте самі: вставте наведений вище шейдер у ShaderToy. Зіставте iResolution → із u_resolution, а iTime — → з u_time. Змінюйте константу N від 2 до 12, щоб спостерігати трансформацію «бульби».

8. Розширення: Mandelbox, Juliabulb та складання простору

Mandelbox

Mandelbox (Том Лоу, 2010) замінює сферичний степінь на складання за кубом з подальшим складанням за сферою:

// Box fold: reflect components outside [-1, 1] z = clamp(z, -1.0, 1.0) * 2.0 - z; // Sphere fold: invert inside minimum radius sphere float r2 = dot(z, z); if (r2 < minR2) z *= fixedR2 / minR2; else if (r2 < fixedR2) z *= fixedR2 / r2; // Scale and translate z = scale * z + c;

Mandelbox породжує нескінченно вкладені кубоподібні структури — рекурсивні коридори, силуети міст, фрактальні корали. Параметр scale (зазвичай від −2,0 до −1,5) визначає загальний характер; значення близько −1,5 дають тонкі папоротеподібні структури, тоді як −2,0 дає масивні кутасті скелі.

Juliabulb

Подібно до того, як 2D-множина Жюліа фіксує c і варіює zâ‚€, Juliabulb фіксує 3D-параметр c і використовує різні початкові точки. Це вирізає іншу область простору параметрів, породжуючи виразно симетричні форми. Анімація c уздовж шляху створює фрактальні анімації з трансформацією.

Клейнові групи та фрактали інверсії за сферою

Загальніші фрактали складання застосовують перетворення Мöbius transformations — інверсії через сфери — рекурсивно. Повʼязані з 3D клейновими граничними множинами та гіперболічною геометрією, вони дають напівпрозорі фрактали-скупчення бульбашок із точним DE, який виводиться з правила ланцюга.

Гібридні фрактали

Чергування кроків ітерації з різних формул — один крок Mandelbulb, потім один крок складання за кубом — створює «гібридні» фрактали з цілком новими морфологіями. Більшість прославлених онлайн-рендерів фракталів — це саме такі гібриди, що використовують безмежну компонованість оцінювачів відстані.

Зауваження щодо продуктивності: на GPU середнього рівня (RTX 3060) рендеринг Mandelbulb у 1080p зі 120 кроками marching і 8 ітераціями працює з ~10 fps у фрагментному шейдері. Зменшення ITER до 5 і STEPS до 80 зазвичай усе ще виглядає чудово на 30+ fps. Адаптивний розмір кроку — збільшення множника просування у відкритому просторі й зменшення поблизу поверхонь — може вдвічі скоротити середню кількість кроків без видимих артефактів.