Посібник із системи частинок Three.js
Як відрендерити 100 000 анімованих частинок при 60 fps
у браузері — за допомогою BufferGeometry та
Float32Array,
щоб тримати всі дані частинок на GPU, не звертаючись до збирача сміття
JavaScript жодного разу за кадр.
Чому BufferGeometry (а не Geometry)
Three.js має два способи визначення геометрії. Застарілий клас
Geometry
зберігав дані як об’єкти та масиви JavaScript — можна було написати
geometry.vertices.push(new THREE.Vector3(...)). Це було
зручно, але повільно: кожен кадр витрачався на зчитування положень з
GPU, запуск збирача сміття JS на тисячах об’єктів Vector3 та повторне
завантаження на GPU.
BufferGeometry зберігає дані як типізовані масиви —
Float32Array, Uint16Array тощо — які є
суміжними блоками пам’яті,
що можуть передаватися безпосередньо на GPU як буферні об’єкти.
Переваги:
- Нуль навантаження на GC: жодних виділень об’єктів JavaScript на гарячому шляху — збирач сміття ніколи не бачить даних частинок під час анімації.
-
Зручна для GPU розкладка: дані положень переплетені як
[x0,y0,z0, x1,y1,z1, …]— саме та розкладка, яку очікує OpenGL/WebGL. -
Часткові оновлення: встановлюйте
needsUpdate = trueлише на тих атрибутах, які ви змінили; незмінні дані залишаються на GPU.
Geometry був
видалений у Three.js r125 (2021). Увесь сучасний код Three.js
використовує BufferGeometry.
Налаштування проєкту
Імпортуйте Three.js із CDN або встановіть через npm:
Мінімальний HTML-каркас:
Виділення положень за допомогою Float32Array
Виділіть типізований масив один раз перед циклом. Три float на частинку (x, y, z). Ніколи не створюйте новий масив усередині циклу анімації.
r * Math.cbrt(Math.random()) забезпечує рівномірну густину
за об’ємом,
а не концентрацію на поверхні біля полюсів.
PointsMaterial та об’єкт Points
THREE.Points рендерить кожну вершину як спрайт в
екранному просторі — найшвидший примітив для частинок; без трикутників,
без індексів. PointsMaterial керує розміром і кольором.
AdditiveBlending означає, що частинки, які
перекриваються, додають свої кольори разом, створюючи «сяйво» там, де
щільно зібрані частинки стають яскраво-білими — класичний вигляд
зорі/туманності. Використовуйте THREE.NormalBlending для
непрозорих частинок (наприклад, пісок, дим).
Кольори вершин для кожної частинки
Додайте атрибут color із трьома float на частинку (r, g, b у
діапазоні 0–1) та увімкніть vertexColors: true:
_color виділяється один раз поза циклом.
setHSL() змінює його на місці. Створення
new THREE.Color() усередині циклу виділило б 100 000
об’єктів — запустивши збирач сміття під час наступного кадру.
Цикл анімації — нуль виділень
Золоте правило:
ніколи не виділяйте пам’ять усередині циклу анімації.
Оголосіть усі тимчасові змінні до того, як почнеться
requestAnimationFrame, а потім змінюйте їх на місці кожного
кадру.
Цикл вище змінює ~1,2 МБ даних Float32Array на кадр повністю в JavaScript. Для інтенсивних для CPU симуляцій (рідина SPH, гравітаційна задача N тіл) ви перенесли б цю логіку у вершинний шейдер GLSL, дозволивши GPU оновлювати всі положення паралельно й повністю усунувши копіювання типізованого масиву.
Власний шейдер: круглі спрайти
За замовчуванням PointsMaterial рендерить квадратні
білборди. Власний шейдер, що відкидає фрагменти поза колом, дає круглі
частинки:
dot(uv, uv) та
порівняння з 0.25 (тобто 0.5²) уникає виклику
sqrt() на кожен фрагмент. Фрагментні шейдери виконуються для
кожного растеризованого пікселя, тож невеликі оптимізації на фрагмент
множаться на мільйони викликів.
Довідка з продуктивності
| Підхід | 100 тис. частинок, настільний dGPU | 100 тис. частинок, мобільний | Навантаження на GC |
|---|---|---|---|
| Float32Array + BufferGeometry (цей посібник) | ~0,5 мс/кадр CPU | ~2–5 мс/кадр | Немає (після ініціалізації) |
| Застарілий Geometry (об’єкти Vector3) | ~5–15 мс/кадр CPU | Непридатний | Високе — паузи GC кожні кілька секунд |
| Обчислення на GPU (обчислювальні шейдери / transform feedback) | <0,1 мс/кадр CPU | <1 мс/кадр | Немає |
Для кількості частинок понад ~500 тис. або симуляцій, що потребують фізики на кожну частинку (гравітація, зіткнення, SPH), перенесіть усю логіку симуляції в обчислювальний шейдер GLSL (WebGPU) або вершинний шейдер із transform feedback (WebGL 2). Тоді CPU лише видає один виклик малювання й нічого не зчитує назад з GPU.
Наступні кроки
-
Текстурні спрайти: передайте текстуру
mapуPointsMaterialдля спрайтів диму, вогню чи кружечків зір, попередньо відрендерених у Canvas. -
Інстансована сітка: якщо кожна частинка потребує
повноцінної 3D-сітки (сфера, куб), використовуйте
THREE.InstancedMesh— один виклик малювання для всіх екземплярів. Див. посібник з інстансованої сітки. -
Частинки на GPU: використовуйте WebGPU
(
THREE.WebGPURenderer, r160+) та обчислювальні шейдери WGSL, щоб симулювати мільйони частинок повністю на GPU. -
React Three Fiber: якщо ви використовуєте React, r3f
огортає Three.js декларативно. Усі ті самі принципи
BufferGeometry/Float32Arrayзастосовуються.