Интеграция 3D-графики в браузер на базе библиотеки Three.js часто приводит к незаметной, но тяжелой проблеме: бесконечный цикл requestAnimationFrame непрерывно отрисовывает 60 кадров в секунду. В прикладных калькуляторах и редакторах постоянный рендеринг статичной сцены быстро разряжает батарею ноутбука, перегревает процессор и обрушивает оценки аудита Google Lighthouse.
Дмитрий Фадеев (@dmitriifadeev), автор пет-проекта Tilely (онлайн-калькулятор раскладки плитки), поделился опытом оптимизации 3D-режима на Three.js. Переход на отрисовку по требованию и адаптация проекций камеры позволили поднять метрики приложения до Performance 90 и Best Practices 100.
Отрисовка по требованию: от 60 fps к нулю в покое
В первой версии вьюпорта сцена рендерилась непрерывно. Решением стал переход на модель On-Demand Rendering: кадр формируется только тогда, когда в сцене происходят реальные изменения (поворот камеры или обновление параметров комнаты).
Контроллер OrbitControls генерирует событие change при любой трансформации матрицы камеры. Подписка на это событие позволяет вызывать перерисовку строго по факту движения. Чтобы исключить повторные вызовы render() в рамках одного тика браузера, запросы объединяются через флаг:
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'low-power' });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
let renderRequested = false;
function render() {
renderRequested = false;
renderer.render(scene, camera);
}
function requestRenderIfNotRequested() {
if (!renderRequested) {
renderRequested = true;
requestAnimationFrame(render);
}
}
// Отрисовка кадра строго при перемещении камеры
controls.addEventListener('change', requestRenderIfNotRequested);
// Анимация затухания инерции (damping)
function animateDamping() {
controls.update();
if (controls.state !== -1 || renderRequested) {
renderer.render(scene, camera);
requestAnimationFrame(animateDamping);
}
}
В состоянии покоя частота отрисовки падает до 0 кадров в секунду, полностью освобождая графический процессор.
Перспектива против ортографии
В классической перспективной камере параллельные линии сходятся к горизонту. Для калькулятора раскладки это неудобно: трудно оценить соосность швов на противоположных стенах и проверить симметрию подрезок.
Для контроля геометрии в Tilely добавлена ортографическая камера (THREE.OrthographicCamera), где масштаб не зависит от дистанции. Чтобы переключение между режимами не сбивало ракурс, фокусная точка и видимый масштаб сцены сохраняются:
// Переключение проекции с сохранением фокуса и масштаба
function switchCamera(toOrthographic, currentCam, controls, aspect) {
const target = controls.target;
const distance = currentCam.position.distanceTo(target);
if (toOrthographic) {
const frustumSize = distance * 2 * Math.tan(THREE.MathUtils.degToRad(currentCam.fov / 2));
const orthoCam = new THREE.OrthographicCamera(
(frustumSize * aspect) / -2,
(frustumSize * aspect) / 2,
frustumSize / 2,
frustumSize / -2,
0.1,
2000
);
orthoCam.position.copy(currentCam.position);
orthoCam.lookAt(target);
orthoCam.zoom = 1;
orthoCam.updateProjectionMatrix();
return orthoCam;
}
const perspCam = new THREE.PerspectiveCamera(45, aspect, 0.1, 1000);
perspCam.position.copy(currentCam.position);
perspCam.lookAt(target);
perspCam.updateProjectionMatrix();
return perspCam;
}
Навигация и интерактивность
Случайный зажим правой кнопки мыши мог увести камеру за пределы видимости. Автор добавил панель быстрых ракурсов (вид сверху, изометрия, сброс) с плавной интерполяцией координат.
Интерфейс дополнен рейкастингом (THREE.Raycaster): при наведении курсора плитка подсвечивается, а во всплывающей подсказке отображаются точные размеры детали и остаток подрезки.
