From 1397673510e7d4c7abb64bdcbd9d5305ed6b95b7 Mon Sep 17 00:00:00 2001 From: Jordan Santell Date: Thu, 11 Jan 2018 11:14:53 -0800 Subject: [PATCH] Use RelativeOrientationSensor when available, falling back to devicemotion-based FusionPoseSensor when not. Fixes #10. --- examples/iframe.html | 1 + src/cardboard-vr-display.js | 15 +-- src/pose-sensor.js | 180 ++++++++++++++++++++++++++++++++++++ 3 files changed, 189 insertions(+), 7 deletions(-) create mode 100644 src/pose-sensor.js diff --git a/examples/iframe.html b/examples/iframe.html index 4bf3220..a18deab 100644 --- a/examples/iframe.html +++ b/examples/iframe.html @@ -46,6 +46,7 @@ iframe.src = 'index.html'; iframe.width = '100%'; iframe.height = '100%'; + iframe.setAttribute('allow', 'gyroscope; accelerometer'); document.body.appendChild(iframe); // iOS, cross-origin iframes cannot access devicemotion events, so the parent diff --git a/src/cardboard-vr-display.js b/src/cardboard-vr-display.js index aafef1e..57efea6 100644 --- a/src/cardboard-vr-display.js +++ b/src/cardboard-vr-display.js @@ -17,7 +17,7 @@ import CardboardDistorter from './cardboard-distorter.js'; import CardboardUI from './cardboard-ui.js'; import DeviceInfo from './device-info.js'; import Dpdb from './dpdb.js'; -import FusionPoseSensor from './sensor-fusion/fusion-pose-sensor.js'; +import PoseSensor from './pose-sensor.js'; import RotateInstructions from './rotate-instructions.js'; import ViewerSelector from './viewer-selector.js'; import { VRDisplay, VRDisplayCapabilities } from './base.js'; @@ -56,10 +56,7 @@ function CardboardVRDisplay(config) { // "Private" members. this.bufferScale_ = this.config.BUFFER_SCALE; - this.poseSensor_ = new FusionPoseSensor(this.config.K_FILTER, - this.config.PREDICTION_TIME_S, - this.config.YAW_ONLY, - this.config.DEBUG); + this.poseSensor_ = new PoseSensor(this.config); this.distorter_ = null; this.cardboardUI_ = null; @@ -85,7 +82,7 @@ CardboardVRDisplay.prototype = Object.create(VRDisplay.prototype); CardboardVRDisplay.prototype._getPose = function() { return { - position: this.poseSensor_.getPosition(), + position: null, orientation: this.poseSensor_.getOrientation(), linearVelocity: null, linearAcceleration: null, @@ -95,7 +92,11 @@ CardboardVRDisplay.prototype._getPose = function() { } CardboardVRDisplay.prototype._resetPose = function() { - this.poseSensor_.resetPose(); + // The non-devicemotion PoseSensor does not have resetPose implemented + // as it has been deprecated from spec. + if (this.poseSensor_.resetPose) { + this.poseSensor_.resetPose(); + } }; CardboardVRDisplay.prototype._getFieldOfView = function(whichEye) { diff --git a/src/pose-sensor.js b/src/pose-sensor.js new file mode 100644 index 0000000..23e2489 --- /dev/null +++ b/src/pose-sensor.js @@ -0,0 +1,180 @@ +/* + * Copyright 2015 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import FusionPoseSensor from './sensor-fusion/fusion-pose-sensor.js'; +import { Vector3, Quaternion } from './math-util.js'; + +// Frequency which the Sensors will attempt to fire their +// `reading` functions at. Use 60hz since we generally +// can't get higher without native VR hardware. +const SENSOR_FREQUENCY = 60; + +const X_AXIS = new Vector3(1, 0, 0); +const Z_AXIS = new Vector3(0, 0, 1); + +let orientation = {}; +if (screen.orientation) { + orientation = screen.orientation; +} else if (screen.msOrientation) { + orientation = screen.msOrientation; +} else { + Object.defineProperty(orientation, 'angle', { + get: () => { return (window.orientation || 0); } + }); +} + +// Quaternion to rotate from sensor coordinates to WebVR coordinates +const SENSOR_TO_VR = new Quaternion(); +SENSOR_TO_VR.setFromAxisAngle(X_AXIS, -Math.PI / 2); +SENSOR_TO_VR.multiply(new Quaternion().setFromAxisAngle(Z_AXIS, Math.PI / 2)); + +/** + * An abstraction class around either using the new RelativeOrientationSensor, + * or `devicemotion` events with complimentary filter via fusion-pose-sensor.js. + */ +export default class PoseSensor { + constructor(config) { + this.config = config; + this.sensor = null; + this.fusionSensor = null; + this._out = new Float32Array(4); + + // Can be 'sensor' (using RelativeOrientationSensor) or + // 'devicemotion' (using devicemotion events via FusionPoseSensor), + // or `null` if not yet set. + this.api = null; + + // Store any errors from Sensors for debugging purposes + this.errors = []; + + // Quaternions for caching transforms + this._sensorQ = new Quaternion(); + this._worldToScreenQ = new Quaternion(); + this._outQ = new Quaternion(); + + this._onSensorRead = this._onSensorRead.bind(this); + this._onSensorError = this._onSensorError.bind(this); + this._onOrientationChange = this._onOrientationChange.bind(this); + + this._onOrientationChange(); + this.init(); + } + + init() { + // Attempt to use the RelativeOrientationSensor from Generic Sensor APIs. + // First available in Chrome M63, this can fail for several reasons, and attempt + // to fallback to devicemotion. Failure scenarios include: + // + // * Generic Sensor APIs do not exist; fallback to devicemotion. + // * Underlying sensor does not exist; no fallback possible. + // * Feature Policy failure (in an iframe); no fallback. + // https://github.com/immersive-web/webxr/issues/86 + // * Permission to sensor data denied; respect user agent; no fallback to devicemotion. + // Browsers are heading towards disabling devicemotion when sensors are denied as well. + // https://www.chromestatus.com/feature/5023919287304192 + let sensor = null; + try { + sensor = new RelativeOrientationSensor({ frequency: SENSOR_FREQUENCY }); + sensor.addEventListener('error', this._onSensorError); + } catch (error) { + this.errors.push(error); + + // Sensors are available in Chrome M63, however the Feature Policy + // integration is not available until Chrome M65, resulting in Sensors + // only being available in main frames. + // https://developers.google.com/web/updates/2017/09/sensors-for-the-web#feature_policy_integration + if (error.name === 'SecurityError') { + console.error('Cannot construct sensors due to the Feature Policy'); + console.warn('Attempting to fall back using "devicemotion"; however this will ' + + 'fail in the future without correct permissions.'); + this.useDeviceMotion(); + } else if (error.name === 'ReferenceError') { + // Fall back to devicemotion. + this.useDeviceMotion(); + } else { + console.error(error); + } + } + + if (sensor) { + this.api = 'sensor'; + this.sensor = sensor; + this.sensor.addEventListener('reading', this._onSensorRead); + this.sensor.start(); + } + + window.addEventListener('orientationchange', this._onOrientationChange); + } + + useDeviceMotion() { + this.api = 'devicemotion'; + this.fusionSensor = new FusionPoseSensor(this.config.K_FILTER, + this.config.PREDICTION_TIME_S, + this.config.YAW_ONLY, + this.config.DEBUG); + } + + getOrientation() { + if (this.fusionSensor) { + return this.fusionSensor.getOrientation(); + } + + if (!this.sensor || !this.sensor.quaternion) { + this._out[0] = this._out[1] = this._out[2] = 0; + this._out[3] = 1; + return this._out; + } + + // Convert to THREE coordinate system: -Z forward, Y up, X right. + const q = this.sensor.quaternion; + this._sensorQ.set(q[0], q[1], q[2], q[3]); + + const out = this._outQ; + out.copy(SENSOR_TO_VR); + out.multiply(this._sensorQ); + out.multiply(this._worldToScreenQ); + + // Handle the yaw-only case. + if (this.config.YAW_ONLY) { + // Make a quaternion that only turns around the Y-axis. + out.x = out.z = 0; + out.normalize(); + } + + this._out[0] = out.x; + this._out[1] = out.y; + this._out[2] = out.z; + this._out[3] = out.w; + return this._out; + } + + _onSensorError(event) { + this.errors.push(event.error); + if (event.error.name === 'NotAllowedError') { + console.error('Permission to access sensor was denied'); + } else if (event.error.name === 'NotReadableError') { + console.error('Sensor could not be read'); + } else { + console.error(event.error); + } + } + + _onSensorRead() {} + + _onOrientationChange() { + const angle = -orientation.angle * Math.PI / 180; + this._worldToScreenQ.setFromAxisAngle(Z_AXIS, angle); + } +}