Problem z milionem cząstek w języku JavaScript
CPU uruchamiający pętlowe aktualizacje cząstek w języku JavaScript, robi to po kolei: odczyt pozycji, aplikowanie siły, integracja, zapisywanie i przesuwanie do następnej cząstki. Przy 100 000 cząsteczek i 60 klatkach na sekundę oznacza to sześć milionów aktualizacji na sekundę na jednym wątku – osiągalne dla prostych ruchów, ale zawala się natychmiast, gdy aktualizacja wymaga sprawdzania sąsiedztwa lub rozgałęzienia na poziomie cząstki. GPU działa w odwrotny sposób: zamiast jednej szybkiej rdzeni robiąc wszystko sekwencyjnie, posiada tysiące małych rdzeni, które każda wykonują ten sam krótki program na różnych danych jednocześnie. Jeśli aktualizacja cząstki może być zapisana jako 'ten sam krótki funkcję stosowana niezależnie do każdej cząstki', GPU może uruchomić wszystkie w zasadzie naraz.
Cząstki jako piksele: tekstura ping-pongowa
Kluczem do możliwości ogólnego obliczeniowego przetwarzania GPU, zanim pojawiły się compute shadery – i nadal najprostszym, wieloplatformowym podejściem w WebGL – jest przechowywanie stanu cząstek jako pikseli w teksturze zmiennoprzecinkowej. Tekstura 512x512 przechowuje 262 144 cząstek, po jednej na każdy texel, a kanały RGBA przenoszą pozycję (x, y, z) oraz ewentualnie wiek lub czas życia. Aktualizacja wszystkich cząstek staje się rysowaniem jednego pełnego kwadratu: fragment shader uruchamiany jest raz na texel, odczytuje poprzednie tekstury pozycji i prędkości, aplikuje fizykę i zapisuje nowy stan w wyjściowej teksturze.
Uproszczony GLSL fragment shader, jeden texel = jedna cząstka vec4 pos = texture2D(texPosition, vUv); vec4 vel = texture2D(texVelocity, vUv); vec3 force = computeForce(pos.xyz); // szum curl / grawitacja / zasada boidów vel.xyz += force * dt; pos.xyz += vel.xyz * dt; gl_FragColor = pos; // zapisywane w WYJŚCIOWEJ teksturze, nigdy wejściowej Ponieważ GPU nie może bezpiecznie odczytywać i zapisywać tę samą teksturę w jednym wywołaniu draw call – fragmenty nie są gwarantowane do wykonania w jakiejkolwiek kolejności, a błąd odczyt po zapisie spowodowałby zanieczyszczenie symulacji – renderer utrzymuje dwie kopie każdej tekstury stanu i przełącza się między nimi: ten kadr odczytuje teksturę A i zapisuje teksturę B, następny kadr odczytuje B i zapisuje A. To przełączanie nazywane jest „ping-pong”, a to jest jedyny pomysł, na którym zbudowane są wszystkie pozostałe elementy symulacji GPGPU.
// simplified GLSL update fragment shader, one texel = one particle vec4 pos = texture2D(texPosition, vUv); vec4 vel = texture2D(texVelocity, vUv); vec3 force = computeForce(pos.xyz); // curl noise / gravity / boids rule vel.xyz += force * dt; pos.xyz += vel.xyz * dt; gl_FragColor = pos; // written into the OUTPUT texture, never the input
Cztery zachowania, jeden strumień danych
Tylko funkcja siły wewnątrz modułu shader update różni się między zachowaniami; hydraulika z wykorzystaniem 'ping-pongu' pozostaje identyczna. Szum kołowy (curl noise) podąża za zwrotem (składową obrotową) pola 3D Perlin/simplex, które jest przez samą konstrukcję rozróżniające się od wartości bezwzględnej, więc cząstki go podążające wirują jak dym, bez zbędnego zlepiania się lub wygładzania. Sumowanie sił grawitacyjnych N-ciała sumuje wpływ każdego innego cząsteczki (lub kilku punktów przyciągających, które są znacznie tańsze i wizualnie bardzo podobne) z rozpadem w postaci 1/(r²+ε), aby uniknąć singularności w pobliżu. Boids ocenia trzy klasyczne zasady sterowania — oddalanie się, ujednolicanie i przyciąganie — względem cząstek znalezionych w małym sąsiedztwie, przybliżone na GPU za pomocą grubej siatki przestrzennej zamiast prawdziwego wyszukiwania sąsiadów. Tryb magnesu Lorenza integruje te same trzy chaotyczne równania różniczkowe, które napędzają samodzielną symulację Lorenza, niezależnie dla każdej cząsteczki z innego punktu początkowego, więc skupisko śledzi całą kształtową 'skrzydełkowatą' atraktor w jednym kroku zamiast pojedynczej trajektorii.
Odczytanie wyniku pozwalają na narysowanie go
Ostateczna pozycja tekstury nigdy nie opuszcza GPU. Wierzchołkowy shader przypisany do dużej siatki point-sprite wyszukuje pozycję własnego cząsteczki poprzez próbkowanie tej samej tekstury – używając indeksu wierzchołka do obliczenia współrzędnej UV – i umieszcza punkt w odpowiednim miejscu, całkowicie unikając ekstremalnie powolnego podróbnika odczytu danych z pamięci GPU do JavaScriptu. Dlatego też ta technika skaluje się tak dobrze: zadanie CPU za każdym kadem (aktualizacja, a następnie renderowanie) to wydanie dwóch wywołań draw call, niezależnie od tego, czy mamy do czynienia z dziesięcioma tysiącami cząstek, czy milionem.
Frequently asked questions
Co oznacza GPGPU w symulacji cząstek?
Ogólnego przeznaczenia obliczenia na GPU: wykorzystanie masowo równoległych rdzeni shaderów graficznych do wykonywania operacji arytmetycznych zamiast kolorowania pikseli. W tym przypadku oznacza to przechowywanie stanu cząstek w teksturach i aktualizację fizyki każdej cząstki za pomocą fragment shaderu zamiast pętli JavaScript.
Dlaczego ping-pong między dwiema teksturami zamiast pisania bezpośrednio?
GPU nie może bezpiecznie odczytywać i zapisywać w tej samej teksturze w jednym wywołaniu rysowania – nie ma gwarancji kolejności fragmentów. Odczytując ostatnią ramkę tekstury podczas zapisu do oddzielnej tekstury wyjściowej, a następnie zamienianie której z nich jest 'aktualną' w następnej ramce, eliminuje się to ryzyko.
Dlaczego ta symulacja może działać z setkami tysięcy cząstek z 60 fps, podczas gdy pętla JavaScript nie mogłaby?
Fragment shader na GPU uruchamia ten sam krótki program na tysiącach pikseli jednocześnie na wielu rdzeniach shaderów, podczas gdy JavaScript wykonuje aktualizację jednej cząstki na raz na jednym wątku CPU. Przeniesienie aktualizacji na poziomie cząstek do shaderu przekształca pętlę O(n) działającą sekwencyjnie w operację równoległą ograniczoną przepustowością tekstury, a nie liczbą cząstek.
Wypróbuj na żywo
Wszystko powyżej działa bezpośrednio w Twojej przeglądarce — otwórz GPGPU Particle System i zmieniaj parametry podczas działania. Nic nie jest instalowane ani przesyłane na serwer, cały model działa w jednej karcie.
▶ Otwórz symulację GPGPU Particle System