Первая сцена и цикл рендера

Собираем первую полноценную сцену с вращающимся кубом и понимаем, что такое цикл рендера.

Цикл рендера (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 — один статичный кадр; движение даёт повторение.
Проверьте себя
1. Зачем нужен requestAnimationFrame?
AЧтобы загрузить текстуры
BЧтобы браузер вызывал функцию перед каждым кадром (обычно ~60 раз в секунду) для плавной анимации
CЧтобы остановить рендер
DЧтобы создать камеру
2. Что произойдёт, если вызвать renderer.render(scene, camera) всего один раз?
AСцена будет плавно анимироваться сама
BНарисуется один статичный кадр, движения не будет
CБраузер зависнет
DКуб начнёт вращаться автоматически
3. Почему внутри цикла обычно меняют cube.rotation.y перед вызовом render?
AЧтобы поменять цвет
BЧтобы к следующему кадру объект слегка повернулся — накопление маленьких поворотов даёт вращение
CЭто обязательная строка в любом коде
DЧтобы загрузить камеру
Поддержать проект