@egjs/view360
Version:
360 integrated viewing solution from inside-out view to outside-in view. It provides user-friendly service by rotating 360 degrees through various user interaction such as motion sensor and touch.
240 lines (200 loc) • 6.96 kB
JavaScript
import Component from "@egjs/component";
import PosePredictor from "webvr-polyfill/src/sensor-fusion/pose-predictor";
import MathUtil from "webvr-polyfill/src/math-util";
import Util from "webvr-polyfill/src/util";
import {window} from "../../utils/browser";
import {quat} from "../../utils/math-util";
import DeviceMotion from "./DeviceMotion";
import ComplementaryFilter from "./ComplementaryFilter";
import {CHROME_VERSION} from "../consts";
const K_FILTER = 0.98;
const PREDICTION_TIME_S = 0.040;
export default class FusionPoseSensor extends Component {
constructor() {
super();
this.deviceMotion = new DeviceMotion();
this.accelerometer = new MathUtil.Vector3();
this.gyroscope = new MathUtil.Vector3();
this._onDeviceMotionChange = this._onDeviceMotionChange.bind(this);
this._onScreenOrientationChange = this._onScreenOrientationChange.bind(this);
this.filter = new ComplementaryFilter(K_FILTER);
this.posePredictor = new PosePredictor(PREDICTION_TIME_S);
this.filterToWorldQ = new MathUtil.Quaternion();
this.isFirefoxAndroid = Util.isFirefoxAndroid();
this.isIOS = Util.isIOS();
// Ref https://github.com/immersive-web/cardboard-vr-display/issues/18
this.isChromeUsingDegrees = CHROME_VERSION >= 66;
this._isEnabled = false;
// Set the filter to world transform, depending on OS.
if (this.isIOS) {
this.filterToWorldQ.setFromAxisAngle(new MathUtil.Vector3(1, 0, 0), Math.PI / 2);
} else {
this.filterToWorldQ.setFromAxisAngle(new MathUtil.Vector3(1, 0, 0), -Math.PI / 2);
}
this.inverseWorldToScreenQ = new MathUtil.Quaternion();
this.worldToScreenQ = new MathUtil.Quaternion();
this.originalPoseAdjustQ = new MathUtil.Quaternion();
this.originalPoseAdjustQ.setFromAxisAngle(new MathUtil.Vector3(0, 0, 1),
-window.orientation * Math.PI / 180);
this._setScreenTransform();
// Adjust this filter for being in landscape mode.
if (Util.isLandscapeMode()) {
this.filterToWorldQ.multiply(this.inverseWorldToScreenQ);
}
// Keep track of a reset transform for resetSensor.
this.resetQ = new MathUtil.Quaternion();
this.deviceMotion.on("devicemotion", this._onDeviceMotionChange);
this.enable();
}
enable() {
if (this.isEnabled()) {
return;
}
this.deviceMotion.enable();
this._isEnabled = true;
window.addEventListener("orientationchange", this._onScreenOrientationChange);
}
disable() {
if (!this.isEnabled()) {
return;
}
this.deviceMotion.disable();
this._isEnabled = false;
window.removeEventListener("orientationchange", this._onScreenOrientationChange);
}
isEnabled() {
return this._isEnabled;
}
destroy() {
this.disable();
this.deviceMotion = null;
}
_triggerChange() {
const orientation = this.getOrientation();
// if orientation is not prepared. don't trigger change event
if (!orientation) {
return;
}
if (!this._prevOrientation) {
this._prevOrientation = orientation;
return;
}
if (quat.equals(this._prevOrientation, orientation)) {
return;
}
this.trigger("change", {quaternion: orientation});
}
getOrientation() {
let orientation;
// Hack around using deviceorientation instead of devicemotion
if (this.deviceMotion.isWithoutDeviceMotion && this._deviceOrientationQ) {
this.deviceOrientationFixQ = this.deviceOrientationFixQ || (function() {
const y =
new MathUtil.Quaternion().setFromAxisAngle(
new MathUtil.Vector3(0, 1, 0), -this._alpha);
return y;
}).bind(this)();
orientation = this._deviceOrientationQ;
const out = new MathUtil.Quaternion();
out.copy(orientation);
out.multiply(this.filterToWorldQ);
out.multiply(this.resetQ);
out.multiply(this.worldToScreenQ);
out.multiplyQuaternions(this.deviceOrientationFixQ, out);
// return quaternion as glmatrix quaternion object
const out_ = quat.fromValues(
out.x,
out.y,
out.z,
out.w
);
return quat.normalize(out_, out_);
} else {
// Convert from filter space to the the same system used by the
// deviceorientation event.
orientation = this.filter.getOrientation();
if (!orientation) {
return null;
}
const out = this._convertFusionToPredicted(orientation);
// return quaternion as glmatrix quaternion object
const out_ = quat.fromValues(
out.x,
out.y,
out.z,
out.w
);
return quat.normalize(out_, out_);
}
}
_convertFusionToPredicted(orientation) {
// Predict orientation.
this.predictedQ =
this.posePredictor.getPrediction(orientation, this.gyroscope, this.previousTimestampS);
// Convert to THREE coordinate system: -Z forward, Y up, X right.
const out = new MathUtil.Quaternion();
out.copy(this.filterToWorldQ);
out.multiply(this.resetQ);
out.multiply(this.predictedQ);
out.multiply(this.worldToScreenQ);
return out;
}
_onDeviceMotionChange({inputEvent}) {
const deviceorientation = inputEvent.deviceorientation;
const deviceMotion = inputEvent;
const accGravity = deviceMotion.accelerationIncludingGravity;
const rotRate = deviceMotion.adjustedRotationRate || deviceMotion.rotationRate;
let timestampS = deviceMotion.timeStamp / 1000;
if (deviceorientation) {
if (!this._alpha) {
this._alpha = deviceorientation.alpha;
}
this._deviceOrientationQ = this._deviceOrientationQ || new MathUtil.Quaternion();
this._deviceOrientationQ.setFromEulerYXZ(
deviceorientation.beta,
deviceorientation.alpha,
deviceorientation.gamma
);
this._triggerChange();
} else {
// Firefox Android timeStamp returns one thousandth of a millisecond.
if (this.isFirefoxAndroid) {
timestampS /= 1000;
}
this.accelerometer.set(-accGravity.x, -accGravity.y, -accGravity.z);
this.gyroscope.set(rotRate.alpha, rotRate.beta, rotRate.gamma);
// Browsers on iOS, Firefox/Android, and Chrome m66/Android `rotationRate`
// is reported in degrees, so we first convert to radians.
if (this.isIOS || this.isFirefoxAndroid || this.isChromeUsingDegrees) {
this.gyroscope.multiplyScalar(Math.PI / 180);
}
this.filter.addAccelMeasurement(this.accelerometer, timestampS);
this.filter.addGyroMeasurement(this.gyroscope, timestampS);
this._triggerChange();
this.previousTimestampS = timestampS;
}
}
_onScreenOrientationChange(screenOrientation) {
this._setScreenTransform(window.orientation);
}
_setScreenTransform() {
this.worldToScreenQ.set(0, 0, 0, 1);
switch (window.orientation) {
case 0:
break;
case 90:
this.worldToScreenQ.setFromAxisAngle(new MathUtil.Vector3(0, 0, 1), 90 / -180 * Math.PI);
break;
case -90:
this.worldToScreenQ.setFromAxisAngle(new MathUtil.Vector3(0, 0, 1), -90 / -180 * Math.PI);
break;
case 180:
this.worldToScreenQ.setFromAxisAngle(new MathUtil.Vector3(0, 0, 1), 180 / -180 * Math.PI);
break;
default:
break;
}
this.inverseWorldToScreenQ.copy(this.worldToScreenQ);
this.inverseWorldToScreenQ.inverse();
}
}