Devlog #94 – Fala 73, Edycja Specjalna WebGL/GLSL: Mandelbulb, Gąbka Mengera, Tunel Czasoprzestrzenny 3D, Superkształt i Cząstki GPGPU

Fala 73 działa w całości na GPU — każda symulacja jest kodem shaderów GLSL wykonywanym na karcie graficznej. Raymarching fraktala 3D, składanie przestrzeni w gąbkę, wykrzywianie czasoprzestrzeni wokół gardła tunelu, rzeźbienie obcej geometrii z superformuły oraz jednoczesna symulacja ćwierć miliona cząstek. Ta fala przesunęła granice tego, co jest w stanie wyrenderować karta przeglądarki.

Fala 73 — 5 dodanych symulacji

Wszystkie pięć symulacji z fali 73 wymaga WebGL2

Każda symulacja wykorzystuje własne fragmentowe shadery GLSL, raymarching, signed distance fields lub GPGPU ping-pong render targets — techniki wykonujące tysiące równoległych obliczeń na klatkę bezpośrednio na GPU. Wymagana jest przeglądarka obsługująca WebGL2 (Chrome, Firefox, Edge, Safari 15+). Symulacje automatycznie wykrywają obsługę WebGL2 i wyświetlają komunikat zastępczy, jeśli nie jest dostępna.

638
Symulacji łącznie
5
Nowych w tej fali
73
Numer fali
94
Numer devlogu

Technologie GPU wykorzystane w tej fali

Sphere-tracing / Raymarching
Przesuwanie promienia przez przestrzeń krokami o długości równej odległości SDF, aż do trafienia lub osiągnięcia maksymalnej liczby kroków
Signed Distance Fields
Funkcja skalarna d(p) = znakowana odległość od punktu p do powierzchni; ujemna wewnątrz, dodatnia na zewnątrz
Estymatory odległości
Dla fraktali: przybliżone dolne ograniczenie odległości do zbioru, umożliwiające bezpieczny rozmiar kroku
GPGPU Ping-Pong
Dwie pary WebGLRenderTargets naprzemiennie zmieniane co klatkę: odczyt z A, zapis do B, zamiana
Three.js BufferGeometry
Dynamiczna siatka z aktualizacją pozycji w JavaScript co klatkę; własne shadery wierzchołków i fragmentów
Normalne z różnic skończonych
Gradient SDF szacowany przez 6 pobliskich próbek; tania alternatywa dla normalnych analitycznych

Nowe symulacje

🌌

Mandelbulb GLSL Raymarching

Raymarching fraktala 3D fragmentowym shaderem GLSL. Potęga triplex n, estymator odległości de=0,5·log(r)·r/dr. 96 kroków + normalne z różnic skończonych + miękkie cienie + AO. Suwak potęgi 2–12. 4 palety.

🧊

Gąbka Mengera GLSL SDF

Iterowane składanie SDF: złożenie do sześcianu jednostkowego, odjęcie krzyżowej wnęki, skalowanie ×3, powtórz. 3 tryby: bryła/siatka/rentgen. Wymiar Hausdorffa log20/log3 ≈ 2,7268. Powierzchnia → 0 przy liczbie iteracji → ∞.

🌀

Tunel Czasoprzestrzenny 3D GLSL Shader

Przekraczalny tunel czasoprzestrzenny Morrisa-Thorne'a. Paraboloida Flamma r(ℓ)=√(b₀²+ℓ²). Mieszanie nieba dwóch wszechświatów w GLSL. 3 tryby: osadzenie/FPV/przekrój. Animacja pulsowania gardła.

🌸

Superkształt Three.js Shader

Superformuła Gielisa r(φ)=(|cos(mφ/4)/a|^n2+|sin(mφ/4)/b|^n3)^(−1/n1) jako iloczyn sferyczny. Żywa BufferGeometry 300×300. 8 presetów. Matowy/metaliczny, siatka, zapis PNG.

Cząstki GPGPU WebGL2 GPGPU

Ping-pong GPGPU: 2×2 WebGLRenderTargets (RGBA FloatType) na pozycje i prędkości. Obliczeniowy shader fragmentowy. 4 zachowania: szum curl/N-ciała/boidy/Lorenz. Do 250 000 cząstek.

Mandelbulb: raymarching fraktala 3D

Mandelbulb to najbardziej udany trójwymiarowy odpowiednik zbioru Mandelbrota. Wykorzystuje algebrę "triplex" — sposób mnożenia wektorów 3D poprzez potęgowanie we współrzędnych sferycznych — aby uogólnić dwuwymiarową iterację z → z^n + c na trzy wymiary.

Potęga triplex i estymator odległości

