Ручное управление камерой через JavaScript

В библиотеке A-Frame камера представлена сущностью <a-entity> с компонентом camera. По умолчанию сцена автоматически создаёт активную камеру, если она не определена вручную. Однако при разработке сложных VR/3D-приложений требуется полный контроль над положением, поворотом и поведением камеры через JavaScript.

Компонент camera в A-Frame является обёрткой над объектом THREE.PerspectiveCamera из библиотеки Three.js. Доступ к нативному объекту осуществляется через:

const cameraEl = document.querySelector('[camera]');
const threeCamera = cameraEl.getObject3D('camera');

Это даёт возможность управлять как атрибутами A-Frame, так и низкоуровневыми параметрами Three.js.


Программное управление позицией камеры

Положение камеры определяется компонентом position. Его можно изменять несколькими способами.

Через атрибут A-Frame

cameraEl.setAttribute('position', { x: 0, y: 2, z: 5 });

Либо строковым форматом:

cameraEl.setAttribute('position', '0 2 5');

Через Object3D (Three.js)

cameraEl.object3D.position.set(0, 2, 5);

Изменение через object3D предпочтительно при частых обновлениях (например, в анимации), поскольку позволяет избежать повторного парсинга атрибутов.


Управление поворотом камеры

Поворот в A-Frame задаётся через компонент rotation (в градусах), однако внутри Three.js используются радианы.

Изменение через A-Frame

cameraEl.setAttribute('rotation', { x: 0, y: 90, z: 0 });

Изменение через Three.js

cameraEl.object3D.rotation.y = Math.PI / 2;

Важно учитывать порядок вращения (по умолчанию XYZ). Его можно изменить:

cameraEl.object3D.rotation.order = 'YXZ';

Это особенно актуально при реализации свободного обзора.


Ручное управление камерой через цикл обновления

Для динамического управления используется метод tick() внутри кастомного компонента.

AFRAME.registerComponent('manual-camera-control', {
  tick: function (time, deltaTime) {
    const camera = this.el;
    camera.object3D.position.z -= 0.01;
  }
});

Подключение:

<a-entity camera manual-camera-control></a-entity>

time — общее время с запуска сцены, deltaTime — время между кадрами (в миллисекундах).

Для корректной скорости движения следует использовать deltaTime:

const speed = 0.002;
camera.object3D.position.z -= speed * deltaTime;

Обработка ввода с клавиатуры

Для ручного управления часто реализуется система WASD.

const keys = {};

window.addEventListener('keydown', e => keys[e.code] = true);
window.addEventListener('keyup', e => keys[e.code] = false);

В компоненте:

AFRAME.registerComponent('keyboard-camera', {
  tick: function (time, deltaTime) {
    const camera = this.el;
    const speed = 0.003 * deltaTime;

    if (keys['KeyW']) camera.object3D.position.z -= speed;
    if (keys['KeyS']) camera.object3D.position.z += speed;
    if (keys['KeyA']) camera.object3D.position.x -= speed;
    if (keys['KeyD']) camera.object3D.position.x += speed;
  }
});

Для перемещения относительно направления взгляда требуется использовать вектор направления:

const direction = new THREE.Vector3();
camera.object3D.getWorldDirection(direction);
camera.object3D.position.addScaledVector(direction, speed);

Управление мышью

A-Frame по умолчанию использует компонент look-controls, который связывает камеру с движением мыши или VR-гарнитуры. Для полного ручного контроля его необходимо отключить:

<a-entity camera look-controls="enabled: false"></a-entity>

Реализация собственного вращения:

let isDragging = false;
let previousMousePosition = { x: 0, y: 0 };

window.addEventListener('mousedown', () => isDragging = true);
window.addEventListener('mouseup', () => isDragging = false);

window.addEventListener('mousemove', e => {
  if (!isDragging) return;

  const deltaMove = {
    x: e.offsetX - previousMousePosition.x,
    y: e.offsetY - previousMousePosition.y
  };

  cameraEl.object3D.rotation.y -= deltaMove.x * 0.002;
  cameraEl.object3D.rotation.x -= deltaMove.y * 0.002;

  previousMousePosition = {
    x: e.offsetX,
    y: e.offsetY
  };
});

Для ограничения вертикального угла:

