Цикл анимации: requestAnimationFrame и метод render

В Three.js создание динамических сцен невозможно без корректной организации цикла анимации. Основной инструмент для этого — метод render вместе с браузерным API requestAnimationFrame. Их правильное сочетание обеспечивает плавную отрисовку и оптимальное использование ресурсов процессора и GPU.

requestAnimationFrame

Метод requestAnimationFrame является стандартным способом реализации анимации в браузере. Его особенности:

  • Синхронизация с частотой обновления экрана — обычно 60 кадров в секунду, что делает анимацию более плавной по сравнению с setInterval или setTimeout.
  • Энергоэффективность — браузер приостанавливает вызовы для вкладок, которые не видны пользователю, экономя ресурсы.
  • Передача временного параметра — функция обратного вызова получает аргумент timestamp, представляющий текущий момент времени в миллисекундах с начала загрузки документа. Он удобен для расчета изменения состояния объектов относительно времени.

Пример базового использования:

function animate(timestamp) {
    // Логика анимации объектов
    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;

    renderer.render(scene, camera); // Отрисовка сцены

    requestAnimationFrame(animate); // Рекурсивный вызов для следующего кадра
}

requestAnimationFrame(animate);

Ключевой момент — рекурсивный вызов requestAnimationFrame внутри самой функции анимации. Это обеспечивает непрерывный цикл.

Метод render

Метод renderer.render(scene, camera) отвечает за фактическую отрисовку сцены на холсте (canvas). Основные аспекты его применения:

  • Передает сцену и камеру — сцена содержит объекты, свет, материалы; камера определяет точку зрения.
  • Не запускает цикл анимации сам по себе — он лишь отрисовывает текущий кадр.
  • Может вызываться произвольно — полезно для статических сцен или сцен, где обновление происходит только при взаимодействии пользователя.

Пример использования без цикла:

renderer.render(scene, camera); // Один кадр, без анимации

В сочетании с requestAnimationFrame метод render становится частью непрерывного цикла:

function animate() {
    updateObjects(); // Обновление состояний объектов
    renderer.render(scene, camera); // Отрисовка
    requestAnimationFrame(animate); // Следующий кадр
}

animate();

Организация цикла анимации

При построении сложных сцен важно разделять логическую часть обновления сцены и отрисовку. Рекомендуется структура:

  1. Обновление состояний объектов — изменение позиции, вращения, масштабов, анимационных параметров.
  2. Обновление вспомогательных систем — камеры, контроллеров, света.
  3. Отрисовка сцены через renderer.render.
  4. Запрос следующего кадра с помощью requestAnimationFrame.

Пример с контроллером камеры:

function animate() {
    controls.update(); // Обновление орбитальных контроллеров
    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;

    renderer.render(scene, camera);

    requestAnimationFrame(animate);
}

animate();

Оптимизация анимации

  1. Ограничение частоты обновления — иногда нет смысла отрисовывать каждый кадр. Можно использовать счетчик или проверку времени:
let lastTime = 0;
function animate(timestamp) {
    if (timestamp - lastTime >= 1000 / 30) { // 30 FPS
        cube.rotation.x += 0.01;
        cube.rotation.y += 0.01;
        renderer.render(scene, camera);
        lastTime = timestamp;
    }
    requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
  1. Использование delta-time — для независимой от FPS анимации:
let previousTime = performance.now();
function animate(currentTime) {
    const delta = (currentTime - previousTime) / 1000; // секунды
    cube.rotation.x += delta * Math.PI; // скорость вращения рад/сек
    cube.rotation.y += delta * Math.PI;

    renderer.render(scene, camera);
    previousTime = currentTime;

    requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
  1. Разделение обновления и рендеринга при сложных сценах — иногда полезно обновлять логику быстрее или медленнее, чем отрисовку, для снижения нагрузки на GPU.

Интеграция с внешними библиотеками

Three.js часто используется совместно с библиотеками анимации, например GSAP. В этом случае requestAnimationFrame может быть управляем библиотекой, а renderer.render вызывается в ее callback для синхронизации с временной шкалой:

gsap.ticker.add(() => {
    renderer.render(scene, camera);
});

Такой подход позволяет легко комбинировать временные анимации объектов с пользовательским циклом рендеринга.

Особенности при использовании WebGLRenderer

  • Активация autoClear — по умолчанию каждый кадр очищает холст. Для наложения нескольких сцен можно выключить renderer.autoClear = false.
  • Использование setAnimationLoop — метод Three.js, альтернативный requestAnimationFrame, особенно полезный для WebXR:
renderer.setAnimationLoop(() => {
    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;
    renderer.render(scene, camera);
});

Он автоматически синхронизируется с VR/AR сеансами, что делает код более универсальным.

Важные моменты

  • requestAnimationFrame гарантирует плавную анимацию, но не фиксированную частоту кадров.
  • renderer.render — чисто функциональная операция, вызываемая столько раз, сколько нужно для визуализации.
  • Разделение логики сцены и рендеринга позволяет масштабировать проекты и интегрировать внешние системы анимации и физики.

Этот цикл анимации является фундаментом для любых интерактивных 3D-приложений на Three.js, от простых вращающихся объектов до сложных VR-сцен с физикой и пользовательскими контроллерами.