Проблема: 80 000 викликів рендерингу
Наївний підхід до рендерингу великої кількості об'єктів у Three.js такий: створити Mesh для кожного, додати його на сцену. Для 80 000 зірок це означає 80 000 об'єктів Mesh, 80 000 викликів рендерингу на кадр і приблизно 4 FPS на відеокарті середнього рівня.
Виклик рендерингу (draw call) — це команда CPU→GPU, яка каже «відрендери цю геометрію з цим матеріалом». Кожен виклик має накладні витрати — зміни стану, прив'язки буферів, валідація API. Сучасні GPU можуть обробляти мільйони трикутників за кадр, але лише близько 1 000–3 000 викликів рендерингу, перш ніж вузьким місцем стає CPU.
InstancedMesh — один виклик рендерингу на всіх
InstancedMesh з Three.js вирішує цю проблему,
пакуючи всі дані інстансів (позицію, обертання, масштаб, колір) у
буфери GPU, а потім виконуючи один-єдиний інстансований виклик
рендерингу. GPU самостійно обробляє варіації між інстансами
повністю у вершинному шейдері.
const starGeometry = new THREE.SphereGeometry(0.05, 4, 4); // крихітна сфера
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++) {
// Розподіл спіральної галактики
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);
// Колір на основі температури (гарячий синій → холодний помаранчевий)
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;
Продуктивність до і після
| Підхід | Виклики рендерингу | FPS (RTX 3060) | FPS (вбудована Intel) |
|---|---|---|---|
| 80 000 × Mesh | 80 000 | 4 | 1 |
| Points (BufferGeometry) | 1 | 60 | 38 |
| InstancedMesh (сфери) | 1 | 60 | 45 |
Points (рендеринг точок на GPU) — теж робочий підхід
і навіть трохи швидший, але InstancedMesh дозволяє кожній зірці
бути справжньою 3D-геометрією — можна змусити їх світитися,
анімувати окремо чи змінювати розмір залежно від спектрального
класу зорі.
Власні інстансовані шейдери
InstancedMesh працює зі стандартними матеріалами Three.js, але для галактики я хотів власні ефекти для кожного інстансу: подібний до bloom ореол, варіацію розміру за спектральним класом і легке мерехтіння. Для цього знадобився власний ShaderMaterial, що читає матрицю інстансу.
Three.js автоматично інжектить матрицю інстансу як
instanceMatrix, коли використовується
USE_INSTANCING. Доступ до кольору кожного інстансу
можна отримати через instanceColor у вершинному
шейдері за допомогою дефайну USE_INSTANCING_COLOR.
Розподіл спіральної галактики
Справжні спіральні галактики мають рукави, що йдуть за логарифмічними спіралями. Розподіл позицій, який я використав:
- Обрати випадковий рукав (4 рукави з кроком π/2)
-
Обрати випадковий радіус
r ∝ sqrt(random)— зміщення через корінь ставить більше зірок ближче до центру -
Обчислити кут:
armAngle + r × spiral_tightness + fbm_noise(r) -
Додати гаусів вертикальний розкид:
y = gaussian(0, r × 0.08)
fbm_noise (фрактальний броунівський рух) додає
органічної неправильності рукавам, щоб результат не виглядав
механічним.
Подивіться на неї в /galaxy/ — покрутіть камеру, щоб побачити диск з ребра або подивитися вздовж галактичної осі.