Девлог №94 – Спецвипуск WebGL/GLSL хвилі 73: Mandelbulb, губка Менгера, кротовина 3D, суперформа та частинки GPGPU

Хвиля 73 повністю працює на GPU — кожна симуляція виконується як GLSL-шейдерний код на відеокарті. Raymarching 3D-фрактала, згортання простору у губку, викривлення простору-часу навколо горловини кротовини, ліплення інопланетної геометрії з суперформули та одночасна симуляція чверті мільйона частинок. Ця хвиля розсунула межі того, що взагалі здатна відрендерити вкладка браузера.

Хвиля 73 — 5 доданих симуляцій

Усі п'ять симуляцій хвилі 73 вимагають WebGL2

Кожна симуляція використовує власні GLSL-фрагментні шейдери, raymarching, signed distance field або GPGPU ping-pong render targets — техніки, що виконують тисячі паралельних обчислень на кадр прямо на GPU. Потрібен браузер із підтримкою WebGL2 (Chrome, Firefox, Edge, Safari 15+). Симуляції автоматично визначають підтримку WebGL2 і показують запасне повідомлення, якщо її немає.

638
Всього симуляцій
5
Нові цієї хвилі
73
Номер хвилі
94
№ девлогу

GPU-технології, використані у цій хвилі

Sphere-tracing / Raymarching
Просування променя крізь простір кроками рівними відстані SDF, доки не станеться влучення чи не вичерпається ліміт кроків
Signed Distance Fields
Скалярна функція d(p) = знакова відстань від точки p до поверхні; від'ємна всередині, додатна зовні
Оцінювачі відстані
Для фракталів: наближена нижня межа відстані до множини, що дозволяє безпечний розмір кроку
GPGPU Ping-Pong
Дві пари WebGLRenderTargets, що чергуються щокадру: читання з A, запис у B, обмін
Three.js BufferGeometry
Динамічна сітка з покадровим оновленням позицій у JavaScript; власні вершинний і фрагментний шейдери
Нормалі скінченних різниць
Градієнт SDF оцінюється через 6 сусідніх вибірок; дешева альтернатива аналітичним нормалям

Нові симуляції

🌌

Mandelbulb GLSL Raymarching

Raymarching 3D-фрактала GLSL-фрагментним шейдером. Триплексна степінь n, оцінювач відстані de=0.5·log(r)·r/dr. 96 кроків + нормалі скінченних різниць + м'які тіні + AO. Повзунок степені 2–12. 4 палітри.

🧊

Губка Менгера GLSL SDF

Ітеративне SDF-згортання: згортання до одиничного куба, віднімання хрестоподібної порожнини, масштабування ×3, повтор. 3 режими: суцільний/каркас/рентген. Розмірність Хаусдорфа log20/log3 ≈ 2,7268. Площа поверхні → 0 при ітераціях → ∞.

🌀

Кротовина 3D GLSL-шейдер

Кротовина Морріса-Торна, яку можна перетнути. Параболоїд Флемма r(ℓ)=√(b₀²+ℓ²). GLSL-змішування неба двох всесвітів. 3 режими: вкладення/від першої особи/поперечний переріз. Анімація пульсації горловини.

🌸

Суперформа Three.js Shader

Суперформула Гіліса r(φ)=(|cos(mφ/4)/a|^n2+|sin(mφ/4)/b|^n3)^(−1/n1) як сферичний добуток. Жива BufferGeometry 300×300. 8 пресетів. Матовий/металевий, каркас, збереження PNG.

Частинки GPGPU WebGL2 GPGPU

Ping-pong GPGPU: 2×2 WebGLRenderTargets (RGBA FloatType) для позицій і швидкостей. Обчислювальний фрагментний шейдер. 4 поведінки: шум завихрення/N-тіла/boids/Лоренц. До 250 000 частинок.

Mandelbulb: raymarching 3D-фрактала

Mandelbulb — найуспішніший 3D-аналог множини Мандельброта. Він використовує "триплексну" алгебру — спосіб множення 3D-векторів через піднесення сферичних координат до степеня — щоб узагальнити двовимірну ітерацію z → z^n + c на три виміри.

Триплексна степінь та оцінювач відстані