Iteracja dla potęgi n odwzorowuje punkt (r, θ, φ) we współrzędnych sferycznych na:

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

Estymator odległości wykorzystuje wielkość pochodnej dr, akumulowaną wraz z r podczas iteracji: dr = n·r^(n-1)·dr + 1. Dolne ograniczenie odległości do zbioru to de = 0,5 · |log(r)| · r / dr. Ten bezpieczny rozmiar kroku pozwala raymarcherowi szybko poruszać się przez pustą przestrzeń i zwalniać w pobliżu powierzchni — sphere tracing zamiast marszu o stałym kroku.

Cieniowanie: normalne, cienie i ambient occlusion

Normalne są szacowane metodą różnic skończonych: SDE jest obliczana w sześciu pobliskich punktach (±ε na każdej osi), a następnie brany jest gradient. Miękkie cienie są przybliżane przez wysłanie drugorzędnego promienia w stronę światła i śledzenie minimalnego stosunku rozmiaru kroku do odległości po drodze. Ambient occlusion próbkuje pięć odległości wzdłuż normalnej powierzchni i oblicza, jak bardzo powierzchnia zakrzywia się sama w siebie — wklęsłe szczeliny są ciemniejsze.

Gąbka Mengera: iterowane składanie SDF

Gąbka Mengera jest zdefiniowana przez rekurencyjne odejmowanie: bierzemy sześcian, dzielimy go na 3×3×3 = 27 podsześcianów, usuwamy 7 centralnych elementów krzyżowych (środki ścian i środek bryły) i powtarzamy operację na każdym z pozostałych podsześcianów. Po nieskończonej liczbie iteracji wynik ma wymiar Hausdorffa log(20)/log(3) ≈ 2,7268 — mniej niż 3D, ale więcej niż 2D.

SDF w GLSL przez iterowane składanie

Funkcja odległości znakowanej dla gąbki jest obliczana w całości wewnątrz shadera fragmentowego przy pomocy eleganckiej iteracyjnej techniki składania:

for (int i = 0; i < iterations; i++) {
    p = abs(p);                    // złożenie do dodatniego oktantu
    if (p.x < p.y) p.xy = p.yx;  // sortowanie osi
    if (p.x < p.z) p.xz = p.zx;
    if (p.y < p.z) p.yz = p.zy;
    p = p * 3.0 - 2.0;            // skalowanie i przesunięcie
    if (p.z < -1.0) p.z += 2.0;  // złożenie z powrotem
}
float d = (length(max(abs(p) - 1.0, 0.0))) / pow(3.0, float(iterations));

Każda zewnętrzna pętla podwaja efektywną głębokość iteracji. Otrzymany SDF można renderować metodą sphere-tracing z interaktywną liczbą klatek na sekundę dla maksymalnie 6 iteracji gąbki.

Trzy tryby renderowania i właściwości matematyczne

Panel właściwości matematycznych pokazuje wartości na żywo: całkowitą powierzchnię (6·(5/9)^n·20^n → 0 przy n → ∞ — powierzchnia kurczy się przy każdym kroku) oraz wzór na wymiar Hausdorffa log(N_zachowanych)/log(współczynnik_skali) = log(20)/log(3).

Tunel Czasoprzestrzenny 3D: shader osadzenia czasoprzestrzeni

Przekraczalny tunel czasoprzestrzenny Morrisa-Thorne'a to hipotetyczne rozwiązanie równań pola Einsteina, które łączy dwa odległe regiony czasoprzestrzeni poprzez "gardło". Nie da się go stworzyć bez egzotycznej materii (ujemnej gęstości energii), ale stanowi spektakularną wizualizację i prawdziwą lekcję geometrii różniczkowej.

Osadzenie paraboloidy Flamma

Geometria przestrzenna tunelu w danej chwili czasu jest opisana przez paraboloidę Flamma. W płaszczyźnie równikowej właściwa odległość radialna ℓ odwzorowuje się na euklidesowy promień osadzenia r(ℓ) = √(b₀² + ℓ²) oraz wysokość z(ℓ) = b₀ · arcsinh(ℓ/b₀), gdzie b₀ to promień gardła. Daje to dwa rozszerzające się leje połączone przy r = b₀.

Shader GLSL: niebo dwóch wszechświatów i gardło

Shader fragmentowy określa, po której stronie gardła znajdzie się promień, integrując ścieżkę geodezyjną przez osadzenie. "Niebo" każdego wszechświata to dyskretny zbiór kolorowych gwiazd — oba wszechświaty mają różne temperatury barwowe. Wraz ze zmniejszaniem b₀ (węższe gardło) przez otwór widać coraz więcej nieba każdego wszechświata. Gradient przesunięcia ku błękitowi przypominający efekt Dopplera w pobliżu gardła daje wizualne wrażenie głębi grawitacyjnej. Trzy tryby:

