Первая сцена и цикл рендера
Собираем первую полноценную сцену с вращающимся кубом и понимаем, что такое цикл рендера.
Цикл рендера (animation loop) — это функция, которая много раз в секунду чуть-чуть меняет сцену и заново её рисует. Серия слегка отличающихся кадров и создаёт ощущение движения.
Полный пример сцены
Вот минимальная, но рабочая сцена целиком. Скопируйте её в проект из прошлого урока. Здесь есть все три кита и цикл анимации.
import * as THREE from 'three';
// 1. Scene
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x15151a);
// 2. Camera
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 4;
// 3. Renderer
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Объект: куб
const cube = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshNormalMaterial()
);
scene.add(cube);
// 4. Цикл рендера
function animate() {
requestAnimationFrame(animate);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();
Мы взяли MeshNormalMaterial — он раскрашивает грани по направлению нормалей и не требует света, поэтому куб сразу видно. Про материалы и свет — отдельный раздел.
Как работает цикл
Ключевая строка — requestAnimationFrame(animate). Это запрос браузеру: «вызови функцию animate перед следующей перерисовкой экрана». Браузер делает это с частотой монитора, обычно около 60 раз в секунду. Внутри функции мы снова запрашиваем следующий кадр — получается бесконечный, но дружелюбный к браузеру цикл.
Почему не обычный setInterval? Потому что requestAnimationFrame синхронизирован с обновлением экрана и автоматически тормозит, когда вкладка неактивна — это экономит батарею и не плодит лишние кадры.
Посчитаем кадры «вживую»
А вот это уже чистая математика без Three.js — её можно запустить прямо здесь. Прикинем, на сколько повернётся куб за секунду и сколько всего градусов накопится за 3 секунды при шаге 0.01 радиан за кадр и 60 кадрах в секунду.
const stepRad = 0.01; // прибавка к повороту за кадр (радианы)
const fps = 60; // кадров в секунду
const seconds = 3;
const perSecondRad = stepRad * fps;
const totalRad = perSecondRad * seconds;
const toDeg = (r) => (r * 180 / Math.PI);
console.log('За кадр:', stepRad, 'рад');
console.log('За секунду:', perSecondRad.toFixed(2), 'рад =', toDeg(perSecondRad).toFixed(1), 'град');
console.log('За 3 секунды:', toDeg(totalRad).toFixed(1), 'град');
Вывод:
За кадр: 0.01 рад За секунду: 0.60 рад = 34.4 град За 3 секунды: 103.1 град
Видно, что «крошечный» шаг 0.01 за кадр на деле даёт заметные 34 градуса в секунду. Так интуиция о скорости анимации становится цифрой — и это пригодится, когда будем привязывать движение к реальному времени (delta time).
Частые ошибки новичка
- Забыли отодвинуть камеру (
camera.position.z) — пустой экран, вы внутри куба. - Вызвали
renderодин раз вне цикла — кадр статичный. - Не добавили
renderer.domElementв DOM — canvas не виден. - Взяли материал, которому нужен свет (Standard), но свет не добавили — чёрный объект.
Итог
- Сцена = Scene + Camera + Renderer + объекты + цикл.
requestAnimationFrameвызывает функцию каждый кадр — основа анимации.- Один
render— один статичный кадр; движение даёт повторение.