I. Raymarching і поля знакових відстаней — основна техніка
Перш ніж розглядати окремі симуляції, варто зрозуміти спільний фундамент, на якому вони всі побудовані. Традиційний 3D-рендеринг працює, перетворюючи трикутники через конвеєр растеризації: кожну поверхню потрібно описати як сітку полігонів. Це чудово працює для інженерних моделей, але погано підходить для об'єктів, заданих неявно — фракталів, поверхонь рідин, алгебричних форм — де жодна трикутна сітка непрактична.
Поле знакових відстаней (SDF) — це функція d(p), що повертає знакову відстань від будь-якої точки p у 3D-просторі до найближчої поверхні: додатну зовні, від'ємну всередині, нуль на поверхні. Для сфери з центром у початку координат і радіусом r:
d(p) = length(p) - r
SDF можна комбінувати простою арифметикою: об'єднання двох форм — це min(d1, d2); перетин — max(d1, d2); віднімання (вирізання однієї форми з іншої) — max(d1, -d2). Ця алгебра форм, яку називають конструктивною твердотільною геометрією (CSG), дозволяє будувати як завгодно складні об'єкти з примітивів, ніколи не конструюючи сітку.
Цикл raymarching
Маючи SDF, raymarching рендерить його, випускаючи промінь від камери крізь кожен піксель екрана й просуваючись уздовж променя кроками, розмір яких визначається значенням SDF:
float t = 0.0;
for (int i = 0; i < MAX_STEPS; i++) {
vec3 p = ray_origin + t * ray_dir;
float d = sdf(p);
if (d < EPSILON) { /* влучили в поверхню */ break; }
if (t > MAX_DIST) { /* промах, колір неба */ break; }
t += d; // безпечний крок: не перескочить поверхню
}
Ключове спостереження в тому, що значення SDF є безпечним розміром кроку: промінь не може перескочити поверхню, оскільки d(p) — це саме мінімальна відстань до будь-якої поверхні з точки p. Цей алгоритм сферичного трасування (Харт, 1996) одночасно безпечний і ефективний: у відкритому просторі кроки великі; поблизу поверхні кроки зменшуються до субпіксельної точності. Типова складна сцена сходиться за 50–100 ітерацій на піксель, кожна ітерація виконує одне обчислення SDF — навантаження, що ідеально лягає на масово-паралельне виконання GPU з одним потоком на піксель.
Нормалі поверхні та освітлення
Щойно знайдено влучення, нормаль поверхні обчислюється скінченними різницями — вибіркою SDF у шести сусідніх точках і взяттям градієнта:
vec3 normal(vec3 p) {
float e = 0.001;
return normalize(vec3(
sdf(p + vec3(e,0,0)) - sdf(p - vec3(e,0,0)),
sdf(p + vec3(0,e,0)) - sdf(p - vec3(0,e,0)),
sdf(p + vec3(0,0,e)) - sdf(p - vec3(0,0,e))
));
}
Це вимагає лише шести додаткових викликів SDF на піксель і працює для будь-якої неявної поверхні без явного обчислення нормалі сітки. Стандартне освітлення за Блінном-Фонгом або фізично коректне освітлення потім використовує нормаль, напрямок огляду й напрямок світла звичним чином. М'які тіні й оточувальна оклюзія обчислюються додатковими короткими трасуваннями променів у бік джерела світла: якщо мінімальне значення SDF тіньового променя дуже мале, точка частково затінена, що дає плавні напівтіні, неможливі з жорсткими картами тіней.
II. Mandelbulb — 3D-фрактал з оцінкою відстані
🌌Mandelbulb — GLSL raymarching 3D-фрактала
Досліджуйте 3D-фрактал у реальному часі. Змінюйте степінь n і параметри забарвлення пасток орбіт.
Множина Мандельброта живе на комплексній площині: для кожної точки c ітеруємо z → z² + c і перевіряємо, чи орбіта втікає в нескінченність. Розширення цього до трьох вимірів вимагає поняття «множення» для 3D-векторів. Mandelbulb (Уайт і Ніландер, 2009) використовує триплексний степінь у сферичних координатах (r, θ, φ):
z^n: r_new = r^n
θ_new = n · θ
φ_new = n · φ
Це не алгебра в строгому сенсі (їй бракує асоціативності при n ≠ 2), але вона дає візуально надзвичайні результати для n = 8: бульбашкоподібну структуру, вкриту складними сферичними опуклостями, кожна з яких, при ближчому розгляді, вкрита меншими копіями тих самих опуклостей — самоподібний фрактал на всіх масштабах.
Оцінювач відстані
Mandelbulb не має SDF у замкненій формі, але має оцінювач відстані (DE), виведений із похідної ітераційного відображення. Відстежуйте водночас поточну ітерацію z і її похідну dz/dc протягом циклу ітерацій:
// Кожна ітерація:
dz = n * pow(r, n-1) * dz + 1.0;
z = triplex_pow(z, n) + c;
if (length(z) > BAILOUT) break;
Після того як ітерація втекла в нескінченність, оцінка відстані дорівнює:
de = 0.5 · log(|z|) · |z| / |dz/dc|
Ця оцінка точна з точністю до невеликого постійного множника поблизу поверхні, чого достатньо для raymarching: цикл сферичного трасування може безпечно використовувати її як розмір кроку. Ціна в тому, що кожен крок raymarching вимагає запуску повної фрактальної ітерації для визначення DE, що робить Mandelbulb однією з найінтенсивніших для GPU симуляцій на сайті — проте вона все ж інтерактивна на 60 кадрах/с на сучасному залізі.
Спробуйте: Змініть степінь із n = 8 на n = 2 і зверніть увагу, що Mandelbulb стає майже сферичним (3D-триплексна ітерація зі степенем 2 не дає самоподібної структури). Степені 3–5 дають видовжені, «інопланетні» форми. Степінь 8 — це «класичний» Mandelbulb із максимальною складністю поверхні.
III. Губка Менгера — IFS через згортання SDF
🧊Губка Менгера — SDF-система ітеративних функцій
Raymarching губки Менгера в реальному часі. Наближайте нескінченну деталізацію кожного рівня ітерації.
Губка Менгера будується повторним видаленням центрального кубика й центрів кожної грані з куба — система ітеративних функцій (IFS), що дає фрактал із розмірністю Хаусдорфа log(20)/log(3) ≈ 2.727. Побудова сітки навіть для 4 ітерацій вимагає мільйонів полігонів. Підхід через SDF, використовуючи згортання простору, обчислює будь-яку глибину ітерації функцією з постійною вартістю.
Ключовий трюк — модулярне згортання: замість рекурсивного поділу простору, простір складається так, щоб одне обчислення SDF базового куба представляло весь фрактал. На кожній ітерації координати згортаються у фундаментальну область групи симетрії, масштабуються втричі, і віднімається хрестоподібна порожнина:
float menger_sdf(vec3 p, int iterations) {
float d = box_sdf(p, vec3(1.0));
float scale = 1.0;
for (int i = 0; i < iterations; i++) {
// Згорнути в одиничну комірку, відобразити крізь осі
vec3 q = mod(p * scale, 2.0) - 1.0;
q = abs(q);
// Хрестоподібна порожнина: видаляємо центральну трубку в кожній парі осей
float cross = min(max(q.x, q.y),
min(max(q.y, q.z),
max(q.z, q.x)));
float cavity = cross - 1.0/3.0;
d = max(d, -cavity / scale);
scale *= 3.0;
}
return d;
}
Кожна ітерація циклу відповідає одному рівню фрактальної ієрархії. Ділення на scale коригує SDF, враховуючи просторове стиснення, внесене згортанням. Краса цього підходу в тому, що ітерації від 1 до 6+ доступні ціною лише кількох додаткових ітерацій циклу без жодної зміни геометрії — просто повзунок, що керує кількістю ітерацій.
Спробуйте: На ітерації 1 структура впізнавана як куб із 7 прямокутними отворами. До ітерації 4 поверхня перетворюється на мереживо з тонких балок. Зверніть увагу, що частота кадрів симуляції падає зі зростанням кількості ітерацій — кожен додатковий рівень множить кроки raymarching, необхідні для кожного пікселя.
IV. Червоточина 3D — геометрія Морріса-Торна в GLSL
🌀Червоточина 3D — вкладення Морріса-Торна в GLSL
Пролетіть камерою крізь горловину прохідної червоточини. Регулюйте радіус горловини та параметри червоного зсуву.
Червоточина Морріса-Торна — розв'язок рівнянь поля Ейнштейна, що описує прохідний тунель між двома областями простору-часу. Метрика у сферичних координатах:
ds² = -e^(2Φ) dt² + dl² + (b₀² + l²)(dθ² + sin²θ dφ²)
де l — власна відстань уздовж осі тунелю, b₀ — радіус горловини (мінімальний радіус кола / 2π), а Φ(l) — функція червоного зсуву. Поверхня вкладення — 2D-зріз, що відображає просторову геометрію — це поверхня обертання z(r) = ±b₀ · ln(r/b₀ + √((r/b₀)²-1)), яка розширюється назовні від горловини, наче два з'єднані рупори.
GLSL-симуляція не розв'язує повні загальнорелятивістські рівняння променів (що вимагало б інтегрування зв'язаних диференціальних рівнянь на кожен піксель у кожному кадрі). Натомість вона використовує геометричне наближення: промені викривляються, наближаючись до горловини, шейдером просторового викривлення, який плавно відображає вхідні напрямки у вихідні відповідно до внутрішньої кривини вкладення. Зорі й туманності, здається, огортають горловину через гравітаційне лінзування. Це та сама техніка, що використана у фільмі «Інтерстеллар» (2014), для якого Кіп Торн і команда візуальних ефектів Double Negative розробили фізично точний GLSL-рендерер червоточини.
Спробуйте: Наблизьтеся до горловини з одного боку й пролетіть до іншого всесвіту. Зменшіть радіус горловини до майже нуля й зверніть увагу на екстремальне лінзувальне спотворення, що концентрує весь вигляд протилежної сторони в одне яскраве кільце — кільце Ейнштейна, утворене гравітаційним мікролінзуванням.
V. Суперформа — суперформула Гілейса на GPU
🌸Суперформа — суперформула Гілейса на GPU
Проходьте шість параметрів суперформули й перетворюйте квіти на морських зірок, кристали й тороїдальні вузли.
Йохан Гілейс представив суперформулу 2003 року як узагальнення кола, здатне описати надзвичайно широкий діапазон природних і математичних форм лише шістьма параметрами (m, n1, n2, n3, a, b):
r(θ) = [ |cos(mθ/4)/a|^n2 + |sin(mθ/4)/b|^n3 ]^(-1/n1)
У полярних координатах r(θ) дає радіус під кутом θ. Для 3D-суперформи два незалежні обчислення суперформули застосовуються як радіуси широти й довготи, породжуючи сфероподібну поверхню, деформовану добутком двох радіальних функцій. GPU відображає кожен піксель безпосередньо в координату (θ, φ), обчислює обидві формули, обчислює 3D-позицію на поверхні й рендерить через стандартну модель освітлення — жодної сітки, жодного геометричного шейдера, лише чиста параметрична поверхня, обчислена на піксель.
Спробуйте: Встановіть m = 3, n1 = n2 = n3 = 5 для округленого трикутника; m = 8, n1 = 2, n2 = n3 = 8 для зірки; m = 5, n1 = n2 = n3 = 1 наближається до п'ятикутника Рело. Простір параметрів величезний — більшість його ще недосліджена.
VI. Частинки GPGPU — 250 000 частинок через render-to-texture
✨Частинки GPGPU — 250 000 частинок через render-to-texture
Півмільйона значень позицій, збережених як пікселі текстури, оновлюються й рендеряться повністю на GPU щокадру.
Центральний виклик GPU-симуляції частинок — обмеження WebGL: шейдер не може одночасно читати з тієї самої текстури й писати в неї. Техніка ping-pong розв'язує це, підтримуючи дві пари цілей рендерингу (A і B). У непарних кадрах обчислювальний прохід читає з A і пише в B; у парних — читає з B і пише в A. ЦП більше не торкається даних частинок після ініціалізації.
// Псевдокод для одного кадру:
gl.bindFramebuffer(writeFBO); // прив'язуємо ціль B
gl.useProgram(computeShader);
gl.bindTexture(readTexture); // прив'язуємо ціль A
gl.drawArrays(TRIANGLES, 0, 6); // повноекранний квад: запускає фізику на GPU
swap(A, B); // наступний кадр читає з B
// Прохід рендерингу:
gl.bindFramebuffer(null); // задній буфер
gl.useProgram(renderShader);
gl.bindTexture(positionTexture); // поточні позиції (щойно записані)
gl.drawArrays(POINTS, 0, N); // кожна вершина точки вибирає свій власний тексель
Позиція кожної частинки зберігається як тексель RGBA32F (чотири 32-бітні числа з рухомою комою: позиція xyz, w для віку чи маси). Текстура 512×512 зберігає 262 144 частинки. Обчислювальний шейдер — це повноекранний квад, чий фрагментний шейдер шукає позицію частинки в gl_FragCoord, застосовує фізичне оновлення (гравітацію, шум завихрення чи атрактор) і записує нову позицію як вихідний колір. Шейдер рендерингу — це виклик малювання точок: кожен індекс вершини точки відображається на координату текселя, зчитує позицію через вибірку текстури у вершинному шейдері та виводить точковий спрайт.
Шум завихрення та бездивергентний потік
Найвізуально переконливіша поведінка частинок використовує шум завихрення (curl noise) для поля швидкостей. Для будь-якого 3D-векторного потенціалу A(p) ротор F = ∇ × A математично гарантовано є бездивергентним:
∇ · (∇ × A) = 0 (тотожність векторного числення)
Бездивергентне поле не має джерел чи стоків — частинки ні не збиваються в щільні кульки, ні не розлітаються врізнобіч. Натомість вони формують нестисливі вихрові потоки, які разюче нагадують дим, хмари й океанські течії. Векторний потенціал A береться як 3D симплекс-шум, а ротор обчислюється аналітично (градієнт шуму диференційовний). Адвекція 250 000 частинок через шум завихрення на 60 кадрах/с не вимагає жодної участі ЦП після компіляції шейдера — GPU повністю обробляє весь конвеєр симуляції й рендерингу.
Спробуйте: Перемкніть поле сил із шуму завихрення на обертовий атрактор. Спостерігайте, як частинки за кілька секунд закручуються в тороїдальну структуру. Потім перемкніться назад на шум завихрення й порівняйте якісно інший, більш турбулентний рух — обидва повністю детерміновані, але шум завихрення досліджує весь 3D-об'єм, тоді як атрактор обмежує частинки багатовидом нижчої розмірності.