cameraEl.object3D.rotation.x = Math.max(
  -Math.PI / 2,
  Math.min(Math.PI / 2, cameraEl.object3D.rotation.x)
);

Использование кватернионов

При сложных сценариях вращения (например, свободная камера без гимбал-лок) применяется quaternion.

const quaternion = new THREE.Quaternion();
quaternion.setFromAxisAngle(
  new THREE.Vector3(0, 1, 0),
  Math.PI / 4
);

cameraEl.object3D.quaternion.multiply(quaternion);

Кватернионы позволяют выполнять плавные интерполяции:

THREE.Quaternion.slerp(
  startQuat,
  endQuat,
  cameraEl.object3D.quaternion,
  0.1
);

Изменение параметров перспективы

Компонент camera поддерживает параметры:

  • fov — угол обзора
  • near — ближняя плоскость отсечения
  • far — дальняя плоскость

Изменение через A-Frame:

cameraEl.setAttribute('camera', {
  fov: 100,
  near: 0.1,
  far: 2000
});

Через Three.js:

const cam = cameraEl.getObject3D('camera');
cam.fov = 100;
cam.updateProjectionMatrix();

После изменения fov, near, far обязательно вызывается updateProjectionMatrix().


Работа с родительскими сущностями

Камеру удобно помещать в контейнер:

<a-entity id="rig">
  <a-entity camera></a-entity>
</a-entity>

Теперь движение rig перемещает камеру вместе с дочерними объектами. Это стандартный подход для реализации персонажа.

const rig = document.querySelector('#rig');
rig.object3D.position.z -= 0.05;

Разделение логики:

  • rig — перемещение
  • camera — поворот

Коллизии и ограничения движения

Для ограничения выхода за пределы сцены можно проверять координаты:

const pos = cameraEl.object3D.position;

if (pos.x > 10) pos.x = 10;
if (pos.x < -10) pos.x = -10;

При использовании физического движка (например, ammo.js или cannon.js) камера может быть связана с физическим телом, однако при ручном управлении важно отключать автоматические контроллеры.


Интерполяция и сглаживание

Для плавного перемещения используется линейная интерполяция:

cameraEl.object3D.position.lerp(targetPosition, 0.1);

Для сглаженного вращения:

cameraEl.object3D.quaternion.slerp(targetQuaternion, 0.1);

Такая техника используется при:

  • следовании за объектом,
  • кинематографическом движении,
  • переходах между точками обзора.

Обработка событий сцены

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

document.querySelector('a-scene').addEventListener('loaded', () => {
  const camera = document.querySelector('[camera]');
});

События:

  • enter-vr
  • exit-vr
  • renderstart

Пример:

const scene = document.querySelector('a-scene');

scene.addEventListener('enter-vr', () => {
  console.log('VR режим активирован');
});

Производительность при ручном управлении

Ключевые рекомендации:

  • Избегать частого использования setAttribute в tick
  • Работать напрямую с object3D
  • Использовать deltaTime
  • Минимизировать создание новых объектов (Vector3, Quaternion) внутри цикла

Нежелательно:

tick() {
  const dir = new THREE.Vector3(); // создаётся каждый кадр
}

Лучше вынести в init():

init() {
  this.direction = new THREE.Vector3();
}

Полный пример кастомного контроллера

AFRAME.registerComponent('custom-fps-camera', {
  init: function () {
    this.keys = {};
    this.speed = 0.005;
    this.direction = new THREE.Vector3();

    window.addEventListener('keydown', e => this.keys[e.code] = true);
    window.addEventListener('keyup', e => this.keys[e.code] = false);
  },

  tick: function (time, deltaTime) {
    const camera = this.el;
    const velocity = this.speed * deltaTime;

    if (this.keys['KeyW']) {
      camera.object3D.getWorldDirection(this.direction);
      camera.object3D.position.addScaledVector(this.direction, velocity);
    }

    if (this.keys['KeyS']) {
      camera.object3D.getWorldDirection(this.direction);
      camera.object3D.position.addScaledVector(this.direction, -velocity);
    }
  }
});

Такой подход позволяет полностью отказаться от стандартных wasd-controls и look-controls, обеспечивая детальный контроль над логикой движения и вращения камеры.