Рендерер, сцена, камера, цикл анімації, частинки — усе з порожнього HTML-файлу. Без npm. Без інструментів збірки. Лише посилання на CDN із Three.js r160 і текстовий редактор.
Створіть новий файл під назвою index.html будь-де на
вашому комп'ютері. Сервер не потрібен — браузер може відкрити його
безпосередньо з файлової системи.
Вставте цей шаблон. Єдина зовнішня залежність — це Three.js, що постачається з CDN jsDelivr як ES-модуль.
index.html<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My First Simulation</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #000; overflow: hidden; }
canvas { display: block; }
</style>
</head>
<body>
<script type="module">
import * as THREE from
'https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.min.js';
// Ваш код буде тут
console.log('Three.js loaded:', THREE.REVISION);
</script>
</body>
</html>
Відкрийте файл у сучасному браузері (Chrome, Firefox або Edge). Відкрийте консоль DevTools (F12) — ви маєте побачити "Three.js loaded: 160". Це підтверджує, що імпорт працює.
Сучасні браузери дозволяють імпорт ES-модулів з CDN навіть тоді,
коли ви відкриваєте файл через file://. Якщо ви
отримуєте помилку CORS, запустіть невеликий локальний сервер:
python -m http.server 8080 або скористайтеся
розширенням Live Server для VS Code.
Кожній симуляції на Three.js потрібні три речі:
<canvas> за допомогою WebGL.
Замініть коментар // Ваш код буде тут на:
// ── Сцена ──────────────────────────────────────────────────
const scene = new THREE.Scene();
scene.background = new THREE.Color('#050813'); // темний синьо-чорний
// ── Камера ─────────────────────────────────────────────────
// PerspectiveCamera(fov, aspect, near, far)
const camera = new THREE.PerspectiveCamera(
60, // поле зору в градусах
window.innerWidth / window.innerHeight, // співвідношення сторін
0.1, // ближня площина відсікання
1000 // дальня площина відсікання
);
camera.position.set(0, 0, 5); // відсуваємо камеру назад уздовж осі Z
// ── Рендерер ────────────────────────────────────────────────
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); // обмежуємо до 2× заради продуктивності
document.body.appendChild(renderer.domElement); // додаємо <canvas> на сторінку
// ── Обробник зміни розміру ──────────────────────────────────
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Рендеримо один кадр, щоб підтвердити, що камера бачить темну сцену
renderer.render(scene, camera);
Перезавантажте браузер. Тепер сторінка темно-синя — її відрендерив WebGL. Геометрії ще немає, але конвеєр працює.
Контекст WebGL 2, перспективну камеру, що стоїть на 5 одиниць позаду вздовж осі Z, і рендерер, який заповнює область перегляду. Чорний кадр — це порожня сцена.
У Three.js видимі об'єкти називаються мешами (Mesh). Меш поєднує:
Додайте сяючу сферу та просте точкове джерело світла після обробника зміни розміру:
// ── Геометрія ────────────────────────────────────────────────
const geometry = new THREE.SphereGeometry(
1, // радіус
32, // widthSegments (більше = гладкіше)
16 // heightSegments
);
const material = new THREE.MeshStandardMaterial({
color: 0x4f88f8, // шістнадцятковий колір (синій)
roughness: 0.3,
metalness: 0.6,
emissive: 0x1a2a6c, // легке власне світіння
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// ── Освітлення ────────────────────────────────────────────────
// MeshStandardMaterial потребує джерел світла, щоб бути видимим
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambientLight);
const pointLight = new THREE.PointLight(0x60a5fa, 80, 20);
pointLight.position.set(3, 4, 3);
scene.add(pointLight);
renderer.render(scene, camera); // оновлюємо один статичний кадр
Він використовує фізично коректний рендеринг (PBR) — ту саму модель
затінення, що й ігрові рушії та 3D-програми на кшталт Blender. Для
швидкого попереднього перегляду без освітлення скористайтеся натомість
MeshBasicMaterial (він ігнорує джерела світла).
Перезавантажте сторінку. Ви маєте побачити затінену синю сферу в центрі екрана. Далі ми змусимо її рухатися.
Статичні рендери — це лише початок. Щоб анімувати, ми замінюємо
одинарний виклик renderer.render() циклом, керованим
requestAnimationFrame.
Чому requestAnimationFrame? Він
синхронізує ваш код із частотою оновлення дисплея (зазвичай 60 Гц),
автоматично призупиняється, коли вкладку приховано (заощаджуючи заряд
батареї), і запобігає розривам зображення.
Видаліть останній рядок renderer.render(scene, camera); і
додайте:
// ── Годинник (для анімації на основі dt) ──────────────────────────
const clock = new THREE.Clock();
// ── Цикл анімації ──────────────────────────────────────────
function animate() {
requestAnimationFrame(animate); // плануємо наступний кадр
const t = clock.getElapsedTime(); // секунд від старту
// Обертаємо сферу
mesh.rotation.y = t * 0.6;
mesh.rotation.x = t * 0.2;
// М'яке погойдування вгору-вниз
mesh.position.y = Math.sin(t) * 0.3;
renderer.render(scene, camera);
}
animate(); // запускаємо цикл
Тепер сфера безперервно обертається та погойдується вгору-вниз. Зверніть
увагу, як clock.getElapsedTime() забезпечує плавний рух,
незалежний від частоти кадрів.
Використання минулого часу (t) замість простого
лічильника гарантує, що швидкість анімації залишається однаковою за
30 fps, 60 fps чи 144 fps. Для фізики ви б використовували дельту
(clock.getDelta()), щоб просувати симуляцію на точну
кількість мілісекунд, що минула з попереднього кадру.
Одна сфера — це іграшковий приклад. Реальним симуляціям потрібні тисячі
об'єктів. Створення окремого Mesh для кожної частинки
зруйнувало б частоту кадрів, адже кожен меш означає окремий виклик
відмалювання на GPU. Рішення — Points:
один виклик відмалювання для всіх частинок.
Замініть усе, починаючи з geometry, на:
const COUNT = 1000;
const positions = new Float32Array(COUNT * 3); // [x,y,z, x,y,z, …]
const velocities = new Float32Array(COUNT * 3); // швидкість кожної частинки
// Засіваємо випадкові позиції в кубі −5 → +5
for (let i = 0; i < COUNT; i++) {
positions[i * 3 ] = (Math.random() - 0.5) * 10;
positions[i * 3 + 1] = (Math.random() - 0.5) * 10;
positions[i * 3 + 2] = (Math.random() - 0.5) * 10;
velocities[i * 3 ] = (Math.random() - 0.5) * 0.02;
velocities[i * 3 + 1] = (Math.random() - 0.5) * 0.02;
velocities[i * 3 + 2] = (Math.random() - 0.5) * 0.02;
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute(
'position',
new THREE.BufferAttribute(positions, 3) // 3 числа float на вершину
);
const material = new THREE.PointsMaterial({
size: 0.06,
color: 0x818cf8, // індиго
transparent: true,
opacity: 0.85,
sizeAttenuation: true // розмір масштабується з глибиною
});
const points = new THREE.Points(geometry, material);
scene.add(points);
// ── Освітлення (необов'язкове для Points — вони його ігнорують) ──────────
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
// ── Годинник ───────────────────────────────────────────────────
const clock = new THREE.Clock();
// ── Цикл анімації ──────────────────────────────────────────
function animate() {
requestAnimationFrame(animate);
const posArr = geometry.attributes.position.array;
for (let i = 0; i < COUNT; i++) {
const xi = i * 3;
// Інтегруємо позицію
posArr[xi ] += velocities[xi ];
posArr[xi + 1] += velocities[xi + 1];
posArr[xi + 2] += velocities[xi + 2];
// Відскакуємо від стінок куба
for (let axis = 0; axis < 3; axis++) {
if (Math.abs(posArr[xi + axis]) > 5) {
velocities[xi + axis] *= -1; // змінюємо напрямок на протилежний
}
}
}
// Повідомляємо Three.js, що позиції змінилися — ВАЖЛИВО!
geometry.attributes.position.needsUpdate = true;
// Повільно обертаємо всю хмару
points.rotation.y += 0.001;
renderer.render(scene, camera);
}
animate();
Після прямої зміни типізованого масиву ви маєте встановити
geometry.attributes.position.needsUpdate = true, щоб
повідомити рендерер про повторне завантаження буфера на GPU. Якщо про
це забути, частинки застигнуть або миготітимуть.
Тепер у вас 1 000 частинок кольору індиго, що відскакують усередині невидимого куба, і все це за один виклик відмалювання. Відкрийте DevTools → Performance, щоб переконатися, що частота кадрів тримається на рівні 60 fps.
Перегляньте симуляції, створені саме за цим підходом — з більшою кількістю частинок, фізичними рушіями та шейдерами GLSL.
Симуляція Boids →У вас є робоча симуляція на Three.js. Ось природний шлях подальшого навчання:
Або переходьте одразу до читання статті про SPH-рідини, щоб дізнатися математику, що стоїть за симуляціями частинок.
Пишіть, запускайте та налаштовуйте код Three.js просто у браузері — без жодного налаштування.
Відкрити пісочницю → Переглянути симуляцію ↗