diff --git a/scripts/rollup-replace.js b/scripts/rollup-replace.js index 13952447..421224f5 100644 --- a/scripts/rollup-replace.js +++ b/scripts/rollup-replace.js @@ -4,9 +4,14 @@ import replace from 'rollup-plugin-re'; const modules = [ 'VRControls', 'VREffect', - 'OrbitControls' + 'OrbitControls', + 'DeviceOrientationControls' ]; +const globalReplace = function(str, pattern, replacement) { + return str.replace(new RegExp(pattern, 'g'), replacement); +}; + export default function(options) { return replace(Object.assign({ include: ['node_modules/three/examples/js/**'], @@ -21,10 +26,20 @@ export default function(options) { code = code.replace(`THREE.${m} =`, `import * as THREE from 'three';\nvar ${m} =`); // change references from the global modification to the local variable - code = code.replace(new RegExp(`THREE.${m}`, 'g'), m); + code = globalReplace(code, `THREE.${m}`, m); // export that local variable as default from this module code += `\nexport default ${m};`; + + // expose private functions so that users can manually use controls + // and we can add orientation controls + if (m === 'OrbitControls') { + code = globalReplace(code, 'function rotateLeft\\(', 'rotateLeft = function('); + code = globalReplace(code, 'function rotateUp\\(', 'rotateUp = function('); + + code = globalReplace(code, 'rotateLeft', 'scope.rotateLeft'); + code = globalReplace(code, 'rotateUp', 'scope.rotateUp'); + } }); return code; }} diff --git a/src/orbit-orientation-controls.js b/src/orbit-orientation-controls.js new file mode 100644 index 00000000..dc17c242 --- /dev/null +++ b/src/orbit-orientation-controls.js @@ -0,0 +1,97 @@ +import * as THREE from 'three'; +import OrbitControls from 'three/examples/js/controls/OrbitControls.js'; +import DeviceOrientationControls from 'three/examples/js/controls/DeviceOrientationControls.js'; + +/** + * Convert a quaternion to an angle + * + * Taken from https://stackoverflow.com/a/35448946 + * Thanks P. Ellul + */ +function Quat2Angle(x, y, z, w) { + const test = x * y + z * w; + + // singularity at north pole + if (test > 0.499) { + const yaw = 2 * Math.atan2(x, w); + const pitch = Math.PI / 2; + const roll = 0; + + return new THREE.Vector3(pitch, roll, yaw); + } + + // singularity at south pole + if (test < -0.499) { + const yaw = -2 * Math.atan2(x, w); + const pitch = -Math.PI / 2; + const roll = 0; + + return new THREE.Vector3(pitch, roll, yaw); + } + + const sqx = x * x; + const sqy = y * y; + const sqz = z * z; + const yaw = Math.atan2(2 * y * w - 2 * x * z, 1 - 2 * sqy - 2 * sqz); + const pitch = Math.asin(2 * test); + const roll = Math.atan2(2 * x * w - 2 * y * z, 1 - 2 * sqx - 2 * sqz); + + return new THREE.Vector3(pitch, roll, yaw); +} + +class OrbitOrientationControls { + constructor(options) { + this.object = options.camera; + this.domElement = options.canvas; + this.orbit = new OrbitControls(this.object, this.domElement); + + this.speed = 0.5; + this.orbit.target.set(0, 0, -1); + this.orbit.enableZoom = false; + this.orbit.enablePan = false; + this.orbit.rotateSpeed = -this.speed; + + // if orientation is supported + if (options.orientation) { + this.orientation = new DeviceOrientationControls(this.object); + } + } + + update() { + // orientation updates the camera using quaternions and + // orbit updates the camera using angles. They are incompatible + // and one update overrides the other. So before + // orbit overrides orientation we convert our quaternion changes to + // an angle change. Then save the angle into orbit so that + // it will take those into account when it updates the camera and overrides + // our changes + if (this.orientation) { + this.orientation.update(); + + const quat = this.orientation.object.quaternion; + const currentAngle = Quat2Angle(quat.x, quat.y, quat.z, quat.w); + + // we also have to store the last angle since quaternions are b + if (typeof this.lastAngle_ === 'undefined') { + this.lastAngle_ = currentAngle; + } + + this.orbit.rotateLeft((this.lastAngle_.z - currentAngle.z) * (1 + this.speed)); + this.orbit.rotateUp((this.lastAngle_.y - currentAngle.y) * (1 + this.speed)); + this.lastAngle_ = currentAngle; + } + + this.orbit.update(); + } + + dispose() { + this.orbit.dispose(); + + if (this.orientation) { + this.orientation.dispose(); + } + } + +} + +export default OrbitOrientationControls; diff --git a/src/plugin.js b/src/plugin.js index 80937173..f076141b 100644 --- a/src/plugin.js +++ b/src/plugin.js @@ -6,7 +6,7 @@ import videojs from 'video.js'; import * as THREE from 'three'; import VRControls from 'three/examples/js/controls/VRControls.js'; import VREffect from 'three/examples/js/effects/VREffect.js'; -import OrbitControls from 'three/examples/js/controls/OrbitControls.js'; +import OrbitOrientationContols from './orbit-orientation-controls.js'; import * as utils from './utils'; // import controls so they get regisetered with videojs @@ -70,10 +70,10 @@ class VR extends Plugin { } this.polyfill_ = new WebVRPolyfill({ - TOUCH_PANNER_DISABLED: false, // do not show rotate instructions ROTATE_INSTRUCTIONS_DISABLED: true }); + this.polyfill_ = new WebVRPolyfill(); this.handleVrDisplayActivate_ = videojs.bind(this, this.handleVrDisplayActivate_); this.handleVrDisplayDeactivate_ = videojs.bind(this, this.handleVrDisplayDeactivate_); @@ -476,22 +476,27 @@ class VR extends Plugin { if (displays.length > 0) { this.log('VR Displays found', displays); this.vrDisplay = displays[0]; - this.log('Going to use VRControls on the first one', this.vrDisplay); // Native WebVR Head Mounted Displays (HMDs) like the HTC Vive // also need the cardboard button to enter fully immersive mode // so, we want to add the button if we're not polyfilled. if (!this.vrDisplay.isPolyfilled) { + this.log('Real HMD found using VRControls', this.vrDisplay); this.addCardboardButton_(); + + // We use VRControls here since we are working with an HMD + // and we only want orientation controls. + this.controls3d = new VRControls(this.camera); } - this.controls3d = new VRControls(this.camera); - } else { - this.log('no vr displays found going to use OrbitControls'); - this.controls3d = new OrbitControls(this.camera, this.renderedCanvas); - this.controls3d.target.set(0, 0, -1); - this.controls3d.enableZoom = false; - this.controls3d.enablePan = false; - this.controls3d.rotateSpeed = -0.5; + } + + if (!this.controls3d) { + this.log('no HMD found Using Orbit & Orientation Controls'); + this.controls3d = new OrbitOrientationContols({ + camera: this.camera, + canvas: this.renderedCanvas, + orientation: videojs.browser.IS_IOS || videojs.browser.IS_ANDROID || false + }); } this.animationFrameId_ = this.requestAnimationFrame(this.animate_); });