This file was deleted.
This file was deleted.
This file was deleted.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,359 @@ | ||
| /** | ||
| * A polyfill for mobile WebVR (targeting Cardboard and similar devices) | ||
| * using DeviceOrientation events. | ||
| * | ||
| * - If WebVR is already available, do nothing. | ||
| * - If this is a mobile device capable of gyro tracking, inject this | ||
| * polyfill. | ||
| * - Provide `function getVRDevices(vrCallback)`, which returns a | ||
| * HMDVRDevice and corresponding PositionSensorVRDevice. | ||
| * - HMDVRDevice should provide the right geometry to render well on | ||
| * Cardboard via getRecommendedEyeFieldOfView and getEyeTranslation. | ||
| * - PositionSensorVRDevice should provide a getState().orientation | ||
| * quaternion based on Device* events. | ||
| * | ||
| * Depends on three.js (for quaternions only). Dependency can be replaced | ||
| * with quite a bit more code: | ||
| * https://dev.opera.com/articles/w3c-device-orientation-usage/#quaternions | ||
| * | ||
| * Author: Boris Smus <boris@smus.com> | ||
| */ | ||
|
|
||
| (function() { | ||
|
|
||
| // Constants from vrtoolkit: https://github.com/googlesamples/cardboard-java. | ||
| var INTERPUPILLARY_DISTANCE = 0.06; | ||
| var DEFAULT_MAX_FOV_LEFT_RIGHT = 40; | ||
| var DEFAULT_MAX_FOV_BOTTOM = 40; | ||
| var DEFAULT_MAX_FOV_TOP = 40; | ||
|
|
||
| // How much to rotate per key stroke. | ||
| var KEY_SPEED = 0.15; | ||
| var KEY_ANIMATION_DURATION = 80; | ||
|
|
||
| // How much to rotate for mouse events. | ||
| var MOUSE_SPEED_X = 0.5; | ||
| var MOUSE_SPEED_Y = 0.3; | ||
|
|
||
| function WebVRPolyfill() { | ||
| this.devices = []; | ||
|
|
||
| // Feature detect for existing WebVR API. | ||
| if (navigator.getVRDevices) { | ||
| return; | ||
| } | ||
|
|
||
| // Initialize our virtual VR devices. | ||
| if (this.isCardboardCompatible()) { | ||
| this.devices.push(new CardboardHMDVRDevice()); | ||
| } | ||
|
|
||
| // Polyfill using the right position sensor. | ||
| if (this.isMobile()) { | ||
| this.devices.push(new GyroPositionSensorVRDevice()); | ||
| } else { | ||
| this.devices.push(new MouseKeyboardPositionSensorVRDevice()); | ||
| } | ||
|
|
||
| // Provide navigator.getVRDevices. | ||
| navigator.getVRDevices = this.getVRDevices.bind(this); | ||
|
|
||
| // Provide the CardboardHMDVRDevice and PositionSensorVRDevice objects. | ||
| window.HMDVRDevice = HMDVRDevice; | ||
| window.PositionSensorVRDevice = PositionSensorVRDevice; | ||
| } | ||
|
|
||
| WebVRPolyfill.prototype.getVRDevices = function() { | ||
| var devices = this.devices; | ||
| return new Promise(function(resolve, reject) { | ||
| try { | ||
| resolve(devices); | ||
| } catch (e) { | ||
| reject(e); | ||
| } | ||
| }); | ||
| }; | ||
|
|
||
| /** | ||
| * Determine if a device is mobile. | ||
| */ | ||
| WebVRPolyfill.prototype.isMobile = function() { | ||
| return /Android/i.test(navigator.userAgent) || | ||
| /iPhone|iPad|iPod/i.test(navigator.userAgent);; | ||
| }; | ||
|
|
||
| WebVRPolyfill.prototype.isCardboardCompatible = function() { | ||
| // For now, support all iOS and Android devices. | ||
| return this.isMobile(); | ||
| }; | ||
|
|
||
| /** | ||
| * The base class for all VR devices. | ||
| */ | ||
| function VRDevice() { | ||
| this.hardwareUnitId = 'polyfill'; | ||
| } | ||
|
|
||
| /** | ||
| * The base class for all VR HMD devices. | ||
| */ | ||
| function HMDVRDevice() { | ||
| } | ||
| HMDVRDevice.prototype = new VRDevice(); | ||
|
|
||
| /** | ||
| * The base class for all VR position sensor devices. | ||
| */ | ||
| function PositionSensorVRDevice() { | ||
| } | ||
| PositionSensorVRDevice.prototype = new VRDevice(); | ||
|
|
||
|
|
||
| /** | ||
| * The HMD itself, providing rendering parameters. | ||
| */ | ||
| function CardboardHMDVRDevice() { | ||
| // Set display constants. | ||
| this.eyeTranslationLeft = { | ||
| x: INTERPUPILLARY_DISTANCE * -0.5, | ||
| y: 0, | ||
| z: 0 | ||
| }; | ||
| this.eyeTranslationRight = { | ||
| x: INTERPUPILLARY_DISTANCE * 0.5, | ||
| y: 0, | ||
| z: 0 | ||
| }; | ||
|
|
||
| // From com/google/vrtoolkit/cardboard/FieldOfView.java. | ||
| this.recommendedFOV = { | ||
| upDegrees: DEFAULT_MAX_FOV_TOP, | ||
| downDegrees: DEFAULT_MAX_FOV_BOTTOM, | ||
| leftDegrees: DEFAULT_MAX_FOV_LEFT_RIGHT, | ||
| rightDegrees: DEFAULT_MAX_FOV_LEFT_RIGHT | ||
| }; | ||
| } | ||
| CardboardHMDVRDevice.prototype = new HMDVRDevice(); | ||
|
|
||
| CardboardHMDVRDevice.prototype.getRecommendedEyeFieldOfView = function(whichEye) { | ||
| return this.recommendedFOV; | ||
| }; | ||
|
|
||
| CardboardHMDVRDevice.prototype.getEyeTranslation = function(whichEye) { | ||
| if (whichEye == 'left') { | ||
| return this.eyeTranslationLeft; | ||
| } | ||
| if (whichEye == 'right') { | ||
| return this.eyeTranslationRight; | ||
| } | ||
| }; | ||
|
|
||
|
|
||
| /** | ||
| * The positional sensor, implemented using web DeviceOrientation APIs. | ||
| */ | ||
| function GyroPositionSensorVRDevice() { | ||
| // Subscribe to deviceorientation events. | ||
| window.addEventListener('deviceorientation', this.onDeviceOrientationChange.bind(this)); | ||
| window.addEventListener('orientationchange', this.onScreenOrientationChange.bind(this)); | ||
| this.deviceOrientation = null; | ||
| this.screenOrientation = window.orientation; | ||
|
|
||
| // Helper objects for calculating orientation. | ||
| this.finalQuaternion = new THREE.Quaternion(); | ||
| this.deviceEuler = new THREE.Euler(); | ||
| this.screenTransform = new THREE.Quaternion(); | ||
| // -PI/2 around the x-axis. | ||
| this.worldTransform = new THREE.Quaternion(-Math.sqrt(0.5), 0, 0, Math.sqrt(0.5)); | ||
|
|
||
| // TODO: Try with DeviceMotionEvent, remove it if it fails. | ||
| // Is devicemotion any better? | ||
| window.addEventListener('devicemotion', this.onDeviceMotionChange.bind(this)); | ||
| this.totalRotation = {alpha: 0, beta: 0, gamma: 0}; | ||
| } | ||
| GyroPositionSensorVRDevice.prototype = new PositionSensorVRDevice(); | ||
|
|
||
| /** | ||
| * Returns {orientation: {x,y,z,w}, position: null}. | ||
| * Position is not supported since we can't do 6DOF. | ||
| */ | ||
| GyroPositionSensorVRDevice.prototype.getState = function() { | ||
| return { | ||
| orientation: this.getOrientation(), | ||
| position: null | ||
| } | ||
| }; | ||
|
|
||
| GyroPositionSensorVRDevice.prototype.onDeviceOrientationChange = | ||
| function(deviceOrientation) { | ||
| this.deviceOrientation = deviceOrientation; | ||
| }; | ||
|
|
||
| GyroPositionSensorVRDevice.prototype.onScreenOrientationChange = | ||
| function(screenOrientation) { | ||
| this.screenOrientation = window.orientation; | ||
| }; | ||
|
|
||
| GyroPositionSensorVRDevice.prototype.onDeviceMotionChange = | ||
| function(deviceMotion) { | ||
| var rotationRate = deviceMotion.rotationRate; | ||
| // Rotation around the y-axis. | ||
| this.totalRotation.alpha += rotationRate.alpha; | ||
| // Rotation around the z-axis. | ||
| this.totalRotation.beta += rotationRate.beta; | ||
| // Rotation around the x-axis. | ||
| this.totalRotation.gamma += rotationRate.gamma; | ||
| }; | ||
|
|
||
| GyroPositionSensorVRDevice.prototype.getOrientation = function() { | ||
| if (this.deviceOrientation == null) { | ||
| return null; | ||
| } | ||
| /* | ||
| // Rotation around the z-axis. | ||
| var alpha = THREE.Math.degToRad(this.totalRotation.alpha); | ||
| // Front-to-back (in portrait) rotation (x-axis). | ||
| var beta = THREE.Math.degToRad(-this.totalRotation.gamma); | ||
| // Left to right (in portrait) rotation (y-axis). | ||
| var gamma = THREE.Math.degToRad(this.totalRotation.beta); | ||
| */ | ||
|
|
||
| // Rotation around the z-axis. | ||
| var alpha = THREE.Math.degToRad(this.deviceOrientation.alpha); | ||
| // Front-to-back (in portrait) rotation (x-axis). | ||
| var beta = THREE.Math.degToRad(this.deviceOrientation.beta); | ||
| // Left to right (in portrait) rotation (y-axis). | ||
| var gamma = THREE.Math.degToRad(this.deviceOrientation.gamma); | ||
| var orient = THREE.Math.degToRad(this.screenOrientation); | ||
|
|
||
| // Use three.js to convert to quaternion. Lifted from | ||
| // https://github.com/richtr/threeVR/blob/master/js/DeviceOrientationController.js | ||
| this.deviceEuler.set(beta, alpha, -gamma, 'YXZ'); | ||
| this.finalQuaternion.setFromEuler(this.deviceEuler); | ||
| this.minusHalfAngle = -orient / 2; | ||
| this.screenTransform.set(0, Math.sin(this.minusHalfAngle), 0, Math.cos(this.minusHalfAngle)); | ||
| this.finalQuaternion.multiply(this.screenTransform); | ||
| this.finalQuaternion.multiply(this.worldTransform); | ||
|
|
||
| return this.finalQuaternion; | ||
| }; | ||
|
|
||
|
|
||
| /** | ||
| * Another virtual position sensor, this time implemented using keyboard and | ||
| * mouse APIs. This is designed as for desktops/laptops where no Device* | ||
| * events work. | ||
| */ | ||
| function MouseKeyboardPositionSensorVRDevice() { | ||
| // Attach to mouse and keyboard events. | ||
| window.addEventListener('keydown', this.onKeyDown_.bind(this)); | ||
| window.addEventListener('mousemove', this.onMouseMove_.bind(this)); | ||
| window.addEventListener('mousedown', this.onMouseDown_.bind(this)); | ||
| window.addEventListener('mouseup', this.onMouseUp_.bind(this)); | ||
|
|
||
| this.phi = 0; | ||
| this.theta = 0; | ||
|
|
||
| // Variables for keyboard-based rotation animation. | ||
| this.targetAngle = null; | ||
|
|
||
| // State variables for calculations. | ||
| this.euler = new THREE.Euler(); | ||
| this.orientation = new THREE.Quaternion(); | ||
|
|
||
| // Variables for mouse-based rotation. | ||
| this.rotateStart = new THREE.Vector2(); | ||
| this.rotateEnd = new THREE.Vector2(); | ||
| this.rotateDelta = new THREE.Vector2(); | ||
| } | ||
| MouseKeyboardPositionSensorVRDevice.prototype = new PositionSensorVRDevice(); | ||
|
|
||
| /** | ||
| * Returns {orientation: {x,y,z,w}, position: null}. | ||
| * Position is not supported for parity with other PositionSensors. | ||
| */ | ||
| MouseKeyboardPositionSensorVRDevice.prototype.getState = function() { | ||
| this.euler.set(this.phi, this.theta, 0, 'YXZ'); | ||
| this.orientation.setFromEuler(this.euler); | ||
|
|
||
| return { | ||
| orientation: this.orientation, | ||
| position: null | ||
| } | ||
| }; | ||
|
|
||
| MouseKeyboardPositionSensorVRDevice.prototype.onKeyDown_ = function(e) { | ||
| // Track WASD and arrow keys. | ||
| if (e.keyCode == 38 || e.keyCode == 87) { // W or Up key. | ||
| this.animatePhi_(this.phi + KEY_SPEED); | ||
| } else if (e.keyCode == 39 || e.keyCode == 68) { // D or Right key. | ||
| this.animateTheta_(this.theta - KEY_SPEED); | ||
| } else if (e.keyCode == 40 || e.keyCode == 83) { // S or Down key. | ||
| this.animatePhi_(this.phi - KEY_SPEED); | ||
| } else if (e.keyCode == 37 || e.keyCode == 65) { // A or Left key. | ||
| this.animateTheta_(this.theta + KEY_SPEED); | ||
| } | ||
| }; | ||
|
|
||
| MouseKeyboardPositionSensorVRDevice.prototype.animateTheta_ = function(targetAngle) { | ||
| this.animateKeyTransitions_('theta', targetAngle); | ||
| }; | ||
|
|
||
| MouseKeyboardPositionSensorVRDevice.prototype.animatePhi_ = function(targetAngle) { | ||
| this.animateKeyTransitions_('phi', targetAngle); | ||
| }; | ||
|
|
||
| /** | ||
| * Start an animation to transition an angle from one value to another. | ||
| */ | ||
| MouseKeyboardPositionSensorVRDevice.prototype.animateKeyTransitions_ = function(angleName, targetAngle) { | ||
| // If an animation is currently running, cancel it. | ||
| if (this.angleAnimation) { | ||
| clearInterval(this.angleAnimation); | ||
| } | ||
| var startAngle = this[angleName]; | ||
| var startTime = new Date(); | ||
| // Set up an interval timer to perform the animation. | ||
| this.angleAnimation = setInterval(function() { | ||
| // Once we're finished the animation, we're done. | ||
| var elapsed = new Date() - startTime; | ||
| if (elapsed >= KEY_ANIMATION_DURATION) { | ||
| this[angleName] = targetAngle; | ||
| clearInterval(this.angleAnimation); | ||
| return; | ||
| } | ||
| // Linearly interpolate the angle some amount. | ||
| var percent = elapsed / KEY_ANIMATION_DURATION; | ||
| this[angleName] = startAngle + (targetAngle - startAngle) * percent; | ||
| }.bind(this), 1000/60); | ||
| }; | ||
|
|
||
| MouseKeyboardPositionSensorVRDevice.prototype.onMouseDown_ = function(e) { | ||
| this.rotateStart.set(e.clientX, e.clientY); | ||
| this.isDragging = true; | ||
| }; | ||
|
|
||
| // Very similar to https://gist.github.com/mrflix/8351020 | ||
| MouseKeyboardPositionSensorVRDevice.prototype.onMouseMove_ = function(e) { | ||
| if (!this.isDragging) { | ||
| return; | ||
| } | ||
| this.rotateEnd.set(e.clientX, e.clientY); | ||
| // Calculate how much we moved in mouse space. | ||
| this.rotateDelta.subVectors(this.rotateEnd, this.rotateStart); | ||
| this.rotateStart.copy(this.rotateEnd); | ||
|
|
||
| // Keep track of the cumulative euler angles. | ||
| var element = document.body; | ||
| this.phi += 2 * Math.PI * this.rotateDelta.y / element.clientHeight * MOUSE_SPEED_Y; | ||
| this.theta += 2 * Math.PI * this.rotateDelta.x / element.clientWidth * MOUSE_SPEED_X; | ||
| }; | ||
|
|
||
| MouseKeyboardPositionSensorVRDevice.prototype.onMouseUp_ = function(e) { | ||
| this.isDragging = false; | ||
| }; | ||
|
|
||
|
|
||
| new WebVRPolyfill(); | ||
|
|
||
| })(); |