ГоловнаСтаттіВізуалізація та Графіка

Частинки GPGPU: Симуляція, що Запускається Повністю на GPU

Використання 'ping-pong' плаваючих текстур перетворює фрагментний шейдер на фізичний двигун для сотень тисяч частинок – жодного JavaScript циклу в полі зору.

mysimulator teamОновлено — червень 2026≈ 8 хв читання▶ Відкрити симуляцію

Проблема з мільйоном частинок у JavaScript

CPU, який виконує цикл оновлення частинок, робить це по черзі: читає положення, застосовує силу, інтегрує, записує назад і переходить до наступної частки. При 100 000 частинок та 60 кадрах за секунду це шість мільйонів оновлень на секунду на одному потоку – досяжно для простого руху, але все руйнується, коли оновлення включає пошук сусідніх частинок або розгалуження окремо для кожної частки. GPU використовує зовсім інший підхід: замість одного швидкого ядра, яке робить усе послідовно, він має тисячі невеликих ядер, які кожен виконують один і той же короткий код на різних даних одночасно. Якщо оновлення частинки може бути записане як "той самий короткий функціонал, застосований незалежно до кожної частки", то GPU може запустити їх усі практично одночасно.

Частинки як пікселі: текстура перемикання

Хитрість, яка зробила можливим загальнопризначене обчислення GPU до появи обчислень за допомогою shaderів, а також найпростіший кросплатформний підхід у WebGL — це зберігати стан частинок як пікселі в плаваючому текстурі. Текстура 512×512 містить 262 144 частинки, по одній на тексель, з каналами RGBA, які несуть позицію (x, y, z) і, можливо, вік або тривалість життя. Оновлення всіх частинок стає рендерингом одного повного квадта: фрагментний шейдер запускається один раз на тексель, читає попередні текстури позиції та швидкості, застосовує фізику та записує новий стан у вихідну текстуру.

// спрощений GLSL шейдер оновлення фрагмента, один тексель = одна частинка 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; // записано в ВИХІДНУ текстуру, ніколи не вхідну Оскільки GPU не може безпечно читати та записувати одну й ту ж текстуру за один виклик малювання — фрагменти не гарантовані для виконання в будь-якому порядку, і збій читання після запису призведе до пошкодження симуляції — рендерер підтримує дві копії кожної текстури стану та чергує їх: цей кадр читає текстуру A та пише текстуру B, наступний кадр читає B та пише A. Це чергування називається «ping-pong», і це єдина ідея, на основі якої будуються всі інші речі в симуляції 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
жива демонстрація · пов'язана симуляція● LIVE

Чотири поведінки, один контур

Лише функція сили всередині шейдера оновлення змінюється між поведінками; гідравлічна система ping-pong залишається ідентичною. Шум куля здає обертну (обертову) компоненту 3D Perlin/simplex шуму, яка є дивергентною за конструкцією, тому частинки, що слідують за нею, спіраються як дим без жодного злипання або розрідження. N-тіло обчислює суму тяжіння кожного іншого частинки (або кількох точок притягання, які значно дешевші та візуально схожі) із затуханням 1/(r²+ε) для запобігання сингулярності на близьких відстанях. Boids оцінює три класичні правила управління — розділення, зближення, узгодженість — від частинок, знайдених у невеликому сусідстві, приблизно на GPU за допомогою грубої просторової сітки замість пошуку справжніх сусідів. Лоренцевий аттрактор інтегрує ті ж три хаотичні РНО, що керують самостійним симулятором Лоренца, незалежно для кожної частинки з різних початкових умов, тому рої відстежують весь криволінійний аттрактор замість однієї окремої траєкторії.

Зчитування результату для відображення

Фінальна позиційна текстура ніколи не покидає GPU. Шейдер вершин, призначений для великої мережі спрайтів, шукає позицію власного частинки шляхом семплювання тієї самої текстури – використовуючи її індекс вершини для обчислення координати UV – і розміщує точку відповідно, повністю уникаючи надзвичайно повільного циклу читання даних з GPU-пам’яті в JavaScript. Саме тому ця техніка масштабується так добре: задача CPU кожного кадру зменшується до двох викликів малювання (оновлення, потім рендеринг), незалежно від того, чи є десять тисяч частинок або мільйон.

Часті запитання

Що таке GPGPU в симуляції частинок?

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

Чому пінґ-понг між двома текстурами замість запису в місце?

GPU не може безпечно читати та писати в одну й ту ж текстуру протягом одного виклику малювання — немає гарантованого порядку між фрагментами. Читання текстури попереднього кадру під час запису до окремої вихідної текстури, а потім перемикання, яка з них є «поточною», наступного кадру повністю усуває цю проблему.

Чому ця симуляція може працювати з сотнями тисяч частинок на 60 кадрів в секунду, коли цикл JavaScript не зміг би?

Фрагментний шейдер GPU виконує один і той самий короткий програма на тисячах пікселів одночасно за допомогою багатьох ядер шейдера, тоді як JavaScript виконує одну частинку за раз на одному CPU потоку. Перенесення оновлення для кожної частинки в шейдер перетворює O(n) серійний цикл на паралельну операцію, обмежену пропускною здатністю текстури, а не кількістю частинок.

Спробуйте наживо

Усе, що вище, працює прямо у вашому браузері — відкрийте GPGPU Particle System і змінюйте параметри під час роботи. Нічого не встановлюється, нічого не завантажується на сервер, уся модель живе в одній вкладці.

▶ Відкрити симуляцію GPGPU Particle System

Що ви знайшли?

Додати кроки відтворення (опційно)