Strona głównaArtykułyRozwój gier

Systemy cząsteczkowe Three.js: 100 000 cząstek przy 60 fps

Jak renderować 100 000 animowanych cząstek w przeglądarce, używając BufferGeometry i Float32Array — utrzymując wszystkie dane dotyczące cząstek na GPU bez angażowania zbieracza śmieci JavaScriptu raz na klatkę.

mysimulator teamZaktualizowano — czerwiec 2026≈ 8 min czytania▶ Otwórz symulację

Dlaczego używać BufferGeometry zamiast Geometry

Klasa Geometry z dziedziny Three.js przechowwała dane jako obiekty JavaScript i tablice — wygodne, ale wolne, ponieważ każdy klatek wymagało odczytu pozycji, uruchamiania zbierania śmieci przez JS na tysiącach obiektów Vector3 oraz ponownego przesyłania wszystkiego do GPU. BufferGeometry przechowuje dane jako ciągłe bloki pamięci typu tablicy Float32Array, Uint16Array — przekazywane bezpośrednio do GPU jako obiekty bufora. Zalety: brak presji na GC (brak alokacji JS w ścieżce gorącej), przyjazny dla GPU układ (dane pozycji zrównoważone jako [x0,y0,z0, x1,y1,z1, …], dokładnie to czego oczekuje WebGL), oraz częściowe aktualizacje — ustaw needsUpdate = true tylko dla atrybutów, które się zmieniły. Klasa Geometry została całkowicie usunięta z Three.js r125 (2021).

Przydzielanie pozycji raz, mutacja na zawsze

Przydziel typowy tablica (tablicę zmiennoprzecinkowych) raz, przed pętlą – trzy liczby zmiennoprzecinkowe na cząstkę dla (x, y, z). Nigdy nie twórz nowej tablicy wewnątrz pętli animacji:

const COUNT = 100_000; const positions = new Float32Array(COUNT * 3); // JEDNE przydzielenie, używane za każdym razem for (let i = 0; i < COUNT; i++) { const r = 50 * Math.cbrt(Math.random()); // jednorodna gęstość objętości sfery 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)); A częstym błędem jest generowanie losowego punktu w sześcianie [−r, +r]³ i odrzucanie punktów spoza kuli. Działa to, ale r * Math.cbrt(Math.random()) gwarantuje jednorodną gęstość objętości zamiast koncentracji na powierzchni przy biegach. THREE.Points renderuje każdy wierzchołek jako sprite na ekranie – najszybszą prymitywą dla cząstek, bez trójkątów i indeksów — a AdditiveBlending sprawia, że nakładające się cząstki wzajemnie się rozjaśniają, tworząc klasyczny blask gwiazd lub mgławicy.

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));
demo na żywo · powiązana symulacja● LIVE

Animacja w pętli: brak alokacji

Zasada złota: nigdy nie alokuj pamięci wewnątrz pętli animacji. Deklaruj wszystkie zmienne tymczasowe przed rozpoczęciem requestAnimationFrame, a następnie modyfikuj je bezpośrednio w każdej klatce – czytając i zapisując Float32Array, a następnie ustawiając posAttr.needsUpdate na wartość true raz na klatkę, aby Three.js wiedział o zmianie bufora. Pętla modyfikuje około 1,2 MB danych Float32Array na klatkę całkowicie w JavaScript; dla symulacji wymagających dużej mocy obliczeniowej, takich jak SPH fluid lub N-body gravity, logika ta przenoszona jest do bufora GLSL vertex shader, pozwalając GPU na równoległe aktualizację wszystkich pozycji i eliminując konieczność kopiowania danych Float32Array.

Referencja wydajności

Dla wartości liczby cząstek powyżej ~500k lub symulacji wymagających fizyki na poziomie cząstek, takich jak kolizje, przesuń całą logikę symulacji do GLSL compute shader (WebGPU) lub vertex shader z transform feedback (WebGL 2) – CPU wtedy emituje tylko jeden wywołanie rysowania i nie odczytuje nic z powrotu z GPU.

Frequently asked questions

Dlaczego należy używać BufferGeometry zamiast starej klasy Geometry Three.js?

BufferGeometry przechowuje dane jako tablice typu (np. Float32Array) – ciągłe bloki pamięci przekazywane bezpośrednio do GPU jako obiekty buforowe, zamiast obiektów JavaScript. Oznacza to brak presji na GC (brak alokacji obiektów podczas animacji), przyjazny dla GPU układ zagnieżdżony i możliwość aktualizowania tylko zmienionych atrybutów. Stara klasa Geometry została całkowicie usunięta z Three.js w r125 (2021).

Dlaczego nigdy nie należy alokować obiektów w pętli animacji?

Tworzenie nowych obiektów (np. new THREE.Color() lub nowy tablica) wewnątrz requestAnimationFrame wyzwala garbage collection, co powoduje widoczne zacięcia w klatkach. Rozwiązaniem jest deklarowanie wszystkich zmiennych tymczasowych przed rozpoczęciem pętli i ich modyfikowanie na miejscu za każdym razem – dla 100 000 cząstek alokacja jednego obiektu na cząstkę na ramkę stworzyłaby 100 000 obiektów co 16ms, czego GC nie może płynnie obsłużyć.

Co robi AdditiveBlending z nakładającymi się cząstkami?

AdditiveBlending powoduje, że nakładające się cząstki dodają ze sobą kolory, tworząc blask, gdzie gęsto upakowane cząstki stają się białe – klasiczny wygląd gwiazd lub mgławic używany dla ognia, iskier i wybuchów. W przypadku nieprzezroczystych cząstek, takich jak piasek lub dym, gdzie nie chcemy tego efektu rozjaśniania, THREE.NormalBlending jest właściwym wyborem.

Wypróbuj na żywo

Wszystko powyżej działa bezpośrednio w Twojej przeglądarce — otwórz the simulation 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ę the simulation

Co znalazłeś?

Dodaj kroki odtworzenia (opcjonalnie)