Technologie GPU wykorzystane w tej fali
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.
- Suwak potęgi n od 2 (klasyczny Mandelbulb) do 12 (coraz bardziej kolczasty)
- 96 kroków raymarchingu na piksel; wczesne zakończenie, gdy odległość < 0,001
- 4 palety kolorów: Inferno (ognista), Viridis (naukowa), Mono (czysta), Iris (fioletowo-cyjanowa)
- Przeciąganie myszą do obracania; przewijanie do przybliżania szczegółów powierzchni
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
- Bryła: standardowy raymarching z cieniowaniem Phonga i ambient occlusion — ujawnia zawiłą teksturę powierzchni
- Siatka: renderowanie krawędzi izopowierzchni SDF z niewielkim przesunięciem — ukazuje sześcienne samopodobieństwo
- Rentgen: akumulacja gęstości wzdłuż każdego promienia proporcjonalnie do odległości "prawie trafienia" — pokazuje wewnętrzną strukturę jak tomografia komputerowa
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:
- Osadzenie: widok z trzeciej osoby na całą powierzchnię paraboloidy obracającą się w przestrzeni
- FPV (pierwsza osoba): przelot przez gardło — obserwuj, jak jeden wszechświat ustępuje miejsca drugiemu
- Przekrój: przekrój przez płaszczyznę równikową ukazujący dwa połączone arkusze
- Suwak b₀ (promień gardła): od wąskiego otworu po szeroki tunel
- Animacja pulsowania gardła: okresowe oscylacje b₀
- Gęstość gwiazd i temperatura barwowa niezależne dla każdego wszechświata
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
- Sfera: m=0, n1=n2=n3=1, a=b=1 — forma bazowa
- Gwiazda: m=5, n1=1, n2=n3=2 — pięciokątna kolczasta forma
- Skręcony pączek: asymetryczne parametry n z niecałkowitym m — samoprzecinający się wariant torusa
- Korona: wysokie m, niskie n1 — postrzępiony pas równikowy
- Poduszka: niskie m, wysokie n — napompowany przekrój kwadratowy
- Diament: m=4, wszystkie n równe — przybliżenie oktaedryczne
- Kwiat: m=7, niskie n1, wysokie n2/n3 — forma z rozchodzącymi się płatkami
- Grzyb: asymetryczne parametry θ/φ — forma z kapeluszem i trzonem
- Matowy i metaliczny tryb materiału PBR
- Przełącznik nakładki siatki
- Przycisk zapisu PNG przechwytuje bieżący widok w pełnej rozdzielczości płótna
- Wszystkie sześć parametrów superformuły dostępnych jako suwaki na żywo
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ę:
- Powiązanie shadera "obliczeniowego" z bieżącymi teksturami pozycji/prędkości jako uniformami
- Renderowanie pełnoekranowego quada do kolejnych tekstur pozycji/prędkości
- Zamiana bieżącej i kolejnej tekstury (ping ↔ pong)
- 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
- Szum curl: pole prędkości wyprowadzone z curla gradientu trójwymiarowego szumu Simplex — bezdywergencyjne, więc cząstki nigdy się nie zbijają ani nie rozpraszają; tworzy płynne, wirujące pasma
- Atraktory N-ciał: 4 stałe punkty przyciągające z
F = m/r²·n̂plus opór kwadratowy; cząstki krążą po chaotycznych ósemkach - Przybliżenie boidów: reguły separacji, wyrównania i kohezji zakodowane jako funkcje ważone odległością lokalnego pola gęstości próbkowanego z tekstury pozycji
- Atraktor Lorenza: każda cząstka niezależnie całkuje równanie różniczkowe Lorenza
(ẋ=σ(y-x), ẏ=rx-y-xz, ż=xy-bz)z nieco innym warunkiem początkowym — atraktor motyla wyrenderowany gęstością cząstek
- Suwak N: od 1000 do 250 000 cząstek (tekstura 512×512)
- Sprawdzenie obsługi WebGL2: łagodny komunikat zastępczy na nieobsługiwanych urządzeniach
- Rozmiar punktu i przezroczystość reagują na wiek cząstki, dając zanikanie przy narodzinach/śmierci
- Licznik FPS pokazuje koszt obliczeniowy GPU przy różnych poziomach N
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.