From 36e7cdcc19fed037c1aa36606cea861090b9538e Mon Sep 17 00:00:00 2001 From: Kevin Staunton-Lambert Date: Tue, 13 Dec 2022 09:40:20 +1100 Subject: [PATCH] chore: Swap webvr ro webxr polyfill packages (continue to use webvr polyfill if webxr immersive is not available) --- package.json | 1 + src/plugin.js | 123 +++++---- vendor/three/VREffect.js | 538 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 616 insertions(+), 46 deletions(-) create mode 100644 vendor/three/VREffect.js diff --git a/package.json b/package.json index 8bf2d002..2a7c1281 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "global": "^4.4.0", "three": "0.125.2", "video.js": "^6 || ^7", + "webvr-polyfill": "0.10.12", "webxr-polyfill": "^2.0.3" }, "devDependencies": { diff --git a/src/plugin.js b/src/plugin.js index 156d1330..8e178fd3 100644 --- a/src/plugin.js +++ b/src/plugin.js @@ -3,9 +3,11 @@ import {version as VERSION} from '../package.json'; import window from 'global/window'; import document from 'global/document'; import WebXRPolyfill from 'webxr-polyfill'; +import WebVRPolyfill from 'webvr-polyfill'; import videojs from 'video.js'; import * as THREE from 'three'; import VRControls from '../vendor/three/VRControls.js'; +import VREffect from '../vendor/three/VREffect.js'; import OrbitOrientationContols from './orbit-orientation-controls.js'; import * as utils from './utils'; import CanvasPlayerControls from './canvas-player-controls'; @@ -76,7 +78,7 @@ class VR extends Plugin { return; } - this.polyfill_ = new WebXRPolyfill({ + this.polyfill_ = new WebVRPolyfill({ // do not show rotate instructions ROTATE_INSTRUCTIONS_DISABLED: true }); @@ -471,7 +473,7 @@ void main() { return; } - // webxr-polyfill/cardboard ui only watches for click events + // webvr-polyfill/cardboard ui only watches for click events // to tell that the back arrow button is pressed during cardboard vr. // but somewhere along the line these events are silenced with preventDefault // but only on iOS, so we translate them ourselves here @@ -523,7 +525,6 @@ void main() { } - /* KJSL: TODO: requestAnimationFrame -> setAnimationLoop */ requestAnimationFrame(fn) { if (this.vrDisplay) { return this.vrDisplay.requestAnimationFrame(fn); @@ -563,6 +564,10 @@ void main() { this.omniController.update(this.camera); } + if (this.effect) { + this.effect.render(this.scene, this.camera); + } + if (window.navigator.getGamepads) { // Grab all gamepads const gamepads = window.navigator.getGamepads(); @@ -586,7 +591,6 @@ void main() { } this.camera.getWorldDirection(this.cameraVector); - /* KJSL: TODO: requestAnimationFrame -> setAnimationLoop */ this.animationFrameId_ = this.requestAnimationFrame(this.animate_); } @@ -594,6 +598,9 @@ void main() { const width = this.player_.currentWidth(); const height = this.player_.currentHeight(); + if (this.effect) { + this.effect.setSize(width, height, false); + } this.camera.aspect = width / height; this.camera.updateProjectionMatrix(); } @@ -681,13 +688,12 @@ void main() { }; this.renderer.setSize(this.player_.currentWidth(), this.player_.currentHeight(), false); + this.vrDisplay = null; // Previous timestamps for gamepad updates this.prevTimestamps_ = []; - this.renderer.setPixelRatio(window.devicePixelRatio); - this.renderedCanvas = this.renderer.domElement; this.renderedCanvas.setAttribute('style', 'width: 100%; height: 100%; position: absolute; top:0;'); @@ -697,56 +703,72 @@ void main() { videoElStyle.zIndex = '-1'; videoElStyle.opacity = '0'; - if (window.navigator.xr) { - this.log('is supported, getting vr displays'); + let displays = []; - window.navigator.xr.isSessionSupported('immersive-vr').then((displays) => { + if (window.navigator.getVRDisplays) { + this.log('is supported, getting vr displays'); - // ShowEnterVRButton - document.body.appendChild(VRButton.createButton(this.renderer)); + window.navigator.getVRDisplays().then((displaysArray) => { + displays = displaysArray; + }); + } - this.renderer.xr.enabled = true; - this.renderer.xr.setReferenceSpaceType('local'); - this.renderer.setAnimationLoop(this.render.bind(this)); + if (window.navigator.xr) { + this.log('is supported, getting vr displays'); - if (1 || displays.length > 0) { - this.log('Displays found', displays); - this.vrDisplay = displays[0]; + window.navigator.xr.isSessionSupported('immersive-vr').then((supportsImmersiveVR) => { + if (supportsImmersiveVR) { + // ShowEnterVRButton + document.body.appendChild(VRButton.createButton(this.renderer)); + + this.renderer.xr.enabled = true; + this.renderer.xr.setReferenceSpaceType('local'); + this.renderer.setPixelRatio(window.devicePixelRatio); + this.renderer.setAnimationLoop(this.render.bind(this)); + } else { + // WebVR polyfill fall back + this.effect = new VREffect(this.renderer); + this.effect.setSize(this.player_.currentWidth(), this.player_.currentHeight(), false); + + if (displays.length > 0) { + this.log('Displays found', displays); + this.vrDisplay = displays[0]; + + // Native WebVR Head Mounted Displays (HMDs) like the HTC Vive + // also need the cardboard button to enter fully immersive mode + // so, we want to add the button if we're not polyfilled. + if (!this.vrDisplay.isPolyfilled) { + this.log('Real HMD found using VRControls', this.vrDisplay); + this.addCardboardButton_(); + + // We use VRControls here since we are working with an HMD + // and we only want orientation controls. + this.controls3d = new VRControls(this.camera); + } + } - // Native WebVR Head Mounted Displays (HMDs) like the HTC Vive - // also need the cardboard button to enter fully immersive mode - // so, we want to add the button if we're not polyfilled. - if (1 || !this.vrDisplay.isPolyfilled) { - this.log('Real HMD found using VRControls', this.vrDisplay); - this.addCardboardButton_(); + if (!this.controls3d) { + this.log('no HMD found Using Orbit & Orientation Controls'); + const options = { + camera: this.camera, + canvas: this.renderedCanvas, + // check if its a half sphere view projection + halfView: this.currentProjection_.indexOf('180') === 0, + orientation: videojs.browser.IS_IOS || videojs.browser.IS_ANDROID || false + }; + + if (this.options_.motionControls === false) { + options.orientation = false; + } - // We use VRControls here since we are working with an HMD - // and we only want orientation controls. - this.controls3d = new VRControls(this.camera); - } - } + this.controls3d = new OrbitOrientationContols(options); + this.canvasPlayerControls = new CanvasPlayerControls(this.player_, this.renderedCanvas, this.options_); - if (!this.controls3d) { - this.log('no HMD found Using Orbit & Orientation Controls'); - const options = { - camera: this.camera, - canvas: this.renderedCanvas, - // check if its a half sphere view projection - halfView: this.currentProjection_.indexOf('180') === 0, - orientation: videojs.browser.IS_IOS || videojs.browser.IS_ANDROID || false - }; - - if (this.options_.motionControls === false) { - options.orientation = false; } - - this.controls3d = new OrbitOrientationContols(options); - this.canvasPlayerControls = new CanvasPlayerControls(this.player_, this.renderedCanvas, this.options_); } - - /* KJSL: TODO: requestAnimationFrame -> setAnimationLoop */ - this.animationFrameId_ = this.requestAnimationFrame(this.animate_); }); + + this.animationFrameId_ = this.requestAnimationFrame(this.animate_); } else if (window.navigator.getVRDevices) { this.triggerError_({code: 'web-vr-out-of-date', dismiss: false}); } else { @@ -813,6 +835,11 @@ void main() { this.canvasPlayerControls = null; } + if (this.effect) { + this.effect.dispose(); + this.effect = null; + } + window.removeEventListener('resize', this.handleResize_, true); window.removeEventListener('vrdisplaypresentchange', this.handleResize_, true); window.removeEventListener('vrdisplayactivate', this.handleVrDisplayActivate_, true); @@ -869,6 +896,10 @@ void main() { } polyfillVersion() { + return WebVRPolyfill.version; + } + + polyfillVersionXR() { return WebXRPolyfill.version; } } diff --git a/vendor/three/VREffect.js b/vendor/three/VREffect.js new file mode 100644 index 00000000..cbc3630f --- /dev/null +++ b/vendor/three/VREffect.js @@ -0,0 +1,538 @@ +/** + * @author dmarcos / https://github.com/dmarcos + * @author mrdoob / http://mrdoob.com + * + * WebVR Spec: http://mozvr.github.io/webvr-spec/webvr.html + * + * Firefox: http://mozvr.com/downloads/ + * Chromium: https://webvr.info/get-chrome + * + * originally from https://github.com/mrdoob/three.js/blob/r93/examples/js/effects/VREffect.js + */ +import * as THREE from 'three'; + +const VREffect = function ( renderer, onError ) { + + var vrDisplay, vrDisplays; + var eyeTranslationL = new THREE.Vector3(); + var eyeTranslationR = new THREE.Vector3(); + var renderRectL, renderRectR; + var headMatrix = new THREE.Matrix4(); + var eyeMatrixL = new THREE.Matrix4(); + var eyeMatrixR = new THREE.Matrix4(); + + var frameData = null; + + if ( 'VRFrameData' in window ) { + + frameData = new window.VRFrameData(); + + } + + function gotVRDisplays( displays ) { + + vrDisplays = displays; + + if ( displays.length > 0 ) { + + vrDisplay = displays[ 0 ]; + + } else { + + if ( onError ) onError( 'HMD not available' ); + + } + + } + + if ( navigator.getVRDisplays ) { + + navigator.getVRDisplays().then( gotVRDisplays ).catch( function () { + + console.warn( 'THREE.VREffect: Unable to get VR Displays' ); + + } ); + + } + + // + + this.isPresenting = false; + + var scope = this; + + var rendererSize = renderer.getSize(); + var rendererUpdateStyle = false; + var rendererPixelRatio = renderer.getPixelRatio(); + + this.getVRDisplay = function () { + + return vrDisplay; + + }; + + this.setVRDisplay = function ( value ) { + + vrDisplay = value; + + }; + + this.getVRDisplays = function () { + + console.warn( 'THREE.VREffect: getVRDisplays() is being deprecated.' ); + return vrDisplays; + + }; + + this.setSize = function ( width, height, updateStyle ) { + + rendererSize = { width: width, height: height }; + rendererUpdateStyle = updateStyle; + + if ( scope.isPresenting ) { + + var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); + renderer.setPixelRatio( 1 ); + renderer.setSize( eyeParamsL.renderWidth * 2, eyeParamsL.renderHeight, false ); + + } else { + + renderer.setPixelRatio( rendererPixelRatio ); + renderer.setSize( width, height, updateStyle ); + + } + + }; + + // VR presentation + + var canvas = renderer.domElement; + var defaultLeftBounds = [ 0.0, 0.0, 0.5, 1.0 ]; + var defaultRightBounds = [ 0.5, 0.0, 0.5, 1.0 ]; + + function onVRDisplayPresentChange() { + + var wasPresenting = scope.isPresenting; + scope.isPresenting = vrDisplay !== undefined && vrDisplay.isPresenting; + + if ( scope.isPresenting ) { + + var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); + var eyeWidth = eyeParamsL.renderWidth; + var eyeHeight = eyeParamsL.renderHeight; + + if ( ! wasPresenting ) { + + rendererPixelRatio = renderer.getPixelRatio(); + rendererSize = renderer.getSize(); + + renderer.setPixelRatio( 1 ); + renderer.setSize( eyeWidth * 2, eyeHeight, false ); + + } + + } else if ( wasPresenting ) { + + renderer.setPixelRatio( rendererPixelRatio ); + renderer.setSize( rendererSize.width, rendererSize.height, rendererUpdateStyle ); + + } + + } + + window.addEventListener( 'vrdisplaypresentchange', onVRDisplayPresentChange, false ); + + this.setFullScreen = function ( boolean ) { + + return new Promise( function ( resolve, reject ) { + + if ( vrDisplay === undefined ) { + + reject( new Error( 'No VR hardware found.' ) ); + return; + + } + + if ( scope.isPresenting === boolean ) { + + resolve(); + return; + + } + + if ( boolean ) { + + resolve( vrDisplay.requestPresent( [ { source: canvas } ] ) ); + + } else { + + resolve( vrDisplay.exitPresent() ); + + } + + } ); + + }; + + this.requestPresent = function () { + + return this.setFullScreen( true ); + + }; + + this.exitPresent = function () { + + return this.setFullScreen( false ); + + }; + + this.requestAnimationFrame = function ( f ) { + + if ( vrDisplay !== undefined ) { + + return vrDisplay.requestAnimationFrame( f ); + + } else { + + return window.requestAnimationFrame( f ); + + } + + }; + + this.cancelAnimationFrame = function ( h ) { + + if ( vrDisplay !== undefined ) { + + vrDisplay.cancelAnimationFrame( h ); + + } else { + + window.cancelAnimationFrame( h ); + + } + + }; + + this.submitFrame = function () { + + if ( vrDisplay !== undefined && scope.isPresenting ) { + + vrDisplay.submitFrame(); + + } + + }; + + this.autoSubmitFrame = true; + + // render + + var cameraL = new THREE.PerspectiveCamera(); + cameraL.layers.enable( 1 ); + + var cameraR = new THREE.PerspectiveCamera(); + cameraR.layers.enable( 2 ); + + this.render = function ( scene, camera, renderTarget, forceClear ) { + + if ( vrDisplay && scope.isPresenting ) { + + var autoUpdate = scene.autoUpdate; + + if ( autoUpdate ) { + + scene.updateMatrixWorld(); + scene.autoUpdate = false; + + } + + if ( Array.isArray( scene ) ) { + + console.warn( 'THREE.VREffect.render() no longer supports arrays. Use object.layers instead.' ); + scene = scene[ 0 ]; + + } + + // When rendering we don't care what the recommended size is, only what the actual size + // of the backbuffer is. + var size = renderer.getSize(); + var layers = vrDisplay.getLayers(); + var leftBounds; + var rightBounds; + + if ( layers.length ) { + + var layer = layers[ 0 ]; + + leftBounds = layer.leftBounds !== null && layer.leftBounds.length === 4 ? layer.leftBounds : defaultLeftBounds; + rightBounds = layer.rightBounds !== null && layer.rightBounds.length === 4 ? layer.rightBounds : defaultRightBounds; + + } else { + + leftBounds = defaultLeftBounds; + rightBounds = defaultRightBounds; + + } + + renderRectL = { + x: Math.round( size.width * leftBounds[ 0 ] ), + y: Math.round( size.height * leftBounds[ 1 ] ), + width: Math.round( size.width * leftBounds[ 2 ] ), + height: Math.round( size.height * leftBounds[ 3 ] ) + }; + renderRectR = { + x: Math.round( size.width * rightBounds[ 0 ] ), + y: Math.round( size.height * rightBounds[ 1 ] ), + width: Math.round( size.width * rightBounds[ 2 ] ), + height: Math.round( size.height * rightBounds[ 3 ] ) + }; + + if ( renderTarget ) { + + renderer.setRenderTarget( renderTarget ); + renderTarget.scissorTest = true; + + } else { + + renderer.setRenderTarget( null ); + renderer.setScissorTest( true ); + + } + + if ( renderer.autoClear || forceClear ) renderer.clear(); + + if ( camera.parent === null ) camera.updateMatrixWorld(); + + camera.matrixWorld.decompose( cameraL.position, cameraL.quaternion, cameraL.scale ); + + cameraR.position.copy( cameraL.position ); + cameraR.quaternion.copy( cameraL.quaternion ); + cameraR.scale.copy( cameraL.scale ); + + if ( vrDisplay.getFrameData ) { + + vrDisplay.depthNear = camera.near; + vrDisplay.depthFar = camera.far; + + vrDisplay.getFrameData( frameData ); + + cameraL.projectionMatrix.elements = frameData.leftProjectionMatrix; + cameraR.projectionMatrix.elements = frameData.rightProjectionMatrix; + + getEyeMatrices( frameData ); + + cameraL.updateMatrix(); + cameraL.matrix.multiply( eyeMatrixL ); + cameraL.matrix.decompose( cameraL.position, cameraL.quaternion, cameraL.scale ); + + cameraR.updateMatrix(); + cameraR.matrix.multiply( eyeMatrixR ); + cameraR.matrix.decompose( cameraR.position, cameraR.quaternion, cameraR.scale ); + + } else { + + var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); + var eyeParamsR = vrDisplay.getEyeParameters( 'right' ); + + cameraL.projectionMatrix = fovToProjection( eyeParamsL.fieldOfView, true, camera.near, camera.far ); + cameraR.projectionMatrix = fovToProjection( eyeParamsR.fieldOfView, true, camera.near, camera.far ); + + eyeTranslationL.fromArray( eyeParamsL.offset ); + eyeTranslationR.fromArray( eyeParamsR.offset ); + + cameraL.translateOnAxis( eyeTranslationL, cameraL.scale.x ); + cameraR.translateOnAxis( eyeTranslationR, cameraR.scale.x ); + + } + + // render left eye + if ( renderTarget ) { + + renderTarget.viewport.set( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); + renderTarget.scissor.set( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); + + } else { + + renderer.setViewport( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); + renderer.setScissor( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); + + } + renderer.render( scene, cameraL, renderTarget, forceClear ); + + // render right eye + if ( renderTarget ) { + + renderTarget.viewport.set( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); + renderTarget.scissor.set( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); + + } else { + + renderer.setViewport( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); + renderer.setScissor( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); + + } + renderer.render( scene, cameraR, renderTarget, forceClear ); + + if ( renderTarget ) { + + renderTarget.viewport.set( 0, 0, size.width, size.height ); + renderTarget.scissor.set( 0, 0, size.width, size.height ); + renderTarget.scissorTest = false; + renderer.setRenderTarget( null ); + + } else { + + renderer.setViewport( 0, 0, size.width, size.height ); + renderer.setScissorTest( false ); + + } + + if ( autoUpdate ) { + + scene.autoUpdate = true; + + } + + if ( scope.autoSubmitFrame ) { + + scope.submitFrame(); + + } + + return; + + } + + // Regular render mode if not HMD + + renderer.render( scene, camera, renderTarget, forceClear ); + + }; + + this.dispose = function () { + + window.removeEventListener( 'vrdisplaypresentchange', onVRDisplayPresentChange, false ); + + }; + + // + + var poseOrientation = new THREE.Quaternion(); + var posePosition = new THREE.Vector3(); + + // Compute model matrices of the eyes with respect to the head. + function getEyeMatrices( frameData ) { + + // Compute the matrix for the position of the head based on the pose + if ( frameData.pose.orientation ) { + + poseOrientation.fromArray( frameData.pose.orientation ); + headMatrix.makeRotationFromQuaternion( poseOrientation ); + + } else { + + headMatrix.identity(); + + } + + if ( frameData.pose.position ) { + + posePosition.fromArray( frameData.pose.position ); + headMatrix.setPosition( posePosition ); + + } + + // The view matrix transforms vertices from sitting space to eye space. As such, the view matrix can be thought of as a product of two matrices: + // headToEyeMatrix * sittingToHeadMatrix + + // The headMatrix that we've calculated above is the model matrix of the head in sitting space, which is the inverse of sittingToHeadMatrix. + // So when we multiply the view matrix with headMatrix, we're left with headToEyeMatrix: + // viewMatrix * headMatrix = headToEyeMatrix * sittingToHeadMatrix * headMatrix = headToEyeMatrix + + eyeMatrixL.fromArray( frameData.leftViewMatrix ); + eyeMatrixL.multiply( headMatrix ); + eyeMatrixR.fromArray( frameData.rightViewMatrix ); + eyeMatrixR.multiply( headMatrix ); + + // The eye's model matrix in head space is the inverse of headToEyeMatrix we calculated above. + + eyeMatrixL.getInverse( eyeMatrixL ); + eyeMatrixR.getInverse( eyeMatrixR ); + + } + + function fovToNDCScaleOffset( fov ) { + + var pxscale = 2.0 / ( fov.leftTan + fov.rightTan ); + var pxoffset = ( fov.leftTan - fov.rightTan ) * pxscale * 0.5; + var pyscale = 2.0 / ( fov.upTan + fov.downTan ); + var pyoffset = ( fov.upTan - fov.downTan ) * pyscale * 0.5; + return { scale: [ pxscale, pyscale ], offset: [ pxoffset, pyoffset ] }; + + } + + function fovPortToProjection( fov, rightHanded, zNear, zFar ) { + + rightHanded = rightHanded === undefined ? true : rightHanded; + zNear = zNear === undefined ? 0.01 : zNear; + zFar = zFar === undefined ? 10000.0 : zFar; + + var handednessScale = rightHanded ? - 1.0 : 1.0; + + // start with an identity matrix + var mobj = new THREE.Matrix4(); + var m = mobj.elements; + + // and with scale/offset info for normalized device coords + var scaleAndOffset = fovToNDCScaleOffset( fov ); + + // X result, map clip edges to [-w,+w] + m[ 0 * 4 + 0 ] = scaleAndOffset.scale[ 0 ]; + m[ 0 * 4 + 1 ] = 0.0; + m[ 0 * 4 + 2 ] = scaleAndOffset.offset[ 0 ] * handednessScale; + m[ 0 * 4 + 3 ] = 0.0; + + // Y result, map clip edges to [-w,+w] + // Y offset is negated because this proj matrix transforms from world coords with Y=up, + // but the NDC scaling has Y=down (thanks D3D?) + m[ 1 * 4 + 0 ] = 0.0; + m[ 1 * 4 + 1 ] = scaleAndOffset.scale[ 1 ]; + m[ 1 * 4 + 2 ] = - scaleAndOffset.offset[ 1 ] * handednessScale; + m[ 1 * 4 + 3 ] = 0.0; + + // Z result (up to the app) + m[ 2 * 4 + 0 ] = 0.0; + m[ 2 * 4 + 1 ] = 0.0; + m[ 2 * 4 + 2 ] = zFar / ( zNear - zFar ) * - handednessScale; + m[ 2 * 4 + 3 ] = ( zFar * zNear ) / ( zNear - zFar ); + + // W result (= Z in) + m[ 3 * 4 + 0 ] = 0.0; + m[ 3 * 4 + 1 ] = 0.0; + m[ 3 * 4 + 2 ] = handednessScale; + m[ 3 * 4 + 3 ] = 0.0; + + mobj.transpose(); + return mobj; + + } + + function fovToProjection( fov, rightHanded, zNear, zFar ) { + + var DEG2RAD = Math.PI / 180.0; + + var fovPort = { + upTan: Math.tan( fov.upDegrees * DEG2RAD ), + downTan: Math.tan( fov.downDegrees * DEG2RAD ), + leftTan: Math.tan( fov.leftDegrees * DEG2RAD ), + rightTan: Math.tan( fov.rightDegrees * DEG2RAD ) + }; + + return fovPortToProjection( fovPort, rightHanded, zNear, zFar ); + + } + +}; + +export default VREffect;