Superkształt: geometria superformuły Gielisa

Superformuła Johana Gielisa z 2003 roku to pojedyncze równanie parametryczne generujące niezwykłą różnorodność kształtów — od brył platońskich przez kwiaty po obce geometrie — poprzez zmianę sześciu parametrów. Zastosowana jako iloczyn sferyczny (raz dla szerokości θ, raz dla długości φ), tworzy powierzchnie 3D o niezwykłym bogactwie.

Superformuła i iloczyn sferyczny

Promień biegunowy w 2D to:

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

Dla 3D iloczynu sferycznego oblicza się niezależnie r₁(θ) oraz r₂(φ). Punkt powierzchni 3D to:

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

Siatka to 300×300 punktów UV, każdy obliczany niezależnie i przesyłany do Three.js BufferGeometry. Zmiana parametrów przebudowuje geometrię w jednym przebiegu JavaScript w około 20 ms.

Osiem presetów i system materiałów

Cząstki GPGPU: 250 000 cząstek na GPU

Obliczenia ogólnego przeznaczenia na GPU (GPGPU) wykorzystują potok graficzny jako masowo równoległy silnik obliczeniowy. Każdy "piksel" zmiennoprzecinkowej tekstury renderowania przechowuje stan jednej cząstki. Shader fragmentowy, który "rysuje" tę teksturę, w rzeczywistości oblicza nowy stan cząstki — a poprzez odczyt z jednej tekstury podczas zapisu do drugiej (ping-pong) możemy symulować setki tysięcy cząstek z interaktywną liczbą klatek na sekundę w całości na GPU.

Architektura ping-pong

Symulacja utrzymuje dwie pary WebGLRenderTargets typu RGBA FloatType: jedną na pozycje (x, y, z, wiek) i jedną na prędkości (vx, vy, vz, masa). Co klatkę:

  1. Powiązanie shadera "obliczeniowego" z bieżącymi teksturami pozycji/prędkości jako uniformami
  2. Renderowanie pełnoekranowego quada do kolejnych tekstur pozycji/prędkości
  3. Zamiana bieżącej i kolejnej tekstury (ping ↔ pong)
  4. Renderowanie cząstek jako GL_POINTS, próbkowanie pozycji z bieżącej tekstury w shaderze wierzchołków

Żadne dane nie opuszczają GPU między klatkami — CPU jedynie ustawia warunki początkowe i dostraja uniformy.

Cztery zachowania cząstek

Notatki inżynierskie: WebGL2 i tekstury zmiennoprzecinkowe

Uzyskanie poprawnego działania 250 000 cząstek wymagało rozwiązania kilku problemów specyficznych dla WebGL2:

Precyzja tekstur zmiennoprzecinkowych

WebGL2 obsługuje OES_texture_float (32-bitowe tekstury RGBA float) jako funkcję rdzenną, eliminując zabawę z rozszerzeniami wymaganą w WebGL1. Jednak filtrowanie liniowe tekstur zmiennoprzecinkowych nadal wymaga OES_texture_float_linear — symulacja cząstek GPGPU korzysta z próbkowania najbliższego sąsiada, ponieważ każda cząstka odpowiada dokładnie jednemu tekselowi.

MRT i jednoczesna aktualizacja pozycji/prędkości

Jednoczesna aktualizacja pozycji i prędkości bez wykorzystania prędkości z poprzedniej klatki wymagałaby odczytu i zapisu tej samej tekstury — niemożliwe w WebGL. Rozwiązaniem są osobne tekstury pozycji i prędkości, każda we własnej parze ping-pong. Shader obliczeniowy odczytuje obie z "bieżącego" zestawu i zapisuje obie do "kolejnego", utrzymując spójność całego stanu.

Indeksowanie cząstek

Współrzędna UV każdej cząstki w teksturze jest obliczana z jej indeksu: uv = vec2(float(index % WIDTH), float(index / WIDTH)) / float(WIDTH). Shader wierzchołków odczytuje tę wartość UV z tekstury pozycji i wykorzystuje wynik jako gl_Position. Przy 250 000 cząstek w teksturze 500×500 każdy teksel to cząstka.

Co dalej

To była fala 73 — wszystkie 5 symulacji, wszystkie natywnie na GPU. Mając już 638 działających symulacji we wszystkich kategoriach, robimy przerwę na konsolidację: poprawiamy układy mobilne, dodajemy brakujące skróty klawiszowe i rozbudowujemy indeks wyszukiwania przed startem fali 74. Sprawdź archiwum bloga, aby zobaczyć wszystkie poprzednie devlogi.

← Devlog #93 Wszystkie wpisy →