ГоловнаСтаттіРозробка Ігор

Системи Частинок Три.js: 100 000 Частинок при 60 кадрах в секунду

Як рендерити 100 000 анімованих частинок у браузері, використовуючи BufferGeometry та Float32Array — не доторкаючись до JavaScript смітника один раз на кадр.

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

Чому BufferGeometry, а не Geometry

Three.js's спадкова Geometry класова зберігала дані як JavaScript об’єкти та масиви – зручно, але повільно, оскільки кожен кадр витрачав час на читання позицій, виконання JS збору сміття для тисяч об’єктів Vector3 і перезавантаження всього в GPU. BufferGeometry зберігає дані як типні масиви – Float32Array, Uint16Array – безперервні блоки пам'яті, які передаються безпосередньо в GPU як буферні об’єкти. Переваги: відсутність тиску на GC (відсутність JS виділень у гарячій стежці), дружня до GPU структура (позиційні дані переплетені як [x0,y0,z0, x1,y1,z1, …], що точно відповідає очікуванням WebGL) та часткове оновлення – встановлюйте needsUpdate = true лише для атрибутів, які змінилися. Спадний Geometry клас було повністю видалено з Three.js r125 (2021).

Розподіл позицій один раз, мутація назавжди

Виділіть масив Float32Array лише один раз, до початку циклу — три числа на частинку для (x, y, z). Ніколи не створюйте новий масив всередині циклу анімації:

const COUNT = 100_000; const positions = new Float32Array(COUNT * 3); // ОДНЕ виділення, яке використовується щодня for (let i = 0; i < COUNT; i++) { const r = 50 * Math.cbrt(Math.random()); // рівномірна щільність об'єму кулі const theta = Math.random() * Math.PI * 2; const phi = Math.acos(2 * Math.random() - 1); positions[i*3] = r * Math.sin(phi) * Math.cos(theta); positions[i*3+1] = r * Math.sin(phi) * Math.sin(theta); positions[i*3+2] = r * Math.cos(phi); } const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));

const COUNT = 100_000;
const positions = new Float32Array(COUNT * 3);   // ONE allocation, reused every frame

for (let i = 0; i < COUNT; i++) {
  const r = 50 * Math.cbrt(Math.random());       // uniform sphere volume density
  const theta = Math.random() * Math.PI * 2;
  const phi = Math.acos(2 * Math.random() - 1);
  positions[i*3]   = r * Math.sin(phi) * Math.cos(theta);
  positions[i*3+1] = r * Math.sin(phi) * Math.sin(theta);
  positions[i*3+2] = r * Math.cos(phi);
}

const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
жива демонстрація · пов'язана симуляція● LIVE

Цикл анімації: відсутність виділення пам’яті

Золоте правило: ніколи не виділяйте пам'ять всередині циклу анімації. Заявляйте всі тимчасові змінні до початку requestAnimationFrame, потім мутуйте їх безпосередньо в кожній кадровій послідовності — читаючи та записуючи Float32Array безпосередньо, а потім перемикаючи posAttr.needsUpdate = true лише один раз на кадр, щоб Three.js знав про зміну буфера. Цикл модифікує приблизно 1,2 МБ даних Float32Array за кадр повністю в JavaScript; для CPU-інтенсивних симуляцій, таких як SPH рідина або гравітація N-тіла, такий логік переміщується у GLSL вершинний шейдер замість цього, дозволяючи GPU оновлювати всі позиції паралельно та повністю усуваючи копію типу даних.

Визначення продуктивності

Для кількостей частинок вище приблизно 500 тисяч або симуляцій, які потребують фізики на рівні окремих частинок, таких як зіткнення, перемістіть весь логіку симуляції в GLSL compute shader (WebGPU) або vertex shader з використанням transform feedback (WebGL 2). Цей процес дозволяє CPU здійснювати лише один виклик draw call і не отримує жодної інформації назад від GPU.

Frequently asked questions

Чому використовувати BufferGeometry замість старого класу Geometry Three.js?

BufferGeometry зберігає дані як TypedArray (наприклад, Float32Array) – безперервні блоки пам’яті, які передаються GPU як буфери об'єкти, замість JavaScript-об'єктів. Це означає відсутність тиску на збирання сміття (відсутність виділення об’єктів під час анімації), GPU-дружній інтерлейокований макет та можливість оновлювати лише змінені атрибути. Нащадком класу Geometry старого типу було повністю видалено з Three.js у r125 (2021).

Чому ніколи не слід виділяти об’єкти всередині циклу анімації?

Створення нових об'єктів (наприклад, new THREE.Color() або новий масив) всередині requestAnimationFrame запускає збирання сміття, що призводить до видимих «зависань» кадрів. Рішення полягає в тому, щоб оголошувати всі тимчасові змінні один раз перед початком циклу та змінювати їх на місці кожного кадру – для 100 000 частинок виділення одного об'єкта на частинку за кадр створить 100 000 об’єктів кожні 16мс, що збирати сміття не зможе плавно.

Що таке AdditiveBlending і як він впливає на перекриваються частинки?

AdditiveBlending робить так, щоб перекриваючіся частинки додавали свої кольори разом, створюючи світіння, де щільно упадені частинки стають білими – класичний вигляд зірок або туманностей, який використовується для вогню, іскор та вибухів. Для непрозорих частинок, таких як пісок чи дим, де не потрібно цього підвищувального ефекту, THREE.NormalBlending є більш відповідним вибором.

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

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

▶ Відкрити симуляцію the simulation

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

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