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

1

8

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.

let deltaBeta = 0;
let deltaGamma = 0;

if (window.DeviceOrientationEvent) {
  window.addEventListener('deviceorientation', function (e) {
    const beta = (e.beta != null) ? Math.round(e.beta) : 0;
    const gamma = (e.gamma != null) ? Math.round(e.gamma) : 0;

    deltaBeta = Math.abs(beta - deltaBeta);
    deltaGamma = Math.abs(gamma - deltaGamma);

    $("#beta").html("Beta: " + beta);
    $("#gamma").html("Gamma: " + gamma);
    
    if (Math.abs(deltaBeta) > Math.abs(Number($("#deltaBeta").html()))) {
      $("#deltaBeta").html(deltaBeta);
      if (Number($("#deltaBeta").html()) >= 30) {
        $("#deltaBeta").removeAttr("class", "blue").addClass("red");
      }
    }
    if (Math.abs(deltaGamma) > Math.abs(Number($("#deltaGamma").html()))) {
      $("#deltaGamma").html(deltaGamma);
      if (Number($("#deltaGamma").html()) >= 30) {
        $("#deltaGamma").removeAttr("class", "blue").addClass("red");
      }
    }
  }, true);

} else {
  $("#gamma").html("deviceorientation not supported");
}
.red {
  color: red;
  font-weight: bold;
}
.blue {
  color: blue;
  font-weight: bold;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<body>
  <div>
    <span id="beta"></span>
    <span> [-180; 180]</span>
  </div>
  <div>
    <span>DeltaMax</span>
    <span id="deltaBeta" class="blue">0</span>
  </div>
  <div>
    <span id="gamma"></span>
    <span> [-90; 90]</span>
  </div>
  <div>
    <span>DeltaMax</span>
    <span id="deltaGamma" class="blue">0</span>
  </div>
</body>
Tidewater answered 9/2, 2016 at 3:18 Comment(7)
Tere is library OrbitControls-Touch.js where are all the configurations you need implemented. I cant find it anywhere, but you can obtain it from my project dev.exostellar.net/js/OrbitControls-Touch.jsPeripeteia
Thanks for you answer. My OrbitControls.js is already working with touch events, my problem here is to add the deviceorientation event's values, and more especially gamma. The site I linked above (Lacoste) is exactly what I need to do. I think the project which is the closest from my needs is Richtr's threeVR, but when 'touchend' listener is triggered camera goes back to deviceorientation rotation. Thanks again for you quick answer.Tidewater
then you can use natural functions like: controls.minDistance = x; controls.maxDistance = controlsDistances.max; controls.noKeys = true; controls.center.set(0,0,0);Peripeteia
if you have fixed center, you can get camera.rotation.y on each frame and use it as absolute nuber. If your object is moveable in the scene, put it vith camera to one Object3D() and move with object, camera center will still be 0,0,0 and you can use RAD values of camera rotation.Peripeteia
Well, I already managed to make my camera rotate up and down with the deviceorientation listener (with rotateUp(lastBeta-beta); ) . While beta is taking value from -180 to 180 ONLY according to the device up/down angle, the gamma (right/left angle) takes awkward values depending on beta and alpha. That's the reason why I can't use it to make my camera rotating normally. I'll try to add a JSFiddle latter to make it clearer.Tidewater
OrbitControls have option controls.maxPolarAngle.Peripeteia
I want to let my minAzimuthAngle and maxAzimuthAngle set to ±Infinity (the vertical rotation is ok).Tidewater
T
16

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);
}
Tidewater answered 17/2, 2016 at 5:54 Comment(6)
Had he same problem, you saved my life!Sordid
Do you have repro of a working solution? That would be very helpful! Thanks!Alfredoalfresco
@cheesyeyes Sorry, I have no public repository of my project since I had to do it for work… Hope it helps you anyway. Good luck!Tidewater
This was super usefulGmt
For further users, replace >> this.rotateLeft((lastGamma - currentAngle.z) / 2); with: >> this.rotateLeft(lastGamma - currentAngle.z); this will allow you to turn 360 degrees horizontally.Gmt
You may also like github.com/richtr/threeVR which provides both touch and device orientation controls.Festus

© 2022 - 2024 — McMap. All rights reserved.