Problem: 80 000 wywołań rysowania
Naiwne podejście do renderowania wielu obiektów w Three.js jest takie: stwórz Mesh dla każdego z nich, dodaj do sceny. Dla 80 000 gwiazd oznacza to 80 000 obiektów Mesh, 80 000 wywołań rysowania na klatkę i około 4 FPS na karcie graficznej średniej klasy.
Wywołanie rysowania to polecenie CPU→GPU, które mówi „narysuj tę geometrię z tym materiałem". Każde wywołanie ma narzut — zmiany stanu, podpięcia buforów, walidację API. Nowoczesne GPU obsłużą miliony trójkątów na klatkę, ale tylko ~1 000–3 000 wywołań rysowania, zanim CPU stanie się wąskim gardłem.
InstancedMesh — jedno wywołanie rysowania dla wszystkich
InstancedMesh z Three.js rozwiązuje to, pakując
wszystkie dane instancji (pozycję, rotację, skalę, kolor) do
buforów GPU, a następnie wykonując jedno instancjonowane
wywołanie rysowania. GPU obsługuje różnicowanie per instancja
całkowicie w shaderze wierzchołków.
const starGeometry = new THREE.SphereGeometry(0.05, 4, 4); // mała sfera
const starMaterial = new THREE.MeshBasicMaterial();
const stars = new THREE.InstancedMesh(starGeometry, starMaterial, STAR_COUNT);
const dummy = new THREE.Object3D();
const color = new THREE.Color();
for (let i = 0; i < STAR_COUNT; i++) {
// Rozkład spiralnej galaktyki
const arm = Math.floor(Math.random() * 4);
const angle = arm * (Math.PI / 2) + r * 2.5 + noise(r);
const r = Math.random() ** 0.5 * RADIUS;
dummy.position.set(
Math.cos(angle) * r,
(Math.random() - 0.5) * r * 0.1,
Math.sin(angle) * r
);
dummy.updateMatrix();
stars.setMatrixAt(i, dummy.matrix);
// Kolor zależny od temperatury (niebieski gorący → pomarańczowy chłodny)
const temp = Math.random();
color.setHSL(0.6 - temp * 0.5, 0.9, 0.7 + temp * 0.2);
stars.setColorAt(i, color);
}
stars.instanceMatrix.needsUpdate = true;
stars.instanceColor.needsUpdate = true;
Wydajność przed i po
| Podejście | Wywołania rysowania | FPS (RTX 3060) | FPS (Intel iGPU) |
|---|---|---|---|
| 80 000 × Mesh | 80 000 | 4 | 1 |
| Points (BufferGeometry) | 1 | 60 | 38 |
| InstancedMesh (sfery) | 1 | 60 | 45 |
Points (renderowanie punktów GPU) to też prawidłowe
podejście, nawet nieco szybsze, ale InstancedMesh pozwala, by
każda gwiazda była prawdziwą geometrią 3D — możesz sprawić, że
będą świecić, animować pojedyncze z nich lub zmieniać ich rozmiar
w zależności od klasy widmowej.
Własne shadery instancjonowane
InstancedMesh współpracuje ze standardowymi materiałami Three.js, ale dla galaktyki chciałem własnych efektów per instancja: aureoli przypominającej bloom, zróżnicowania rozmiaru według klasy gwiazdowej i subtelnego migotania. Wymagało to własnego ShaderMaterial, który odczytuje macierz instancji.
Three.js automatycznie wstrzykuje macierz instancji jako
instanceMatrix, gdy używasz
USE_INSTANCING. Dostęp do koloru per instancja
uzyskasz przez instanceColor w shaderze
wierzchołków, z definicją USE_INSTANCING_COLOR.
Rozkład spiralnej galaktyki
Prawdziwe galaktyki spiralne mają ramiona podążające za spiralami logarytmicznymi. Użyty rozkład pozycji:
- Wybierz losowe ramię (4 ramiona, rozmieszczone co π/2)
-
Wybierz losowy promień
r ∝ sqrt(random)— odchylenie sqrt umieszcza więcej gwiazd bliżej centrum -
Oblicz kąt:
armAngle + r × spiral_tightness + fbm_noise(r) -
Dodaj gaussowski rozrzut pionowy:
y = gaussian(0, r × 0.08)
fbm_noise (fraktalny szum Browna) dodaje organiczną
nieregularność ramion, by wynik nie wyglądał mechanicznie.
Zobacz ją pod adresem /galaxy/ — obróć kamerę, by zobaczyć dysk z boku lub spojrzeć wzdłuż osi galaktycznej.