Ітерація для степеня n відображає точку (r, θ, φ) у сферичних координатах у:

r_new    = r^n
theta_new = n * theta
phi_new   = n * phi
x_new    = r_new * sin(theta_new) * cos(phi_new) + cx
y_new    = r_new * cos(theta_new) + cy
z_new    = r_new * sin(theta_new) * sin(phi_new) + cz

Оцінювач відстані використовує величину похідної dr, що накопичується разом з r під час ітерації: dr = n·r^(n-1)·dr + 1. Нижня межа відстані до множини — de = 0.5 · |log(r)| · r / dr. Цей безпечний розмір кроку дозволяє алгоритму raymarching швидко просуватись крізь порожній простір і сповільнюватись поблизу поверхні — sphere tracing, а не марш фіксованими кроками.

Затінення: нормалі, тіні та ambient occlusion

Нормалі оцінюються скінченними різницями: обчислюємо SDE у шести сусідніх точках (±ε по кожній осі) і беремо градієнт. М'які тіні наближаються, просуваючи додатковий промінь до джерела світла й відслідковуючи мінімальне співвідношення розміру кроку до відстані по дорозі. Ambient occlusion вибирає п'ять відстаней вздовж нормалі поверхні й обчислює, наскільки поверхня загинається сама на себе — вгнуті щілини темніші.

Губка Менгера: ітеративне SDF-згортання

Губка Менгера визначається через рекурсивне віднімання: беремо куб, ділимо його на 3×3×3 = 27 підкубів, видаляємо 7 центральних хрестоподібних частин (центри граней і центр тіла) і повторюємо на кожному з підкубів, що залишились. Після нескінченної кількості ітерацій результат має розмірність Хаусдорфа log(20)/log(3) ≈ 2,7268 — менше за 3D, але більше за 2D.

GLSL SDF через ітеративне згортання

Функція знакової відстані для губки обчислюється повністю всередині фрагментного шейдера елегантною ітеративною технікою згортання:

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 - 2.0;            // масштабування та зміщення
    if (p.z < -1.0) p.z += 2.0;  // згортання назад
}
float d = (length(max(abs(p) - 1.0, 0.0))) / pow(3.0, float(iterations));

Кожна зовнішня ітерація подвоює ефективну глибину ітерацій. Отриманий SDF можна trace-ити sphere-tracing'ом з інтерактивною частотою кадрів для до 6 ітерацій губки.

Три режими рендерингу та математичні властивості

Панель математичних властивостей показує живі значення: загальну площу поверхні (6·(5/9)^n·20^n → 0 при n → ∞ — поверхня зменшується на кожному кроці) та формулу розмірності Хаусдорфа log(N_залишених)/log(коефіцієнт_масштабу) = log(20)/log(3).

Кротовина 3D: шейдер вкладення простору-часу

