diff --git a/package.json b/package.json index 84264d4471c..3e1ffc17683 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "present": "0.0.6", "promise-polyfill": "^3.1.0", "style-attr": "^1.0.2", - "three": "^0.87.0", + "three": "mrdoob/three.js#dev", "three-bmfont-text": "^2.1.0", "webvr-polyfill": "^0.9.40" }, diff --git a/src/components/camera.js b/src/components/camera.js index d24d0d959b5..5a858758c9c 100644 --- a/src/components/camera.js +++ b/src/components/camera.js @@ -28,6 +28,9 @@ module.exports.Component = registerComponent('camera', { var el = this.el; var sceneEl = el.sceneEl; + // To save / restore camera pose + this.savedRotation = new THREE.Vector3(); + this.savedPosition = new THREE.Vector3(); this.savedPose = null; // Create camera. @@ -153,13 +156,15 @@ module.exports.Component = registerComponent('camera', { */ saveCameraPose: function () { var el = this.el; + var position = el.getAttribute('position'); + var rotation = el.getAttribute('rotation'); var hasPositionalTracking = this.hasPositionalTracking !== undefined ? this.hasPositionalTracking : checkHasPositionalTracking(); if (this.savedPose || !hasPositionalTracking) { return; } this.savedPose = { - position: el.getAttribute('position').clone(), - rotation: el.getAttribute('rotation') + position: this.savedPosition.copy(position), + rotation: this.savedRotation.copy(rotation) }; }, diff --git a/src/components/look-controls.js b/src/components/look-controls.js index 878c45776dc..7ec71403634 100644 --- a/src/components/look-controls.js +++ b/src/components/look-controls.js @@ -1,7 +1,7 @@ var registerComponent = require('../core/component').registerComponent; var THREE = require('../lib/three'); -var DEFAULT_CAMERA_HEIGHT = require('../constants').DEFAULT_CAMERA_HEIGHT; var bind = require('../utils/bind'); +var PolyfillControls = require('../utils').device.PolyfillControls; // To avoid recalculation at every mouse movement tick var GRABBING_CLASS = 'a-grabbing'; @@ -23,21 +23,16 @@ module.exports.Component = registerComponent('look-controls', { }, init: function () { - var sceneEl = this.el.sceneEl; - this.previousHMDPosition = new THREE.Vector3(); this.hmdQuaternion = new THREE.Quaternion(); this.hmdEuler = new THREE.Euler(); this.position = new THREE.Vector3(); + this.polyfillObject = new THREE.Object3D(); + this.polyfillControls = new PolyfillControls(this.polyfillObject); this.rotation = {}; this.deltaRotation = {}; - this.setupMouseControls(); - this.setupHMDControls(); this.bindMethods(); - - // Reset previous HMD position when we exit VR. - sceneEl.addEventListener('exit-vr', this.onExitVR); }, update: function (oldData) { @@ -57,22 +52,9 @@ module.exports.Component = registerComponent('look-controls', { tick: function (t) { var data = this.data; - if (!data.enabled) { return; } - this.controls.standing = data.standing; - this.controls.userHeight = this.getUserHeight(); - this.controls.update(); - this.updateOrientation(); + if (!data.enabled || this.el.sceneEl.is('vr-mode')) { return; } this.updatePosition(); - }, - - /** - * Return user height to use for standing poses, where a device doesn't provide an offset. - */ - getUserHeight: function () { - var el = this.el; - return el.hasAttribute('camera') - ? el.getAttribute('camera').userHeight - : DEFAULT_CAMERA_HEIGHT; + this.updateOrientation(); }, play: function () { @@ -108,16 +90,6 @@ module.exports.Component = registerComponent('look-controls', { this.yawObject.add(this.pitchObject); }, - /** - * Set up VR controls that will copy data to the dolly. - */ - setupHMDControls: function () { - this.dolly = new THREE.Object3D(); - this.euler = new THREE.Euler(); - this.controls = new THREE.VRControls(this.dolly); - this.controls.userHeight = 0.0; - }, - /** * Add mouse and touch event listeners to canvas. */ @@ -140,6 +112,9 @@ module.exports.Component = registerComponent('look-controls', { canvasEl.addEventListener('touchstart', this.onTouchStart); window.addEventListener('touchmove', this.onTouchMove); window.addEventListener('touchend', this.onTouchEnd); + + // sceneEl events. + sceneEl.addEventListener('exit-vr', this.onExitVR); }, /** @@ -161,6 +136,9 @@ module.exports.Component = registerComponent('look-controls', { canvasEl.removeEventListener('touchstart', this.onTouchStart); canvasEl.removeEventListener('touchmove', this.onTouchMove); canvasEl.removeEventListener('touchend', this.onTouchEnd); + + // sceneEl events. + sceneEl.removeEventListener('exit-vr', this.onExitVR); }, /** @@ -168,64 +146,29 @@ module.exports.Component = registerComponent('look-controls', { * Mouse-drag only enabled if HMD is not active. */ updateOrientation: function () { - var currentRotation; - var deltaRotation = this.deltaRotation; var hmdEuler = this.hmdEuler; - var hmdQuaternion = this.hmdQuaternion; var pitchObject = this.pitchObject; var yawObject = this.yawObject; var sceneEl = this.el.sceneEl; var rotation = this.rotation; - // Calculate HMD quaternion. - hmdQuaternion = hmdQuaternion.copy(this.dolly.quaternion); - hmdEuler.setFromQuaternion(hmdQuaternion, 'YXZ'); - - if (sceneEl.isMobile) { - // On mobile, do camera rotation with touch events and sensors. - rotation.x = radToDeg(hmdEuler.x) + radToDeg(pitchObject.rotation.x); - rotation.y = radToDeg(hmdEuler.y) + radToDeg(yawObject.rotation.y); - rotation.z = radToDeg(hmdEuler.z); - } else if (!sceneEl.is('vr-mode') || isNullVector(hmdEuler) || !this.data.hmdEnabled) { - // Mouse drag if WebVR not active (not connected, no incoming sensor data). - currentRotation = this.el.getAttribute('rotation'); - this.calculateDeltaRotation(); - if (this.data.reverseMouseDrag) { - rotation.x = currentRotation.x - deltaRotation.x; - rotation.y = currentRotation.y - deltaRotation.y; - rotation.z = currentRotation.z; - } else { - rotation.x = currentRotation.x + deltaRotation.x; - rotation.y = currentRotation.y + deltaRotation.y; - rotation.z = currentRotation.z; - } - } else { - // Mouse rotation ignored with an active headset. Use headset rotation. - rotation.x = radToDeg(hmdEuler.x); - rotation.y = radToDeg(hmdEuler.y); - rotation.z = radToDeg(hmdEuler.z); - } + // In VR mode THREE is in charge of updating the camera rotation. + if (sceneEl.is('vr-mode')) { return; } - this.el.setAttribute('rotation', rotation); - }, + // Calculate polyfilled HMD quaternion. + this.polyfillControls.update(); + hmdEuler.setFromQuaternion(this.polyfillObject.quaternion, 'YXZ'); + // On mobile, do camera rotation with touch events and sensors. + rotation.x = radToDeg(hmdEuler.x) + radToDeg(pitchObject.rotation.x); + rotation.y = radToDeg(hmdEuler.y) + radToDeg(yawObject.rotation.y); + rotation.z = 0; - /** - * Calculate delta rotation for mouse-drag and touch-drag. - */ - calculateDeltaRotation: function () { - var currentRotationX = radToDeg(this.pitchObject.rotation.x); - var currentRotationY = radToDeg(this.yawObject.rotation.y); - this.deltaRotation.x = currentRotationX - (this.previousRotationX || 0); - this.deltaRotation.y = currentRotationY - (this.previousRotationY || 0); - // Store current rotation for next tick. - this.previousRotationX = currentRotationX; - this.previousRotationY = currentRotationY; - return this.deltaRotation; + this.el.setAttribute('rotation', rotation); }, /** - * Handle positional tracking. - */ + * Handle positional tracking. + */ updatePosition: function () { var el = this.el; var currentHMDPosition; @@ -238,7 +181,6 @@ module.exports.Component = registerComponent('look-controls', { // Calculate change in position. currentHMDPosition = this.calculateHMDPosition(); - currentPosition = el.getAttribute('position'); position.copy(currentPosition).sub(previousHMDPosition).add(currentHMDPosition); @@ -246,18 +188,30 @@ module.exports.Component = registerComponent('look-controls', { previousHMDPosition.copy(currentHMDPosition); }, - /** - * Get headset position from VRControls. - */ calculateHMDPosition: (function () { var position = new THREE.Vector3(); return function () { - this.dolly.updateMatrix(); - position.setFromMatrixPosition(this.dolly.matrix); + var object3D = this.el.object3D; + object3D.updateMatrix(); + position.setFromMatrixPosition(object3D.matrix); return position; }; })(), + /** + * Calculate delta rotation for mouse-drag and touch-drag. + */ + calculateDeltaRotation: function () { + var currentRotationX = radToDeg(this.pitchObject.rotation.x); + var currentRotationY = radToDeg(this.yawObject.rotation.y); + this.deltaRotation.x = currentRotationX - (this.previousRotationX || 0); + this.deltaRotation.y = currentRotationY - (this.previousRotationY || 0); + // Store current rotation for next tick. + this.previousRotationX = currentRotationX; + this.previousRotationY = currentRotationY; + return this.deltaRotation; + }, + /** * Translate mouse drag into rotation. * @@ -377,7 +331,3 @@ module.exports.Component = registerComponent('look-controls', { disableGrabCursor(); } }); - -function isNullVector (vector) { - return vector.x === 0 && vector.y === 0 && vector.z === 0; -} diff --git a/src/components/scene/screenshot.js b/src/components/scene/screenshot.js index b346ffc24b5..3e73e55f94a 100644 --- a/src/components/scene/screenshot.js +++ b/src/components/scene/screenshot.js @@ -61,6 +61,7 @@ module.exports.Component = registerComponent('screenshot', { function setup () { var gl = el.renderer.getContext(); + if (!gl) { return; } self.cubeMapSize = gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE); self.material = new THREE.RawShaderMaterial({ uniforms: {map: {type: 't', value: null}}, diff --git a/src/core/scene/a-scene.js b/src/core/scene/a-scene.js index b87f17b55d3..6ab0590bac5 100644 --- a/src/core/scene/a-scene.js +++ b/src/core/scene/a-scene.js @@ -21,7 +21,6 @@ var warn = utils.debug('core:a-scene:warn'); /** * Scene element, holds all entities. * - * @member {number} animationFrameID * @member {array} behaviors - Component instances that have registered themselves to be updated on every tick. * @member {object} camera - three.js Camera object. @@ -31,7 +30,6 @@ var warn = utils.debug('core:a-scene:warn'); * @member {object} object3D - Root three.js Scene object. * @member {object} renderer * @member {bool} renderStarted - * @member {object} effect - three.js VREffect * @member {object} systems - Registered instantiated systems. * @member {number} time */ @@ -167,17 +165,8 @@ module.exports.AScene = registerElement('a-scene', { */ detachedCallback: { value: function () { - var sceneIndex; - - if (this.effect && this.effect.cancelAnimationFrame) { - this.effect.cancelAnimationFrame(this.animationFrameID); - } else { - window.cancelAnimationFrame(this.animationFrameID); - } - this.animationFrameID = null; - // Remove from scene index. - sceneIndex = scenes.indexOf(this); + var sceneIndex = scenes.indexOf(this); scenes.splice(sceneIndex, 1); window.removeEventListener('vrdisplaypresentchange', this.onVRPresentChangeBound); @@ -242,24 +231,23 @@ module.exports.AScene = registerElement('a-scene', { enterVR: { value: function (fromExternal) { var self = this; - var effect = this.effect; - + var vrDisplay = utils.device.getVRDisplay(); + var vrManager = self.renderer.vr; // Don't enter VR if already in VR. if (this.is('vr-mode')) { return Promise.resolve('Already in VR.'); } - // Enter VR via WebVR API. - if (!fromExternal && (this.checkHeadsetConnected() || this.isMobile)) { - return effect && effect.requestPresent().then(enterVRSuccess, enterVRFailure) || Promise.reject(new Error('VREffect not initialized')); + if (!fromExternal && (self.checkHeadsetConnected() || self.isMobile)) { + vrManager.setDevice(vrDisplay); + vrManager.enabled = true; + vrManager.setPoseTarget(this.camera.el.object3D); + return vrDisplay.requestPresent([{source: this.canvas}]).then(enterVRSuccess, enterVRFailure); } - - // Either entered VR already via WebVR API or VR not supported. enterVRSuccess(); return Promise.resolve(); function enterVRSuccess () { self.addState('vr-mode'); self.emit('enter-vr', {target: self}); - // Lock to landscape orientation on mobile. if (self.isMobile && screen.orientation && screen.orientation.lock) { screen.orientation.lock('landscape'); @@ -305,7 +293,7 @@ module.exports.AScene = registerElement('a-scene', { // Handle exiting VR if not yet already and in a headset or polyfill. if (!fromExternal && (this.checkHeadsetConnected() || this.isMobile)) { - return this.effect.exitPresent().then(exitVRSuccess, exitVRFailure); + this.renderer.vr.enabled = false; } // Handle exiting VR in all other cases (2D fullscreen, external exit VR event). @@ -325,14 +313,6 @@ module.exports.AScene = registerElement('a-scene', { if (self.isIOS) { utils.forceCanvasResizeSafariMobile(this.canvas); } self.emit('exit-vr', {target: self}); } - - function exitVRFailure (err) { - if (err && err.message) { - throw new Error('Failed to exit VR mode (`exitPresent`): ' + err.message); - } else { - throw new Error('Failed to exit VR mode (`exitPresent`).'); - } - } }, writable: window.debug }, @@ -453,18 +433,19 @@ module.exports.AScene = registerElement('a-scene', { }, resize: { - value: function () { + value: function (ll) { var camera = this.camera; var canvas = this.canvas; var embedded = this.getAttribute('embedded') && !this.is('vr-mode'); var size; - var isEffectPresenting = this.effect && this.effect.isPresenting; + var vrDevice = this.renderer.vr.getDevice(); + var isVRPresenting = this.renderer.vr.enabled && vrDevice && vrDevice.isPresenting; // Do not update renderer, if a camera or a canvas have not been injected. - // In VR mode, VREffect handles canvas resize based on the dimensions returned by + // In VR mode, three handles canvas resize based on the dimensions returned by // the getEyeParameters function of the WebVR API. These dimensions are independent of // the window size, therefore should not be overwritten with the window's width and height, // except when in fullscreen mode. - if (!camera || !canvas || (this.is('vr-mode') && (this.isMobile || isEffectPresenting))) { return; } + if (!camera || !canvas || (this.is('vr-mode') && (this.isMobile || isVRPresenting))) { return; } // Update camera. size = getCanvasSize(canvas, embedded); camera.aspect = size.width / size.height; @@ -478,7 +459,6 @@ module.exports.AScene = registerElement('a-scene', { setupRenderer: { value: function () { var renderer; - renderer = this.renderer = new THREE.WebGLRenderer({ canvas: this.canvas, antialias: shouldAntiAlias(this), @@ -486,8 +466,6 @@ module.exports.AScene = registerElement('a-scene', { }); renderer.setPixelRatio(window.devicePixelRatio); renderer.sortObjects = false; - this.effect = new THREE.VREffect(renderer); - this.effect.autoSubmitFrame = false; }, writable: window.debug }, @@ -610,18 +588,16 @@ module.exports.AScene = registerElement('a-scene', { */ render: { value: function () { - var effect = this.effect; var delta = this.clock.getDelta() * 1000; + var renderer = this.renderer; this.time = this.clock.elapsedTime * 1000; if (this.isPlaying) { this.tick(this.time, delta); } - this.animationFrameID = effect.requestAnimationFrame(this.render); - effect.render(this.object3D, this.camera, this.renderTarget); + renderer.animate(this.render); + renderer.render(this.object3D, this.camera, this.renderTarget); if (this.isPlaying) { this.tock(this.time, delta); } - - this.effect.submitFrame(); }, writable: true } diff --git a/src/index.js b/src/index.js index 2c0250c826b..3330e08c6ec 100644 --- a/src/index.js +++ b/src/index.js @@ -83,7 +83,7 @@ require('./core/a-mixin'); require('./extras/components/'); require('./extras/primitives/'); -console.log('A-Frame Version: 0.7.0 (Date 2017-12-04, Commit #9687531)'); +console.log('A-Frame Version: 0.7.0 (Date 22-09-2017, Commit #75ac4d8)'); console.log('three Version:', pkg.dependencies['three']); console.log('WebVR Polyfill Version:', pkg.dependencies['webvr-polyfill']); diff --git a/src/lib/three.js b/src/lib/three.js index 25e33f03607..ba9bf203191 100644 --- a/src/lib/three.js +++ b/src/lib/three.js @@ -23,8 +23,6 @@ require('three/examples/js/loaders/GLTFLoader'); // THREE.GLTFLoader require('three/examples/js/loaders/OBJLoader'); // THREE.OBJLoader require('three/examples/js/loaders/MTLLoader'); // THREE.MTLLoader require('three/examples/js/loaders/ColladaLoader'); // THREE.ColladaLoader -require('../../vendor/VRControls'); // THREE.VRControls -require('../../vendor/VREffect'); // THREE.VREffect THREE.ColladaLoader.prototype.crossOrigin = 'anonymous'; THREE.GLTFLoader.prototype.crossOrigin = 'anonymous'; diff --git a/src/utils/device.js b/src/utils/device.js index 3cdff7c15b7..7df3dcc12be 100644 --- a/src/utils/device.js +++ b/src/utils/device.js @@ -1,31 +1,28 @@ -var THREE = require('../lib/three'); -var dolly = new THREE.Object3D(); -var controls = new THREE.VRControls(dolly); +var vrDisplay; +var polyfilledVRDisplay; +var POLYFILL_VRDISPLAY_ID = 'Cardboard VRDisplay (webvr-polyfill)'; + +if (navigator.getVRDisplays) { + navigator.getVRDisplays().then(function (displays) { + vrDisplay = displays.length && displays[0]; + polyfilledVRDisplay = vrDisplay.displayName === POLYFILL_VRDISPLAY_ID; + }); +} + +function getVRDisplay () { return vrDisplay; } +module.exports.getVRDisplay = getVRDisplay; /** - * Determine if a headset is connected by checking if the orientation is available. + * Determine if a headset is connected by checking if a vrDisplay is available. */ -function checkHeadsetConnected () { - var orientation; - var vrDisplay = controls.getVRDisplay(); - - // If `isConnected` is available, just use that. - if (vrDisplay && 'isConnected' in vrDisplay) { return vrDisplay.isConnected; } - - controls.update(); - orientation = dolly.quaternion; - if (orientation._x !== 0 || orientation._y !== 0 || orientation._z !== 0) { - return true; - } - return false; -} +function checkHeadsetConnected () { return !!getVRDisplay(); } module.exports.checkHeadsetConnected = checkHeadsetConnected; /** * Check for positional tracking. */ function checkHasPositionalTracking () { - var vrDisplay = controls.getVRDisplay(); + var vrDisplay = getVRDisplay(); if (isMobile() || isGearVR()) { return false; } return vrDisplay && vrDisplay.capabilities.hasPosition; } @@ -106,3 +103,26 @@ module.exports.isBrowserEnvironment = !!(!process || process.browser); * Check if running in node on the server. */ module.exports.isNodeEnvironment = !module.exports.isBrowserEnvironment; + +/** + * Update an Object3D pose if a polyfilled vrDisplay is present. + */ +module.exports.PolyfillControls = function PolyfillControls (object) { + var frameData; + if (window.VRFrameData) { frameData = new window.VRFrameData(); } + this.update = function () { + var pose; + if (!vrDisplay || !polyfilledVRDisplay) { return; } + vrDisplay.getFrameData(frameData); + pose = frameData.pose; + if (pose.orientation !== null) { + object.quaternion.fromArray(pose.orientation); + } + if (pose.position !== null) { + object.position.fromArray(pose.position); + } else { + object.position.set(0, 0, 0); + } + }; +}; + diff --git a/tests/__init.test.js b/tests/__init.test.js index 60d0f0e9c7f..8d13d7a7feb 100644 --- a/tests/__init.test.js +++ b/tests/__init.test.js @@ -28,6 +28,17 @@ setup(function () { this.sinon.stub(AScene.prototype, 'render'); this.sinon.stub(AScene.prototype, 'resize'); this.sinon.stub(AScene.prototype, 'setupRenderer'); + // Mock renderer. + AScene.prototype.renderer = { + vr: { + getDevice: function () { return {requestPresent: function () {}}; }, + setDevice: function () {}, + setPoseTarget: function () {}, + enabled: false + }, + getContext: function () { return undefined; }, + shadowMap: {} + }; }); teardown(function (done) { diff --git a/tests/components/camera.test.js b/tests/components/camera.test.js index 91547f79630..6b499761727 100644 --- a/tests/components/camera.test.js +++ b/tests/components/camera.test.js @@ -98,10 +98,11 @@ suite('camera', function () { var cameraEl = this.el; var sceneEl = cameraEl.sceneEl; cameraEl.components.camera.hasPositionalTracking = true; - sceneEl.emit('enter-vr'); cameraEl.setAttribute('position', {x: 6, y: 6, z: 6}); + sceneEl.emit('enter-vr'); + cameraEl.setAttribute('position', {x: 9, y: 9, z: 9}); sceneEl.emit('exit-vr'); - assert.shallowDeepEqual(cameraEl.getAttribute('position'), {x: 0, y: 1.6, z: 0}); + assert.shallowDeepEqual(cameraEl.getAttribute('position'), {x: 6, y: 6, z: 6}); }); test('does not restore camera pose without headset', function () { diff --git a/tests/components/look-controls.test.js b/tests/components/look-controls.test.js index 7caaeda7a4b..609a996c025 100644 --- a/tests/components/look-controls.test.js +++ b/tests/components/look-controls.test.js @@ -2,7 +2,6 @@ var CANVAS_GRAB_CLASS = 'a-grab-cursor'; var GRABBING_CLASS = 'a-grabbing'; -var DEFAULT_USER_HEIGHT = 1.6; suite('look-controls', function () { setup(function (done) { @@ -53,22 +52,4 @@ suite('look-controls', function () { window.dispatchEvent(new Event('mouseup')); }); }); - - suite('head height', function () { - test('returns head height from camera device', function () { - var el = this.sceneEl; - var cameraEl = el.camera.el; - var cameraHeight = 2.5; - var lookControls = el.camera.el.components['look-controls']; - cameraEl.setAttribute('camera', 'userHeight', cameraHeight); - assert.shallowDeepEqual(lookControls.getUserHeight(), cameraHeight); - }); - - test('returns default head height for poses where device does not provide offset', function () { - var el = this.sceneEl; - var lookControls = el.camera.el.components['look-controls']; - el.camera.el.removeAttribute('camera'); - assert.shallowDeepEqual(lookControls.getUserHeight(), DEFAULT_USER_HEIGHT); - }); - }); }); diff --git a/tests/components/scene/fog.test.js b/tests/components/scene/fog.test.js index b1ec08f4e76..f770c5621b0 100644 --- a/tests/components/scene/fog.test.js +++ b/tests/components/scene/fog.test.js @@ -9,7 +9,6 @@ suite('fog', function () { el.addEventListener('loaded', function () { self.updateMaterialsSpy = self.sinon.spy(el.systems.material, 'updateMaterials'); - // Stub scene load to avoid WebGL code. el.hasLoaded = true; el.setAttribute('fog', ''); diff --git a/tests/components/scene/vr-mode-ui.test.js b/tests/components/scene/vr-mode-ui.test.js index 0d744073717..8cefc6ff399 100644 --- a/tests/components/scene/vr-mode-ui.test.js +++ b/tests/components/scene/vr-mode-ui.test.js @@ -1,5 +1,6 @@ /* global assert, process, setup, suite, test */ var entityFactory = require('../../helpers').entityFactory; +var utils = require('index').utils; var UI_CLASSES = ['.a-orientation-modal', '.a-enter-vr-button']; @@ -7,12 +8,11 @@ suite('vr-mode-ui', function () { setup(function (done) { this.entityEl = entityFactory(); var el = this.el = this.entityEl.parentNode; - var resolvePromise = function () { return Promise.resolve(); }; - el.setAttribute('vr-mode-ui', ''); - el.effect = { - requestPresent: resolvePromise, - exitPresent: resolvePromise - }; + this.sinon.stub(utils.device, 'getVRDisplay').returns({ + requestPresent: function () { + return Promise.resolve(); + } + }); el.addEventListener('loaded', function () { done(); }); }); @@ -33,7 +33,8 @@ suite('vr-mode-ui', function () { test('hides on enter VR', function () { var scene = this.el; - scene.renderer = {}; + // mock camera + scene.camera = {el: {object3D: {}}}; scene.enterVR(); UI_CLASSES.forEach(function (uiClass) { assert.ok(scene.querySelector(uiClass).className.indexOf('a-hidden')); @@ -42,7 +43,8 @@ suite('vr-mode-ui', function () { test('shows on exit VR', function (done) { var scene = this.el; - scene.renderer = {}; + // mock camera + scene.camera = {el: {object3D: {}}}; scene.enterVR(); scene.exitVR(); diff --git a/tests/core/a-entity.test.js b/tests/core/a-entity.test.js index 09a6e5c6ec2..b0e25206f3d 100644 --- a/tests/core/a-entity.test.js +++ b/tests/core/a-entity.test.js @@ -1220,10 +1220,10 @@ suite('a-entity', function () { var sceneEl = el.sceneEl; var component; el.play(); - el.setAttribute('look-controls', ''); - component = el.components['look-controls']; + el.setAttribute('raycaster', ''); + component = el.components['raycaster']; assert.notEqual(sceneEl.behaviors.tick.indexOf(component), -1); - el.removeAttribute('look-controls'); + el.removeAttribute('raycaster'); assert.equal(sceneEl.behaviors.tick.indexOf(component), -1); }); diff --git a/tests/core/controls.test.js b/tests/core/controls.test.js index b648ee8d552..a53544bb861 100644 --- a/tests/core/controls.test.js +++ b/tests/core/controls.test.js @@ -1,7 +1,5 @@ /* global Event, assert, process, setup, suite, test */ var helpers = require('../helpers'); -var THREE = require('lib/three'); - var PI = Math.PI; suite('position controls on camera with WASD controls (integration unit test)', function () { @@ -129,191 +127,182 @@ suite('position controls on camera with WASD controls (integration unit test)', }); }); -suite('rotation controls on camera with VRControls (integration unit test)', function () { +suite('rotation controls on camera in VR mode', function () { setup(function (done) { - var el = helpers.entityFactory(); + var el = this.el = helpers.entityFactory(); + var sceneEl = el.parentNode; var self = this; - - function StubVRControls (dolly) { - var sceneEl = el.sceneEl; - self.dolly = dolly; - self.el = sceneEl.querySelector('[camera]'); - self.el.addEventListener('componentinitialized', function (evt) { - if (evt.detail.name !== 'look-controls') { return; } + this.position = [0, 0, 0]; + this.orientation = [0, 0, 0]; + sceneEl.addEventListener('camera-ready', function () { + sceneEl.renderer.vr.getCamera = function (obj) { + if (!this.enabled) { return; } + obj.position.fromArray(self.position); + obj.rotation.fromArray(self.orientation); + obj.updateMatrixWorld(); + }; + sceneEl.querySelector('[camera]').addEventListener('loaded', function () { + sceneEl.addState('vr-mode'); + el.sceneEl.renderer.vr.enabled = true; + sceneEl.render = function () { + sceneEl.renderer.vr.getCamera(sceneEl.camera.el.object3D); + }; done(); }); - } - - el.addEventListener('loaded', function () { - StubVRControls.prototype.update = function () { /* no-op */ }; - self.sinon.stub(THREE, 'VRControls', StubVRControls); }); }); - test('rotates camera around Y', function (done) { + test('rotates camera around Y', function () { var el = this.el; - el.sceneEl.addState('vr-mode'); - this.dolly.quaternion.setFromEuler(new THREE.Euler(0, PI, 0)); - el.sceneEl.tick(); - process.nextTick(function () { - var rotation = el.getAttribute('rotation'); - assert.equal(Math.round(rotation.x), 0); - assert.equal(Math.round(rotation.y), 180); - assert.equal(Math.round(rotation.z), 0); - done(); - }); + var cameraEl = el.sceneEl.querySelector('[camera]'); + this.orientation = [0, Math.PI, 0]; + el.sceneEl.render(); + var rotation = cameraEl.getAttribute('rotation'); + assert.equal(Math.round(rotation.x), 0); + assert.equal(Math.round(rotation.y), 180); + assert.equal(Math.round(rotation.z), 0); }); - test('rotates camera composing X and Y', function (done) { + test('rotates camera composing X and Y', function () { var el = this.el; - el.sceneEl.addState('vr-mode'); - this.dolly.quaternion.setFromEuler(new THREE.Euler(PI / 4, PI, 0)); - el.sceneEl.tick(); - process.nextTick(function () { - var rotation = el.getAttribute('rotation'); - assert.equal(Math.round(rotation.x), -45); - assert.equal(Math.round(rotation.y), 180); - assert.equal(Math.round(rotation.z), 0); - done(); - }); + var cameraEl = el.sceneEl.querySelector('[camera]'); + this.orientation = [PI / 4, PI, 0]; + el.sceneEl.render(); + var rotation = cameraEl.getAttribute('rotation'); + assert.equal(Math.round(rotation.x), 45); + assert.equal(Math.round(rotation.y), 180); + assert.equal(Math.round(rotation.z), 0); }); - test('rotates camera composing X and Y and Z', function (done) { + test('rotates camera composing X and Y and Z', function () { var el = this.el; - el.sceneEl.addState('vr-mode'); - this.dolly.quaternion.setFromEuler(new THREE.Euler(PI / 2, PI / 6, PI / 2)); - el.sceneEl.tick(); - process.nextTick(function () { - var rotation = el.getAttribute('rotation'); - assert.equal(Math.round(rotation.x), 60); - assert.equal(Math.round(rotation.y), 90); - assert.equal(Math.round(rotation.z), 180); - done(); - }); + var cameraEl = el.sceneEl.querySelector('[camera]'); + this.orientation = [PI / 4, PI / 6, PI / 2]; + el.sceneEl.render(); + var rotation = cameraEl.getAttribute('rotation'); + assert.equal(Math.round(rotation.x), 45); + assert.equal(Math.round(rotation.y), 30); + assert.equal(Math.round(rotation.z), 90); }); - test('replaces previous rotation', function (done) { + test('replaces previous rotation', function () { var el = this.el; - el.sceneEl.addState('vr-mode'); - el.setAttribute('rotation', {x: -10000, y: -10000, z: -10000}); - this.dolly.quaternion.setFromEuler(new THREE.Euler(PI / 2, PI / 6, PI / 2)); - el.sceneEl.tick(); - process.nextTick(function () { - var rotation = el.getAttribute('rotation'); - assert.equal(Math.round(rotation.x), 60); - assert.equal(Math.round(rotation.y), 90); - assert.equal(Math.round(rotation.z), 180); - done(); - }); + var cameraEl = el.sceneEl.querySelector('[camera]'); + this.orientation = [PI / 4, PI / 6, PI / 2]; + cameraEl.setAttribute('rotation', {x: -10000, y: -10000, z: -10000}); + el.sceneEl.render(); + var rotation = cameraEl.getAttribute('rotation'); + assert.equal(Math.round(rotation.x), 45); + assert.equal(Math.round(rotation.y), 30); + assert.equal(Math.round(rotation.z), 90); }); - test('does not rotate camera if not in VR', function (done) { + test('does not rotate camera if not in VR', function () { var el = this.el; - this.dolly.quaternion.setFromEuler(new THREE.Euler(0, PI, 0)); - el.sceneEl.tick(); - process.nextTick(function () { - var rotation = el.getAttribute('rotation'); - assert.equal(rotation.x, 0); - assert.equal(rotation.y, 0); - assert.equal(rotation.z, 0); - done(); - }); + var cameraEl = el.sceneEl.querySelector('[camera]'); + this.orientation = [PI / 4, PI / 6, PI / 2]; + cameraEl.setAttribute('rotation', {x: 0, y: 0, z: 0}); + el.sceneEl.renderer.vr.enabled = false; + el.sceneEl.render(); + var rotation = cameraEl.getAttribute('rotation'); + assert.equal(Math.round(rotation.x), 0); + assert.equal(Math.round(rotation.y), 0); + assert.equal(Math.round(rotation.z), 0); }); }); suite('rotation controls on camera with mouse drag (integration unit test)', function () { setup(function (done) { - var el = helpers.entityFactory(); + var el = this.el = helpers.entityFactory(); var self = this; - - function StubVRControls (dolly) { - self.dolly = dolly; - self.el = el.sceneEl.querySelector('[camera]'); // Default camera. - process.nextTick(function () { - done(); // Done once we get a grip on VRControls created through the default camera. + el.addEventListener('loaded', function () { + el.sceneEl.addEventListener('camera-ready', function () { + var cameraEl = self.cameraEl = el.sceneEl.querySelector('[camera]'); // Default camera. + cameraEl.addEventListener('loaded', function () { done(); }); }); - } - StubVRControls.prototype.update = function () { /* no-op */ }; - this.sinon.stub(THREE, 'VRControls', StubVRControls); + }); }); test('rotates camera on dragging mouse around X', function (done) { var el = this.el; + var self = this; var mousedownEvent = new Event('mousedown'); mousedownEvent.button = 0; el.sceneEl.canvas.dispatchEvent(mousedownEvent); - process.nextTick(function afterMousedown () { var mouseMoveEvent = new Event('mousemove'); mouseMoveEvent.movementX = 1000; - mouseMoveEvent.movementY = 1; + mouseMoveEvent.movementY = 0.1; mouseMoveEvent.screenX = 1000; mouseMoveEvent.screenY = 1000; window.dispatchEvent(mouseMoveEvent); process.nextTick(function afterMousemove () { - el.sceneEl.tick(); - process.nextTick(function doAssert () { - var rotation = el.getAttribute('rotation'); - assert.ok(Math.abs(Math.round(rotation.y)) > 0); - done(); - }); + var cameraEl = self.cameraEl; + var rotation; + cameraEl.components['look-controls'].updateOrientation(); + rotation = cameraEl.getAttribute('rotation'); + assert.equal(Math.floor(Math.abs(rotation.x)), 0); + assert.notEqual(Math.floor(Math.abs(rotation.y)), 0); + done(); }); }); }); test('rotates camera on dragging mouse along Y', function (done) { var el = this.el; + var self = this; var mousedownEvent = new Event('mousedown'); mousedownEvent.button = 0; el.sceneEl.canvas.dispatchEvent(mousedownEvent); - process.nextTick(function afterMousedown () { var mouseMoveEvent = new Event('mousemove'); - mouseMoveEvent.movementX = 1; + mouseMoveEvent.movementX = 0.1; mouseMoveEvent.movementY = 1000; mouseMoveEvent.screenX = 1000; mouseMoveEvent.screenY = 1000; window.dispatchEvent(mouseMoveEvent); process.nextTick(function afterMousemove () { - el.sceneEl.tick(); - process.nextTick(function doAssert () { - var rotation = el.getAttribute('rotation'); - assert.equal(Math.round(rotation.x), -90); - done(); - }); + var cameraEl = self.cameraEl; + var rotation; + cameraEl.components['look-controls'].updateOrientation(); + rotation = cameraEl.getAttribute('rotation'); + assert.equal(Math.floor(Math.abs(rotation.y)), 0); + assert.notEqual(Math.floor(Math.abs(rotation.x)), 0); + done(); }); }); }); test('rotates camera dragging mouse on already rotated camera', function (done) { var el = this.el; + var self = this; + this.cameraEl.setAttribute('rotation', '45 45 0'); var mousedownEvent = new Event('mousedown'); mousedownEvent.button = 0; el.sceneEl.canvas.dispatchEvent(mousedownEvent); - - el.setAttribute('rotation', {x: 0, y: -360, z: 0}); - process.nextTick(function afterMousedown () { var mouseMoveEvent = new Event('mousemove'); - mouseMoveEvent.movementX = 1000; - mouseMoveEvent.movementY = 1; + mouseMoveEvent.movementX = -1000; + mouseMoveEvent.movementY = -1000; mouseMoveEvent.screenX = 1000; mouseMoveEvent.screenY = 1000; window.dispatchEvent(mouseMoveEvent); process.nextTick(function afterMousemove () { - el.sceneEl.tick(); - process.nextTick(function doAssert () { - var rotation = el.getAttribute('rotation'); - assert.ok(rotation.y < -360, 'Drag applies delta to current rotation.'); - done(); - }); + var cameraEl = self.cameraEl; + var rotation; + cameraEl.components['look-controls'].updateOrientation(); + rotation = cameraEl.getAttribute('rotation'); + assert.ok(rotation.x > 45); + assert.ok(rotation.y > 45); + done(); }); }); }); test('does not rotate camera when dragging in VR with headset', function (done) { var el = this.el; - this.dolly.quaternion.setFromEuler(new THREE.Euler(0, PI, 0)); + var cameraEl = this.cameraEl; + cameraEl.setAttribute('rotation', '30 45 60'); el.sceneEl.addState('vr-mode'); el.sceneEl.canvas.dispatchEvent(new Event('mousedown')); @@ -325,14 +314,12 @@ suite('rotation controls on camera with mouse drag (integration unit test)', fun mouseMoveEvent.screenY = 1000; window.dispatchEvent(mouseMoveEvent); process.nextTick(function afterMousemove () { - el.sceneEl.tick(); - process.nextTick(function doAssert () { - var rotation = el.getAttribute('rotation'); - assert.equal(Math.round(rotation.x), 0); - assert.equal(Math.round(rotation.y), 180, 'Use VR controls rotation'); - assert.equal(Math.round(rotation.z), 0); - done(); - }); + var rotation = cameraEl.getAttribute('rotation'); + cameraEl.components['look-controls'].updateOrientation(); + assert.equal(Math.ceil(rotation.x), 30); + assert.equal(Math.ceil(rotation.y), 45); + assert.equal(Math.ceil(rotation.z), 60); + done(); }); }); }); @@ -340,24 +327,21 @@ suite('rotation controls on camera with mouse drag (integration unit test)', fun suite('rotation controls on camera with touch drag (integration unit test)', function () { setup(function (done) { - var el = helpers.entityFactory(); + var el = this.el = helpers.entityFactory(); var self = this; - - function StubVRControls (dolly) { - self.dolly = dolly; - self.el = el.sceneEl.querySelector('[camera]'); // Default camera. - process.nextTick(function () { - done(); // Done once we get a grip on VRControls created through the default camera. + el.addEventListener('loaded', function () { + el.sceneEl.addEventListener('camera-ready', function () { + var cameraEl = self.cameraEl = el.sceneEl.querySelector('[camera]'); // Default camera. + cameraEl.addEventListener('loaded', function () { done(); }); }); - } - StubVRControls.prototype.update = function () { /* no-op */ }; - this.sinon.stub(THREE, 'VRControls', StubVRControls); + }); }); test('rotates camera on touch dragging around X', function (done) { var canvas; var el = this.el; var sceneEl = el.sceneEl; + var cameraEl = this.cameraEl; var touchStartEvent; canvas = sceneEl.canvas; @@ -374,12 +358,11 @@ suite('rotation controls on camera with touch drag (integration unit test)', fun touchMoveEvent.touches = [{pageX: 500, pageY: 0}]; window.dispatchEvent(touchMoveEvent); process.nextTick(function afterTouchmove () { - sceneEl.tick(); - process.nextTick(function doAssert () { - var rotation = el.getAttribute('rotation'); - assert.ok(Math.abs(Math.round(rotation.y)) > 0); - done(); - }); + var rotation; + cameraEl.components['look-controls'].updateOrientation(); + rotation = cameraEl.getAttribute('rotation'); + assert.ok(Math.abs(Math.round(rotation.y)) > 0); + done(); }); }); }); @@ -388,6 +371,7 @@ suite('rotation controls on camera with touch drag (integration unit test)', fun var canvas; var el = this.el; var sceneEl = el.sceneEl; + var cameraEl = this.cameraEl; var touchStartEvent; canvas = sceneEl.canvas; @@ -404,13 +388,12 @@ suite('rotation controls on camera with touch drag (integration unit test)', fun touchMoveEvent.touches = [{pageX: 0, pageY: 500}]; window.dispatchEvent(touchMoveEvent); process.nextTick(function afterTouchmove () { - sceneEl.tick(); - process.nextTick(function doAssert () { - var rotation = el.getAttribute('rotation'); - assert.equal(Math.round(rotation.x), 0); - assert.equal(Math.round(rotation.y), 0); - done(); - }); + var rotation; + cameraEl.components['look-controls'].updateOrientation(); + rotation = cameraEl.getAttribute('rotation'); + assert.equal(Math.round(rotation.x), 0); + assert.equal(Math.round(rotation.y), 0); + done(); }); }); }); @@ -418,39 +401,43 @@ suite('rotation controls on camera with touch drag (integration unit test)', fun suite('position controls on camera with VRControls (integration unit test)', function () { setup(function (done) { - var el = helpers.entityFactory(); + var el = this.el = helpers.entityFactory(); + var sceneEl = el.parentNode; var self = this; - - function StubVRControls (dolly) { - self.dolly = dolly; - self.el = el.sceneEl.querySelector('[camera]'); // Default camera. - process.nextTick(function () { - done(); // Done once we get a grip on VRControls created through the default camera. + this.position = [0, 0, 0]; + this.orientation = [0, 0, 0]; + sceneEl.addEventListener('camera-ready', function () { + var cameraEl = self.cameraEl = sceneEl.querySelector('[camera]'); + sceneEl.renderer.vr.getCamera = function (obj) { + if (!this.enabled) { return; } + obj.position.fromArray(self.position); + obj.rotation.fromArray(self.orientation); + obj.updateMatrixWorld(); + }; + cameraEl.addEventListener('loaded', function () { + sceneEl.addState('vr-mode'); + el.sceneEl.renderer.vr.enabled = true; + sceneEl.render = function () { + sceneEl.renderer.vr.getCamera(cameraEl.object3D); + }; + done(); }); - } - StubVRControls.prototype.update = function () { /* no-op */ }; - this.sinon.stub(THREE, 'VRControls', StubVRControls); + }); }); - test('copies matrix to camera in VR mode', function (done) { + test('copies matrix to camera in VR mode', function () { var el = this.el; - var sceneEl = el.sceneEl; - var dolly = this.dolly; + var sceneEl = el.parentNode; + var cameraEl = this.cameraEl; sceneEl.addState('vr-mode'); - el.components.camera.hasPositionalTracking = true; - el.components.camera.removeHeightOffset(); - - process.nextTick(function () { - var position; - dolly.position.set(-1, 2, 3); - sceneEl.tick(); - - position = el.getAttribute('position'); - assert.equal(position.x, -1); - assert.equal(position.y, 2); - assert.equal(position.z, 3); - done(); - }); + cameraEl.components.camera.hasPositionalTracking = true; + cameraEl.components.camera.removeHeightOffset(); + this.position = [-1, 2, 3]; + el.sceneEl.render(); + var position = cameraEl.getAttribute('position'); + assert.equal(position.x, -1); + assert.equal(position.y, 2); + assert.equal(position.z, 3); }); }); diff --git a/tests/core/scene/a-scene.test.js b/tests/core/scene/a-scene.test.js index cb74822023f..1bf84610f69 100644 --- a/tests/core/scene/a-scene.test.js +++ b/tests/core/scene/a-scene.test.js @@ -7,6 +7,7 @@ var setupCanvas = require('core/scene/a-scene').setupCanvas; var systems = require('core/system').systems; var helpers = require('../../helpers'); +var utils = require('index').utils; /** * Tests in this suite should not involve WebGL contexts or renderer. @@ -20,6 +21,11 @@ suite('a-scene (without renderer)', function () { setup(function (done) { var el = this.el = document.createElement('a-scene'); el.addEventListener('nodeready', function () { done(); }); + this.sinon.stub(utils.device, 'getVRDisplay').returns({ + requestPresent: function () { + return Promise.resolve(); + } + }); document.body.appendChild(el); }); @@ -64,7 +70,6 @@ suite('a-scene (without renderer)', function () { var event; var sceneEl = this.el; var exitVRStub = this.sinon.stub(sceneEl, 'exitVR'); - sceneEl.effect = {requestPresent: function () { return Promise.resolve(); }}; event = new CustomEvent('vrdisplaydisconnect'); window.dispatchEvent(event); process.nextTick(function () { @@ -112,57 +117,61 @@ suite('a-scene (without renderer)', function () { // Stub canvas. sceneEl.canvas = document.createElement('canvas'); - // Stub requestPresent. - sceneEl.effect = {requestPresent: function () { return Promise.resolve(); }}; - this.requestSpy = this.sinon.spy(sceneEl.effect, 'requestPresent'); + // Stub renderer. + sceneEl.renderer = { + vr: { + getDevice: function () {}, + setDevice: function () {}, + setPoseTarget: function () {} + } + }; + + // mock camera + sceneEl.camera = {el: {object3D: {}}}; }); test('does not try to enter VR if already in VR', function (done) { var sceneEl = this.el; - var requestSpy = this.requestSpy; sceneEl.addState('vr-mode'); sceneEl.enterVR().then(function (val) { assert.equal(val, 'Already in VR.'); - assert.notOk(requestSpy.called); + assert.notOk(sceneEl.renderer.vr.enabled); done(); }); }); test('calls requestPresent if headset connected', function (done) { var sceneEl = this.el; - var requestSpy = this.requestSpy; this.sinon.stub(sceneEl, 'checkHeadsetConnected').returns(true); sceneEl.enterVR().then(function () { - assert.ok(requestSpy.called); + assert.ok(sceneEl.renderer.vr.enabled); done(); }); }); test('calls requestPresent on mobile', function (done) { var sceneEl = this.el; - var requestSpy = this.requestSpy; sceneEl.isMobile = true; sceneEl.enterVR().then(function () { - assert.ok(requestSpy.called); + assert.ok(sceneEl.renderer.vr.enabled); done(); }); }); test('does not call requestPresent if flat desktop', function (done) { var sceneEl = this.el; - var requestSpy = this.requestSpy; + this.sinon.stub(sceneEl, 'checkHeadsetConnected').returns(false); sceneEl.enterVR().then(function () { - assert.notOk(requestSpy.called); + assert.notOk(sceneEl.renderer.vr.enabled); done(); }); }); test('does not call requestPresent if flag passed', function (done) { var sceneEl = this.el; - var requestSpy = this.requestSpy; this.sinon.stub(sceneEl, 'checkHeadsetConnected').returns(true); sceneEl.enterVR(true).then(function () { - assert.notOk(requestSpy.called); + assert.notOk(sceneEl.renderer.vr.enabled); done(); }); }); @@ -195,6 +204,7 @@ suite('a-scene (without renderer)', function () { fullscreenSpy = this.sinon.spy(sceneEl.canvas, 'requestFullscreen'); } + this.sinon.stub(sceneEl, 'checkHeadsetConnected').returns(false); sceneEl.enterVR().then(function () { assert.ok(fullscreenSpy.called); done(); @@ -215,59 +225,60 @@ suite('a-scene (without renderer)', function () { // Stub canvas. sceneEl.canvas = document.createElement('canvas'); - // Stub exitPresent. - sceneEl.effect = {exitPresent: function () { return Promise.resolve(); }}; - this.exitSpy = this.sinon.spy(sceneEl.effect, 'exitPresent'); + // Stub renderer. + sceneEl.renderer = {vr: { + getDevice: function () {}, + setDevice: function () {}, + setPoseTarget: function () {} + }}; sceneEl.addState('vr-mode'); }); test('does not try to exit VR if not in VR', function (done) { var sceneEl = this.el; - var exitSpy = this.exitSpy; sceneEl.removeState('vr-mode'); sceneEl.exitVR().then(function (val) { assert.equal(val, 'Not in VR.'); - assert.notOk(exitSpy.called); done(); }); }); test('calls exitPresent if headset connected', function (done) { var sceneEl = this.el; - var exitSpy = this.exitSpy; this.sinon.stub(sceneEl, 'checkHeadsetConnected').returns(true); sceneEl.exitVR().then(function () { - assert.ok(exitSpy.called); + assert.notOk(sceneEl.renderer.vr.enabled); done(); }); }); test('calls exitPresent on mobile', function (done) { var sceneEl = this.el; - var exitSpy = this.exitSpy; sceneEl.isMobile = true; sceneEl.exitVR().then(function () { - assert.ok(exitSpy.called); + assert.notOk(sceneEl.renderer.vr.enabled); done(); }); }); - test('does not call exitPresent if flat desktop', function (done) { + test('does not call exitPresent on desktop without a headset', function (done) { var sceneEl = this.el; - var exitSpy = this.exitSpy; + sceneEl.renderer.vr.enabled = true; + sceneEl.isMobile = false; + this.sinon.stub(sceneEl, 'checkHeadsetConnected').returns(false); sceneEl.exitVR().then(function () { - assert.notOk(exitSpy.called); + assert.ok(sceneEl.renderer.vr.enabled); done(); }); }); test('does not call exitPresent if flag passed', function (done) { var sceneEl = this.el; - var exitSpy = this.exitSpy; + sceneEl.renderer.vr.enabled = true; this.sinon.stub(sceneEl, 'checkHeadsetConnected').returns(true); sceneEl.exitVR(true).then(function () { - assert.notOk(exitSpy.called); + assert.ok(sceneEl.renderer.vr.enabled); done(); }); }); @@ -373,25 +384,27 @@ suite('a-scene (without renderer)', function () { AScene.prototype.resize.restore(); sceneEl.camera = { updateProjectionMatrix: function () {} }; sceneEl.canvas = document.createElement('canvas'); - sceneEl.renderer = { setSize: function () {} }; - - setSizeSpy = this.sinon.spy(sceneEl.renderer, 'setSize'); + setSizeSpy = this.sinon.spy(); + + // Stub renderer. + sceneEl.renderer = { + vr: { + getDevice: function () { return {isPresenting: false}; }, + setDevice: function () {} + }, + setSize: setSizeSpy + }; }); test('resize renderer when not in vr mode', function () { sceneEl.resize(); - assert.ok(setSizeSpy.called); }); test('resize renderer when in vr mode in fullscreen presentation (desktop, no headset)', function () { - sceneEl.effect = { - isPresenting: false - }; + sceneEl.renderer.vr.enabled = true; sceneEl.addState('vr-mode'); - sceneEl.resize(); - assert.ok(setSizeSpy.called); }); @@ -405,11 +418,9 @@ suite('a-scene (without renderer)', function () { }); test('does not resize renderer when in vr mode and presenting in a headset', function () { - sceneEl.effect = { - isPresenting: true - }; + sceneEl.renderer.vr.getDevice = function () { return {isPresenting: true}; }; + sceneEl.renderer.vr.enabled = true; sceneEl.addState('vr-mode'); - sceneEl.resize(); assert.notOk(setSizeSpy.called); @@ -543,7 +554,6 @@ suite('a-scene (without renderer)', function () { var childEl; var componentUpdateStub = sinon.stub(); var sceneEl; - AFRAME.registerComponent('test', { schema: {componentProp: {default: 'foo'}}, update: componentUpdateStub diff --git a/tests/helpers.js b/tests/helpers.js index 979d3db9bc4..9f7bcec2f29 100644 --- a/tests/helpers.js +++ b/tests/helpers.js @@ -23,6 +23,7 @@ module.exports.entityFactory = function (opts) { var scene = document.createElement('a-scene'); var assets = document.createElement('a-assets'); var entity = document.createElement('a-entity'); + scene.appendChild(assets); scene.appendChild(entity); diff --git a/vendor/VRControls.js b/vendor/VRControls.js deleted file mode 100644 index 0ef95d06295..00000000000 --- a/vendor/VRControls.js +++ /dev/null @@ -1,176 +0,0 @@ -/** - * @author dmarcos / https://github.com/dmarcos - * @author mrdoob / http://mrdoob.com - */ - -THREE.VRControls = function ( object, onError ) { - - var scope = this; - - var vrDisplay, vrDisplays; - - var standingMatrix = new THREE.Matrix4(); - - var frameData = null; - - if ( 'VRFrameData' in window ) { - - frameData = new window.VRFrameData(); - - } - - window.addEventListener('vrdisplayconnect', function (evt) { vrDisplay = evt.display; }); - window.addEventListener('vrdisplaydisconnect', function () { vrDisplay = undefined }); - - function gotVRDisplays( displays ) { - - vrDisplays = displays; - - if ( displays.length > 0 ) { - - vrDisplay = displays[ 0 ]; - - } else { - - if ( onError ) onError( 'VR input not available.' ); - - } - - } - - if ( navigator.getVRDisplays ) { - - navigator.getVRDisplays().then( gotVRDisplays ).catch ( function () { - - console.warn( 'THREE.VRControls: Unable to get VR Displays' ); - - } ); - - } - - // the Rift SDK returns the position in meters - // this scale factor allows the user to define how meters - // are converted to scene units. - - this.scale = 1; - - // If true will use "standing space" coordinate system where y=0 is the - // floor and x=0, z=0 is the center of the room. - this.standing = false; - - // Distance from the users eyes to the floor in meters. Used when - // standing=true but the VRDisplay doesn't provide stageParameters. - this.userHeight = 1.6; - - this.getVRDisplay = function () { - - return vrDisplay; - - }; - - this.setVRDisplay = function ( value ) { - - vrDisplay = value; - - }; - - this.getVRDisplays = function () { - - console.warn( 'THREE.VRControls: getVRDisplays() is being deprecated.' ); - return vrDisplays; - - }; - - this.getStandingMatrix = function () { - - return standingMatrix; - - }; - - this.update = function () { - - if ( vrDisplay ) { - - var pose; - - if ( vrDisplay.getFrameData ) { - - vrDisplay.getFrameData( frameData ); - pose = frameData.pose; - - } else if ( vrDisplay.getPose ) { - - pose = vrDisplay.getPose(); - - } - - if ( pose.orientation !== null ) { - - object.quaternion.fromArray( pose.orientation ); - - } - - if ( pose.position !== null ) { - - object.position.fromArray( pose.position ); - - } else { - - object.position.set( 0, 0, 0 ); - - } - - if ( this.standing ) { - - if ( vrDisplay.stageParameters ) { - - object.updateMatrix(); - - standingMatrix.fromArray( vrDisplay.stageParameters.sittingToStandingTransform ); - object.applyMatrix( standingMatrix ); - - } else { - - object.position.setY( object.position.y + this.userHeight ); - - } - - } - - object.position.multiplyScalar( scope.scale ); - - } - - }; - - this.resetPose = function () { - - if ( vrDisplay ) { - - vrDisplay.resetPose(); - - } - - }; - - this.resetSensor = function () { - - console.warn( 'THREE.VRControls: .resetSensor() is now .resetPose().' ); - this.resetPose(); - - }; - - this.zeroSensor = function () { - - console.warn( 'THREE.VRControls: .zeroSensor() is now .resetPose().' ); - this.resetPose(); - - }; - - this.dispose = function () { - - vrDisplay = null; - - }; - -}; diff --git a/vendor/VREffect.js b/vendor/VREffect.js deleted file mode 100644 index 3daf9c7827b..00000000000 --- a/vendor/VREffect.js +++ /dev/null @@ -1,496 +0,0 @@ -/** - * @author dmarcos / https://github.com/dmarcos - * @author mrdoob / http://mrdoob.com - * - * WebVR Spec: http://mozvr.github.io/webvr-spec/webvr.html - * - * Firefox: http://mozvr.com/downloads/ - * Chromium: https://webvr.info/get-chrome - * - */ - -THREE.VREffect = function( renderer, onError ) { - - var vrDisplay, vrDisplays; - var eyeTranslationL = new THREE.Vector3(); - var eyeTranslationR = new THREE.Vector3(); - var renderRectL, renderRectR; - - var frameData = null; - - if ( 'VRFrameData' in window ) { - - frameData = new window.VRFrameData(); - - } - - window.addEventListener('vrdisplayconnect', function (evt) { vrDisplay = evt.display; }); - window.addEventListener('vrdisplaydisconnect', function (evt) { - var f; - - scope.exitPresent(); - // Cancels current request animation frame. - f = scope.cancelAnimationFrame(); - vrDisplay = undefined; - // Resumes the request animation frame. - scope.requestAnimationFrame(f); - }); - - function gotVRDisplays( displays ) { - - vrDisplays = displays; - - if ( displays.length > 0 ) { - - vrDisplay = displays[ 0 ]; - - } else { - - if ( onError ) onError( 'HMD not available' ); - - } - - } - - if ( navigator.getVRDisplays ) { - - navigator.getVRDisplays().then( gotVRDisplays ).catch( function() { - - console.warn( 'THREE.VREffect: Unable to get VR Displays' ); - - } ); - - } - - // - - this.isPresenting = false; - - var scope = this; - - var rendererSize = renderer.getSize(); - var rendererUpdateStyle = false; - var rendererPixelRatio = renderer.getPixelRatio(); - - this.getVRDisplay = function() { - - return vrDisplay; - - }; - - this.setVRDisplay = function( value ) { - - vrDisplay = value; - - }; - - this.getVRDisplays = function() { - - console.warn( 'THREE.VREffect: getVRDisplays() is being deprecated.' ); - return vrDisplays; - - }; - - this.setSize = function( width, height, updateStyle ) { - - rendererSize = { width: width, height: height }; - rendererUpdateStyle = updateStyle; - - if ( scope.isPresenting ) { - - var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); - renderer.setPixelRatio( 1 ); - renderer.setSize( eyeParamsL.renderWidth * 2, eyeParamsL.renderHeight, false ); - - } else { - - renderer.setPixelRatio( rendererPixelRatio ); - renderer.setSize( width, height, updateStyle ); - - } - - }; - - // VR presentation - - var canvas = renderer.domElement; - var defaultLeftBounds = [ 0.0, 0.0, 0.5, 1.0 ]; - var defaultRightBounds = [ 0.5, 0.0, 0.5, 1.0 ]; - - function onVRDisplayPresentChange() { - - var wasPresenting = scope.isPresenting; - scope.isPresenting = vrDisplay !== undefined && vrDisplay.isPresenting; - - if ( scope.isPresenting ) { - - var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); - var eyeWidth = eyeParamsL.renderWidth; - var eyeHeight = eyeParamsL.renderHeight; - - if ( ! wasPresenting ) { - - rendererPixelRatio = renderer.getPixelRatio(); - rendererSize = renderer.getSize(); - - renderer.setPixelRatio( 1 ); - renderer.setSize( eyeWidth * 2, eyeHeight, false ); - - } - - } else if ( wasPresenting ) { - - renderer.setPixelRatio( rendererPixelRatio ); - renderer.setSize( rendererSize.width, rendererSize.height, rendererUpdateStyle ); - - } - - } - - window.addEventListener( 'vrdisplaypresentchange', onVRDisplayPresentChange, false ); - - this.setFullScreen = function( boolean ) { - - return new Promise( function( resolve, reject ) { - - if ( vrDisplay === undefined ) { - - reject( new Error( 'No VR hardware found.' ) ); - return; - - } - - if ( scope.isPresenting === boolean ) { - - resolve(); - return; - - } - - if ( boolean ) { - - resolve( vrDisplay.requestPresent( [ { source: canvas } ] ) ); - - } else { - - resolve( vrDisplay.exitPresent() ); - - } - - } ); - - }; - - this.requestPresent = function() { - - return this.setFullScreen( true ); - - }; - - this.exitPresent = function() { - - return this.setFullScreen( false ); - - }; - - this.requestAnimationFrame = function( f ) { - - var f = scope.f = f || scope.f; - - if (!f) { return; } - - if ( vrDisplay !== undefined ) { - return vrDisplay.requestAnimationFrame( f ); - } else { - - return window.requestAnimationFrame( f ); - - } - - }; - - this.cancelAnimationFrame = function( h ) { - - var f = scope.f; - - scope.f = undefined; - - if ( vrDisplay !== undefined ) { - - vrDisplay.cancelAnimationFrame( h ); - - } else { - - window.cancelAnimationFrame( h ); - - } - - return f; - }; - - this.submitFrame = function() { - - if ( vrDisplay !== undefined && scope.isPresenting ) { - - vrDisplay.submitFrame(); - - } - - }; - - this.autoSubmitFrame = true; - - // render - - var cameraL = new THREE.PerspectiveCamera(); - cameraL.layers.enable( 1 ); - - var cameraR = new THREE.PerspectiveCamera(); - cameraR.layers.enable( 2 ); - - this.render = function( scene, camera, renderTarget, forceClear ) { - - if ( vrDisplay && scope.isPresenting ) { - - var autoUpdate = scene.autoUpdate; - - if ( autoUpdate ) { - - scene.updateMatrixWorld(); - scene.autoUpdate = false; - - } - - var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); - var eyeParamsR = vrDisplay.getEyeParameters( 'right' ); - - eyeTranslationL.fromArray( eyeParamsL.offset ); - eyeTranslationR.fromArray( eyeParamsR.offset ); - - if ( Array.isArray( scene ) ) { - - console.warn( 'THREE.VREffect.render() no longer supports arrays. Use object.layers instead.' ); - scene = scene[ 0 ]; - - } - - // When rendering we don't care what the recommended size is, only what the actual size - // of the backbuffer is. - var size = renderer.getSize(); - var layers = vrDisplay.getLayers(); - var leftBounds; - var rightBounds; - - if ( layers.length ) { - - var layer = layers[ 0 ]; - - leftBounds = layer.leftBounds !== null && layer.leftBounds.length === 4 ? layer.leftBounds : defaultLeftBounds; - rightBounds = layer.rightBounds !== null && layer.rightBounds.length === 4 ? layer.rightBounds : defaultRightBounds; - - } else { - - leftBounds = defaultLeftBounds; - rightBounds = defaultRightBounds; - - } - - renderRectL = { - x: Math.round( size.width * leftBounds[ 0 ] ), - y: Math.round( size.height * leftBounds[ 1 ] ), - width: Math.round( size.width * leftBounds[ 2 ] ), - height: Math.round( size.height * leftBounds[ 3 ] ) - }; - renderRectR = { - x: Math.round( size.width * rightBounds[ 0 ] ), - y: Math.round( size.height * rightBounds[ 1 ] ), - width: Math.round( size.width * rightBounds[ 2 ] ), - height: Math.round( size.height * rightBounds[ 3 ] ) - }; - - if ( renderTarget ) { - - renderer.setRenderTarget( renderTarget ); - renderTarget.scissorTest = true; - - } else { - - renderer.setRenderTarget( null ); - renderer.setScissorTest( true ); - - } - - if ( renderer.autoClear || forceClear ) renderer.clear(); - - if ( camera.parent === null ) camera.updateMatrixWorld(); - - camera.matrixWorld.decompose( cameraL.position, cameraL.quaternion, cameraL.scale ); - - cameraR.position.copy(cameraL.position); - cameraR.quaternion.copy(cameraL.quaternion); - cameraR.scale.copy(cameraL.scale); - - cameraL.translateOnAxis( eyeTranslationL, cameraL.scale.x ); - cameraR.translateOnAxis( eyeTranslationR, cameraR.scale.x ); - - if ( vrDisplay.getFrameData ) { - - vrDisplay.depthNear = camera.near; - vrDisplay.depthFar = camera.far; - - vrDisplay.getFrameData( frameData ); - - cameraL.projectionMatrix.elements = frameData.leftProjectionMatrix; - cameraR.projectionMatrix.elements = frameData.rightProjectionMatrix; - - } else { - - cameraL.projectionMatrix = fovToProjection( eyeParamsL.fieldOfView, true, camera.near, camera.far ); - cameraR.projectionMatrix = fovToProjection( eyeParamsR.fieldOfView, true, camera.near, camera.far ); - - } - - // render left eye - if ( renderTarget ) { - - renderTarget.viewport.set( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); - renderTarget.scissor.set( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); - - } else { - - renderer.setViewport( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); - renderer.setScissor( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); - - } - renderer.render( scene, cameraL, renderTarget, forceClear ); - - // render right eye - if ( renderTarget ) { - - renderTarget.viewport.set( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); - renderTarget.scissor.set( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); - - } else { - - renderer.setViewport( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); - renderer.setScissor( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); - - } - renderer.render( scene, cameraR, renderTarget, forceClear ); - - if ( renderTarget ) { - - renderTarget.viewport.set( 0, 0, size.width, size.height ); - renderTarget.scissor.set( 0, 0, size.width, size.height ); - renderTarget.scissorTest = false; - renderer.setRenderTarget( null ); - - } else { - - renderer.setViewport( 0, 0, size.width, size.height ); - renderer.setScissorTest( false ); - - } - - if ( autoUpdate ) { - - scene.autoUpdate = true; - - } - - if ( scope.autoSubmitFrame ) { - - scope.submitFrame(); - - } - - return; - - } - - // Regular render mode if not HMD - - renderer.render( scene, camera, renderTarget, forceClear ); - - }; - - this.dispose = function() { - - window.removeEventListener( 'vrdisplaypresentchange', onVRDisplayPresentChange, false ); - - }; - - // - - function fovToNDCScaleOffset( fov ) { - - var pxscale = 2.0 / ( fov.leftTan + fov.rightTan ); - var pxoffset = ( fov.leftTan - fov.rightTan ) * pxscale * 0.5; - var pyscale = 2.0 / ( fov.upTan + fov.downTan ); - var pyoffset = ( fov.upTan - fov.downTan ) * pyscale * 0.5; - return { scale: [ pxscale, pyscale ], offset: [ pxoffset, pyoffset ] }; - - } - - function fovPortToProjection( fov, rightHanded, zNear, zFar ) { - - rightHanded = rightHanded === undefined ? true : rightHanded; - zNear = zNear === undefined ? 0.01 : zNear; - zFar = zFar === undefined ? 10000.0 : zFar; - - var handednessScale = rightHanded ? - 1.0 : 1.0; - - // start with an identity matrix - var mobj = new THREE.Matrix4(); - var m = mobj.elements; - - // and with scale/offset info for normalized device coords - var scaleAndOffset = fovToNDCScaleOffset( fov ); - - // X result, map clip edges to [-w,+w] - m[ 0 * 4 + 0 ] = scaleAndOffset.scale[ 0 ]; - m[ 0 * 4 + 1 ] = 0.0; - m[ 0 * 4 + 2 ] = scaleAndOffset.offset[ 0 ] * handednessScale; - m[ 0 * 4 + 3 ] = 0.0; - - // Y result, map clip edges to [-w,+w] - // Y offset is negated because this proj matrix transforms from world coords with Y=up, - // but the NDC scaling has Y=down (thanks D3D?) - m[ 1 * 4 + 0 ] = 0.0; - m[ 1 * 4 + 1 ] = scaleAndOffset.scale[ 1 ]; - m[ 1 * 4 + 2 ] = - scaleAndOffset.offset[ 1 ] * handednessScale; - m[ 1 * 4 + 3 ] = 0.0; - - // Z result (up to the app) - m[ 2 * 4 + 0 ] = 0.0; - m[ 2 * 4 + 1 ] = 0.0; - m[ 2 * 4 + 2 ] = zFar / ( zNear - zFar ) * - handednessScale; - m[ 2 * 4 + 3 ] = ( zFar * zNear ) / ( zNear - zFar ); - - // W result (= Z in) - m[ 3 * 4 + 0 ] = 0.0; - m[ 3 * 4 + 1 ] = 0.0; - m[ 3 * 4 + 2 ] = handednessScale; - m[ 3 * 4 + 3 ] = 0.0; - - mobj.transpose(); - return mobj; - - } - - function fovToProjection( fov, rightHanded, zNear, zFar ) { - - var DEG2RAD = Math.PI / 180.0; - - var fovPort = { - upTan: Math.tan( fov.upDegrees * DEG2RAD ), - downTan: Math.tan( fov.downDegrees * DEG2RAD ), - leftTan: Math.tan( fov.leftDegrees * DEG2RAD ), - rightTan: Math.tan( fov.rightDegrees * DEG2RAD ) - }; - - return fovPortToProjection( fovPort, rightHanded, zNear, zFar ); - - } - -};