Three.js: rotate camera with both touch and device orientation

pellul picture pellul · Feb 9, 2016 · Viewed 9k times · Source

I am making a 3D project with threejs which allows control of the camera with mouse for computer devices, and also allows control with touch events and deviceorientation event for smartphones. As an example, this site works the same way as what I want to do.

As I am using OrbitControls to move camera on the PC version, I bound the touchstart/move/end events to mousedown/move/up and it works perfectly.
The problem is when I try to add the device orientation event's values. Here is what I tried to add in OrbitControls.js :

THREE.OrbitControls = function (object, domElement) {
  const scope = this;
  let lastBeta = 0;
  let lastGamma = 0;
  this.deviceOrientation = {};

  function onDeviceOrientationChangeEvent(event) {
    scope.deviceOrientation = event;
    // Z
    var alpha = scope.deviceOrientation.alpha
      ? THREE.Math.degToRad(scope.deviceOrientation.alpha) 
      : 0;

    // X'
    var beta = scope.deviceOrientation.beta
      ? THREE.Math.degToRad(scope.deviceOrientation.beta)
      : 0;

    // Y''
    var gamma = scope.deviceOrientation.gamma 
      ? THREE.Math.degToRad(scope.deviceOrientation.gamma) 
      : 0;

    // O
    var orient = scope.screenOrientation 
      ? THREE.Math.degToRad(scope.screenOrientation) 
      : 0;

    rotateLeft(lastGamma - gamma);
    rotateUp(lastBeta - beta);

    lastBeta = beta; //is working
    lastGamma = gamma; //doesn't work properly
  }

  window.addEventListener('deviceorientation', onDeviceOrientationChangeEvent, false);
};

As beta's values are within a [-180,180] degree range the vertical rotation encounters no problem, whereas gamma's range is [-90,90] and values are also changing suddenly when orientating device' screen up and down (even if, I think, it should return horizontal rotation). And even when converting gamma's range to make it takes values from -180 to 180, the sudden shifts make everything goes wrong.

I guess that I have to use quaternions as in deviceOrientationControls.js, but I really don't know how it works and every attempt I've made so far was a fail. Can someone help me please?

PS: Here is a link to the description on the deviceorientation event to have a better comprehension of what really are alpha beta and gamma.

EDIT
I added a snippet bellow to show the beta and gamma variations.

Answer

pellul picture pellul · Feb 17, 2016

I found a solution using a function to convert quaternions to radians, so I wanted to share it if someone wants to do a click/touch+device orientation control using OrbitControls.

I take the initial orientation (x1,y1,z1) and calculate the new one (x2,y2,z3) and the difference between them is the variation of the rotation done by the camera. I add these line to the initial update function

this.update = function () {
  // Z
  const alpha = scope.deviceOrientation.alpha 
    ? THREE.Math.degToRad(scope.deviceOrientation.alpha)
    : 0;

  // X'
  const beta = scope.deviceOrientation.beta
    ? THREE.Math.degToRad(scope.deviceOrientation.beta)
    : 0;

  // Y''
  const gamma = scope.deviceOrientation.gamma 
    ? THREE.Math.degToRad(scope.deviceOrientation.gamma)
    : 0;

  // O
  const orient = scope.screenOrientation
    ? THREE.Math.degToRad(scope.screenOrientation) 
    : 0;

  const currentQ = new THREE.Quaternion().copy(scope.object.quaternion);

  setObjectQuaternion(currentQ, alpha, beta, gamma, orient);
  const currentAngle = Quat2Angle(currentQ.x, currentQ.y, currentQ.z, currentQ.w);

  // currentAngle.z = left - right
  this.rotateLeft((lastGamma - currentAngle.z) / 2);
  lastGamma = currentAngle.z;

  // currentAngle.y = up - down
  this.rotateUp(lastBeta - currentAngle.y);
  lastBeta = currentAngle.y;
}

Listeners

function onDeviceOrientationChangeEvent(event) {
  scope.deviceOrientation = event;
}

window.addEventListener('deviceorientation', onDeviceOrientationChangeEvent, false);


function onScreenOrientationChangeEvent(event) {
  scope.screenOrientation = window.orientation || 0;
}

window.addEventListener('orientationchange', onScreenOrientationChangeEvent, false);

Functions

var setObjectQuaternion = function () {
  const zee = new THREE.Vector3(0, 0, 1);
  const euler = new THREE.Euler();
  const q0 = new THREE.Quaternion();
  const q1 = new THREE.Quaternion(-Math.sqrt(0.5), 0, 0,  Math.sqrt(0.5));

  return function (quaternion, alpha, beta, gamma, orient) {
    // 'ZXY' for the device, but 'YXZ' for us
    euler.set(beta, alpha, -gamma, 'YXZ');

    // Orient the device
    quaternion.setFromEuler(euler);

    // camera looks out the back of the device, not the top
    quaternion.multiply(q1);

    // adjust for screen orientation
    quaternion.multiply(q0.setFromAxisAngle(zee, -orient));
  }
} ();


function Quat2Angle(x, y, z, w) {
  let pitch, roll, yaw;

  const test = x * y + z * w;
  // singularity at north pole
  if (test > 0.499) {
    yaw = Math.atan2(x, w) * 2;
    pitch = Math.PI / 2;
    roll = 0;

    return new THREE.Vector3(pitch, roll, yaw);
  }

  // singularity at south pole
  if (test < -0.499) {
    yaw = -2 * Math.atan2(x, w);
    pitch = -Math.PI / 2;
    roll = 0;
    return new THREE.Vector3(pitch, roll, yaw);
  }

  const sqx = x * x;
  const sqy = y * y;
  const sqz = z * z;

  yaw = Math.atan2((2 * y * w) - (2 * x * z), 1 - (2 * sqy) - (2 * sqz));
  pitch = Math.asin(2 * test);
  roll = Math.atan2((2 * x * w) - (2 * y * z), 1 - (2 * sqx) - (2 * sqz));

  return new THREE.Vector3(pitch, roll, yaw);
}