Кротовина Морріса-Торна, яку можна перетнути, — гіпотетичний розв'язок рівнянь поля Ейнштейна, що з'єднує дві віддалені області простору-часу через "горловину". Її неможливо створити без екзотичної матерії (від'ємної густини енергії), але вона дає ефектну візуалізацію та справжній урок диференціальної геометрії.

Вкладення параболоїда Флемма

Просторова геометрія кротовини в певний момент часу описується параболоїдом Флемма. У екваторіальній площині власна радіальна відстань ℓ відображається у евклідів радіус вкладення r(ℓ) = √(b₀² + ℓ²) та висоту z(ℓ) = b₀ · arcsinh(ℓ/b₀), де b₀ — радіус горловини. Це дає дві розтрублені вирви, з'єднані при r = b₀.

GLSL-шейдер: небо двох всесвітів та горловина

Фрагментний шейдер визначає, з якого боку горловини опиниться промінь, інтегруючи геодезичний шлях крізь вкладення. "Небо" кожного всесвіту — дискретна множина кольорових зірок; два всесвіти мають різні кольорові температури. При зменшенні b₀ (вужча горловина) крізь отвір видно більше неба кожного всесвіту. Градієнт доплерівського синього зсуву поблизу горловини дає візуальне відчуття гравітаційної глибини. Три режими:

Суперформа: геометрія суперформули Гіліса

Суперформула Йохана Гіліса 2003 року — це єдине параметричне рівняння, що генерує надзвичайну різноманітність форм — від платонових тіл до квітів та інопланетних геометрій — варіюючи шість параметрів. Застосована як сферичний добуток (один раз для широти θ, один раз для довготи φ), вона створює 3D-поверхні надзвичайної багатоманітності.

Суперформула та сферичний добуток

Полярний радіус у 2D:

r(φ) = (|cos(m·φ/4) / a|^n2 + |sin(m·φ/4) / b|^n3)^(-1/n1)

Для 3D сферичного добутку обчислюємо r₁(θ) та r₂(φ) незалежно. Точка 3D-поверхні:

x = r1(θ) · cos(θ) · r2(φ) · cos(φ)
y = r1(θ) · sin(θ) · r2(φ) · cos(φ)
z = r2(φ) · sin(φ)

Сітка — це решітка UV-точок 300×300, кожна обчислюється незалежно й завантажується в Three.js BufferGeometry. Зміна параметрів перебудовує геометрію за один прохід JavaScript приблизно за 20 мс.

Вісім пресетів та система матеріалів

Частинки GPGPU: 250 000 частинок на GPU

General-Purpose GPU computing (GPGPU) перепрофільовує графічний конвеєр у масово паралельний обчислювальний двигун. Кожен "піксель" текстури рендеру з плаваючою комою зберігає стан однієї частинки. Фрагментний шейдер, що "малює" цю текстуру, насправді обчислює новий стан частинки — і, читаючи з однієї текстури під час запису в іншу (ping-pong), ми можемо симулювати сотні тисяч частинок з інтерактивною частотою кадрів повністю на GPU.

Архітектура ping-pong

Симуляція підтримує дві пари WebGLRenderTargets типу RGBA FloatType: одну для позицій (x, y, z, вік) і одну для швидкостей (vx, vy, vz, маса). Щокадру:

  1. Прив'язується "обчислювальний" шейдер із поточними текстурами позицій/швидкостей як uniforms
  2. Рендериться повноекранний квад у наступні текстури позицій/швидкостей
  3. Поточна й наступна текстури міняються місцями (ping ↔ pong)
  4. Частинки рендеряться як GL_POINTS, вибірка позиції з поточної текстури відбувається у вершинному шейдері

Дані ніколи не покидають GPU між кадрами — CPU лише задає початкові умови й підкручує uniforms.

Чотири поведінки частинок

Інженерні нотатки: WebGL2 та текстури з плаваючою комою

Щоб змусити 250 000 частинок працювати коректно, довелось розв'язати кілька специфічних для WebGL2 проблем:

Точність текстур з плаваючою комою

WebGL2 підтримує OES_texture_float (32-бітні RGBA-текстури з плаваючою комою) як базову можливість, усуваючи танці з розширеннями, потрібні у WebGL1. Проте лінійна фільтрація текстур з плаваючою комою досі вимагає OES_texture_float_linear — GPGPU-симуляція частинок використовує вибірку найближчого сусіда, оскільки кожна частинка відповідає рівно одному тексел.

MRT та одночасне оновлення позиції/швидкості

Одночасне оновлення позиції та швидкості без використання швидкості з попереднього кадру вимагало б читання й запису в ту саму текстуру — неможливо у WebGL. Рішення — окремі текстури позицій і швидкостей, кожна у своїй парі ping-pong. Обчислювальний шейдер читає обидві з "поточного" набору й записує обидві у "наступний", зберігаючи весь стан узгодженим.

Індексація частинок

UV-координата кожної частинки в текстурі обчислюється з її індексу: uv = vec2(float(index % WIDTH), float(index / WIDTH)) / float(WIDTH). Вершинний шейдер читає цю UV з текстури позицій і використовує результат як gl_Position. При 250 000 частинок у текстурі 500×500 кожен тексел — це частинка.

Що далі

Це була хвиля 73 — усі 5 симуляцій, усі нативні для GPU. Маючи вже 638 живих симуляцій у всіх категоріях, ми робимо паузу для консолідації: виправляємо мобільні макети, додаємо відсутні клавіатурні скорочення й розширюємо пошуковий індекс перед стартом хвилі 74. Перегляньте архів блогу, щоб побачити всі попередні девлоги.

← Девлог №93 Усі дописи →