From d6abbea01c3171c2172cb98ea0257920cf575a7b Mon Sep 17 00:00:00 2001 From: Maksims Mihejevs Date: Sun, 19 Jan 2020 19:09:50 +0200 Subject: [PATCH 01/20] origin/webxr --- build/dependencies.txt | 3 +- src/framework/application.js | 53 +-- src/framework/components/camera/component.js | 141 +------- src/graphics/device.js | 3 +- src/scene/forward-renderer.js | 156 ++------- src/vr/vr-display.js | 328 ------------------- src/vr/vr-manager.js | 186 ----------- src/xr/xr-manager.js | 272 +++++++++++++++ 8 files changed, 331 insertions(+), 811 deletions(-) delete mode 100644 src/vr/vr-display.js delete mode 100644 src/vr/vr-manager.js create mode 100644 src/xr/xr-manager.js diff --git a/build/dependencies.txt b/build/dependencies.txt index 4201f2cbe54..69c49c7e926 100644 --- a/build/dependencies.txt +++ b/build/dependencies.txt @@ -109,8 +109,7 @@ ../src/input/touch.js ../src/input/controller.js ../src/input/element-input.js -../src/vr/vr-manager.js -../src/vr/vr-display.js +../src/xr/xr-manager.js ../src/net/http.js ../src/script/script-registry.js ../src/script/script.js diff --git a/src/framework/application.js b/src/framework/application.js index 00e8ab90352..12c1791e9e8 100644 --- a/src/framework/application.js +++ b/src/framework/application.js @@ -257,6 +257,8 @@ Object.assign(pc, function () { // for compatibility this.context = this; + options.graphicsDeviceOptions.xrCompatible = true; + this.graphicsDevice = new pc.GraphicsDevice(canvas, options.graphicsDeviceOptions); this.stats = new pc.ApplicationStats(this.graphicsDevice); this._audioManager = new pc.SoundManager(options); @@ -532,7 +534,7 @@ Object.assign(pc, function () { if (this.elementInput) this.elementInput.app = this; - this.vr = null; + this.xr = new pc.XrManager(this); this._inTools = false; @@ -1165,8 +1167,6 @@ Object.assign(pc, function () { this.graphicsDevice.updateClientRect(); - if (this.vr) this.vr.poll(); - // #ifdef PROFILER this.stats.frame.updateStart = pc.now(); // #endif @@ -1572,29 +1572,6 @@ Object.assign(pc, function () { } }, - /** - * @function - * @name pc.Application#enableVr - * @description Create and assign a {@link pc.VrManager} object to allow this application render in VR. - */ - enableVr: function () { - if (!this.vr) { - this.vr = new pc.VrManager(this); - } - }, - - /** - * @function - * @name pc.Application#disableVr - * @description Destroy the {@link pc.VrManager} - */ - disableVr: function () { - if (this.vr) { - this.vr.destroy(); - this.vr = null; - } - }, - _onSkyboxChange: function (asset) { this.scene.setSkybox(asset.resources); }, @@ -1747,10 +1724,8 @@ Object.assign(pc, function () { pc.destroyPostEffectQuad(); - if (this.vr) { - this.vr.destroy(); - this.vr = null; - } + // TODO + // destroy XR this.graphicsDevice.destroy(); this.graphicsDevice = null; @@ -1796,7 +1771,7 @@ Object.assign(pc, function () { // create tick function to be wrapped in closure var makeTick = function (_app) { var app = _app; - return function (timestamp) { + return function (timestamp, frame) { if (!app.graphicsDevice) { return; } @@ -1815,8 +1790,8 @@ Object.assign(pc, function () { app._time = now; // Submit a request to queue up a new animation frame immediately - if (app.vr && app.vr.display) { - app.vr.display.requestAnimationFrame(app.tick); + if (app.xr.session) { + app.xr.session.requestAnimationFrame(app.tick); } else { requestAnimationFrame(app.tick); } @@ -1829,6 +1804,14 @@ Object.assign(pc, function () { app._fillFrameStats(now, dt, ms); // #endif + if (frame) app.xr.calculateViews(frame); + + if (frame && app.xr.pose) { + app.graphicsDevice.defaultFramebuffer = frame.session.renderState.baseLayer.framebuffer; + } else { + app.graphicsDevice.defaultFramebuffer = null; + } + app.update(dt); if (app.autoRender || app.renderNextFrame) { @@ -1842,10 +1825,6 @@ Object.assign(pc, function () { app.fire("frameend", _frameEndData); app.fire("frameEnd", _frameEndData);// deprecated old event, remove when editor updated - - if (app.vr && app.vr.display && app.vr.display.presenting) { - app.vr.display.submitFrame(); - } }; }; diff --git a/src/framework/components/camera/component.js b/src/framework/components/camera/component.js index 3def3a99a86..74691020124 100644 --- a/src/framework/components/camera/component.js +++ b/src/framework/components/camera/component.js @@ -131,37 +131,6 @@ Object.assign(pc, function () { } }); - /** - * @name pc.CameraComponent#vrDisplay - * @type pc.VrDisplay - * @description The {@link pc.VrDisplay} that the camera is current displaying to. This is set automatically by calls to {@link pc.CameraComponent#enterVr} - * or {@link pc.CameraComponent#exitVr}. Setting this property to a display directly enables the camera to use the transformation information - * from a display without rendering stereo to it, e.g. for "magic window" style experiences. - * @example - * // enable magic window style interface - * var display = this.app.vr.display; - * if (display) { - * this.entity.camera.vrDisplay = display; - * } - * - * var camera = this.entity.camera; - * camera.enterVr(function (err) { - * if (err) { return; } - * var display = camera.vrDisplay; // access presenting pc.VrDisplay - * }); - */ - Object.defineProperty(CameraComponent.prototype, "vrDisplay", { - get: function () { - return this.data.camera.vrDisplay; - }, - set: function (value) { - this.data.camera.vrDisplay = value; - if (value) { - value._camera = this.data.camera; - } - } - }); - /** * @readonly * @name pc.CameraComponent#node @@ -449,112 +418,10 @@ Object.assign(pc, function () { this.data.isRendering = false; }, - /** - * @function - * @name pc.CameraComponent#enterVr - * @description Attempt to start presenting this camera to a {@link pc.VrDisplay}. - * @param {pc.callbacks.VrCamera} callback Function called once to indicate success of failure. The callback takes one argument (err). - * On success it returns null on failure it returns the error message. - * @example - * // On an entity with a camera component - * this.entity.camera.enterVr(function (err) { - * if (err) { - * console.error(err); - * return; - * } else { - * // in VR! - * } - * }); - *//** - * @function - * @name pc.CameraComponent#enterVr - * @variation 2 - * @description Attempt to start presenting this camera to a {@link pc.VrDisplay}. - * @param {pc.VrDisplay} display The VrDisplay to present. If not supplied this uses {@link pc.VrManager#display} as the default - * @param {pc.callbacks.VrCamera} callback Function called once to indicate success of failure. The callback takes one argument (err). - * On success it returns null on failure it returns the error message. - * @example - * // On an entity with a camera component - * this.entity.camera.enterVr(function (err) { - * if (err) { - * console.error(err); - * return; - * } else { - * // in VR! - * } - * }); - */ - enterVr: function (display, callback) { - if ((display instanceof Function) && !callback) { - callback = display; - display = null; - } - - if (!this.system.app.vr) { - callback("VrManager not created. Enable VR in project settings."); - return; - } - - if (!display) { - display = this.system.app.vr.display; - } - - if (display) { - var self = this; - if (display.capabilities.canPresent) { - // try and present - display.requestPresent(function (err) { - if (!err) { - self.vrDisplay = display; - // camera component uses internal 'before' event - // this means display nulled before anyone other - // code gets to update - self.vrDisplay.once('beforepresentchange', function (display) { - if (!display.presenting) { - self.vrDisplay = null; - } - }); - } - callback(err); - }); - } else { - // mono rendering - self.vrDisplay = display; - callback(); - } - } else { - callback("No pc.VrDisplay to present"); - } - }, - - /** - * @function - * @name pc.CameraComponent#exitVr - * @description Attempt to stop presenting this camera. - * @param {pc.callbacks.VrCamera} callback Function called once to indicate success of failure. The callback takes one argument (err). - * On success it returns null on failure it returns the error message. - * @example - * this.entity.camera.exitVr(function (err) { - * if (err) { - * console.error(err); - * } else { - * - * } - * }); - */ - exitVr: function (callback) { - if (this.vrDisplay) { - if (this.vrDisplay.capabilities.canPresent) { - var display = this.vrDisplay; - this.vrDisplay = null; - display.exitPresent(callback); - } else { - this.vrDisplay = null; - callback(); - } - } else { - callback("Not presenting VR"); - } + // TODO + // better API + enterXr: function() { + this.camera.xr = this.system.app.xr; } }); diff --git a/src/graphics/device.js b/src/graphics/device.js index ac3ad67dde9..997c9b222dc 100644 --- a/src/graphics/device.js +++ b/src/graphics/device.js @@ -180,6 +180,7 @@ Object.assign(pc, function () { this._enableAutoInstancing = false; this.autoInstancingMaxObjects = 16384; this.attributesInvalidated = true; + this.defaultFramebuffer = null; this.boundBuffer = null; this.boundElementBuffer = null; this.instancedAttribs = { }; @@ -1247,7 +1248,7 @@ Object.assign(pc, function () { this.setFramebuffer(target._glFrameBuffer); } } else { - this.setFramebuffer(null); + this.setFramebuffer(this.defaultFramebuffer); } }, diff --git a/src/scene/forward-renderer.js b/src/scene/forward-renderer.js index 2e8106451cf..763fcd8e72f 100644 --- a/src/scene/forward-renderer.js +++ b/src/scene/forward-renderer.js @@ -34,17 +34,7 @@ Object.assign(pc, function () { var viewProjMat = new pc.Mat4(); var projMat; - var viewInvL = new pc.Mat4(); - var viewInvR = new pc.Mat4(); - var viewL = new pc.Mat4(); - var viewR = new pc.Mat4(); - var viewPosL = new pc.Vec3(); - var viewPosR = new pc.Vec3(); - var projL, projR; - var viewMat3L = new pc.Mat4(); - var viewMat3R = new pc.Mat4(); - var viewProjMatL = new pc.Mat4(); - var viewProjMatR = new pc.Mat4(); + var viewPos = new pc.Vec3(); var worldMatX = new pc.Vec3(); var worldMatY = new pc.Vec3(); @@ -544,20 +534,6 @@ Object.assign(pc, function () { }, updateCameraFrustum: function (camera) { - if (camera.vrDisplay && camera.vrDisplay.presenting) { - projMat = camera.vrDisplay.combinedProj; - var parent = camera._node.parent; - if (parent) { - viewMat.copy(parent.getWorldTransform()).mul(camera.vrDisplay.combinedViewInv).invert(); - } else { - viewMat.copy(camera.vrDisplay.combinedView); - } - viewInvMat.copy(viewMat).invert(); - this.viewInvId.setValue(viewInvMat.data); - camera.frustum.update(projMat, viewMat); - return; - } - projMat = camera.getProjectionMatrix(); if (camera.overrideCalculateProjection) camera.calculateProjection(projMat, pc.VIEW_CENTER); @@ -576,8 +552,19 @@ Object.assign(pc, function () { // make sure colorWrite is set to true to all channels, if you want to fully clear the target setCamera: function (camera, target, clear, cullBorder) { - var vrDisplay = camera.vrDisplay; - if (!vrDisplay || !vrDisplay.presenting) { + if (camera.xr && camera.xr.session) { + // TODO + // respect parent transform + + camera._node.setLocalPosition(camera.xr.position); + camera._node.setLocalRotation(camera.xr.rotation); + + camera.nearClip = camera.xr.session.renderState.depthNear; + camera.farClip = camera.xr.session.renderState.depthFar; + + // TODO + // calculate frustum culling + } else { // Projection Matrix projMat = camera.getProjectionMatrix(); if (camera.overrideCalculateProjection) camera.calculateProjection(projMat, pc.VIEW_CENTER); @@ -612,71 +599,6 @@ Object.assign(pc, function () { this.viewPos[2] = cameraPos.z; this.viewPosId.setValue(this.viewPos); - camera.frustum.update(projMat, viewMat); - } else { - // Projection LR - projL = vrDisplay.leftProj; - projR = vrDisplay.rightProj; - projMat = vrDisplay.combinedProj; - if (camera.overrideCalculateProjection) { - camera.calculateProjection(projL, pc.VIEW_LEFT); - camera.calculateProjection(projR, pc.VIEW_RIGHT); - camera.calculateProjection(projMat, pc.VIEW_CENTER); - } - - if (camera.overrideCalculateTransform) { - camera.calculateTransform(viewInvL, pc.VIEW_LEFT); - camera.calculateTransform(viewInvR, pc.VIEW_RIGHT); - camera.calculateTransform(viewInvMat, pc.VIEW_CENTER); - viewL.copy(viewInvL).invert(); - viewR.copy(viewInvR).invert(); - viewMat.copy(viewInvMat).invert(); - } else { - var parent = camera._node.parent; - if (parent) { - var transform = parent.getWorldTransform(); - - // ViewInverse LR (parent) - viewInvL.mul2(transform, vrDisplay.leftViewInv); - viewInvR.mul2(transform, vrDisplay.rightViewInv); - - // View LR (parent) - viewL.copy(viewInvL).invert(); - viewR.copy(viewInvR).invert(); - - // Combined view (parent) - viewMat.copy(parent.getWorldTransform()).mul(vrDisplay.combinedViewInv).invert(); - } else { - // ViewInverse LR - viewInvL.copy(vrDisplay.leftViewInv); - viewInvR.copy(vrDisplay.rightViewInv); - - // View LR - viewL.copy(vrDisplay.leftView); - viewR.copy(vrDisplay.rightView); - - // Combined view - viewMat.copy(vrDisplay.combinedView); - } - } - - // View 3x3 LR - mat3FromMat4(viewMat3L, viewL); - mat3FromMat4(viewMat3R, viewR); - - // ViewProjection LR - viewProjMatL.mul2(projL, viewL); - viewProjMatR.mul2(projR, viewR); - - // View Position LR - viewPosL.x = viewInvL.data[12]; - viewPosL.y = viewInvL.data[13]; - viewPosL.z = viewInvL.data[14]; - - viewPosR.x = viewInvR.data[12]; - viewPosR.y = viewInvR.data[13]; - viewPosR.z = viewInvR.data[14]; - camera.frustum.update(projMat, viewMat); } @@ -1510,7 +1432,6 @@ Object.assign(pc, function () { renderForward: function (camera, drawCalls, drawCallsCount, sortedLights, pass, cullingMask, drawCallback, layer) { var device = this.device; var scene = this.scene; - var vrDisplay = camera.vrDisplay; var passFlag = 1 << pass; var lightHash = layer ? layer._lightHash : 0; @@ -1695,34 +1616,29 @@ Object.assign(pc, function () { drawCallback(drawCall, i); } - if (vrDisplay && vrDisplay.presenting) { - // Left - device.setViewport(0, 0, halfWidth, device.height); - this.projId.setValue(projL.data); - this.viewInvId.setValue(viewInvL.data); - this.viewId.setValue(viewL.data); - this.viewId3.setValue(viewMat3L.data); - this.viewProjId.setValue(viewProjMatL.data); - this.viewPos[0] = viewPosL.x; - this.viewPos[1] = viewPosL.y; - this.viewPos[2] = viewPosL.z; - this.viewPosId.setValue(this.viewPos); - i += this.drawInstance(device, drawCall, mesh, style, true); - this._forwardDrawCalls++; + if (camera.xr && camera.xr.session && camera.xr.views.length) { + var views = camera.xr.views; - // Right - device.setViewport(halfWidth, 0, halfWidth, device.height); - this.projId.setValue(projR.data); - this.viewInvId.setValue(viewInvR.data); - this.viewId.setValue(viewR.data); - this.viewId3.setValue(viewMat3R.data); - this.viewProjId.setValue(viewProjMatR.data); - this.viewPos[0] = viewPosR.x; - this.viewPos[1] = viewPosR.y; - this.viewPos[2] = viewPosR.z; - this.viewPosId.setValue(this.viewPos); - i += this.drawInstance2(device, drawCall, mesh, style); - this._forwardDrawCalls++; + for(var v = 0; v < views.length; v++) { + var view = views[v]; + + device.setViewport(view.viewport.x, view.viewport.y, view.viewport.z, view.viewport.w); + + this.projId.setValue(view.projMat.data); + this.viewId.setValue(view.viewMat.data); + this.viewInvId.setValue(view.viewInvMat.data); + this.viewId3.setValue(view.viewMat3.data); + this.viewProjId.setValue(view.projViewMat.data); + this.viewPosId.setValue(view.position.data); + + if (v === 0) { + i += this.drawInstance(device, drawCall, mesh, style); + } else { + i += this.drawInstance2(device, drawCall, mesh, style); + } + + this._forwardDrawCalls++; + } } else { i += this.drawInstance(device, drawCall, mesh, style, true); this._forwardDrawCalls++; diff --git a/src/vr/vr-display.js b/src/vr/vr-display.js deleted file mode 100644 index f71ea822e74..00000000000 --- a/src/vr/vr-display.js +++ /dev/null @@ -1,328 +0,0 @@ -Object.assign(pc, function () { - /** - * @constructor - * @name pc.VrDisplay - * @extends pc.EventHandler - * @classdesc Represents a single Display for VR content. This could be a Head Mounted display that can present content on a separate screen - * or a phone which can display content full screen on the same screen. This object contains the native `navigator.VRDisplay` object - * from the WebVR API. - * @description Represents a single Display for VR content. This could be a Head Mounted display that can present content on a separate screen - * or a phone which can display content full screen on the same screen. This object contains the native `navigator.VRDisplay` object - * from the WebVR API. - * @param {pc.Application} app The application outputting to this VR display. - * @param {VRDisplay} display The native VRDisplay object from the WebVR API. - * @property {Number} id An identifier for this distinct VRDisplay - * @property {VRDisplay} display The native VRDisplay object from the WebVR API - * @property {Boolean} presenting True if this display is currently presenting VR content - * @property {VRDisplayCapabilities} capabilities Returns the VRDisplayCapabilities object from the VRDisplay. - * This can be used to determine what features are available on this display. - */ - var VrDisplay = function (app, display) { - pc.EventHandler.call(this); - - var self = this; - - this._app = app; - this._device = app.graphicsDevice; - - this.id = display.displayId; - - this._frameData = null; - if (window.VRFrameData) { - this._frameData = new window.VRFrameData(); - } - this.display = display; - - this._camera = null; // camera component - - this.sitToStandInv = new pc.Mat4(); - - this.leftView = new pc.Mat4(); - this.leftProj = new pc.Mat4(); - this.leftViewInv = new pc.Mat4(); - this.leftPos = new pc.Vec3(); - - this.rightView = new pc.Mat4(); - this.rightProj = new pc.Mat4(); - this.rightViewInv = new pc.Mat4(); - this.rightPos = new pc.Vec3(); - - this.combinedPos = new pc.Vec3(); - this.combinedView = new pc.Mat4(); - this.combinedProj = new pc.Mat4(); - this.combinedViewInv = new pc.Mat4(); - this.combinedFov = 0; - this.combinedAspect = 0; - - this.presenting = false; - - self._presentChange = function (event) { - var display; - // handle various events formats - if (event.display) { - // this is the official spec event format - display = event.display; - } else if (event.detail && event.detail.display) { - // webvr-polyfill uses this - display = event.detail.display; - } else if (event.detail && event.detail.vrdisplay) { - // this was used in the webvr emulation chrome extension - display = event.detail.vrdisplay; - } else { - // final catch all is to use this display as Firefox Nightly (54.0a1) - // does not include the display within the event data - display = self.display; - } - - // check if event refers to this display - if (display === self.display) { - self.presenting = (self.display && self.display.isPresenting); - - if (self.presenting) { - var leftEye = self.display.getEyeParameters("left"); - var rightEye = self.display.getEyeParameters("right"); - var w = Math.max(leftEye.renderWidth, rightEye.renderWidth) * 2; - var h = Math.max(leftEye.renderHeight, rightEye.renderHeight); - // set canvas resolution to the display resolution - self._app.graphicsDevice.setResolution(w, h); - // prevent window resizing from resizing it - self._app._allowResize = false; - } else { - // restore original resolution - self._app.setCanvasResolution(pc.RESOLUTION_AUTO); - self._app._allowResize = true; - } - - self.fire('beforepresentchange', self); // fire internal event for camera component - self.fire('presentchange', self); - } - }; - window.addEventListener('vrdisplaypresentchange', self._presentChange, false); - }; - VrDisplay.prototype = Object.create(pc.EventHandler.prototype); - VrDisplay.prototype.constructor = VrDisplay; - - Object.assign(VrDisplay.prototype, { - /** - * @function - * @name pc.VrDisplay#destroy - * @description Destroy this display object - */ - destroy: function () { - window.removeEventListener('vrdisplaypresentchange', self._presentChange); - if (this._camera) this._camera.vrDisplay = null; - this._camera = null; - }, - - /** - * @function - * @name pc.VrDisplay#poll - * @description Called once per frame to update the current status from the display. Usually called by {@link pc.VrManager}. - */ - poll: function () { - if (this.display) { - this.display.getFrameData(this._frameData); - - this.leftProj.data = this._frameData.leftProjectionMatrix; - this.rightProj.data = this._frameData.rightProjectionMatrix; - - var stage = this.display.stageParameters; - if (stage) { - - this.sitToStandInv.set(stage.sittingToStandingTransform).invert(); - - this.combinedView.set(this._frameData.leftViewMatrix); - this.leftView.mul2(this.combinedView, this.sitToStandInv); - - this.combinedView.set(this._frameData.rightViewMatrix); - this.rightView.mul2(this.combinedView, this.sitToStandInv); - } else { - - this.leftView.set(this._frameData.leftViewMatrix); - this.rightView.set(this._frameData.rightViewMatrix); - } - - // Find combined position and view matrix - // Camera is offset backwards to cover both frustums - - // Extract widest frustum plane and calculate fov - var nx = this.leftProj.data[3] + this.leftProj.data[0]; - var nz = this.leftProj.data[11] + this.leftProj.data[8]; - var l = 1.0 / Math.sqrt(nx * nx + nz * nz); - nx *= l; - nz *= l; - var maxFov = -Math.atan2(nz, nx); - - nx = this.rightProj.data[3] + this.rightProj.data[0]; - nz = this.rightProj.data[11] + this.rightProj.data[8]; - l = 1.0 / Math.sqrt(nx * nx + nz * nz); - nx *= l; - nz *= l; - maxFov = Math.max(maxFov, -Math.atan2(nz, nx)); - maxFov *= 2.0; - - this.combinedFov = maxFov; - - var aspect = this.rightProj.data[5] / this.rightProj.data[0]; - this.combinedAspect = aspect; - - var view = this.combinedView; - view.copy(this.leftView); - view.invert(); - this.leftViewInv.copy(view); - var pos = this.combinedPos; - pos.x = this.leftPos.x = view.data[12]; - pos.y = this.leftPos.y = view.data[13]; - pos.z = this.leftPos.z = view.data[14]; - view.copy(this.rightView); - view.invert(); - this.rightViewInv.copy(view); - var deltaX = pos.x - view.data[12]; - var deltaY = pos.y - view.data[13]; - var deltaZ = pos.z - view.data[14]; - var dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ); - this.rightPos.x = view.data[12]; - this.rightPos.y = view.data[13]; - this.rightPos.z = view.data[14]; - pos.x += view.data[12]; - pos.y += view.data[13]; - pos.z += view.data[14]; - pos.x *= 0.5; // middle pos - pos.y *= 0.5; - pos.z *= 0.5; - var b = Math.PI * 0.5; - var c = maxFov * 0.5; - var a = Math.PI - (b + c); - var offset = dist * 0.5 * ( Math.sin(a) );// / Math.sin(b) ); // equals 1 - var fwdX = view.data[8]; - var fwdY = view.data[9]; - var fwdZ = view.data[10]; - view.data[12] = pos.x + fwdX * offset; // our forward goes backwards so + instead of - - view.data[13] = pos.y + fwdY * offset; - view.data[14] = pos.z + fwdZ * offset; - this.combinedViewInv.copy(view); - view.invert(); - - // Find combined projection matrix - this.combinedProj.setPerspective(maxFov * pc.math.RAD_TO_DEG, - aspect, - this.display.depthNear + offset, - this.display.depthFar + offset, - true); - } - }, - - /** - * @function - * @name pc.VrDisplay#requestPresent - * @description Try to present full screen VR content on this display - * @param {pc.callbacks.VrDisplay} callback Called when the request is completed. Callback takes a single argument (err) that is the error message return - * if presenting fails, or null if the call succeeds. Usually called by {@link pc.CameraComponent#enterVr}. - */ - requestPresent: function (callback) { - if (!this.display) { - if (callback) callback(new Error("No VrDisplay to requestPresent")); - return; - } - - if (this.presenting) { - if (callback) callback(new Error("VrDisplay already presenting")); - return; - } - - this.display.requestPresent([{ source: this._device.canvas }]).then(function () { - if (callback) callback(); - }, function (err) { - if (callback) callback(err); - }); - }, - - /** - * @function - * @name pc.VrDisplay#exitPresent - * @description Try to stop presenting VR content on this display - * @param {pc.callbacks.VrDisplay} callback Called when the request is completed. Callback takes a single argument (err) that is the error message return - * if presenting fails, or null if the call succeeds. Usually called by {@link pc.CameraComponent#exitVr}. - */ - exitPresent: function (callback) { - if (!this.display) { - if (callback) callback(new Error("No VrDisplay to exitPresent")); - } - - if (!this.presenting) { - if (callback) callback(new Error("VrDisplay not presenting")); - return; - } - - this.display.exitPresent().then(function () { - if (callback) callback(); - }, function () { - if (callback) callback(new Error("exitPresent failed")); - }); - }, - - /** - * @function - * @name pc.VrDisplay#requestAnimationFrame - * @description Used in the main application loop instead of the regular `window.requestAnimationFrame`. Usually only called from inside {@link pc.Application} - * @param {pc.callbacks.VrFrame} fn Function called when it is time to update the frame. - */ - requestAnimationFrame: function (fn) { - if (this.display) this.display.requestAnimationFrame(fn); - }, - - /** - * @function - * @name pc.VrDisplay#submitFrame - * @description Called when animation update is complete and the frame is ready to be sent to the display. Usually only called from inside {@link pc.Application}. - */ - submitFrame: function () { - if (this.display) this.display.submitFrame(); - }, - - /** - * @function - * @name pc.VrDisplay#reset - * @description Called to reset the pose of the pc.VrDisplay. Treating its current pose as the origin/zero. This should only be called in 'sitting' experiences. - */ - reset: function () { - if (this.display) this.display.resetPose(); - }, - - /** - * @function - * @name pc.VrDisplay#setClipPlanes - * @description Set the near and far depth plans of the display. This enables mapping of values in the - * render target depth attachment to scene coordinates - * @param {Number} n The near depth distance - * @param {Number} f The far depth distance - */ - setClipPlanes: function (n, f) { - if (this.display) { - this.display.depthNear = n; - this.display.depthFar = f; - } - }, - - /** - * @function - * @name pc.VrDisplay#getFrameData - * @description Return the current frame data that is updated during polling. - * @returns {VRFrameData} The frame data object - */ - getFrameData: function () { - if (this.display) return this._frameData; - } - }); - - Object.defineProperty(VrDisplay.prototype, "capabilities", { - get: function () { - if (this.display) return this.display.capabilities; - return {}; - } - }); - - return { - VrDisplay: VrDisplay - }; -}()); diff --git a/src/vr/vr-manager.js b/src/vr/vr-manager.js deleted file mode 100644 index 8e4329fc116..00000000000 --- a/src/vr/vr-manager.js +++ /dev/null @@ -1,186 +0,0 @@ -Object.assign(pc, function () { - /** - * @constructor - * @name pc.VrManager - * @extends pc.EventHandler - * @classdesc Manage and update {@link pc.VrDisplay}s that are attached to this device. - * @description Manage and update {@link pc.VrDisplay}s that are attached to this device. - * @param {pc.Application} app The main application - * @property {pc.VrDisplay[]} displays The list of {@link pc.VrDisplay}s that are attached to this device - * @property {pc.VrDisplay} display The default {@link pc.VrDisplay} to be used. Usually the first in the `displays` list - * @property {Boolean} isSupported Reports whether this device supports the WebVR API - */ - var VrManager = function (app) { - pc.EventHandler.call(this); - - var self = this; - - this.isSupported = VrManager.isSupported; - - this._index = { }; - this.displays = []; - this.display = null; // primary display (usually the first in list) - - this._app = app; - - // bind functions for event callbacks - this._onDisplayConnect = this._onDisplayConnect.bind(this); - this._onDisplayDisconnect = this._onDisplayDisconnect.bind(this); - - self._attach(); - - this._getDisplays(function (err, displays) { - if (err) { - // webvr not available - self.fire('error', err); - } else { - for (var i = 0; i < displays.length; i++) { - self._addDisplay(displays[i]); - } - - self.fire('ready', self.displays); - } - }); - }; - VrManager.prototype = Object.create(pc.EventHandler.prototype); - VrManager.prototype.constructor = VrManager; - - /** - * @event - * @name pc.VrManager#displayconnect - * @description Fired when an VR display is connected - * @param {pc.VrDisplay} display The {@link pc.VrDisplay} that has just been connected - * @example - * this.app.vr.on("displayconnect", function (display) { - * // use `display` here - * }); - */ - - /** - * @event - * @name pc.VrManager#displaydisconnect - * @description Fired when an VR display is disconnected - * @param {pc.VrDisplay} display The {@link pc.VrDisplay} that has just been disconnected - * @example - * this.app.vr.on("displaydisconnect", function (display) { - * // `display` is no longer connected - * }); - */ - - /** - * @static - * @name pc.VrManager.isSupported - * @type Boolean - * @description Reports whether this device supports the WebVR API - */ - if (typeof navigator !== 'undefined') { - VrManager.isSupported = !!navigator.getVRDisplays; - } else { - VrManager.isSupported = false; - } - - Object.assign(VrManager.prototype, { - _attach: function () { - window.addEventListener('vrdisplayconnect', this._onDisplayConnect); - window.addEventListener('vrdisplaydisconnect', this._onDisplayDisconnect); - }, - - _detach: function () { - window.removeEventListener('vrdisplayconnect', this._onDisplayConnect); - window.removeEventListener('vrdisplaydisconnect', this._onDisplayDisconnect); - }, - - /** - * @function - * @name pc.VrManager#destroy - * @description Remove events and clear up manager - */ - destroy: function () { - this._detach(); - }, - - /** - * @function - * @name pc.VrManager#poll - * @description Called once per frame to poll all attached displays - */ - poll: function () { - var l = this.displays.length; - if (!l) return; - for (var i = 0; i < l; i++) { - if (this.displays[i]._camera) this.displays[i].poll(); - } - }, - - _getDisplays: function (callback) { - if (navigator.getVRDisplays) { - navigator.getVRDisplays().then(function (displays) { - if (callback) callback(null, displays); - }); - } else { - if (callback) callback(new Error('WebVR not supported')); - } - }, - - _addDisplay: function (vrDisplay) { - if (this._index[vrDisplay.displayId]) - return; - - var display = new pc.VrDisplay(this._app, vrDisplay); - this._index[display.id] = display; - this.displays.push(display); - - if (!this.display) - this.display = display; - - this.fire('displayconnect', display); - }, - - _onDisplayConnect: function (e) { - if (e.detail && e.detail.display) { - // polyfill has different event format - this._addDisplay(e.detail.display); - } else { - // real event API - this._addDisplay(e.display); - } - - }, - - _onDisplayDisconnect: function (e) { - var id; - if (e.detail && e.detail.display) { - // polyfill has different event format - id = e.detail.display.displayId; - } else { - // real event API - id = e.display.displayId; - } - - var display = this._index[id]; - if (!display) - return; - - display.destroy(); - - delete this._index[display.id]; - - var ind = this.displays.indexOf(display); - this.displays.splice(ind, 1); - - if (this.display === display) { - if (this.displays.length) { - this.display = this.displays[0]; - } else { - this.display = null; - } - } - - this.fire('displaydisconnect', display); - } - }); - - return { - VrManager: VrManager - }; -}()); diff --git a/src/xr/xr-manager.js b/src/xr/xr-manager.js new file mode 100644 index 00000000000..d4cc77e433c --- /dev/null +++ b/src/xr/xr-manager.js @@ -0,0 +1,272 @@ +Object.assign(pc, function () { + function mat3FromMat4(m3, m4) { + m3.data[0] = m4.data[0]; + m3.data[1] = m4.data[1]; + m3.data[2] = m4.data[2]; + + m3.data[3] = m4.data[4]; + m3.data[4] = m4.data[5]; + m3.data[5] = m4.data[6]; + + m3.data[6] = m4.data[8]; + m3.data[7] = m4.data[9]; + m3.data[8] = m4.data[10]; + } + + var XrManager = function (app) { + pc.EventHandler.call(this); + + var self = this; + + this.app = app; + + this._supported = !! navigator.xr; + this._available = false; + this._session = null; + this._baseLayer = null; + this._referenceSpace = null; + this._inputSources = [ ]; + + this._pose = null; + this.views = [ ]; + this.viewsPool = [ ]; + this.position = new pc.Vec3(); + this.rotation = new pc.Quat(); + + this.depthNear = 0.1; + this.depthFar = 1000; + + // TODO + // 1. HMD class with its params + // 2. Space class + // 3. Controllers class + + // TODO + // better APIs + + if (this._supported) { + navigator.xr.addEventListener('devicechange', function() { + self._deviceVailabilityCheck(); + }); + this._deviceVailabilityCheck(); + } + }; + XrManager.prototype = Object.create(pc.EventHandler.prototype); + XrManager.prototype.constructor = XrManager; + + XrManager.prototype._deviceVailabilityCheck = function() { + var self = this; + + navigator.xr.isSessionSupported('immersive-vr').then(function (available) { + if (self._available === available) + return; + + self._available = available; + self.fire('available', self._available); + }); + }; + + XrManager.prototype.sessionStart = function (callback) { + if (! this._available) + return callback(new Error('XR is not available')); + + if (this._session) + return callback(new Error('XR session is already started')); + + var self = this; + + // TODO + // makeXRCompatible + // scenario to test: + // 1. app is running on integrated GPU + // 2. XR device is connected, to another GPU + // 3. probably immersive-vr will fail to be created + // 4. call makeXRCompatible, very likely will lead to context loss + + navigator.xr.requestSession('immersive-vr').then(function (session) { + self._onSessionStart(session, callback); + }); + }; + + XrManager.prototype.sessionEnd = function () { + if (! this._session) + return; + + this._session.end(); + }; + + XrManager.prototype._onSessionStart = function(session, callback) { + var self = this; + + this._session = session; + + var onEnd = function () { + self._session = null; + self._referenceSpace = null; + self._inputSources = [ ]; + self._pose = null; + self.views = [ ]; + + session.removeEventListener('end', onEnd); + session.removeEventListener('visibilitychange', onVisibilityChange); + session.removeEventListener('inputsourceschange', onInputSourcesChange); + + self.fire('session:end', session); + }; + + var onVisibilityChange = function () { + self.fire('visibility:change', session.visibilityState); + }; + + var onInputSourcesChange = function (evt) { + var i; + + for(i = 0; i < evt.removed.length; i++) { + self._inputSourceRemove(evt.removed[i]); + } + for(i = 0; i < evt.added.length; i++) { + self._inputSourceAdd(evt.added[i]); + } + }; + + session.addEventListener('end', onEnd); + session.addEventListener('visibilitychange', onVisibilityChange); + session.addEventListener('inputsourceschange', onInputSourcesChange); + + this._baseLayer = new XRWebGLLayer(session, this.app.graphicsDevice.gl); + + session.updateRenderState({ + baseLayer: this._baseLayer + }); + + session.requestReferenceSpace('local').then(function(referenceSpace) { + self._referenceSpace = referenceSpace; + + if (callback) callback(null, session); + self.fire('session:start', session); + }); + }; + + XrManager.prototype._inputSourceAdd = function (inputSource) { + this._inputSources.push(inputSource); + this.fire('inputSource:add', inputSource); + }; + + XrManager.prototype._inputSourceRemove = function (inputSource) { + var ind = this._inputSources.indexOf(inputSource); + if (ind === -1) return; + this._inputSources.splice(ind, 1); + this.fire('inputSource:remove', inputSource); + }; + + XrManager.prototype.setClipPlanes = function(near, far) { + if (this.depthNear === near && this.depthFar === far) + return; + + this.depthNear = near; + this.depthFar = far; + + // TODO + // update clip planes + } + + XrManager.prototype.calculateViews = function (frame) { + if (! this._session) return; + + var i, view, viewRaw, layer, viewport, position, rotation; + + this._pose = frame.getViewerPose(this._referenceSpace); + + if (this._pose.views.length > this.views.length) { + for(i = 0; i <= (this._pose.views.length - this.views.length); i++) { + view = this.viewsPool.pop(); + if (! view) { + view = { + viewport: new pc.Vec4(), + projMat: new pc.Mat4(), + viewMat: new pc.Mat4(), + viewInvMat: new pc.Mat4(), + projViewMat: new pc.Mat4(), + viewMat3: new pc.Mat3(), + position: new pc.Vec3(), + rotation: new pc.Quat() + }; + } + + this.views.push(view); + } + } else if (this._pose.views.length <= this.views.length) { + for(i = 0; i < (this.views.length - this._pose.views.length); i++) { + this.viewsPool.push(this.views.pop()); + } + } + + this.position.set(0, 0, 0); + + layer = frame.session.renderState.baseLayer; + + for(i = 0; i < this._pose.views.length; i++) { + viewRaw = this._pose.views[i]; + view = this.views[i]; + viewport = layer.getViewport(viewRaw); + + view.viewport.x = viewport.x; + view.viewport.y = viewport.y; + view.viewport.z = viewport.width; + view.viewport.w = viewport.height; + + view.projMat.set(viewRaw.projectionMatrix); + view.viewMat.set(viewRaw.transform.inverse.matrix); + view.viewInvMat.set(viewRaw.transform.matrix); + view.projViewMat.mul2(view.projMat, view.viewMat); + mat3FromMat4(view.viewMat3, view.viewMat); + + position = viewRaw.transform.position; + view.position.set(position.x, position.y, position.z); + this.position.add(view.position); + + rotation = viewRaw.transform.orientation; + view.rotation.set(rotation.x, rotation.y, rotation.z, rotation.w); + this.rotation.copy(view.rotation); + } + + this.position.scale(1 / this._pose.views.length); + }; + + Object.defineProperty(XrManager.prototype, 'isSupported', { + get: function () { + return this._supported; + } + }); + + Object.defineProperty(XrManager.prototype, 'isAvailable', { + get: function () { + return this._available; + } + }); + + Object.defineProperty(XrManager.prototype, 'session', { + get: function () { + return this._session; + } + }); + + Object.defineProperty(XrManager.prototype, 'visibilityState', { + get: function () { + if (! this._session) + return null; + + return this._session.visibilityState; + } + }); + + Object.defineProperty(XrManager.prototype, 'inputSources', { + get: function () { + return this._inputSources; + } + }); + + return { + XrManager: XrManager + }; +}()); From e14f9b96761bd7000b4829a3375f9ca9a9b96d12 Mon Sep 17 00:00:00 2001 From: Maksims Mihejevs Date: Sun, 19 Jan 2020 19:15:13 +0200 Subject: [PATCH 02/20] lint fixes --- src/framework/components/camera/component.js | 2 +- src/scene/forward-renderer.js | 6 +-- src/xr/xr-manager.js | 56 ++++++++++---------- 3 files changed, 30 insertions(+), 34 deletions(-) diff --git a/src/framework/components/camera/component.js b/src/framework/components/camera/component.js index 74691020124..37b085ecb65 100644 --- a/src/framework/components/camera/component.js +++ b/src/framework/components/camera/component.js @@ -420,7 +420,7 @@ Object.assign(pc, function () { // TODO // better API - enterXr: function() { + enterXr: function () { this.camera.xr = this.system.app.xr; } }); diff --git a/src/scene/forward-renderer.js b/src/scene/forward-renderer.js index 763fcd8e72f..aac48c066d7 100644 --- a/src/scene/forward-renderer.js +++ b/src/scene/forward-renderer.js @@ -34,8 +34,6 @@ Object.assign(pc, function () { var viewProjMat = new pc.Mat4(); var projMat; - var viewPos = new pc.Vec3(); - var worldMatX = new pc.Vec3(); var worldMatY = new pc.Vec3(); var worldMatZ = new pc.Vec3(); @@ -1445,8 +1443,6 @@ Object.assign(pc, function () { var paramName, parameter, parameters; var stencilFront, stencilBack; - var halfWidth = device.width * 0.5; - // Render the scene for (i = 0; i < drawCallsCount; i++) { @@ -1619,7 +1615,7 @@ Object.assign(pc, function () { if (camera.xr && camera.xr.session && camera.xr.views.length) { var views = camera.xr.views; - for(var v = 0; v < views.length; v++) { + for (var v = 0; v < views.length; v++) { var view = views[v]; device.setViewport(view.viewport.x, view.viewport.y, view.viewport.z, view.viewport.w); diff --git a/src/xr/xr-manager.js b/src/xr/xr-manager.js index d4cc77e433c..697fa0eca0d 100644 --- a/src/xr/xr-manager.js +++ b/src/xr/xr-manager.js @@ -25,11 +25,11 @@ Object.assign(pc, function () { this._session = null; this._baseLayer = null; this._referenceSpace = null; - this._inputSources = [ ]; + this._inputSources = []; this._pose = null; - this.views = [ ]; - this.viewsPool = [ ]; + this.views = []; + this.viewsPool = []; this.position = new pc.Vec3(); this.rotation = new pc.Quat(); @@ -45,7 +45,7 @@ Object.assign(pc, function () { // better APIs if (this._supported) { - navigator.xr.addEventListener('devicechange', function() { + navigator.xr.addEventListener('devicechange', function () { self._deviceVailabilityCheck(); }); this._deviceVailabilityCheck(); @@ -54,7 +54,7 @@ Object.assign(pc, function () { XrManager.prototype = Object.create(pc.EventHandler.prototype); XrManager.prototype.constructor = XrManager; - XrManager.prototype._deviceVailabilityCheck = function() { + XrManager.prototype._deviceVailabilityCheck = function () { var self = this; navigator.xr.isSessionSupported('immersive-vr').then(function (available) { @@ -95,25 +95,11 @@ Object.assign(pc, function () { this._session.end(); }; - XrManager.prototype._onSessionStart = function(session, callback) { + XrManager.prototype._onSessionStart = function (session, callback) { var self = this; this._session = session; - var onEnd = function () { - self._session = null; - self._referenceSpace = null; - self._inputSources = [ ]; - self._pose = null; - self.views = [ ]; - - session.removeEventListener('end', onEnd); - session.removeEventListener('visibilitychange', onVisibilityChange); - session.removeEventListener('inputsourceschange', onInputSourcesChange); - - self.fire('session:end', session); - }; - var onVisibilityChange = function () { self.fire('visibility:change', session.visibilityState); }; @@ -121,14 +107,28 @@ Object.assign(pc, function () { var onInputSourcesChange = function (evt) { var i; - for(i = 0; i < evt.removed.length; i++) { + for (i = 0; i < evt.removed.length; i++) { self._inputSourceRemove(evt.removed[i]); } - for(i = 0; i < evt.added.length; i++) { + for (i = 0; i < evt.added.length; i++) { self._inputSourceAdd(evt.added[i]); } }; + var onEnd = function () { + self._session = null; + self._referenceSpace = null; + self._inputSources = []; + self._pose = null; + self.views = []; + + session.removeEventListener('end', onEnd); + session.removeEventListener('visibilitychange', onVisibilityChange); + session.removeEventListener('inputsourceschange', onInputSourcesChange); + + self.fire('session:end', session); + }; + session.addEventListener('end', onEnd); session.addEventListener('visibilitychange', onVisibilityChange); session.addEventListener('inputsourceschange', onInputSourcesChange); @@ -139,7 +139,7 @@ Object.assign(pc, function () { baseLayer: this._baseLayer }); - session.requestReferenceSpace('local').then(function(referenceSpace) { + session.requestReferenceSpace('local').then(function (referenceSpace) { self._referenceSpace = referenceSpace; if (callback) callback(null, session); @@ -159,7 +159,7 @@ Object.assign(pc, function () { this.fire('inputSource:remove', inputSource); }; - XrManager.prototype.setClipPlanes = function(near, far) { + XrManager.prototype.setClipPlanes = function (near, far) { if (this.depthNear === near && this.depthFar === far) return; @@ -168,7 +168,7 @@ Object.assign(pc, function () { // TODO // update clip planes - } + }; XrManager.prototype.calculateViews = function (frame) { if (! this._session) return; @@ -178,7 +178,7 @@ Object.assign(pc, function () { this._pose = frame.getViewerPose(this._referenceSpace); if (this._pose.views.length > this.views.length) { - for(i = 0; i <= (this._pose.views.length - this.views.length); i++) { + for (i = 0; i <= (this._pose.views.length - this.views.length); i++) { view = this.viewsPool.pop(); if (! view) { view = { @@ -196,7 +196,7 @@ Object.assign(pc, function () { this.views.push(view); } } else if (this._pose.views.length <= this.views.length) { - for(i = 0; i < (this.views.length - this._pose.views.length); i++) { + for (i = 0; i < (this.views.length - this._pose.views.length); i++) { this.viewsPool.push(this.views.pop()); } } @@ -205,7 +205,7 @@ Object.assign(pc, function () { layer = frame.session.renderState.baseLayer; - for(i = 0; i < this._pose.views.length; i++) { + for (i = 0; i < this._pose.views.length; i++) { viewRaw = this._pose.views[i]; view = this.views[i]; viewport = layer.getViewport(viewRaw); From 5b8d0d9db26d8784cb0efa214497dcdf54a8829a Mon Sep 17 00:00:00 2001 From: Maksims Mihejevs Date: Mon, 20 Jan 2020 18:12:27 +0200 Subject: [PATCH 03/20] respect parent transform, fix start/end session rendering --- src/framework/application.js | 19 ++++++++++------- src/scene/forward-renderer.js | 39 ++++++++++++++++++++++++++++++----- src/xr/xr-manager.js | 9 ++++++++ 3 files changed, 55 insertions(+), 12 deletions(-) diff --git a/src/framework/application.js b/src/framework/application.js index 12c1791e9e8..253546d008a 100644 --- a/src/framework/application.js +++ b/src/framework/application.js @@ -1771,13 +1771,19 @@ Object.assign(pc, function () { // create tick function to be wrapped in closure var makeTick = function (_app) { var app = _app; + var frameRequest; + return function (timestamp, frame) { - if (!app.graphicsDevice) { + if (!app.graphicsDevice) return; - } Application._currentApplication = app; + if (frameRequest) { + cancelAnimationFrame(frameRequest); + frameRequest = null; + } + // have current application pointer in pc pc.app = app; @@ -1791,14 +1797,13 @@ Object.assign(pc, function () { // Submit a request to queue up a new animation frame immediately if (app.xr.session) { - app.xr.session.requestAnimationFrame(app.tick); + frameRequest = app.xr.session.requestAnimationFrame(app.tick); } else { - requestAnimationFrame(app.tick); + frameRequest = requestAnimationFrame(app.tick); } - if (app.graphicsDevice.contextLost) { + if (app.graphicsDevice.contextLost) return; - } // #ifdef PROFILER app._fillFrameStats(now, dt, ms); @@ -1806,7 +1811,7 @@ Object.assign(pc, function () { if (frame) app.xr.calculateViews(frame); - if (frame && app.xr.pose) { + if (frame) { app.graphicsDevice.defaultFramebuffer = frame.session.renderState.baseLayer.framebuffer; } else { app.graphicsDevice.defaultFramebuffer = null; diff --git a/src/scene/forward-renderer.js b/src/scene/forward-renderer.js index aac48c066d7..afe17429418 100644 --- a/src/scene/forward-renderer.js +++ b/src/scene/forward-renderer.js @@ -560,6 +560,35 @@ Object.assign(pc, function () { camera.nearClip = camera.xr.session.renderState.depthNear; camera.farClip = camera.xr.session.renderState.depthFar; + var parent = camera._node.parent; + var transform; + if (parent) { + transform = parent.getWorldTransform(); + } + + var views = camera.xr.views; + + for (var v = 0; v < views.length; v++) { + var view = views[v]; + + if (parent) { + view.viewInvOffMat.mul2(transform, view.viewInvMat); + view.viewOffMat.copy(view.viewInvOffMat).invert(); + } else { + view.viewInvOffMat.copy(view.viewInvMat); + view.viewOffMat.copy(view.viewMat); + } + + mat3FromMat4(view.viewMat3Off, view.viewOffMat); + view.projViewOffMat.mul2(view.projMat, view.viewOffMat); + + view.positionOff.x = view.viewInvOffMat.data[12]; + view.positionOff.y = view.viewInvOffMat.data[13]; + view.positionOff.z = view.viewInvOffMat.data[14]; + + camera.frustum.update(view.projMat, view.viewOffMat); + } + // TODO // calculate frustum culling } else { @@ -1621,11 +1650,11 @@ Object.assign(pc, function () { device.setViewport(view.viewport.x, view.viewport.y, view.viewport.z, view.viewport.w); this.projId.setValue(view.projMat.data); - this.viewId.setValue(view.viewMat.data); - this.viewInvId.setValue(view.viewInvMat.data); - this.viewId3.setValue(view.viewMat3.data); - this.viewProjId.setValue(view.projViewMat.data); - this.viewPosId.setValue(view.position.data); + this.viewId.setValue(view.viewOffMat.data); + this.viewInvId.setValue(view.viewInvOffMat.data); + this.viewId3.setValue(view.viewMat3Off.data); + this.viewProjId.setValue(view.projViewOffMat.data); + this.viewPosId.setValue(view.positionOff.data); if (v === 0) { i += this.drawInstance(device, drawCall, mesh, style); diff --git a/src/xr/xr-manager.js b/src/xr/xr-manager.js index 697fa0eca0d..e838bf24515 100644 --- a/src/xr/xr-manager.js +++ b/src/xr/xr-manager.js @@ -126,6 +126,8 @@ Object.assign(pc, function () { session.removeEventListener('visibilitychange', onVisibilityChange); session.removeEventListener('inputsourceschange', onInputSourcesChange); + self.app.tick(); + self.fire('session:end', session); }; @@ -142,6 +144,8 @@ Object.assign(pc, function () { session.requestReferenceSpace('local').then(function (referenceSpace) { self._referenceSpace = referenceSpace; + self.app.tick(); + if (callback) callback(null, session); self.fire('session:start', session); }); @@ -185,10 +189,15 @@ Object.assign(pc, function () { viewport: new pc.Vec4(), projMat: new pc.Mat4(), viewMat: new pc.Mat4(), + viewOffMat: new pc.Mat4(), viewInvMat: new pc.Mat4(), + viewInvOffMat: new pc.Mat4(), projViewMat: new pc.Mat4(), + projViewOffMat: new pc.Mat4(), viewMat3: new pc.Mat3(), + viewMat3Off: new pc.Mat3(), position: new pc.Vec3(), + positionOff: new pc.Vec3(), rotation: new pc.Quat() }; } From 31054d6932e8c5b3697441d077d28eb1d5e01f10 Mon Sep 17 00:00:00 2001 From: Maksims Mihejevs Date: Sat, 25 Jan 2020 02:51:10 +0200 Subject: [PATCH 04/20] resize graphics device to respect XR session framebuffer --- src/framework/application.js | 5 +++-- src/scene/forward-renderer.js | 1 - src/xr/xr-manager.js | 15 ++++++++++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/framework/application.js b/src/framework/application.js index 253546d008a..0e68f668b1d 100644 --- a/src/framework/application.js +++ b/src/framework/application.js @@ -252,7 +252,6 @@ Object.assign(pc, function () { this._librariesLoaded = false; this._fillMode = pc.FILLMODE_KEEP_ASPECT; this._resolutionMode = pc.RESOLUTION_FIXED; - this._allowResize = true; // for compatibility this.context = this; @@ -1383,7 +1382,9 @@ Object.assign(pc, function () { * @returns {Object} A object containing the values calculated to use as width and height. */ resizeCanvas: function (width, height) { - if (!this._allowResize) return; // prevent resizing (e.g. if presenting in VR HMD) + // prevent resizing when in XR session + if (this.xr && this.xr.session) + return; var windowWidth = window.innerWidth; var windowHeight = window.innerHeight; diff --git a/src/scene/forward-renderer.js b/src/scene/forward-renderer.js index afe17429418..e3337634aab 100644 --- a/src/scene/forward-renderer.js +++ b/src/scene/forward-renderer.js @@ -2225,7 +2225,6 @@ Object.assign(pc, function () { } for (pass = 0; pass < passes; pass++) { - if (type === pc.LIGHTTYPE_POINT) { shadowCamNode.setRotation(pointLightRotations[pass]); shadowCam.renderTarget = light._shadowCubeMap[pass]; diff --git a/src/xr/xr-manager.js b/src/xr/xr-manager.js index e838bf24515..d87d355ab72 100644 --- a/src/xr/xr-manager.js +++ b/src/xr/xr-manager.js @@ -36,6 +36,9 @@ Object.assign(pc, function () { this.depthNear = 0.1; this.depthFar = 1000; + this._width = 0; + this._height = 0; + // TODO // 1. HMD class with its params // 2. Space class @@ -101,7 +104,7 @@ Object.assign(pc, function () { this._session = session; var onVisibilityChange = function () { - self.fire('visibility:change', session.visibilityState); + self.fire('visibility:change', session.visibilityState); }; var onInputSourcesChange = function (evt) { @@ -121,6 +124,8 @@ Object.assign(pc, function () { self._inputSources = []; self._pose = null; self.views = []; + self._width = 0; + self._height = 0; session.removeEventListener('end', onEnd); session.removeEventListener('visibilitychange', onVisibilityChange); @@ -179,6 +184,14 @@ Object.assign(pc, function () { var i, view, viewRaw, layer, viewport, position, rotation; + var width = frame.session.renderState.baseLayer.framebufferWidth; + var height = frame.session.renderState.baseLayer.framebufferHeight; + if (this._width !== width || this._height !== height) { + this._width = width; + this._height = height; + this.app.graphicsDevice.resizeCanvas(width, height); + } + this._pose = frame.getViewerPose(this._referenceSpace); if (this._pose.views.length > this.views.length) { From 5f8ff76fb16f204bf81be2ff59fc2494620efc20 Mon Sep 17 00:00:00 2001 From: Maksims Mihejevs Date: Sat, 25 Jan 2020 05:40:20 +0200 Subject: [PATCH 05/20] provide multiple XR session types; AR --- src/framework/components/camera/component.js | 54 ++++++- src/scene/forward-renderer.js | 4 - src/xr/xr-manager.js | 153 ++++++++++++++----- 3 files changed, 163 insertions(+), 48 deletions(-) diff --git a/src/framework/components/camera/component.js b/src/framework/components/camera/component.js index 37b085ecb65..dbfc1382c40 100644 --- a/src/framework/components/camera/component.js +++ b/src/framework/components/camera/component.js @@ -143,6 +143,18 @@ Object.assign(pc, function () { } }); + /** + * @readonly + * @name pc.CameraComponent#xr + * @type Boolean + * @description Queries if camera is in XR mode. + */ + Object.defineProperty(CameraComponent.prototype, "xr", { + get: function () { + return !! this.camera.xr; + } + }); + Object.assign(CameraComponent.prototype, { /** * @function @@ -418,10 +430,44 @@ Object.assign(pc, function () { this.data.isRendering = false; }, - // TODO - // better API - enterXr: function () { - this.camera.xr = this.system.app.xr; + /** + * @function + * @name pc.CameraComponent#startXr + * @description Attempt to start XR session with this camera + * @param {pc.callbacks.XrErrSession} callback - Function called once to indicate success or failure. The callback takes two arguments (err) and (session). + * @example + * // On an entity with a camera component + * this.entity.camera.startXr(function (err, session) { + * if (err) { + * console.error(err); + * } else { + * // in XR! + * } + * }); + */ + startXr: function (type, callback) { + this.system.app.xr.sessionStart(this.camera, type, callback); + }, + + /** + * @function + * @name pc.CameraComponent#endXr + * @description Attempt to end XR of this camera + * @param {pc.callbacks.XrSession} callback - Function called once session is ended. The callback takes one argument (old session). + * @example + * // On an entity with a camera component + * this.entity.camera.endXr(function (session) { + * // not anymore in XR! + * }); + */ + endXr: function(callback) { + if (! this.camera.xr || ! this.camera.xr.session) + return; + + if (callback) + this.camera.xr.once('session:end', callback); + + this.camera.xr.sessionEnd(); } }); diff --git a/src/scene/forward-renderer.js b/src/scene/forward-renderer.js index e3337634aab..3ad7bac713a 100644 --- a/src/scene/forward-renderer.js +++ b/src/scene/forward-renderer.js @@ -553,10 +553,6 @@ Object.assign(pc, function () { if (camera.xr && camera.xr.session) { // TODO // respect parent transform - - camera._node.setLocalPosition(camera.xr.position); - camera._node.setLocalRotation(camera.xr.rotation); - camera.nearClip = camera.xr.session.renderState.depthNear; camera.farClip = camera.xr.session.renderState.depthFar; diff --git a/src/xr/xr-manager.js b/src/xr/xr-manager.js index d87d355ab72..8ea5b7433ac 100644 --- a/src/xr/xr-manager.js +++ b/src/xr/xr-manager.js @@ -13,6 +13,32 @@ Object.assign(pc, function () { m3.data[8] = m4.data[10]; } + var sessionTypes = { + /** + * @constant + * @type String + * @name pc.XR_TYPE_INLINE + * @description XR session type. TODO + */ + XR_TYPE_INLINE: 'inline', + + /** + * @constant + * @type String + * @name pc.XR_TYPE_IMMERSIVE_VR + * @description XR session type. TODO + */ + XR_TYPE_IMMERSIVE_VR: 'immersive-vr', + + /** + * @constant + * @type String + * @name pc.XR_TYPE_IMMERSIVE_AR + * @description XR session type. TODO + */ + XR_TYPE_IMMERSIVE_AR: 'immersive-ar' + }; + var XrManager = function (app) { pc.EventHandler.call(this); @@ -21,12 +47,19 @@ Object.assign(pc, function () { this.app = app; this._supported = !! navigator.xr; - this._available = false; + + this._available = { }; + for(var key in sessionTypes) { + this._available[sessionTypes[key]] = false; + } + + this._type = null; this._session = null; this._baseLayer = null; this._referenceSpace = null; this._inputSources = []; + this._camera = null; this._pose = null; this.views = []; this.viewsPool = []; @@ -49,28 +82,35 @@ Object.assign(pc, function () { if (this._supported) { navigator.xr.addEventListener('devicechange', function () { - self._deviceVailabilityCheck(); + self._deviceAvailabilityCheck(); }); - this._deviceVailabilityCheck(); + this._deviceAvailabilityCheck(); } }; XrManager.prototype = Object.create(pc.EventHandler.prototype); XrManager.prototype.constructor = XrManager; - XrManager.prototype._deviceVailabilityCheck = function () { + XrManager.prototype._deviceAvailabilityCheck = function () { + for(var key in this._available) { + this._sessionSupportCheck(key); + } + }; + + XrManager.prototype._sessionSupportCheck = function (type) { var self = this; - navigator.xr.isSessionSupported('immersive-vr').then(function (available) { - if (self._available === available) + navigator.xr.isSessionSupported(type).then(function (available) { + if (self._available[type] === available) return; - self._available = available; - self.fire('available', self._available); + self._available[type] = available; + self.fire('available', type, available); + self.fire('available:' + type, available); }); }; - XrManager.prototype.sessionStart = function (callback) { - if (! this._available) + XrManager.prototype.sessionStart = function (camera, type, callback) { + if (! this._available[type]) return callback(new Error('XR is not available')); if (this._session) @@ -78,6 +118,10 @@ Object.assign(pc, function () { var self = this; + this._camera = camera; + this._camera.xr = this; + this._type = type; + // TODO // makeXRCompatible // scenario to test: @@ -86,7 +130,7 @@ Object.assign(pc, function () { // 3. probably immersive-vr will fail to be created // 4. call makeXRCompatible, very likely will lead to context loss - navigator.xr.requestSession('immersive-vr').then(function (session) { + navigator.xr.requestSession(type).then(function (session) { self._onSessionStart(session, callback); }); }; @@ -126,6 +170,12 @@ Object.assign(pc, function () { self.views = []; self._width = 0; self._height = 0; + self._type = null; + + if (self._camera) { + self._camera.xr = null; + self._camera = null; + } session.removeEventListener('end', onEnd); session.removeEventListener('visibilitychange', onVisibilityChange); @@ -189,13 +239,14 @@ Object.assign(pc, function () { if (this._width !== width || this._height !== height) { this._width = width; this._height = height; - this.app.graphicsDevice.resizeCanvas(width, height); + this.app.graphicsDevice.setResolution(width, height); } this._pose = frame.getViewerPose(this._referenceSpace); + var lengthNew = this._pose ? this._pose.views.length : 0; - if (this._pose.views.length > this.views.length) { - for (i = 0; i <= (this._pose.views.length - this.views.length); i++) { + if (lengthNew > this.views.length) { + for (i = 0; i <= (lengthNew - this.views.length); i++) { view = this.viewsPool.pop(); if (! view) { view = { @@ -217,56 +268,73 @@ Object.assign(pc, function () { this.views.push(view); } - } else if (this._pose.views.length <= this.views.length) { - for (i = 0; i < (this.views.length - this._pose.views.length); i++) { + } else if (lengthNew <= this.views.length) { + for (i = 0; i < (this.views.length - lengthNew); i++) { this.viewsPool.push(this.views.pop()); } } this.position.set(0, 0, 0); - layer = frame.session.renderState.baseLayer; + if (this._pose) { + layer = frame.session.renderState.baseLayer; + + for (i = 0; i < this._pose.views.length; i++) { + viewRaw = this._pose.views[i]; + view = this.views[i]; + viewport = layer.getViewport(viewRaw); - for (i = 0; i < this._pose.views.length; i++) { - viewRaw = this._pose.views[i]; - view = this.views[i]; - viewport = layer.getViewport(viewRaw); + view.viewport.x = viewport.x; + view.viewport.y = viewport.y; + view.viewport.z = viewport.width; + view.viewport.w = viewport.height; - view.viewport.x = viewport.x; - view.viewport.y = viewport.y; - view.viewport.z = viewport.width; - view.viewport.w = viewport.height; + view.projMat.set(viewRaw.projectionMatrix); + view.viewMat.set(viewRaw.transform.inverse.matrix); + view.viewInvMat.set(viewRaw.transform.matrix); + view.projViewMat.mul2(view.projMat, view.viewMat); + mat3FromMat4(view.viewMat3, view.viewMat); - view.projMat.set(viewRaw.projectionMatrix); - view.viewMat.set(viewRaw.transform.inverse.matrix); - view.viewInvMat.set(viewRaw.transform.matrix); - view.projViewMat.mul2(view.projMat, view.viewMat); - mat3FromMat4(view.viewMat3, view.viewMat); + position = viewRaw.transform.position; + view.position.set(position.x, position.y, position.z); + this.position.add(view.position); - position = viewRaw.transform.position; - view.position.set(position.x, position.y, position.z); - this.position.add(view.position); + rotation = viewRaw.transform.orientation; + view.rotation.set(rotation.x, rotation.y, rotation.z, rotation.w); + this.rotation.copy(view.rotation); + } - rotation = viewRaw.transform.orientation; - view.rotation.set(rotation.x, rotation.y, rotation.z, rotation.w); - this.rotation.copy(view.rotation); + this.position.scale(1 / this._pose.views.length); } - this.position.scale(1 / this._pose.views.length); + this._camera._node.setLocalPosition(this.position); + this._camera._node.setLocalRotation(this.rotation); }; - Object.defineProperty(XrManager.prototype, 'isSupported', { + Object.defineProperty(XrManager.prototype, 'supported', { get: function () { return this._supported; } }); - Object.defineProperty(XrManager.prototype, 'isAvailable', { + Object.defineProperty(XrManager.prototype, 'available', { get: function () { return this._available; } }); + Object.defineProperty(XrManager.prototype, 'active', { + get: function () { + return !! this._session; + } + }); + + Object.defineProperty(XrManager.prototype, 'type', { + get: function () { + return this._type; + } + }); + Object.defineProperty(XrManager.prototype, 'session', { get: function () { return this._session; @@ -288,7 +356,12 @@ Object.assign(pc, function () { } }); - return { + + var obj = { XrManager: XrManager }; + Object.assign(obj, sessionTypes); + + + return obj; }()); From f9d8568a1d61f97911dca19a2d477e3ac760b357 Mon Sep 17 00:00:00 2001 From: Maksims Mihejevs Date: Thu, 30 Jan 2020 03:35:50 +0200 Subject: [PATCH 06/20] better API, tidy up, lint clean --- src/callbacks.js | 17 +- src/framework/application.js | 3 +- src/framework/components/camera/component.js | 35 ++- src/scene/forward-renderer.js | 12 +- src/xr/xr-manager.js | 280 +++++++++++++------ 5 files changed, 225 insertions(+), 122 deletions(-) diff --git a/src/callbacks.js b/src/callbacks.js index 3a0dddf2c84..0fd1937fd16 100644 --- a/src/callbacks.js +++ b/src/callbacks.js @@ -78,12 +78,6 @@ * @param {Number} view Type of view. Can be pc.VIEW_CENTER, pc.VIEW_LEFT or pc.VIEW_RIGHT. Left and right are only used in stereo rendering. */ -/** - * @callback pc.callbacks.VrCamera - * @description Callback used by {@link pc.CameraComponent#enterVr} and {@link pc.CameraComponent#exitVr}. - * @param {String|Null} err On success it is null on failure it is the error message. - */ - /** * @private * @callback pc.callbacks.CreateScript @@ -154,12 +148,7 @@ */ /** - * @callback pc.callbacks.VrDisplay - * @description Callback used by {@link pc.VrDisplay#requestPresent} and {@link pc.VrDisplay#exitPresent}. - * @param {String|Null} err The error message if presenting fails, or null if the call succeeds. - */ - -/** - * @callback pc.callbacks.VrFrame - * @description Callback used by {@link pc.VrDisplay#requestAnimationFrame}. + * @callback pc.callbacks.XrError + * @description Callback used by {@link pc.XrManager#endXr} and {@link pc.XrManager#startXr}. + * @param {Error|Null} err The Error object or null if operation was successfull. */ diff --git a/src/framework/application.js b/src/framework/application.js index 0e68f668b1d..0ef940a7594 100644 --- a/src/framework/application.js +++ b/src/framework/application.js @@ -1725,8 +1725,7 @@ Object.assign(pc, function () { pc.destroyPostEffectQuad(); - // TODO - // destroy XR + this.xr.sessionEnd(); this.graphicsDevice.destroy(); this.graphicsDevice = null; diff --git a/src/framework/components/camera/component.js b/src/framework/components/camera/component.js index dbfc1382c40..a06233ce970 100644 --- a/src/framework/components/camera/component.js +++ b/src/framework/components/camera/component.js @@ -434,40 +434,45 @@ Object.assign(pc, function () { * @function * @name pc.CameraComponent#startXr * @description Attempt to start XR session with this camera - * @param {pc.callbacks.XrErrSession} callback - Function called once to indicate success or failure. The callback takes two arguments (err) and (session). + * @param {String} type - The type of session. Can be one of the following: + * + * * {@link pc.XR_TYPE_INLINE}: Inline - always available type of session. It has limited features availability and is rendered into HTML element. + * * {@link pc.XR_TYPE_IMMERSIVE_VR}: Immersive VR - session that provides exclusive access to VR device with best available tracking features. + * * {@link pc.XR_TYPE_IMMERSIVE_AR}: Immersive AR - session that provides exclusive access to VR/AR device that is intended to be blended with real-world environment. + * + * @param {pc.callbacks.XrError} [callback] - Optional callback function called once session is started. The callback has one argument Error - it is null if successfully started XR session. * @example * // On an entity with a camera component - * this.entity.camera.startXr(function (err, session) { + * this.entity.camera.startXr(PC.XR_TYPE_IMMERSIVE_VR, function (err) { * if (err) { - * console.error(err); + * // failed to start XR session * } else { - * // in XR! + * // in XR * } * }); */ startXr: function (type, callback) { - this.system.app.xr.sessionStart(this.camera, type, callback); + this.system.app.xr.sessionStart(this, type, callback); }, /** * @function * @name pc.CameraComponent#endXr - * @description Attempt to end XR of this camera - * @param {pc.callbacks.XrSession} callback - Function called once session is ended. The callback takes one argument (old session). + * @description Attempt to end XR session of this camera + * @param {pc.callbacks.XrError} [callback] - Optional callback function called once session is ended. The callback has one argument Error - it is null if successfully ended XR session. * @example * // On an entity with a camera component - * this.entity.camera.endXr(function (session) { - * // not anymore in XR! + * this.entity.camera.endXr(function (err) { + * // not anymore in XR * }); */ - endXr: function(callback) { - if (! this.camera.xr || ! this.camera.xr.session) + endXr: function (callback) { + if (! this.camera.xr) { + if (callback) callback(new Error("Camera is not in XR")); return; + } - if (callback) - this.camera.xr.once('session:end', callback); - - this.camera.xr.sessionEnd(); + this.camera.xr.sessionEnd(callback); } }); diff --git a/src/scene/forward-renderer.js b/src/scene/forward-renderer.js index 3ad7bac713a..d24c6609704 100644 --- a/src/scene/forward-renderer.js +++ b/src/scene/forward-renderer.js @@ -551,11 +551,6 @@ Object.assign(pc, function () { // make sure colorWrite is set to true to all channels, if you want to fully clear the target setCamera: function (camera, target, clear, cullBorder) { if (camera.xr && camera.xr.session) { - // TODO - // respect parent transform - camera.nearClip = camera.xr.session.renderState.depthNear; - camera.farClip = camera.xr.session.renderState.depthFar; - var parent = camera._node.parent; var transform; if (parent) { @@ -575,7 +570,7 @@ Object.assign(pc, function () { view.viewOffMat.copy(view.viewMat); } - mat3FromMat4(view.viewMat3Off, view.viewOffMat); + mat3FromMat4(view.viewMat3, view.viewOffMat); view.projViewOffMat.mul2(view.projMat, view.viewOffMat); view.positionOff.x = view.viewInvOffMat.data[12]; @@ -584,9 +579,6 @@ Object.assign(pc, function () { camera.frustum.update(view.projMat, view.viewOffMat); } - - // TODO - // calculate frustum culling } else { // Projection Matrix projMat = camera.getProjectionMatrix(); @@ -1648,7 +1640,7 @@ Object.assign(pc, function () { this.projId.setValue(view.projMat.data); this.viewId.setValue(view.viewOffMat.data); this.viewInvId.setValue(view.viewInvOffMat.data); - this.viewId3.setValue(view.viewMat3Off.data); + this.viewId3.setValue(view.viewMat3.data); this.viewProjId.setValue(view.projViewOffMat.data); this.viewPosId.setValue(view.positionOff.data); diff --git a/src/xr/xr-manager.js b/src/xr/xr-manager.js index 8ea5b7433ac..b2565a59156 100644 --- a/src/xr/xr-manager.js +++ b/src/xr/xr-manager.js @@ -1,24 +1,10 @@ Object.assign(pc, function () { - function mat3FromMat4(m3, m4) { - m3.data[0] = m4.data[0]; - m3.data[1] = m4.data[1]; - m3.data[2] = m4.data[2]; - - m3.data[3] = m4.data[4]; - m3.data[4] = m4.data[5]; - m3.data[5] = m4.data[6]; - - m3.data[6] = m4.data[8]; - m3.data[7] = m4.data[9]; - m3.data[8] = m4.data[10]; - } - var sessionTypes = { /** * @constant * @type String * @name pc.XR_TYPE_INLINE - * @description XR session type. TODO + * @description Inline - always available type of session. It has limited features availability and is rendered into HTML element. */ XR_TYPE_INLINE: 'inline', @@ -26,7 +12,7 @@ Object.assign(pc, function () { * @constant * @type String * @name pc.XR_TYPE_IMMERSIVE_VR - * @description XR session type. TODO + * @description Immersive VR - session that provides exclusive access to VR device with best available tracking features. */ XR_TYPE_IMMERSIVE_VR: 'immersive-vr', @@ -34,11 +20,23 @@ Object.assign(pc, function () { * @constant * @type String * @name pc.XR_TYPE_IMMERSIVE_AR - * @description XR session type. TODO + * @description Immersive AR - session that provides exclusive access to VR/AR device that is intended to be blended with real-world environment. */ XR_TYPE_IMMERSIVE_AR: 'immersive-ar' }; + + /** + * @class + * @name pc.XrManager + * @augments pc.EventHandler + * @classdesc Manage and update XR session and its states. + * @description Manage and update XR session and its states. + * @param {pc.Application} app - The main application. + * @property {Boolean} supported Returns true if XR is supported. + * @property {Boolean} active Returns true if XR session is running. + * @property {String|Null} type Returns type of curently running XR session or null if no session is running. + */ var XrManager = function (app) { pc.EventHandler.call(this); @@ -49,7 +47,7 @@ Object.assign(pc, function () { this._supported = !! navigator.xr; this._available = { }; - for(var key in sessionTypes) { + for (var key in sessionTypes) { this._available[sessionTypes[key]] = false; } @@ -66,8 +64,8 @@ Object.assign(pc, function () { this.position = new pc.Vec3(); this.rotation = new pc.Quat(); - this.depthNear = 0.1; - this.depthFar = 1000; + this._depthNear = 0.1; + this._depthFar = 1000; this._width = 0; this._height = 0; @@ -77,9 +75,6 @@ Object.assign(pc, function () { // 2. Space class // 3. Controllers class - // TODO - // better APIs - if (this._supported) { navigator.xr.addEventListener('devicechange', function () { self._deviceAvailabilityCheck(); @@ -90,38 +85,85 @@ Object.assign(pc, function () { XrManager.prototype = Object.create(pc.EventHandler.prototype); XrManager.prototype.constructor = XrManager; - XrManager.prototype._deviceAvailabilityCheck = function () { - for(var key in this._available) { - this._sessionSupportCheck(key); - } - }; - - XrManager.prototype._sessionSupportCheck = function (type) { - var self = this; - - navigator.xr.isSessionSupported(type).then(function (available) { - if (self._available[type] === available) - return; - - self._available[type] = available; - self.fire('available', type, available); - self.fire('available:' + type, available); - }); - }; - + /** + * @event + * @name pc.XrManager#available + * @description Fired when availability of specific XR type is changed. + * @param {String} type The session type that has changed availability. + * @param {Boolean} available True if specified session type is now available. + * @example + * app.xr.on('available', function (type, available) { + * console.log('"' + type + '" XR session is now ' + (available ? 'available' : 'unavailable')); + * }); + */ + + /** + * @event + * @name pc.XrManager#available:[type] + * @description Fired when availability of specific XR type is changed. + * @param {Boolean} available True if specified session type is now available. + * @example + * app.xr.on('available:' + pc.XR_TYPE_IMMERSIVE_VR, function (available) { + * console.log('Immersive VR session is now ' + (available ? 'available' : 'unavailable')); + * }); + */ + + /** + * @event + * @name pc.XrManager#session:start + * @description Fired when XR session is started + * @example + * app.xr.on('session:start', function () { + * // XR session has started + * }); + */ + + /** + * @event + * @name pc.XrManager#session:end + * @description Fired when XR session is ended + * @example + * app.xr.on('session:end', function () { + * // XR session has ended + * }); + */ + + /** + * @function + * @name pc.XrManager#sessionStart + * @description Attempts to start XR session for provided {@link pc.CameraComponent} and optionally fires callback when session is created or failed to create. + * @param {pc.CameraComponent} camera it will be used to render XR session and manipulated based on pose tracking + * @param {String} type session type. Can be one of the following: + * + * * {@link pc.XR_TYPE_INLINE}: Inline - always available type of session. It has limited features availability and is rendered into HTML element. + * * {@link pc.XR_TYPE_IMMERSIVE_VR}: Immersive VR - session that provides exclusive access to VR device with best available tracking features. + * * {@link pc.XR_TYPE_IMMERSIVE_AR}: Immersive AR - session that provides exclusive access to VR/AR device that is intended to be blended with real-world environment. + * + * @example + * button.on('click', function () { + * app.xr.sessionStart(camera, PC.XR_TYPE_IMMERSIVE_VR); + * }); + * @param {pc.callbacks.XrError} [callback] - Optional callback function called once session is started. The callback has one argument Error - it is null if successfully started XR session. + */ XrManager.prototype.sessionStart = function (camera, type, callback) { - if (! this._available[type]) - return callback(new Error('XR is not available')); + if (! this._available[type]) { + if (callback) callback(new Error('XR is not available')); + return; + } - if (this._session) - return callback(new Error('XR session is already started')); + if (this._session) { + if (callback) callback(new Error('XR session is already started')); + return; + } var self = this; this._camera = camera; - this._camera.xr = this; + this._camera.camera.xr = this; this._type = type; + this._setClipPlanes(camera.nearClip, camera.farClip); + // TODO // makeXRCompatible // scenario to test: @@ -135,13 +177,68 @@ Object.assign(pc, function () { }); }; - XrManager.prototype.sessionEnd = function () { - if (! this._session) + /** + * @function + * @name pc.XrManager#sessionEnd + * @description Attempts to end XR session and optionally fires callback when session is ended or failed to end. + * @example + * app.keyboard.on('keydown', function (evt) { + * if (evt.key === pc.KEY_ESCAPE && app.xr.active) { + * app.xr.sessionEnd(); + * } + * }); + * @param {pc.callbacks.XrError} [callback] - Optional callback function called once session is started. The callback has one argument Error - it is null if successfully started XR session. + */ + XrManager.prototype.sessionEnd = function (callback) { + if (! this._session) { + if (callback) callback(new Error('XR Session is not initialized')); return; + } + + if (callback) this.once('session:end', callback); this._session.end(); }; + /** + * @function + * @name pc.XrManager#isAvailable + * @description Check if specific type of session is available + * @param {String} type session type. Can be one of the following: + * + * * {@link pc.XR_TYPE_INLINE}: Inline - always available type of session. It has limited features availability and is rendered into HTML element. + * * {@link pc.XR_TYPE_IMMERSIVE_VR}: Immersive VR - session that provides exclusive access to VR device with best available tracking features. + * * {@link pc.XR_TYPE_IMMERSIVE_AR}: Immersive AR - session that provides exclusive access to VR/AR device that is intended to be blended with real-world environment. + * + * @example + * if (app.xr.isAvailable(pc.XR_TYPE_IMMERSIVE_VR)) { + * // VR is available + * } + * @returns {Boolean} True if specified session type is available. + */ + XrManager.prototype.isAvailable = function (type) { + return this._available[type]; + }; + + XrManager.prototype._deviceAvailabilityCheck = function () { + for (var key in this._available) { + this._sessionSupportCheck(key); + } + }; + + XrManager.prototype._sessionSupportCheck = function (type) { + var self = this; + + navigator.xr.isSessionSupported(type).then(function (available) { + if (self._available[type] === available) + return; + + self._available[type] = available; + self.fire('available', type, available); + self.fire('available:' + type, available); + }); + }; + XrManager.prototype._onSessionStart = function (session, callback) { var self = this; @@ -162,6 +259,11 @@ Object.assign(pc, function () { } }; + var onClipPlanesChange = function () { + self._setClipPlanes(self._camera.nearClip, self._camera.farClip); + }; + + // clean up once session is ended var onEnd = function () { self._session = null; self._referenceSpace = null; @@ -173,7 +275,10 @@ Object.assign(pc, function () { self._type = null; if (self._camera) { - self._camera.xr = null; + self._camera.off('set_nearClip', onClipPlanesChange); + self._camera.off('set_farClip', onClipPlanesChange); + + self._camera.camera.xr = null; self._camera = null; } @@ -181,28 +286,38 @@ Object.assign(pc, function () { session.removeEventListener('visibilitychange', onVisibilityChange); session.removeEventListener('inputsourceschange', onInputSourcesChange); + // old requestAnimationFrame will never be triggered, + // so queue up new tick self.app.tick(); - self.fire('session:end', session); + self.fire('session:end'); }; session.addEventListener('end', onEnd); session.addEventListener('visibilitychange', onVisibilityChange); session.addEventListener('inputsourceschange', onInputSourcesChange); + this._camera.on('set_nearClip', onClipPlanesChange); + this._camera.on('set_farClip', onClipPlanesChange); + this._baseLayer = new XRWebGLLayer(session, this.app.graphicsDevice.gl); session.updateRenderState({ - baseLayer: this._baseLayer + baseLayer: this._baseLayer, + depthNear: this._depthNear, + depthFar: this._depthFar }); + // request reference space session.requestReferenceSpace('local').then(function (referenceSpace) { self._referenceSpace = referenceSpace; + // old requestAnimationFrame will never be triggered, + // so queue up new tick self.app.tick(); - if (callback) callback(null, session); - self.fire('session:start', session); + if (callback) callback(null); + self.fire('session:start'); }); }; @@ -218,22 +333,35 @@ Object.assign(pc, function () { this.fire('inputSource:remove', inputSource); }; - XrManager.prototype.setClipPlanes = function (near, far) { - if (this.depthNear === near && this.depthFar === far) + XrManager.prototype._setClipPlanes = function (near, far) { + near = Math.min(0.0001, Math.max(0.1, near)); + far = Math.max(1000, far); + + if (this._depthNear === near && this._depthFar === far) return; - this.depthNear = near; - this.depthFar = far; + this._depthNear = near; + this._depthFar = far; - // TODO - // update clip planes + if (! this._session) + return; + + // if session is available, + // queue up render state update + this._session.updateRenderState({ + depthNear: this._depthNear, + depthFar: this._depthFar + }); }; XrManager.prototype.calculateViews = function (frame) { if (! this._session) return; - var i, view, viewRaw, layer, viewport, position, rotation; + var i, view, viewRaw, layer; + var viewport, position, rotation; + var lengthNew; + // canvas resolution should be set on first frame availability or resolution changes var width = frame.session.renderState.baseLayer.framebufferWidth; var height = frame.session.renderState.baseLayer.framebufferHeight; if (this._width !== width || this._height !== height) { @@ -243,9 +371,10 @@ Object.assign(pc, function () { } this._pose = frame.getViewerPose(this._referenceSpace); - var lengthNew = this._pose ? this._pose.views.length : 0; + lengthNew = this._pose ? this._pose.views.length : 0; if (lengthNew > this.views.length) { + // add new views into list for (i = 0; i <= (lengthNew - this.views.length); i++) { view = this.viewsPool.pop(); if (! view) { @@ -256,10 +385,8 @@ Object.assign(pc, function () { viewOffMat: new pc.Mat4(), viewInvMat: new pc.Mat4(), viewInvOffMat: new pc.Mat4(), - projViewMat: new pc.Mat4(), projViewOffMat: new pc.Mat4(), viewMat3: new pc.Mat3(), - viewMat3Off: new pc.Mat3(), position: new pc.Vec3(), positionOff: new pc.Vec3(), rotation: new pc.Quat() @@ -269,17 +396,20 @@ Object.assign(pc, function () { this.views.push(view); } } else if (lengthNew <= this.views.length) { + // remove views from list into pool for (i = 0; i < (this.views.length - lengthNew); i++) { this.viewsPool.push(this.views.pop()); } } + // reset position this.position.set(0, 0, 0); if (this._pose) { layer = frame.session.renderState.baseLayer; for (i = 0; i < this._pose.views.length; i++) { + // for each view, calculate matrices viewRaw = this._pose.views[i]; view = this.views[i]; viewport = layer.getViewport(viewRaw); @@ -292,8 +422,6 @@ Object.assign(pc, function () { view.projMat.set(viewRaw.projectionMatrix); view.viewMat.set(viewRaw.transform.inverse.matrix); view.viewInvMat.set(viewRaw.transform.matrix); - view.projViewMat.mul2(view.projMat, view.viewMat); - mat3FromMat4(view.viewMat3, view.viewMat); position = viewRaw.transform.position; view.position.set(position.x, position.y, position.z); @@ -301,14 +429,16 @@ Object.assign(pc, function () { rotation = viewRaw.transform.orientation; view.rotation.set(rotation.x, rotation.y, rotation.z, rotation.w); - this.rotation.copy(view.rotation); + + if (i === 0) this.rotation.copy(view.rotation); } this.position.scale(1 / this._pose.views.length); } - this._camera._node.setLocalPosition(this.position); - this._camera._node.setLocalRotation(this.rotation); + // position and rotate camera based on calculated vectors + this._camera.camera._node.setLocalPosition(this.position); + this._camera.camera._node.setLocalRotation(this.rotation); }; Object.defineProperty(XrManager.prototype, 'supported', { @@ -317,12 +447,6 @@ Object.assign(pc, function () { } }); - Object.defineProperty(XrManager.prototype, 'available', { - get: function () { - return this._available; - } - }); - Object.defineProperty(XrManager.prototype, 'active', { get: function () { return !! this._session; @@ -350,12 +474,6 @@ Object.assign(pc, function () { } }); - Object.defineProperty(XrManager.prototype, 'inputSources', { - get: function () { - return this._inputSources; - } - }); - var obj = { XrManager: XrManager From 45903baf1745130c82c273665d6ea72a577b4eb7 Mon Sep 17 00:00:00 2001 From: Maksims Mihejevs Date: Thu, 30 Jan 2020 03:59:30 +0200 Subject: [PATCH 07/20] jsdocs fixes --- src/callbacks.js | 2 +- src/framework/application.js | 11 ++++++++++ src/framework/components/camera/component.js | 2 +- src/xr/xr-manager.js | 22 ++++++++++---------- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/callbacks.js b/src/callbacks.js index 9fd6c6ff1a3..73bc93f9f86 100644 --- a/src/callbacks.js +++ b/src/callbacks.js @@ -158,5 +158,5 @@ /** * @callback pc.callbacks.XrError * @description Callback used by {@link pc.XrManager#endXr} and {@link pc.XrManager#startXr}. - * @param {Error|Null} err The Error object or null if operation was successfull. + * @param {Error|null} err The Error object or null if operation was successfull. */ diff --git a/src/framework/application.js b/src/framework/application.js index 4b1fca6830f..d7fe820d0a5 100644 --- a/src/framework/application.js +++ b/src/framework/application.js @@ -124,6 +124,17 @@ Object.assign(pc, function () { * this.app.systems.sound.volume = 0.5; */ + /** + * @name pc.Application#xr + * @type {pc.XrManager} + * @description The XR Manager that provides ability to start VR/AR sessions. + * @example + * // check if VR is available + * if (app.xr.isAvailable(pc.XR_TYPE_IMMERSIVE_VR)) { + * // VR is available + * } + */ + /** * @name pc.Application#loader * @type {pc.ResourceLoader} diff --git a/src/framework/components/camera/component.js b/src/framework/components/camera/component.js index 9c003e90b5a..f260195804a 100644 --- a/src/framework/components/camera/component.js +++ b/src/framework/components/camera/component.js @@ -436,7 +436,7 @@ Object.assign(pc, function () { * @function * @name pc.CameraComponent#startXr * @description Attempt to start XR session with this camera - * @param {String} type - The type of session. Can be one of the following: + * @param {string} type - The type of session. Can be one of the following: * * * {@link pc.XR_TYPE_INLINE}: Inline - always available type of session. It has limited features availability and is rendered into HTML element. * * {@link pc.XR_TYPE_IMMERSIVE_VR}: Immersive VR - session that provides exclusive access to VR device with best available tracking features. diff --git a/src/xr/xr-manager.js b/src/xr/xr-manager.js index b2565a59156..84fa6e9ea19 100644 --- a/src/xr/xr-manager.js +++ b/src/xr/xr-manager.js @@ -2,7 +2,7 @@ Object.assign(pc, function () { var sessionTypes = { /** * @constant - * @type String + * @type string * @name pc.XR_TYPE_INLINE * @description Inline - always available type of session. It has limited features availability and is rendered into HTML element. */ @@ -10,7 +10,7 @@ Object.assign(pc, function () { /** * @constant - * @type String + * @type string * @name pc.XR_TYPE_IMMERSIVE_VR * @description Immersive VR - session that provides exclusive access to VR device with best available tracking features. */ @@ -18,7 +18,7 @@ Object.assign(pc, function () { /** * @constant - * @type String + * @type string * @name pc.XR_TYPE_IMMERSIVE_AR * @description Immersive AR - session that provides exclusive access to VR/AR device that is intended to be blended with real-world environment. */ @@ -33,9 +33,9 @@ Object.assign(pc, function () { * @classdesc Manage and update XR session and its states. * @description Manage and update XR session and its states. * @param {pc.Application} app - The main application. - * @property {Boolean} supported Returns true if XR is supported. - * @property {Boolean} active Returns true if XR session is running. - * @property {String|Null} type Returns type of curently running XR session or null if no session is running. + * @property {boolean} supported Returns true if XR is supported. + * @property {boolean} active Returns true if XR session is running. + * @property {string|null} type Returns type of curently running XR session or null if no session is running. */ var XrManager = function (app) { pc.EventHandler.call(this); @@ -89,8 +89,8 @@ Object.assign(pc, function () { * @event * @name pc.XrManager#available * @description Fired when availability of specific XR type is changed. - * @param {String} type The session type that has changed availability. - * @param {Boolean} available True if specified session type is now available. + * @param {string} type The session type that has changed availability. + * @param {boolean} available True if specified session type is now available. * @example * app.xr.on('available', function (type, available) { * console.log('"' + type + '" XR session is now ' + (available ? 'available' : 'unavailable')); @@ -101,7 +101,7 @@ Object.assign(pc, function () { * @event * @name pc.XrManager#available:[type] * @description Fired when availability of specific XR type is changed. - * @param {Boolean} available True if specified session type is now available. + * @param {boolean} available True if specified session type is now available. * @example * app.xr.on('available:' + pc.XR_TYPE_IMMERSIVE_VR, function (available) { * console.log('Immersive VR session is now ' + (available ? 'available' : 'unavailable')); @@ -133,7 +133,7 @@ Object.assign(pc, function () { * @name pc.XrManager#sessionStart * @description Attempts to start XR session for provided {@link pc.CameraComponent} and optionally fires callback when session is created or failed to create. * @param {pc.CameraComponent} camera it will be used to render XR session and manipulated based on pose tracking - * @param {String} type session type. Can be one of the following: + * @param {string} type session type. Can be one of the following: * * * {@link pc.XR_TYPE_INLINE}: Inline - always available type of session. It has limited features availability and is rendered into HTML element. * * {@link pc.XR_TYPE_IMMERSIVE_VR}: Immersive VR - session that provides exclusive access to VR device with best available tracking features. @@ -214,7 +214,7 @@ Object.assign(pc, function () { * if (app.xr.isAvailable(pc.XR_TYPE_IMMERSIVE_VR)) { * // VR is available * } - * @returns {Boolean} True if specified session type is available. + * @returns {boolean} True if specified session type is available. */ XrManager.prototype.isAvailable = function (type) { return this._available[type]; From 95c3fff7f28cd8ab4f16ec6264b4869109484816 Mon Sep 17 00:00:00 2001 From: Maksims Mihejevs Date: Thu, 30 Jan 2020 20:59:58 +0200 Subject: [PATCH 08/20] fix tests --- src/framework/application.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/framework/application.js b/src/framework/application.js index d7fe820d0a5..d2af1d30978 100644 --- a/src/framework/application.js +++ b/src/framework/application.js @@ -270,6 +270,9 @@ Object.assign(pc, function () { // for compatibility this.context = this; + if (! options.graphicsDeviceOptions) + options.graphicsDeviceOptions = { }; + options.graphicsDeviceOptions.xrCompatible = true; this.graphicsDevice = new pc.GraphicsDevice(canvas, options.graphicsDeviceOptions); From d8b95994ec30bce726f6c45cb378d0f123a2fc4d Mon Sep 17 00:00:00 2001 From: Maksims Mihejevs Date: Thu, 30 Jan 2020 21:19:40 +0200 Subject: [PATCH 09/20] add backwards compatibility warnings for VR --- src/backwards-compatibility.js | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/backwards-compatibility.js b/src/backwards-compatibility.js index aa5f19766f4..69fb766a59c 100644 --- a/src/backwards-compatibility.js +++ b/src/backwards-compatibility.js @@ -514,3 +514,36 @@ pc.SoundManager.prototype.setVolume = function (volume) { // #endif this.volume = volume; }; + +Object.defineProperty(pc.Application.prototype, "vr", { + get: function () { + // #ifdef DEBUG + console.warn('DEPRECATED: pc.Application#vr is deprecated, use pc.Application#xr instead.'); + // #endif + return { isSupported: false }; + } +}); + +pc.Application.prototype.enableVr = function () { + // #ifdef DEBUG + console.warn('DEPRECATED: pc.Application#enableVr is deprecated. Use pc.Application#xr instead.'); + // #endif +}; + +pc.Application.prototype.disableVr = function () { + // #ifdef DEBUG + console.warn('DEPRECATED: pc.Application#disableVr is deprecated. Use pc.Application#xr instead.'); + // #endif +}; + +pc.CameraComponent.prototype.enterVr = function () { + // #ifdef DEBUG + console.warn('DEPRECATED: pc.CameraComponent#enterVr is deprecated. Use pc.CameraComponent#startXr instead.'); + // #endif +}; + +pc.CameraComponent.prototype.exitVr = function () { + // #ifdef DEBUG + console.warn('DEPRECATED: pc.CameraComponent#exitVr is deprecated. Use pc.CameraComponent#endXr instead.'); + // #endif +}; From 5d0c342978a04a4fb501d917d95ef3ac12ea49f7 Mon Sep 17 00:00:00 2001 From: Maksims Mihejevs Date: Thu, 30 Jan 2020 21:23:10 +0200 Subject: [PATCH 10/20] remove deprecated VR camera logic --- src/framework/components/camera/system.js | 27 ----------------------- 1 file changed, 27 deletions(-) diff --git a/src/framework/components/camera/system.js b/src/framework/components/camera/system.js index 454d7d5745d..eec09307cf0 100644 --- a/src/framework/components/camera/system.js +++ b/src/framework/components/camera/system.js @@ -54,8 +54,6 @@ Object.assign(pc, function () { this.on('beforeremove', this.onBeforeRemove, this); this.on('remove', this.onRemove, this); this.app.on("prerender", this.onPrerender, this); - - pc.ComponentSystem.bind('update', this.onUpdate, this); }; CameraComponentSystem.prototype = Object.create(pc.ComponentSystem.prototype); CameraComponentSystem.prototype.constructor = CameraComponentSystem; @@ -156,31 +154,6 @@ Object.assign(pc, function () { data.camera = null; }, - onUpdate: function (dt) { - var components = this.store; - var component, componentData, cam, vrDisplay; - - if (this.app.vr) { - for (var id in components) { - component = components[id]; - componentData = component.data; - cam = componentData.camera; - vrDisplay = cam.vrDisplay; - if (componentData.enabled && component.entity.enabled && vrDisplay) { - // Change WebVR near/far planes based on the stereo camera - vrDisplay.setClipPlanes(cam._nearClip, cam._farClip); - - // update camera node transform from VrDisplay - if (cam._node) { - cam._node.localTransform.copy(vrDisplay.combinedViewInv); - cam._node._dirtyLocal = false; - cam._node._dirtifyWorld(); - } - } - } - } - }, - onPrerender: function () { for (var i = 0, len = this.cameras.length; i < len; i++) { this.cameras[i].onPrerender(); From 92fb7e68f44f600cccc786d6f755fd17114da3d7 Mon Sep 17 00:00:00 2001 From: Maksims Mihejevs Date: Thu, 30 Jan 2020 22:09:13 +0200 Subject: [PATCH 11/20] remove VR examples and add XR examples --- examples/examples.js | 11 +- examples/style.css | 85 +++++---- examples/style.css.map | 8 +- examples/style.scss | 6 +- examples/xr/augmented-reality-basic.html | 170 ++++++++++++++++++ .../virtual-reality-basic.html} | 74 +++++--- 6 files changed, 282 insertions(+), 72 deletions(-) create mode 100644 examples/xr/augmented-reality-basic.html rename examples/{graphics/virtual-reality.html => xr/virtual-reality-basic.html} (64%) diff --git a/examples/examples.js b/examples/examples.js index 10675d8f3e1..5328655d502 100644 --- a/examples/examples.js +++ b/examples/examples.js @@ -30,8 +30,7 @@ var categories = [ "shader-burn", "shader-toon", "shader-wobble", - "texture-basis", - "virtual-reality" + "texture-basis" ] }, { name: "input", @@ -70,5 +69,11 @@ var categories = [ "text-wrap", "various" ] + }, { + name: "xr", + examples: [ + 'augmented-reality-basic', + 'virtual-reality-basic' + ] } -]; \ No newline at end of file +]; diff --git a/examples/style.css b/examples/style.css index c35a8f7813f..cd40ee09016 100644 --- a/examples/style.css +++ b/examples/style.css @@ -4,7 +4,8 @@ body { font-family: "Proxima Nova", Helvetica, arial, sans-serif; font-size: 16px; margin: 0; - overscroll-behavior: none; } + overscroll-behavior: none; +} /* Sidebar menu */ .sidenav { @@ -22,13 +23,17 @@ body { display: flex; flex-direction: column; background: #364346; - transition: all 240ms ease-in-out; } - @media screen and (max-width: 1024px) { - .sidenav { - background: rgba(54, 67, 70, 0.98); - width: 100%; } - .sidenav.closed { - left: -100%; } } + transition: all 240ms ease-in-out; +} +@media screen and (max-width: 1024px) { + .sidenav { + background: rgba(54, 67, 70, 0.98); + width: 100%; + } + .sidenav.closed { + left: -100%; + } +} .sidenav-toggle { position: absolute; @@ -42,39 +47,50 @@ body { display: none; background: #20292b; border-radius: 300px; - cursor: pointer; } - .sidenav-toggle:hover { - background: black; } - @media screen and (max-width: 1024px) { - .sidenav-toggle { - display: block; } } + cursor: pointer; +} +.sidenav-toggle:hover { + background: black; +} +@media screen and (max-width: 1024px) { + .sidenav-toggle { + display: block; + } +} /* Navigation menu links */ .sidenav a, p { text-decoration: none; - display: block; } + display: block; +} .sidenav a { padding: 6px 8px 6px 32px; font-size: 1rem; color: #b1b8ba; - cursor: pointer; } - .sidenav a:hover { - color: #ffffff; } - .sidenav a.active { - color: #f60; } - @media screen and (max-width: 1024px) { - .sidenav a { - font-size: 1.2rem; - padding: 6px 8px 12px 32px; } } + cursor: pointer; +} +.sidenav a:hover { + color: #ffffff; +} +.sidenav a.active { + color: #f60; +} +@media screen and (max-width: 1024px) { + .sidenav a { + font-size: 1.2rem; + padding: 6px 8px 12px 32px; + } +} .sidenav p { padding: 24px 8px 8px 24px; margin: 0; font-size: 1.4rem; font-weight: bold; - color: #ffffff; } + color: #ffffff; +} #example, iframe { @@ -82,14 +98,17 @@ iframe { border: 0px; left: 0; right: 0; - padding-left: 160px; - width: calc(100% - 160px); + padding-left: 240px; + width: calc(100% - 240px); height: 100%; - overflow: auto; } - @media screen and (max-width: 1024px) { - #example, - iframe { - padding-left: 0; - width: 100%; } } + overflow: auto; +} +@media screen and (max-width: 1024px) { + #example, +iframe { + padding-left: 0; + width: 100%; + } +} /*# sourceMappingURL=style.css.map */ diff --git a/examples/style.css.map b/examples/style.css.map index ad59ed9f2e7..5bb4af39e5e 100644 --- a/examples/style.css.map +++ b/examples/style.css.map @@ -1,7 +1 @@ -{ -"version": 3, -"mappings": "AAmBA,IAAK;EACH,gBAAgB,EAAE,IAAI;EACtB,KAAK,EAAE,OAAO;EACd,WAAW,EAAE,4CAA4C;EACzD,SAAS,EAAE,IAAI;EACf,MAAM,EAAE,CAAC;EACT,mBAAmB,EAAE,IAAI;;AAG3B,kBAAkB;AAClB,QAAS;EACP,MAAM,EAAE,IAAI;EACZ,KAAK,EAAE,KAAK;EACZ,QAAQ,EAAE,KAAK;EAAE,6CAA6C;EAC9D,OAAO,EAAE,CAAC;EAAE,iBAAiB;EAC7B,GAAG,EAAE,CAAC;EACN,IAAI,EAAE,CAAC;EACP,UAAU,EAAE,MAAM;EAAE,+BAA+B;EACnD,WAAW,EAAE,GAAG;EAChB,OAAO,EAAE,IAAI;EACb,cAAc,EAAE,MAAM;EACtB,UAAU,EApCE,OAAO;EAqCnB,UAAU,EAAE,qBAAqB;EACjC,qCAA6C;IAb/C,QAAS;MAcL,UAAU,EAAE,sBAAsB;MAClC,KAAK,EAAE,IAAI;MACX,eAAS;QACP,IAAI,EAAE,KAAK;;AAKjB,eAAgB;EACd,QAAQ,EAAE,QAAQ;EAClB,KAAK,EAAE,IAAI;EACX,GAAG,EAAE,IAAI;EACT,MAAM,EAAE,IAAI;EACZ,KAAK,EAAE,IAAI;EACX,OAAO,EAAE,IAAI;EACb,KAAK,EAAE,KAAK;EACZ,OAAO,EAAE,GAAG;EACZ,OAAO,EAAE,IAAI;EACb,UAAU,EA5DE,OAAO;EA6DnB,aAAa,EAAE,KAAK;EACpB,MAAM,EAAE,OAAO;EACf,qBAAQ;IACN,UAAU,EAAE,KAAK;EAEnB,qCAA6C;IAhB/C,eAAgB;MAiBZ,OAAO,EAAE,KAAK;;AAIlB,2BAA2B;AAC3B;CACE;EACA,eAAe,EAAE,IAAI;EACrB,OAAO,EAAE,KAAK;;AAGhB,UAAW;EACT,OAAO,EAAE,gBAAgB;EACzB,SAAS,EAAE,IAAI;EACf,KAAK,EA3EU,OAAO;EA4EtB,MAAM,EAAE,OAAO;EACf,gBAAQ;IACN,KAAK,EA7EM,OAAO;EA+EpB,iBAAS;IACP,KAAK,EA/EK,IAAI;EAiFhB,qCAA6C;IAX/C,UAAW;MAYP,SAAS,EAAE,MAAM;MACjB,OAAO,EAAE,iBAAiB;;AAI9B,UAAW;EACT,OAAO,EAAE,iBAAiB;EAC1B,MAAM,EAAE,CAAC;EACT,SAAS,EAAE,MAAM;EACjB,WAAW,EAAE,IAAI;EACjB,KAAK,EA7FQ,OAAO;;AAgGtB;MACO;EACL,QAAQ,EAAE,KAAK;EACf,MAAM,EAAE,GAAG;EACX,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;EACR,YAAY,EAAE,KAAK;EACnB,KAAK,EAAE,kBAAkB;EACzB,MAAM,EAAE,IAAI;EACZ,QAAQ,EAAE,IAAI;EACd,qCAA6C;IAV/C;UACO;MAUH,YAAY,EAAE,CAAC;MACf,KAAK,EAAE,IAAI", -"sources": ["style.scss"], -"names": [], -"file": "style.css" -} \ No newline at end of file +{"version":3,"sourceRoot":"","sources":["style.scss"],"names":[],"mappings":"AAmBA;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;AACA;EACE;EACA;EACA;AAAiB;EACjB;AAAY;EACZ;EACA;EACA;AAAoB;EACpB;EACA;EACA;EACA,YApCY;EAqCZ;;AACA;EAbF;IAcI;IACA;;EACA;IACE;;;;AAKN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,YA5DY;EA6DZ;EACA;;AACA;EACE;;AAEF;EAhBF;IAiBI;;;;AAIJ;AACA;AAAA;EAEE;EACA;;;AAGF;EACE;EACA;EACA,OA3Ee;EA4Ef;;AACA;EACE,OA7EW;;AA+Eb;EACE,OA/EU;;AAiFZ;EAXF;IAYI;IACA;;;;AAIJ;EACE;EACA;EACA;EACA;EACA,OA7Fa;;;AAgGf;AAAA;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AACA;EAVF;AAAA;IAWI;IACA","file":"style.css"} \ No newline at end of file diff --git a/examples/style.scss b/examples/style.scss index 373d90b8883..199ba6a2575 100644 --- a/examples/style.scss +++ b/examples/style.scss @@ -48,7 +48,7 @@ body { } } } - + .sidenav-toggle { position: absolute; right: 12px; @@ -108,8 +108,8 @@ iframe { border: 0px; left: 0; right: 0; - padding-left: 160px; - width: calc(100% - 160px); + padding-left: 240px; + width: calc(100% - 240px); height: 100%; overflow: auto; @media screen and (max-width: $break-medium) { diff --git a/examples/xr/augmented-reality-basic.html b/examples/xr/augmented-reality-basic.html new file mode 100644 index 00000000000..9b61b812c3a --- /dev/null +++ b/examples/xr/augmented-reality-basic.html @@ -0,0 +1,170 @@ + + + + PlayCanvas Virtual Reality + + + + + + + + + +
+

+
+ + + + + + diff --git a/examples/graphics/virtual-reality.html b/examples/xr/virtual-reality-basic.html similarity index 64% rename from examples/graphics/virtual-reality.html rename to examples/xr/virtual-reality-basic.html index 6abe4b65642..a4bffa38615 100644 --- a/examples/graphics/virtual-reality.html +++ b/examples/xr/virtual-reality-basic.html @@ -6,7 +6,6 @@ - @@ -64,7 +65,7 @@ var app = new pc.Application(canvas, { mouse: new pc.Mouse(canvas), touch: new pc.TouchDevice(canvas), - vr: true + keyboard: new pc.Keyboard(window) }); app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW); app.setCanvasResolution(pc.RESOLUTION_AUTO); @@ -113,36 +114,57 @@ } } - var activate = function () { - if (app.vr && app.vr.display) { - c.camera.enterVr(function (err) { - if (err) {message(err);} - message(""); + if (app.xr.supported) { + var activate = function () { + if (app.xr.isAvailable(pc.XR_TYPE_IMMERSIVE_VR)) { + c.camera.startXr(pc.XR_TYPE_IMMERSIVE_VR, function (err) { + if (err) message("WebXR Immersive VR failed to start: " + err.message); + }); + } else { + message("WebXR Immersive VR is not available"); + } + }; + + app.mouse.on("mousedown", function () { + if (! app.xr.active) + activate(); + }); + + if (app.touch) { + app.touch.on("touchend", function () { + if (! app.xr.active) { + // if not in VR, activate + activate(); + } else { + // otherwise reset camera + c.camera.endXr(); + } }); - } else { - message("WebVR not available") } - }; - app.mouse.on("mousedown", function () { - activate(); - }); - - if (app.touch) { - app.touch.on("touchend", function () { - if (!c.camera.vrDisplay) { - // if not in VR, activate - activate(); - } else { - // otherwise reset camera - c.camera.vrDisplay.reset(); + // end session by keyboard ESC + app.keyboard.on('keydown', function (evt) { + if (evt.key === pc.KEY_ESCAPE && app.xr.active) { + app.xr.sessionEnd(); } }); - } - app.vr.on("ready", function (displays) { - message("VRDisplays: " + displays.length); - }); + app.xr.on('session:start', function () { + message("WebXR Immersive VR session has started"); + }); + app.xr.on('session:end', function () { + message("WebXR Immersive VR session has ended"); + }); + app.xr.on('available:' + pc.XR_TYPE_IMMERSIVE_VR, function (available) { + message("WebXR Immersive VR is now " + (available ? 'available' : 'unavailable')); + }); + + if (! app.xr.isAvailable(pc.XR_TYPE_IMMERSIVE_VR)) { + message("WebXR Immersive VR is not available"); + } + } else { + message("WebXR is not supported"); + } From 2deb5f5cdbeaaad8a0122b31ff91c39af376ca57 Mon Sep 17 00:00:00 2001 From: Maksims Mihejevs Date: Thu, 30 Jan 2020 22:11:10 +0200 Subject: [PATCH 12/20] lint errors --- src/callbacks.js | 2 +- src/framework/application.js | 2 +- src/xr/xr-manager.js | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/callbacks.js b/src/callbacks.js index 73bc93f9f86..c2adb29bda9 100644 --- a/src/callbacks.js +++ b/src/callbacks.js @@ -158,5 +158,5 @@ /** * @callback pc.callbacks.XrError * @description Callback used by {@link pc.XrManager#endXr} and {@link pc.XrManager#startXr}. - * @param {Error|null} err The Error object or null if operation was successfull. + * @param {Error|null} err - The Error object or null if operation was successfull. */ diff --git a/src/framework/application.js b/src/framework/application.js index d2af1d30978..48c215673f6 100644 --- a/src/framework/application.js +++ b/src/framework/application.js @@ -272,7 +272,7 @@ Object.assign(pc, function () { if (! options.graphicsDeviceOptions) options.graphicsDeviceOptions = { }; - + options.graphicsDeviceOptions.xrCompatible = true; this.graphicsDevice = new pc.GraphicsDevice(canvas, options.graphicsDeviceOptions); diff --git a/src/xr/xr-manager.js b/src/xr/xr-manager.js index 84fa6e9ea19..6f52c6db105 100644 --- a/src/xr/xr-manager.js +++ b/src/xr/xr-manager.js @@ -89,8 +89,8 @@ Object.assign(pc, function () { * @event * @name pc.XrManager#available * @description Fired when availability of specific XR type is changed. - * @param {string} type The session type that has changed availability. - * @param {boolean} available True if specified session type is now available. + * @param {string} type - The session type that has changed availability. + * @param {boolean} available - True if specified session type is now available. * @example * app.xr.on('available', function (type, available) { * console.log('"' + type + '" XR session is now ' + (available ? 'available' : 'unavailable')); @@ -101,7 +101,7 @@ Object.assign(pc, function () { * @event * @name pc.XrManager#available:[type] * @description Fired when availability of specific XR type is changed. - * @param {boolean} available True if specified session type is now available. + * @param {boolean} available - True if specified session type is now available. * @example * app.xr.on('available:' + pc.XR_TYPE_IMMERSIVE_VR, function (available) { * console.log('Immersive VR session is now ' + (available ? 'available' : 'unavailable')); @@ -132,8 +132,8 @@ Object.assign(pc, function () { * @function * @name pc.XrManager#sessionStart * @description Attempts to start XR session for provided {@link pc.CameraComponent} and optionally fires callback when session is created or failed to create. - * @param {pc.CameraComponent} camera it will be used to render XR session and manipulated based on pose tracking - * @param {string} type session type. Can be one of the following: + * @param {pc.CameraComponent} camera - it will be used to render XR session and manipulated based on pose tracking + * @param {string} type - session type. Can be one of the following: * * * {@link pc.XR_TYPE_INLINE}: Inline - always available type of session. It has limited features availability and is rendered into HTML element. * * {@link pc.XR_TYPE_IMMERSIVE_VR}: Immersive VR - session that provides exclusive access to VR device with best available tracking features. @@ -204,7 +204,7 @@ Object.assign(pc, function () { * @function * @name pc.XrManager#isAvailable * @description Check if specific type of session is available - * @param {String} type session type. Can be one of the following: + * @param {string} type - session type. Can be one of the following: * * * {@link pc.XR_TYPE_INLINE}: Inline - always available type of session. It has limited features availability and is rendered into HTML element. * * {@link pc.XR_TYPE_IMMERSIVE_VR}: Immersive VR - session that provides exclusive access to VR device with best available tracking features. From 1ebcd83b30db376e4e394cffc2d0812ca536c329 Mon Sep 17 00:00:00 2001 From: Maksims Mihejevs Date: Tue, 4 Feb 2020 19:37:04 +0200 Subject: [PATCH 13/20] address PR comments --- src/framework/application.js | 3 +-- src/framework/components/camera/component.js | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/framework/application.js b/src/framework/application.js index 48c215673f6..f370693a943 100644 --- a/src/framework/application.js +++ b/src/framework/application.js @@ -1816,9 +1816,8 @@ Object.assign(pc, function () { app._fillFrameStats(now, dt, ms); // #endif - if (frame) app.xr.calculateViews(frame); - if (frame) { + app.xr.calculateViews(frame); app.graphicsDevice.defaultFramebuffer = frame.session.renderState.baseLayer.framebuffer; } else { app.graphicsDevice.defaultFramebuffer = null; diff --git a/src/framework/components/camera/component.js b/src/framework/components/camera/component.js index f260195804a..48032096611 100644 --- a/src/framework/components/camera/component.js +++ b/src/framework/components/camera/component.js @@ -147,11 +147,11 @@ Object.assign(pc, function () { /** * @readonly - * @name pc.CameraComponent#xr - * @type Boolean + * @name pc.CameraComponent#isXr + * @type boolean * @description Queries if camera is in XR mode. */ - Object.defineProperty(CameraComponent.prototype, "xr", { + Object.defineProperty(CameraComponent.prototype, "isXr", { get: function () { return !! this.camera.xr; } From 9aedc7421d06bd57b1cd01ccdebce739fcc6b6b8 Mon Sep 17 00:00:00 2001 From: Maksims Mihejevs Date: Tue, 4 Feb 2020 20:28:46 +0200 Subject: [PATCH 14/20] calculate camera XR frustum based on XR view --- src/scene/forward-renderer.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/scene/forward-renderer.js b/src/scene/forward-renderer.js index 5f60f159312..85594d220b7 100644 --- a/src/scene/forward-renderer.js +++ b/src/scene/forward-renderer.js @@ -532,6 +532,13 @@ Object.assign(pc, function () { }, updateCameraFrustum: function (camera) { + // calculate frustum based on XR view + if (camera.xr && camera.xr.views.length) { + var view = camera.xr.views[0]; + camera.frustum.update(view.projMat, view.viewOffMat); + return; + } + projMat = camera.getProjectionMatrix(); if (camera.overrideCalculateProjection) camera.calculateProjection(projMat, pc.VIEW_CENTER); From 462d537c8427ad44fff50d63fd542822b445e54a Mon Sep 17 00:00:00 2001 From: Maksims Mihejevs Date: Tue, 4 Feb 2020 21:25:51 +0200 Subject: [PATCH 15/20] fix XR normals bug, and change position/rotation calculation --- src/scene/forward-renderer.js | 5 +++-- src/xr/xr-manager.js | 16 ++++------------ 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/scene/forward-renderer.js b/src/scene/forward-renderer.js index 85594d220b7..87828435cc5 100644 --- a/src/scene/forward-renderer.js +++ b/src/scene/forward-renderer.js @@ -619,6 +619,7 @@ Object.assign(pc, function () { this.viewPos[0] = cameraPos.x; this.viewPos[1] = cameraPos.y; this.viewPos[2] = cameraPos.z; + this.viewPosId.setValue(this.viewPos); camera.frustum.update(projMat, viewMat); @@ -1652,9 +1653,9 @@ Object.assign(pc, function () { this.viewPosId.setValue(view.positionOff.data); if (v === 0) { - i += this.drawInstance(device, drawCall, mesh, style); + i += this.drawInstance(device, drawCall, mesh, style, true); } else { - i += this.drawInstance2(device, drawCall, mesh, style); + i += this.drawInstance2(device, drawCall, mesh, style, true); } this._forwardDrawCalls++; diff --git a/src/xr/xr-manager.js b/src/xr/xr-manager.js index 6f52c6db105..e92c94403f8 100644 --- a/src/xr/xr-manager.js +++ b/src/xr/xr-manager.js @@ -403,7 +403,10 @@ Object.assign(pc, function () { } // reset position - this.position.set(0, 0, 0); + var posePosition = this._pose.transform.position; + var poseOrientation = this._pose.transform.orientation; + this.position.set(posePosition.x, posePosition.y, posePosition.z); + this.rotation.set(poseOrientation.x, poseOrientation.y, poseOrientation.z, poseOrientation.w); if (this._pose) { layer = frame.session.renderState.baseLayer; @@ -422,18 +425,7 @@ Object.assign(pc, function () { view.projMat.set(viewRaw.projectionMatrix); view.viewMat.set(viewRaw.transform.inverse.matrix); view.viewInvMat.set(viewRaw.transform.matrix); - - position = viewRaw.transform.position; - view.position.set(position.x, position.y, position.z); - this.position.add(view.position); - - rotation = viewRaw.transform.orientation; - view.rotation.set(rotation.x, rotation.y, rotation.z, rotation.w); - - if (i === 0) this.rotation.copy(view.rotation); } - - this.position.scale(1 / this._pose.views.length); } // position and rotate camera based on calculated vectors From 2616d31aaff7d2cf023ecae424636c53999cd38c Mon Sep 17 00:00:00 2001 From: Maksims Mihejevs Date: Wed, 5 Feb 2020 03:20:47 +0200 Subject: [PATCH 16/20] API renaming/simplification --- examples/xr/augmented-reality-basic.html | 6 ++--- examples/xr/virtual-reality-basic.html | 6 ++--- src/framework/application.js | 2 +- src/framework/components/camera/component.js | 4 +-- src/xr/xr-manager.js | 26 ++++++++++---------- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/examples/xr/augmented-reality-basic.html b/examples/xr/augmented-reality-basic.html index 9b61b812c3a..28e38cc487d 100644 --- a/examples/xr/augmented-reality-basic.html +++ b/examples/xr/augmented-reality-basic.html @@ -145,14 +145,14 @@ // end session by keyboard ESC app.keyboard.on('keydown', function (evt) { if (evt.key === pc.KEY_ESCAPE && app.xr.active) { - app.xr.sessionEnd(); + app.xr.end(); } }); - app.xr.on('session:start', function () { + app.xr.on('start', function () { message("WebXR Immersive AR session has started"); }); - app.xr.on('session:end', function () { + app.xr.on('end', function () { message("WebXR Immersive AR session has ended"); }); app.xr.on('available:' + pc.XR_TYPE_IMMERSIVE_AR, function (available) { diff --git a/examples/xr/virtual-reality-basic.html b/examples/xr/virtual-reality-basic.html index a4bffa38615..73c16def9bb 100644 --- a/examples/xr/virtual-reality-basic.html +++ b/examples/xr/virtual-reality-basic.html @@ -145,14 +145,14 @@ // end session by keyboard ESC app.keyboard.on('keydown', function (evt) { if (evt.key === pc.KEY_ESCAPE && app.xr.active) { - app.xr.sessionEnd(); + app.xr.end(); } }); - app.xr.on('session:start', function () { + app.xr.on('start', function () { message("WebXR Immersive VR session has started"); }); - app.xr.on('session:end', function () { + app.xr.on('end', function () { message("WebXR Immersive VR session has ended"); }); app.xr.on('available:' + pc.XR_TYPE_IMMERSIVE_VR, function (available) { diff --git a/src/framework/application.js b/src/framework/application.js index f370693a943..bdd64cdbe02 100644 --- a/src/framework/application.js +++ b/src/framework/application.js @@ -1732,7 +1732,7 @@ Object.assign(pc, function () { pc.destroyPostEffectQuad(); - this.xr.sessionEnd(); + this.xr.end(); this.graphicsDevice.destroy(); this.graphicsDevice = null; diff --git a/src/framework/components/camera/component.js b/src/framework/components/camera/component.js index 48032096611..caae7b2d5dd 100644 --- a/src/framework/components/camera/component.js +++ b/src/framework/components/camera/component.js @@ -454,7 +454,7 @@ Object.assign(pc, function () { * }); */ startXr: function (type, callback) { - this.system.app.xr.sessionStart(this, type, callback); + this.system.app.xr.start(this, type, callback); }, /** @@ -474,7 +474,7 @@ Object.assign(pc, function () { return; } - this.camera.xr.sessionEnd(callback); + this.camera.xr.end(callback); } }); diff --git a/src/xr/xr-manager.js b/src/xr/xr-manager.js index e92c94403f8..d207fb1c2e2 100644 --- a/src/xr/xr-manager.js +++ b/src/xr/xr-manager.js @@ -110,27 +110,27 @@ Object.assign(pc, function () { /** * @event - * @name pc.XrManager#session:start + * @name pc.XrManager#start * @description Fired when XR session is started * @example - * app.xr.on('session:start', function () { + * app.xr.on('start', function () { * // XR session has started * }); */ /** * @event - * @name pc.XrManager#session:end + * @name pc.XrManager#end * @description Fired when XR session is ended * @example - * app.xr.on('session:end', function () { + * app.xr.on('end', function () { * // XR session has ended * }); */ /** * @function - * @name pc.XrManager#sessionStart + * @name pc.XrManager#start * @description Attempts to start XR session for provided {@link pc.CameraComponent} and optionally fires callback when session is created or failed to create. * @param {pc.CameraComponent} camera - it will be used to render XR session and manipulated based on pose tracking * @param {string} type - session type. Can be one of the following: @@ -141,11 +141,11 @@ Object.assign(pc, function () { * * @example * button.on('click', function () { - * app.xr.sessionStart(camera, PC.XR_TYPE_IMMERSIVE_VR); + * app.xr.start(camera, PC.XR_TYPE_IMMERSIVE_VR); * }); * @param {pc.callbacks.XrError} [callback] - Optional callback function called once session is started. The callback has one argument Error - it is null if successfully started XR session. */ - XrManager.prototype.sessionStart = function (camera, type, callback) { + XrManager.prototype.start = function (camera, type, callback) { if (! this._available[type]) { if (callback) callback(new Error('XR is not available')); return; @@ -179,23 +179,23 @@ Object.assign(pc, function () { /** * @function - * @name pc.XrManager#sessionEnd + * @name pc.XrManager#end * @description Attempts to end XR session and optionally fires callback when session is ended or failed to end. * @example * app.keyboard.on('keydown', function (evt) { * if (evt.key === pc.KEY_ESCAPE && app.xr.active) { - * app.xr.sessionEnd(); + * app.xr.end(); * } * }); * @param {pc.callbacks.XrError} [callback] - Optional callback function called once session is started. The callback has one argument Error - it is null if successfully started XR session. */ - XrManager.prototype.sessionEnd = function (callback) { + XrManager.prototype.end = function (callback) { if (! this._session) { if (callback) callback(new Error('XR Session is not initialized')); return; } - if (callback) this.once('session:end', callback); + if (callback) this.once('end', callback); this._session.end(); }; @@ -290,7 +290,7 @@ Object.assign(pc, function () { // so queue up new tick self.app.tick(); - self.fire('session:end'); + self.fire('end'); }; session.addEventListener('end', onEnd); @@ -317,7 +317,7 @@ Object.assign(pc, function () { self.app.tick(); if (callback) callback(null); - self.fire('session:start'); + self.fire('start'); }); }; From 8b84b9d6eb42e134a8568bd5e12a314ec969d9d6 Mon Sep 17 00:00:00 2001 From: Maksims Mihejevs Date: Wed, 5 Feb 2020 03:40:01 +0200 Subject: [PATCH 17/20] undepricate WebVR --- examples/examples.js | 3 +- examples/graphics/virtual-reality.html | 148 +++++++++ src/backwards-compatibility.js | 33 -- src/callbacks.js | 17 + src/framework/application.js | 41 ++- src/framework/components/camera/component.js | 138 ++++++++ src/framework/components/camera/system.js | 27 ++ src/scene/forward-renderer.js | 135 +++++++- src/vr/vr-display.js | 328 +++++++++++++++++++ src/vr/vr-manager.js | 186 +++++++++++ src/xr/xr-manager.js | 3 +- 11 files changed, 1014 insertions(+), 45 deletions(-) create mode 100644 examples/graphics/virtual-reality.html create mode 100644 src/vr/vr-display.js create mode 100644 src/vr/vr-manager.js diff --git a/examples/examples.js b/examples/examples.js index 5328655d502..8674a7ef6e0 100644 --- a/examples/examples.js +++ b/examples/examples.js @@ -30,7 +30,8 @@ var categories = [ "shader-burn", "shader-toon", "shader-wobble", - "texture-basis" + "texture-basis", + "virtual-reality" ] }, { name: "input", diff --git a/examples/graphics/virtual-reality.html b/examples/graphics/virtual-reality.html new file mode 100644 index 00000000000..6abe4b65642 --- /dev/null +++ b/examples/graphics/virtual-reality.html @@ -0,0 +1,148 @@ + + + + PlayCanvas Virtual Reality + + + + + + + + + + +
+

+
+ + + + + + diff --git a/src/backwards-compatibility.js b/src/backwards-compatibility.js index 69fb766a59c..aa5f19766f4 100644 --- a/src/backwards-compatibility.js +++ b/src/backwards-compatibility.js @@ -514,36 +514,3 @@ pc.SoundManager.prototype.setVolume = function (volume) { // #endif this.volume = volume; }; - -Object.defineProperty(pc.Application.prototype, "vr", { - get: function () { - // #ifdef DEBUG - console.warn('DEPRECATED: pc.Application#vr is deprecated, use pc.Application#xr instead.'); - // #endif - return { isSupported: false }; - } -}); - -pc.Application.prototype.enableVr = function () { - // #ifdef DEBUG - console.warn('DEPRECATED: pc.Application#enableVr is deprecated. Use pc.Application#xr instead.'); - // #endif -}; - -pc.Application.prototype.disableVr = function () { - // #ifdef DEBUG - console.warn('DEPRECATED: pc.Application#disableVr is deprecated. Use pc.Application#xr instead.'); - // #endif -}; - -pc.CameraComponent.prototype.enterVr = function () { - // #ifdef DEBUG - console.warn('DEPRECATED: pc.CameraComponent#enterVr is deprecated. Use pc.CameraComponent#startXr instead.'); - // #endif -}; - -pc.CameraComponent.prototype.exitVr = function () { - // #ifdef DEBUG - console.warn('DEPRECATED: pc.CameraComponent#exitVr is deprecated. Use pc.CameraComponent#endXr instead.'); - // #endif -}; diff --git a/src/callbacks.js b/src/callbacks.js index c2adb29bda9..3acaa1aff73 100644 --- a/src/callbacks.js +++ b/src/callbacks.js @@ -86,6 +86,12 @@ * @param {pc.Vec3} cameraForward - The forward vector of the camera. */ +/** + * @callback pc.callbacks.VrCamera + * @description Callback used by {@link pc.CameraComponent#enterVr} and {@link pc.CameraComponent#exitVr}. + * @param {string|null} err - On success it is null on failure it is the error message. + */ + /** * @private * @callback pc.callbacks.CreateScript @@ -155,6 +161,17 @@ * @returns {object} Returned settings will be used by the shader. */ +/** + * @callback pc.callbacks.VrDisplay + * @description Callback used by {@link pc.VrDisplay#requestPresent} and {@link pc.VrDisplay#exitPresent}. + * @param {string|null} err - The error message if presenting fails, or null if the call succeeds. + */ + +/** + * @callback pc.callbacks.VrFrame + * @description Callback used by {@link pc.VrDisplay#requestAnimationFrame}. + */ + /** * @callback pc.callbacks.XrError * @description Callback used by {@link pc.XrManager#endXr} and {@link pc.XrManager#startXr}. diff --git a/src/framework/application.js b/src/framework/application.js index bdd64cdbe02..b186bde76be 100644 --- a/src/framework/application.js +++ b/src/framework/application.js @@ -266,6 +266,7 @@ Object.assign(pc, function () { this._librariesLoaded = false; this._fillMode = pc.FILLMODE_KEEP_ASPECT; this._resolutionMode = pc.RESOLUTION_FIXED; + this._allowResize = true; // for compatibility this.context = this; @@ -550,6 +551,7 @@ Object.assign(pc, function () { if (this.elementInput) this.elementInput.app = this; + this.vr = null; this.xr = new pc.XrManager(this); this._inTools = false; @@ -1178,6 +1180,8 @@ Object.assign(pc, function () { this.graphicsDevice.updateClientRect(); + if (this.vr) this.vr.poll(); + // #ifdef PROFILER this.stats.frame.updateStart = pc.now(); // #endif @@ -1392,6 +1396,8 @@ Object.assign(pc, function () { * @returns {object} A object containing the values calculated to use as width and height. */ resizeCanvas: function (width, height) { + if (!this._allowResize) return; // prevent resizing (e.g. if presenting in VR HMD) + // prevent resizing when in XR session if (this.xr && this.xr.session) return; @@ -1580,6 +1586,29 @@ Object.assign(pc, function () { } }, + /** + * @function + * @name pc.Application#enableVr + * @description Create and assign a {@link pc.VrManager} object to allow this application render in VR. + */ + enableVr: function () { + if (!this.vr) { + this.vr = new pc.VrManager(this); + } + }, + + /** + * @function + * @name pc.Application#disableVr + * @description Destroy the {@link pc.VrManager}. + */ + disableVr: function () { + if (this.vr) { + this.vr.destroy(); + this.vr = null; + } + }, + _onSkyboxChange: function (asset) { this.scene.setSkybox(asset.resources); }, @@ -1732,6 +1761,10 @@ Object.assign(pc, function () { pc.destroyPostEffectQuad(); + if (this.vr) { + this.vr.destroy(); + this.vr = null; + } this.xr.end(); this.graphicsDevice.destroy(); @@ -1803,7 +1836,9 @@ Object.assign(pc, function () { app._time = now; // Submit a request to queue up a new animation frame immediately - if (app.xr.session) { + if (app.vr && app.vr.display) { + frameRequest = app.vr.display.requestAnimationFrame(app.tick); + } else if (app.xr.session) { frameRequest = app.xr.session.requestAnimationFrame(app.tick); } else { frameRequest = requestAnimationFrame(app.tick); @@ -1836,6 +1871,10 @@ Object.assign(pc, function () { app.fire("frameend", _frameEndData); app.fire("frameEnd", _frameEndData);// deprecated old event, remove when editor updated + + if (app.vr && app.vr.display && app.vr.display.presenting) { + app.vr.display.submitFrame(); + } }; }; diff --git a/src/framework/components/camera/component.js b/src/framework/components/camera/component.js index caae7b2d5dd..2a0fbcda4af 100644 --- a/src/framework/components/camera/component.js +++ b/src/framework/components/camera/component.js @@ -133,6 +133,37 @@ Object.assign(pc, function () { } }); + /** + * @name pc.CameraComponent#vrDisplay + * @type {pc.VrDisplay} + * @description The {@link pc.VrDisplay} that the camera is current displaying to. This is set automatically by calls to {@link pc.CameraComponent#enterVr} + * or {@link pc.CameraComponent#exitVr}. Setting this property to a display directly enables the camera to use the transformation information + * from a display without rendering stereo to it, e.g. for "magic window" style experiences. + * @example + * // enable magic window style interface + * var display = this.app.vr.display; + * if (display) { + * this.entity.camera.vrDisplay = display; + * } + * + * var camera = this.entity.camera; + * camera.enterVr(function (err) { + * if (err) return; + * var display = camera.vrDisplay; // access presenting pc.VrDisplay + * }); + */ + Object.defineProperty(CameraComponent.prototype, "vrDisplay", { + get: function () { + return this.data.camera.vrDisplay; + }, + set: function (value) { + this.data.camera.vrDisplay = value; + if (value) { + value._camera = this.data.camera; + } + } + }); + /** * @readonly * @name pc.CameraComponent#node @@ -432,6 +463,113 @@ Object.assign(pc, function () { this.data.isRendering = false; }, + /** + * @function + * @name pc.CameraComponent#enterVr + * @description Attempt to start presenting this camera to a {@link pc.VrDisplay}. + * @param {pc.callbacks.VrCamera} callback - Function called once to indicate success of failure. The callback takes one argument (err). + * On success it returns null on failure it returns the error message. + * @example + * // On an entity with a camera component + * this.entity.camera.enterVr(function (err) { + * if (err) { + * console.error(err); + * } else { + * // in VR! + * } + * }); + */ + /** + * @function + * @name pc.CameraComponent#enterVr + * @variation 2 + * @description Attempt to start presenting this camera to a {@link pc.VrDisplay}. + * @param {pc.VrDisplay} display - The VrDisplay to present. If not supplied this uses {@link pc.VrManager#display} as the default. + * @param {pc.callbacks.VrCamera} callback - Function called once to indicate success of failure. The callback takes one argument (err). + * On success it returns null on failure it returns the error message. + * @example + * // On an entity with a camera component + * this.entity.camera.enterVr(function (err) { + * if (err) { + * console.error(err); + * } else { + * // in VR! + * } + * }); + */ + enterVr: function (display, callback) { + if ((display instanceof Function) && !callback) { + callback = display; + display = null; + } + + if (!this.system.app.vr) { + callback("VrManager not created. Enable VR in project settings."); + return; + } + + if (!display) { + display = this.system.app.vr.display; + } + + if (display) { + var self = this; + if (display.capabilities.canPresent) { + // try and present + display.requestPresent(function (err) { + if (!err) { + self.vrDisplay = display; + // camera component uses internal 'before' event + // this means display nulled before anyone other + // code gets to update + self.vrDisplay.once('beforepresentchange', function (display) { + if (!display.presenting) { + self.vrDisplay = null; + } + }); + } + callback(err); + }); + } else { + // mono rendering + self.vrDisplay = display; + callback(); + } + } else { + callback("No pc.VrDisplay to present"); + } + }, + + /** + * @function + * @name pc.CameraComponent#exitVr + * @description Attempt to stop presenting this camera. + * @param {pc.callbacks.VrCamera} callback - Function called once to indicate success of failure. The callback takes one argument (err). + * On success it returns null on failure it returns the error message. + * @example + * this.entity.camera.exitVr(function (err) { + * if (err) { + * console.error(err); + * } else { + * // exited successfully + * } + * }); + */ + exitVr: function (callback) { + if (this.vrDisplay) { + if (this.vrDisplay.capabilities.canPresent) { + var display = this.vrDisplay; + this.vrDisplay = null; + display.exitPresent(callback); + } else { + this.vrDisplay = null; + callback(); + } + } else { + callback("Not presenting VR"); + } + }, + /** * @function * @name pc.CameraComponent#startXr diff --git a/src/framework/components/camera/system.js b/src/framework/components/camera/system.js index eec09307cf0..454d7d5745d 100644 --- a/src/framework/components/camera/system.js +++ b/src/framework/components/camera/system.js @@ -54,6 +54,8 @@ Object.assign(pc, function () { this.on('beforeremove', this.onBeforeRemove, this); this.on('remove', this.onRemove, this); this.app.on("prerender", this.onPrerender, this); + + pc.ComponentSystem.bind('update', this.onUpdate, this); }; CameraComponentSystem.prototype = Object.create(pc.ComponentSystem.prototype); CameraComponentSystem.prototype.constructor = CameraComponentSystem; @@ -154,6 +156,31 @@ Object.assign(pc, function () { data.camera = null; }, + onUpdate: function (dt) { + var components = this.store; + var component, componentData, cam, vrDisplay; + + if (this.app.vr) { + for (var id in components) { + component = components[id]; + componentData = component.data; + cam = componentData.camera; + vrDisplay = cam.vrDisplay; + if (componentData.enabled && component.entity.enabled && vrDisplay) { + // Change WebVR near/far planes based on the stereo camera + vrDisplay.setClipPlanes(cam._nearClip, cam._farClip); + + // update camera node transform from VrDisplay + if (cam._node) { + cam._node.localTransform.copy(vrDisplay.combinedViewInv); + cam._node._dirtyLocal = false; + cam._node._dirtifyWorld(); + } + } + } + } + }, + onPrerender: function () { for (var i = 0, len = this.cameras.length; i < len; i++) { this.cameras[i].onPrerender(); diff --git a/src/scene/forward-renderer.js b/src/scene/forward-renderer.js index 87828435cc5..f04789a8002 100644 --- a/src/scene/forward-renderer.js +++ b/src/scene/forward-renderer.js @@ -34,6 +34,18 @@ Object.assign(pc, function () { var viewProjMat = new pc.Mat4(); var projMat; + var viewInvL = new pc.Mat4(); + var viewInvR = new pc.Mat4(); + var viewL = new pc.Mat4(); + var viewR = new pc.Mat4(); + var viewPosL = new pc.Vec3(); + var viewPosR = new pc.Vec3(); + var projL, projR; + var viewMat3L = new pc.Mat4(); + var viewMat3R = new pc.Mat4(); + var viewProjMatL = new pc.Mat4(); + var viewProjMatR = new pc.Mat4(); + var worldMatX = new pc.Vec3(); var worldMatY = new pc.Vec3(); var worldMatZ = new pc.Vec3(); @@ -532,8 +544,19 @@ Object.assign(pc, function () { }, updateCameraFrustum: function (camera) { - // calculate frustum based on XR view - if (camera.xr && camera.xr.views.length) { + if (camera.vrDisplay && camera.vrDisplay.presenting) { + projMat = camera.vrDisplay.combinedProj; + var parent = camera._node.parent; + if (parent) { + viewMat.copy(parent.getWorldTransform()).mul(camera.vrDisplay.combinedViewInv).invert(); + } else { + viewMat.copy(camera.vrDisplay.combinedView); + } + viewInvMat.copy(viewMat).invert(); + this.viewInvId.setValue(viewInvMat.data); + camera.frustum.update(projMat, viewMat); + } else if (camera.xr && camera.xr.views.length) { + // calculate frustum based on XR view var view = camera.xr.views[0]; camera.frustum.update(view.projMat, view.viewOffMat); return; @@ -557,13 +580,78 @@ Object.assign(pc, function () { // make sure colorWrite is set to true to all channels, if you want to fully clear the target setCamera: function (camera, target, clear, cullBorder) { - if (camera.xr && camera.xr.session) { - var parent = camera._node.parent; - var transform; - if (parent) { - transform = parent.getWorldTransform(); + var vrDisplay = camera.vrDisplay; + var parent, transform; + + if (vrDisplay && vrDisplay.presenting) { + // Projection LR + projL = vrDisplay.leftProj; + projR = vrDisplay.rightProj; + projMat = vrDisplay.combinedProj; + if (camera.overrideCalculateProjection) { + camera.calculateProjection(projL, pc.VIEW_LEFT); + camera.calculateProjection(projR, pc.VIEW_RIGHT); + camera.calculateProjection(projMat, pc.VIEW_CENTER); } + if (camera.overrideCalculateTransform) { + camera.calculateTransform(viewInvL, pc.VIEW_LEFT); + camera.calculateTransform(viewInvR, pc.VIEW_RIGHT); + camera.calculateTransform(viewInvMat, pc.VIEW_CENTER); + viewL.copy(viewInvL).invert(); + viewR.copy(viewInvR).invert(); + viewMat.copy(viewInvMat).invert(); + } else { + parent = camera._node.parent; + if (parent) { + transform = parent.getWorldTransform(); + + // ViewInverse LR (parent) + viewInvL.mul2(transform, vrDisplay.leftViewInv); + viewInvR.mul2(transform, vrDisplay.rightViewInv); + + // View LR (parent) + viewL.copy(viewInvL).invert(); + viewR.copy(viewInvR).invert(); + + // Combined view (parent) + viewMat.copy(parent.getWorldTransform()).mul(vrDisplay.combinedViewInv).invert(); + } else { + // ViewInverse LR + viewInvL.copy(vrDisplay.leftViewInv); + viewInvR.copy(vrDisplay.rightViewInv); + + // View LR + viewL.copy(vrDisplay.leftView); + viewR.copy(vrDisplay.rightView); + + // Combined view + viewMat.copy(vrDisplay.combinedView); + } + } + + // View 3x3 LR + mat3FromMat4(viewMat3L, viewL); + mat3FromMat4(viewMat3R, viewR); + + // ViewProjection LR + viewProjMatL.mul2(projL, viewL); + viewProjMatR.mul2(projR, viewR); + + // View Position LR + viewPosL.x = viewInvL.data[12]; + viewPosL.y = viewInvL.data[13]; + viewPosL.z = viewInvL.data[14]; + + viewPosR.x = viewInvR.data[12]; + viewPosR.y = viewInvR.data[13]; + viewPosR.z = viewInvR.data[14]; + + camera.frustum.update(projMat, viewMat); + } else if (camera.xr && camera.xr.session) { + parent = camera._node.parent; + if (parent) transform = parent.getWorldTransform(); + var views = camera.xr.views; for (var v = 0; v < views.length; v++) { @@ -1455,6 +1543,7 @@ Object.assign(pc, function () { renderForward: function (camera, drawCalls, drawCallsCount, sortedLights, pass, cullingMask, drawCallback, layer) { var device = this.device; var scene = this.scene; + var vrDisplay = camera.vrDisplay; var passFlag = 1 << pass; var lightHash = layer ? layer._lightHash : 0; @@ -1468,6 +1557,8 @@ Object.assign(pc, function () { var paramName, parameter, parameters; var stencilFront, stencilBack; + var halfWidth = device.width * 0.5; + // Render the scene for (i = 0; i < drawCallsCount; i++) { @@ -1637,7 +1728,35 @@ Object.assign(pc, function () { drawCallback(drawCall, i); } - if (camera.xr && camera.xr.session && camera.xr.views.length) { + if (vrDisplay && vrDisplay.presenting) { + // Left + device.setViewport(0, 0, halfWidth, device.height); + this.projId.setValue(projL.data); + this.viewInvId.setValue(viewInvL.data); + this.viewId.setValue(viewL.data); + this.viewId3.setValue(viewMat3L.data); + this.viewProjId.setValue(viewProjMatL.data); + this.viewPos[0] = viewPosL.x; + this.viewPos[1] = viewPosL.y; + this.viewPos[2] = viewPosL.z; + this.viewPosId.setValue(this.viewPos); + i += this.drawInstance(device, drawCall, mesh, style, true); + this._forwardDrawCalls++; + + // Right + device.setViewport(halfWidth, 0, halfWidth, device.height); + this.projId.setValue(projR.data); + this.viewInvId.setValue(viewInvR.data); + this.viewId.setValue(viewR.data); + this.viewId3.setValue(viewMat3R.data); + this.viewProjId.setValue(viewProjMatR.data); + this.viewPos[0] = viewPosR.x; + this.viewPos[1] = viewPosR.y; + this.viewPos[2] = viewPosR.z; + this.viewPosId.setValue(this.viewPos); + i += this.drawInstance2(device, drawCall, mesh, style); + this._forwardDrawCalls++; + } else if (camera.xr && camera.xr.session && camera.xr.views.length) { var views = camera.xr.views; for (var v = 0; v < views.length; v++) { diff --git a/src/vr/vr-display.js b/src/vr/vr-display.js new file mode 100644 index 00000000000..102a60af889 --- /dev/null +++ b/src/vr/vr-display.js @@ -0,0 +1,328 @@ +Object.assign(pc, function () { + /** + * @class + * @name pc.VrDisplay + * @augments pc.EventHandler + * @classdesc Represents a single Display for VR content. This could be a Head Mounted display that can present content on a separate screen + * or a phone which can display content full screen on the same screen. This object contains the native `navigator.VRDisplay` object + * from the WebVR API. + * @description Represents a single Display for VR content. This could be a Head Mounted display that can present content on a separate screen + * or a phone which can display content full screen on the same screen. This object contains the native `navigator.VRDisplay` object + * from the WebVR API. + * @param {pc.Application} app - The application outputting to this VR display. + * @param {VRDisplay} display - The native VRDisplay object from the WebVR API. + * @property {number} id An identifier for this distinct VRDisplay. + * @property {VRDisplay} display The native VRDisplay object from the WebVR API. + * @property {boolean} presenting True if this display is currently presenting VR content. + * @property {VRDisplayCapabilities} capabilities Returns the VRDisplayCapabilities object from the VRDisplay. + * This can be used to determine what features are available on this display. + */ + var VrDisplay = function (app, display) { + pc.EventHandler.call(this); + + var self = this; + + this._app = app; + this._device = app.graphicsDevice; + + this.id = display.displayId; + + this._frameData = null; + if (window.VRFrameData) { + this._frameData = new window.VRFrameData(); + } + this.display = display; + + this._camera = null; // camera component + + this.sitToStandInv = new pc.Mat4(); + + this.leftView = new pc.Mat4(); + this.leftProj = new pc.Mat4(); + this.leftViewInv = new pc.Mat4(); + this.leftPos = new pc.Vec3(); + + this.rightView = new pc.Mat4(); + this.rightProj = new pc.Mat4(); + this.rightViewInv = new pc.Mat4(); + this.rightPos = new pc.Vec3(); + + this.combinedPos = new pc.Vec3(); + this.combinedView = new pc.Mat4(); + this.combinedProj = new pc.Mat4(); + this.combinedViewInv = new pc.Mat4(); + this.combinedFov = 0; + this.combinedAspect = 0; + + this.presenting = false; + + self._presentChange = function (event) { + var display; + // handle various events formats + if (event.display) { + // this is the official spec event format + display = event.display; + } else if (event.detail && event.detail.display) { + // webvr-polyfill uses this + display = event.detail.display; + } else if (event.detail && event.detail.vrdisplay) { + // this was used in the webvr emulation chrome extension + display = event.detail.vrdisplay; + } else { + // final catch all is to use this display as Firefox Nightly (54.0a1) + // does not include the display within the event data + display = self.display; + } + + // check if event refers to this display + if (display === self.display) { + self.presenting = (self.display && self.display.isPresenting); + + if (self.presenting) { + var leftEye = self.display.getEyeParameters("left"); + var rightEye = self.display.getEyeParameters("right"); + var w = Math.max(leftEye.renderWidth, rightEye.renderWidth) * 2; + var h = Math.max(leftEye.renderHeight, rightEye.renderHeight); + // set canvas resolution to the display resolution + self._app.graphicsDevice.setResolution(w, h); + // prevent window resizing from resizing it + self._app._allowResize = false; + } else { + // restore original resolution + self._app.setCanvasResolution(pc.RESOLUTION_AUTO); + self._app._allowResize = true; + } + + self.fire('beforepresentchange', self); // fire internal event for camera component + self.fire('presentchange', self); + } + }; + window.addEventListener('vrdisplaypresentchange', self._presentChange, false); + }; + VrDisplay.prototype = Object.create(pc.EventHandler.prototype); + VrDisplay.prototype.constructor = VrDisplay; + + Object.assign(VrDisplay.prototype, { + /** + * @function + * @name pc.VrDisplay#destroy + * @description Destroy this display object. + */ + destroy: function () { + window.removeEventListener('vrdisplaypresentchange', self._presentChange); + if (this._camera) this._camera.vrDisplay = null; + this._camera = null; + }, + + /** + * @function + * @name pc.VrDisplay#poll + * @description Called once per frame to update the current status from the display. Usually called by {@link pc.VrManager}. + */ + poll: function () { + if (this.display) { + this.display.getFrameData(this._frameData); + + this.leftProj.data = this._frameData.leftProjectionMatrix; + this.rightProj.data = this._frameData.rightProjectionMatrix; + + var stage = this.display.stageParameters; + if (stage) { + + this.sitToStandInv.set(stage.sittingToStandingTransform).invert(); + + this.combinedView.set(this._frameData.leftViewMatrix); + this.leftView.mul2(this.combinedView, this.sitToStandInv); + + this.combinedView.set(this._frameData.rightViewMatrix); + this.rightView.mul2(this.combinedView, this.sitToStandInv); + } else { + + this.leftView.set(this._frameData.leftViewMatrix); + this.rightView.set(this._frameData.rightViewMatrix); + } + + // Find combined position and view matrix + // Camera is offset backwards to cover both frustums + + // Extract widest frustum plane and calculate fov + var nx = this.leftProj.data[3] + this.leftProj.data[0]; + var nz = this.leftProj.data[11] + this.leftProj.data[8]; + var l = 1.0 / Math.sqrt(nx * nx + nz * nz); + nx *= l; + nz *= l; + var maxFov = -Math.atan2(nz, nx); + + nx = this.rightProj.data[3] + this.rightProj.data[0]; + nz = this.rightProj.data[11] + this.rightProj.data[8]; + l = 1.0 / Math.sqrt(nx * nx + nz * nz); + nx *= l; + nz *= l; + maxFov = Math.max(maxFov, -Math.atan2(nz, nx)); + maxFov *= 2.0; + + this.combinedFov = maxFov; + + var aspect = this.rightProj.data[5] / this.rightProj.data[0]; + this.combinedAspect = aspect; + + var view = this.combinedView; + view.copy(this.leftView); + view.invert(); + this.leftViewInv.copy(view); + var pos = this.combinedPos; + pos.x = this.leftPos.x = view.data[12]; + pos.y = this.leftPos.y = view.data[13]; + pos.z = this.leftPos.z = view.data[14]; + view.copy(this.rightView); + view.invert(); + this.rightViewInv.copy(view); + var deltaX = pos.x - view.data[12]; + var deltaY = pos.y - view.data[13]; + var deltaZ = pos.z - view.data[14]; + var dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ); + this.rightPos.x = view.data[12]; + this.rightPos.y = view.data[13]; + this.rightPos.z = view.data[14]; + pos.x += view.data[12]; + pos.y += view.data[13]; + pos.z += view.data[14]; + pos.x *= 0.5; // middle pos + pos.y *= 0.5; + pos.z *= 0.5; + var b = Math.PI * 0.5; + var c = maxFov * 0.5; + var a = Math.PI - (b + c); + var offset = dist * 0.5 * ( Math.sin(a) );// / Math.sin(b) ); // equals 1 + var fwdX = view.data[8]; + var fwdY = view.data[9]; + var fwdZ = view.data[10]; + view.data[12] = pos.x + fwdX * offset; // our forward goes backwards so + instead of - + view.data[13] = pos.y + fwdY * offset; + view.data[14] = pos.z + fwdZ * offset; + this.combinedViewInv.copy(view); + view.invert(); + + // Find combined projection matrix + this.combinedProj.setPerspective(maxFov * pc.math.RAD_TO_DEG, + aspect, + this.display.depthNear + offset, + this.display.depthFar + offset, + true); + } + }, + + /** + * @function + * @name pc.VrDisplay#requestPresent + * @description Try to present full screen VR content on this display. + * @param {pc.callbacks.VrDisplay} callback - Called when the request is completed. Callback takes a single argument (err) that is the error message return + * if presenting fails, or null if the call succeeds. Usually called by {@link pc.CameraComponent#enterVr}. + */ + requestPresent: function (callback) { + if (!this.display) { + if (callback) callback(new Error("No VrDisplay to requestPresent")); + return; + } + + if (this.presenting) { + if (callback) callback(new Error("VrDisplay already presenting")); + return; + } + + this.display.requestPresent([{ source: this._device.canvas }]).then(function () { + if (callback) callback(); + }, function (err) { + if (callback) callback(err); + }); + }, + + /** + * @function + * @name pc.VrDisplay#exitPresent + * @description Try to stop presenting VR content on this display. + * @param {pc.callbacks.VrDisplay} callback - Called when the request is completed. Callback takes a single argument (err) that is the error message return + * if presenting fails, or null if the call succeeds. Usually called by {@link pc.CameraComponent#exitVr}. + */ + exitPresent: function (callback) { + if (!this.display) { + if (callback) callback(new Error("No VrDisplay to exitPresent")); + } + + if (!this.presenting) { + if (callback) callback(new Error("VrDisplay not presenting")); + return; + } + + this.display.exitPresent().then(function () { + if (callback) callback(); + }, function () { + if (callback) callback(new Error("exitPresent failed")); + }); + }, + + /** + * @function + * @name pc.VrDisplay#requestAnimationFrame + * @description Used in the main application loop instead of the regular `window.requestAnimationFrame`. Usually only called from inside {@link pc.Application}. + * @param {pc.callbacks.VrFrame} fn - Function called when it is time to update the frame. + */ + requestAnimationFrame: function (fn) { + if (this.display) this.display.requestAnimationFrame(fn); + }, + + /** + * @function + * @name pc.VrDisplay#submitFrame + * @description Called when animation update is complete and the frame is ready to be sent to the display. Usually only called from inside {@link pc.Application}. + */ + submitFrame: function () { + if (this.display) this.display.submitFrame(); + }, + + /** + * @function + * @name pc.VrDisplay#reset + * @description Called to reset the pose of the pc.VrDisplay. Treating its current pose as the origin/zero. This should only be called in 'sitting' experiences. + */ + reset: function () { + if (this.display) this.display.resetPose(); + }, + + /** + * @function + * @name pc.VrDisplay#setClipPlanes + * @description Set the near and far depth plans of the display. This enables mapping of values in the + * render target depth attachment to scene coordinates. + * @param {number} n - The near depth distance. + * @param {number} f - The far depth distance. + */ + setClipPlanes: function (n, f) { + if (this.display) { + this.display.depthNear = n; + this.display.depthFar = f; + } + }, + + /** + * @function + * @name pc.VrDisplay#getFrameData + * @description Return the current frame data that is updated during polling. + * @returns {VRFrameData} The frame data object. + */ + getFrameData: function () { + if (this.display) return this._frameData; + } + }); + + Object.defineProperty(VrDisplay.prototype, "capabilities", { + get: function () { + if (this.display) return this.display.capabilities; + return {}; + } + }); + + return { + VrDisplay: VrDisplay + }; +}()); diff --git a/src/vr/vr-manager.js b/src/vr/vr-manager.js new file mode 100644 index 00000000000..5933f49e461 --- /dev/null +++ b/src/vr/vr-manager.js @@ -0,0 +1,186 @@ +Object.assign(pc, function () { + /** + * @class + * @name pc.VrManager + * @augments pc.EventHandler + * @classdesc Manage and update {@link pc.VrDisplay}s that are attached to this device. + * @description Manage and update {@link pc.VrDisplay}s that are attached to this device. + * @param {pc.Application} app - The main application. + * @property {pc.VrDisplay[]} displays The list of {@link pc.VrDisplay}s that are attached to this device. + * @property {pc.VrDisplay} display The default {@link pc.VrDisplay} to be used. Usually the first in the `displays` list. + * @property {boolean} isSupported Reports whether this device supports the WebVR API. + */ + var VrManager = function (app) { + pc.EventHandler.call(this); + + var self = this; + + this.isSupported = VrManager.isSupported; + + this._index = { }; + this.displays = []; + this.display = null; // primary display (usually the first in list) + + this._app = app; + + // bind functions for event callbacks + this._onDisplayConnect = this._onDisplayConnect.bind(this); + this._onDisplayDisconnect = this._onDisplayDisconnect.bind(this); + + self._attach(); + + this._getDisplays(function (err, displays) { + if (err) { + // webvr not available + self.fire('error', err); + } else { + for (var i = 0; i < displays.length; i++) { + self._addDisplay(displays[i]); + } + + self.fire('ready', self.displays); + } + }); + }; + VrManager.prototype = Object.create(pc.EventHandler.prototype); + VrManager.prototype.constructor = VrManager; + + /** + * @event + * @name pc.VrManager#displayconnect + * @description Fired when an VR display is connected. + * @param {pc.VrDisplay} display - The {@link pc.VrDisplay} that has just been connected. + * @example + * this.app.vr.on("displayconnect", function (display) { + * // use `display` here + * }); + */ + + /** + * @event + * @name pc.VrManager#displaydisconnect + * @description Fired when an VR display is disconnected. + * @param {pc.VrDisplay} display - The {@link pc.VrDisplay} that has just been disconnected. + * @example + * this.app.vr.on("displaydisconnect", function (display) { + * // `display` is no longer connected + * }); + */ + + /** + * @static + * @name pc.VrManager.isSupported + * @type {boolean} + * @description Reports whether this device supports the WebVR API. + */ + if (typeof navigator !== 'undefined') { + VrManager.isSupported = !!navigator.getVRDisplays; + } else { + VrManager.isSupported = false; + } + + Object.assign(VrManager.prototype, { + _attach: function () { + window.addEventListener('vrdisplayconnect', this._onDisplayConnect); + window.addEventListener('vrdisplaydisconnect', this._onDisplayDisconnect); + }, + + _detach: function () { + window.removeEventListener('vrdisplayconnect', this._onDisplayConnect); + window.removeEventListener('vrdisplaydisconnect', this._onDisplayDisconnect); + }, + + /** + * @function + * @name pc.VrManager#destroy + * @description Remove events and clear up manager. + */ + destroy: function () { + this._detach(); + }, + + /** + * @function + * @name pc.VrManager#poll + * @description Called once per frame to poll all attached displays. + */ + poll: function () { + var l = this.displays.length; + if (!l) return; + for (var i = 0; i < l; i++) { + if (this.displays[i]._camera) this.displays[i].poll(); + } + }, + + _getDisplays: function (callback) { + if (navigator.getVRDisplays) { + navigator.getVRDisplays().then(function (displays) { + if (callback) callback(null, displays); + }); + } else { + if (callback) callback(new Error('WebVR not supported')); + } + }, + + _addDisplay: function (vrDisplay) { + if (this._index[vrDisplay.displayId]) + return; + + var display = new pc.VrDisplay(this._app, vrDisplay); + this._index[display.id] = display; + this.displays.push(display); + + if (!this.display) + this.display = display; + + this.fire('displayconnect', display); + }, + + _onDisplayConnect: function (e) { + if (e.detail && e.detail.display) { + // polyfill has different event format + this._addDisplay(e.detail.display); + } else { + // real event API + this._addDisplay(e.display); + } + + }, + + _onDisplayDisconnect: function (e) { + var id; + if (e.detail && e.detail.display) { + // polyfill has different event format + id = e.detail.display.displayId; + } else { + // real event API + id = e.display.displayId; + } + + var display = this._index[id]; + if (!display) + return; + + display.destroy(); + + delete this._index[display.id]; + + var ind = this.displays.indexOf(display); + this.displays.splice(ind, 1); + + if (this.display === display) { + if (this.displays.length) { + this.display = this.displays[0]; + } else { + this.display = null; + } + } + + this.fire('displaydisconnect', display); + } + }); + + return { + VrManager: VrManager + }; +}()); diff --git a/src/xr/xr-manager.js b/src/xr/xr-manager.js index d207fb1c2e2..115d3c3b768 100644 --- a/src/xr/xr-manager.js +++ b/src/xr/xr-manager.js @@ -357,8 +357,7 @@ Object.assign(pc, function () { XrManager.prototype.calculateViews = function (frame) { if (! this._session) return; - var i, view, viewRaw, layer; - var viewport, position, rotation; + var i, view, viewRaw, layer, viewport; var lengthNew; // canvas resolution should be set on first frame availability or resolution changes From f500fb2babe4865602a1ab3775108e4dd38d8799 Mon Sep 17 00:00:00 2001 From: Maksims Mihejevs Date: Wed, 5 Feb 2020 03:45:15 +0200 Subject: [PATCH 18/20] fixes --- build/dependencies.txt | 2 ++ src/framework/components/camera/component.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/build/dependencies.txt b/build/dependencies.txt index 69c49c7e926..89e0193954b 100644 --- a/build/dependencies.txt +++ b/build/dependencies.txt @@ -109,6 +109,8 @@ ../src/input/touch.js ../src/input/controller.js ../src/input/element-input.js +../src/vr/vr-manager.js +../src/vr/vr-display.js ../src/xr/xr-manager.js ../src/net/http.js ../src/script/script-registry.js diff --git a/src/framework/components/camera/component.js b/src/framework/components/camera/component.js index 2a0fbcda4af..e51ca404d82 100644 --- a/src/framework/components/camera/component.js +++ b/src/framework/components/camera/component.js @@ -167,7 +167,7 @@ Object.assign(pc, function () { /** * @readonly * @name pc.CameraComponent#node - * @type pc.GraphNode + * @type {pc.GraphNode} * @description Queries the camera's GraphNode. Can be used to get position and rotation. */ Object.defineProperty(CameraComponent.prototype, "node", { From c6dd5164c22ab7703ad4dc6f02d02030c0fef733 Mon Sep 17 00:00:00 2001 From: Maksims Mihejevs Date: Thu, 6 Feb 2020 09:02:33 +0200 Subject: [PATCH 19/20] rename pc.XR_TYPE_* to simplified version --- src/framework/application.js | 2 +- src/framework/components/camera/component.js | 8 +++--- src/xr/xr-manager.js | 30 ++++++++++---------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/framework/application.js b/src/framework/application.js index b186bde76be..965e224e15a 100644 --- a/src/framework/application.js +++ b/src/framework/application.js @@ -130,7 +130,7 @@ Object.assign(pc, function () { * @description The XR Manager that provides ability to start VR/AR sessions. * @example * // check if VR is available - * if (app.xr.isAvailable(pc.XR_TYPE_IMMERSIVE_VR)) { + * if (app.xr.isAvailable(pc.XRTYPE_VR)) { * // VR is available * } */ diff --git a/src/framework/components/camera/component.js b/src/framework/components/camera/component.js index e51ca404d82..5f957172f03 100644 --- a/src/framework/components/camera/component.js +++ b/src/framework/components/camera/component.js @@ -576,14 +576,14 @@ Object.assign(pc, function () { * @description Attempt to start XR session with this camera * @param {string} type - The type of session. Can be one of the following: * - * * {@link pc.XR_TYPE_INLINE}: Inline - always available type of session. It has limited features availability and is rendered into HTML element. - * * {@link pc.XR_TYPE_IMMERSIVE_VR}: Immersive VR - session that provides exclusive access to VR device with best available tracking features. - * * {@link pc.XR_TYPE_IMMERSIVE_AR}: Immersive AR - session that provides exclusive access to VR/AR device that is intended to be blended with real-world environment. + * * {@link pc.XRTYPE_INLINE}: Inline - always available type of session. It has limited features availability and is rendered into HTML element. + * * {@link pc.XRTYPE_VR}: Immersive VR - session that provides exclusive access to VR device with best available tracking features. + * * {@link pc.XRTYPE_AR}: Immersive AR - session that provides exclusive access to VR/AR device that is intended to be blended with real-world environment. * * @param {pc.callbacks.XrError} [callback] - Optional callback function called once session is started. The callback has one argument Error - it is null if successfully started XR session. * @example * // On an entity with a camera component - * this.entity.camera.startXr(PC.XR_TYPE_IMMERSIVE_VR, function (err) { + * this.entity.camera.startXr(pc.XRTYPE_VR, function (err) { * if (err) { * // failed to start XR session * } else { diff --git a/src/xr/xr-manager.js b/src/xr/xr-manager.js index 115d3c3b768..26763f9f340 100644 --- a/src/xr/xr-manager.js +++ b/src/xr/xr-manager.js @@ -3,26 +3,26 @@ Object.assign(pc, function () { /** * @constant * @type string - * @name pc.XR_TYPE_INLINE + * @name pc.XRTYPE_INLINE * @description Inline - always available type of session. It has limited features availability and is rendered into HTML element. */ - XR_TYPE_INLINE: 'inline', + XRTYPE_INLINE: 'inline', /** * @constant * @type string - * @name pc.XR_TYPE_IMMERSIVE_VR + * @name pc.XRTYPE_VR * @description Immersive VR - session that provides exclusive access to VR device with best available tracking features. */ - XR_TYPE_IMMERSIVE_VR: 'immersive-vr', + XRTYPE_VR: 'immersive-vr', /** * @constant * @type string - * @name pc.XR_TYPE_IMMERSIVE_AR + * @name pc.XRTYPE_AR * @description Immersive AR - session that provides exclusive access to VR/AR device that is intended to be blended with real-world environment. */ - XR_TYPE_IMMERSIVE_AR: 'immersive-ar' + XRTYPE_AR: 'immersive-ar' }; @@ -103,7 +103,7 @@ Object.assign(pc, function () { * @description Fired when availability of specific XR type is changed. * @param {boolean} available - True if specified session type is now available. * @example - * app.xr.on('available:' + pc.XR_TYPE_IMMERSIVE_VR, function (available) { + * app.xr.on('available:' + pc.XRTYPE_VR, function (available) { * console.log('Immersive VR session is now ' + (available ? 'available' : 'unavailable')); * }); */ @@ -135,13 +135,13 @@ Object.assign(pc, function () { * @param {pc.CameraComponent} camera - it will be used to render XR session and manipulated based on pose tracking * @param {string} type - session type. Can be one of the following: * - * * {@link pc.XR_TYPE_INLINE}: Inline - always available type of session. It has limited features availability and is rendered into HTML element. - * * {@link pc.XR_TYPE_IMMERSIVE_VR}: Immersive VR - session that provides exclusive access to VR device with best available tracking features. - * * {@link pc.XR_TYPE_IMMERSIVE_AR}: Immersive AR - session that provides exclusive access to VR/AR device that is intended to be blended with real-world environment. + * * {@link pc.XRTYPE_INLINE}: Inline - always available type of session. It has limited features availability and is rendered into HTML element. + * * {@link pc.XRTYPE_VR}: Immersive VR - session that provides exclusive access to VR device with best available tracking features. + * * {@link pc.XRTYPE_AR}: Immersive AR - session that provides exclusive access to VR/AR device that is intended to be blended with real-world environment. * * @example * button.on('click', function () { - * app.xr.start(camera, PC.XR_TYPE_IMMERSIVE_VR); + * app.xr.start(camera, PC.XRTYPE_VR); * }); * @param {pc.callbacks.XrError} [callback] - Optional callback function called once session is started. The callback has one argument Error - it is null if successfully started XR session. */ @@ -206,12 +206,12 @@ Object.assign(pc, function () { * @description Check if specific type of session is available * @param {string} type - session type. Can be one of the following: * - * * {@link pc.XR_TYPE_INLINE}: Inline - always available type of session. It has limited features availability and is rendered into HTML element. - * * {@link pc.XR_TYPE_IMMERSIVE_VR}: Immersive VR - session that provides exclusive access to VR device with best available tracking features. - * * {@link pc.XR_TYPE_IMMERSIVE_AR}: Immersive AR - session that provides exclusive access to VR/AR device that is intended to be blended with real-world environment. + * * {@link pc.XRTYPE_INLINE}: Inline - always available type of session. It has limited features availability and is rendered into HTML element. + * * {@link pc.XRTYPE_VR}: Immersive VR - session that provides exclusive access to VR device with best available tracking features. + * * {@link pc.XRTYPE_AR}: Immersive AR - session that provides exclusive access to VR/AR device that is intended to be blended with real-world environment. * * @example - * if (app.xr.isAvailable(pc.XR_TYPE_IMMERSIVE_VR)) { + * if (app.xr.isAvailable(pc.XRTYPE_VR)) { * // VR is available * } * @returns {boolean} True if specified session type is available. From b0c78bf7e8bfdd6f94a5c4f1796bf46a66e73d86 Mon Sep 17 00:00:00 2001 From: Maksims Mihejevs Date: Thu, 6 Feb 2020 09:11:40 +0200 Subject: [PATCH 20/20] remove CameraComponent.isXr property --- src/framework/components/camera/component.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/framework/components/camera/component.js b/src/framework/components/camera/component.js index 5f957172f03..4d57f31bf43 100644 --- a/src/framework/components/camera/component.js +++ b/src/framework/components/camera/component.js @@ -176,18 +176,6 @@ Object.assign(pc, function () { } }); - /** - * @readonly - * @name pc.CameraComponent#isXr - * @type boolean - * @description Queries if camera is in XR mode. - */ - Object.defineProperty(CameraComponent.prototype, "isXr", { - get: function () { - return !! this.camera.xr; - } - }); - Object.assign(CameraComponent.prototype, { /** * @function