diff --git a/src/renderers/webxr/WebXRController.js b/src/renderers/webxr/WebXRController.js index 62ed1f9c1d07b9..ec7d6c1c904e1b 100644 --- a/src/renderers/webxr/WebXRController.js +++ b/src/renderers/webxr/WebXRController.js @@ -1,18 +1,16 @@ import { Group } from '../../objects/Group.js'; -function WebXRController() { +class WebXRController { - this._targetRay = null; - this._grip = null; - this._hand = null; + constructor() { -} - -Object.assign( WebXRController.prototype, { + this._targetRay = null; + this._grip = null; + this._hand = null; - constructor: WebXRController, + } - getHandSpace: function () { + getHandSpace() { if ( this._hand === null ) { @@ -27,9 +25,9 @@ Object.assign( WebXRController.prototype, { return this._hand; - }, + } - getTargetRaySpace: function () { + getTargetRaySpace() { if ( this._targetRay === null ) { @@ -41,9 +39,9 @@ Object.assign( WebXRController.prototype, { return this._targetRay; - }, + } - getGripSpace: function () { + getGripSpace() { if ( this._grip === null ) { @@ -55,9 +53,9 @@ Object.assign( WebXRController.prototype, { return this._grip; - }, + } - dispatchEvent: function ( event ) { + dispatchEvent( event ) { if ( this._targetRay !== null ) { @@ -79,9 +77,9 @@ Object.assign( WebXRController.prototype, { return this; - }, + } - disconnect: function ( inputSource ) { + disconnect( inputSource ) { this.dispatchEvent( { type: 'disconnected', data: inputSource } ); @@ -105,9 +103,9 @@ Object.assign( WebXRController.prototype, { return this; - }, + } - update: function ( inputSource, frame, referenceSpace ) { + update( inputSource, frame, referenceSpace ) { let inputPose = null; let gripPose = null; @@ -238,7 +236,6 @@ Object.assign( WebXRController.prototype, { } -} ); - +} export { WebXRController }; diff --git a/src/renderers/webxr/WebXRManager.js b/src/renderers/webxr/WebXRManager.js index df740a0198ea77..6931cc416fc3e0 100644 --- a/src/renderers/webxr/WebXRManager.js +++ b/src/renderers/webxr/WebXRManager.js @@ -6,136 +6,348 @@ import { Vector4 } from '../../math/Vector4.js'; import { WebGLAnimation } from '../webgl/WebGLAnimation.js'; import { WebXRController } from './WebXRController.js'; -function WebXRManager( renderer, gl ) { +let scope; +let state; +let gl; +let renderer; - const scope = this; - const state = renderer.state; +let session = null; - let session = null; +let framebufferScaleFactor = 1.0; - let framebufferScaleFactor = 1.0; +let referenceSpace = null; +let referenceSpaceType = 'local-floor'; - let referenceSpace = null; - let referenceSpaceType = 'local-floor'; +let pose = null; - let pose = null; +const controllers = []; +const inputSourcesMap = new Map(); - const controllers = []; - const inputSourcesMap = new Map(); +// - // +const cameraL = new PerspectiveCamera(); +cameraL.layers.enable( 1 ); +cameraL.viewport = new Vector4(); + +const cameraR = new PerspectiveCamera(); +cameraR.layers.enable( 2 ); +cameraR.viewport = new Vector4(); + +const cameras = [ cameraL, cameraR ]; + +const cameraVR = new ArrayCamera(); +cameraVR.layers.enable( 1 ); +cameraVR.layers.enable( 2 ); + +let _currentDepthNear = null; +let _currentDepthFar = null; + +// + +const cameraLPos = new Vector3(); +const cameraRPos = new Vector3(); - const cameraL = new PerspectiveCamera(); - cameraL.layers.enable( 1 ); - cameraL.viewport = new Vector4(); +// - const cameraR = new PerspectiveCamera(); - cameraR.layers.enable( 2 ); - cameraR.viewport = new Vector4(); +const animation = new WebGLAnimation(); +animation.setAnimationLoop( onAnimationFrame ); - const cameras = [ cameraL, cameraR ]; +// - const cameraVR = new ArrayCamera(); - cameraVR.layers.enable( 1 ); - cameraVR.layers.enable( 2 ); +function onSessionEvent( event ) { + + const controller = inputSourcesMap.get( event.inputSource ); + + if ( controller ) { + + controller.dispatchEvent( { type: event.type, data: event.inputSource } ); + + } + +} - let _currentDepthNear = null; - let _currentDepthFar = null; +function onSessionEnd() { + + inputSourcesMap.forEach( function ( controller, inputSource ) { + + controller.disconnect( inputSource ); + + } ); + + inputSourcesMap.clear(); + + _currentDepthNear = null; + _currentDepthFar = null; + + // restore framebuffer/rendering state + + state.bindXRFramebuffer( null ); + renderer.setRenderTarget( renderer.getRenderTarget() ); // - this.enabled = false; + animation.stop(); - this.isPresenting = false; + scope.isPresenting = false; - this.getController = function ( index ) { + scope.dispatchEvent( { type: 'sessionend' } ); - let controller = controllers[ index ]; +} - if ( controller === undefined ) { +function onInputSourcesChange( event ) { - controller = new WebXRController(); - controllers[ index ] = controller; + const inputSources = session.inputSources; + + // Assign inputSources to available controllers + + for ( let i = 0; i < controllers.length; i ++ ) { + + inputSourcesMap.set( inputSources[ i ], controllers[ i ] ); + + } + + // Notify disconnected + + for ( let i = 0; i < event.removed.length; i ++ ) { + + const inputSource = event.removed[ i ]; + const controller = inputSourcesMap.get( inputSource ); + + if ( controller ) { + + controller.dispatchEvent( { type: 'disconnected', data: inputSource } ); + inputSourcesMap.delete( inputSource ); } - return controller.getTargetRaySpace(); + } - }; + // Notify connected - this.getControllerGrip = function ( index ) { + for ( let i = 0; i < event.added.length; i ++ ) { - let controller = controllers[ index ]; + const inputSource = event.added[ i ]; + const controller = inputSourcesMap.get( inputSource ); - if ( controller === undefined ) { + if ( controller ) { - controller = new WebXRController(); - controllers[ index ] = controller; + controller.dispatchEvent( { type: 'connected', data: inputSource } ); } - return controller.getGripSpace(); + } - }; +} - this.getHand = function ( index ) { +/** + * Assumes 2 cameras that are parallel and share an X-axis, and that + * the cameras' projection and world matrices have already been set. + * And that near and far planes are identical for both cameras. + * Visualization of this technique: https://computergraphics.stackexchange.com/a/4765 + */ +function setProjectionFromUnion( camera, cameraL, cameraR ) { + + cameraLPos.setFromMatrixPosition( cameraL.matrixWorld ); + cameraRPos.setFromMatrixPosition( cameraR.matrixWorld ); + + const ipd = cameraLPos.distanceTo( cameraRPos ); + + const projL = cameraL.projectionMatrix.elements; + const projR = cameraR.projectionMatrix.elements; + + // VR systems will have identical far and near planes, and + // most likely identical top and bottom frustum extents. + // Use the left camera for these values. + const near = projL[ 14 ] / ( projL[ 10 ] - 1 ); + const far = projL[ 14 ] / ( projL[ 10 ] + 1 ); + const topFov = ( projL[ 9 ] + 1 ) / projL[ 5 ]; + const bottomFov = ( projL[ 9 ] - 1 ) / projL[ 5 ]; + + const leftFov = ( projL[ 8 ] - 1 ) / projL[ 0 ]; + const rightFov = ( projR[ 8 ] + 1 ) / projR[ 0 ]; + const left = near * leftFov; + const right = near * rightFov; + + // Calculate the new camera's position offset from the + // left camera. xOffset should be roughly half `ipd`. + const zOffset = ipd / ( - leftFov + rightFov ); + const xOffset = zOffset * - leftFov; + + // TODO: Better way to apply this offset? + cameraL.matrixWorld.decompose( camera.position, camera.quaternion, camera.scale ); + camera.translateX( xOffset ); + camera.translateZ( zOffset ); + camera.matrixWorld.compose( camera.position, camera.quaternion, camera.scale ); + camera.matrixWorldInverse.copy( camera.matrixWorld ).invert(); + + // Find the union of the frustum values of the cameras and scale + // the values so that the near plane's position does not change in world space, + // although must now be relative to the new union camera. + const near2 = near + zOffset; + const far2 = far + zOffset; + const left2 = left - xOffset; + const right2 = right + ( ipd - xOffset ); + const top2 = topFov * far / far2 * near2; + const bottom2 = bottomFov * far / far2 * near2; + + camera.projectionMatrix.makePerspective( left2, right2, top2, bottom2, near2, far2 ); - let controller = controllers[ index ]; +} - if ( controller === undefined ) { +function updateCamera( camera, parent ) { - controller = new WebXRController(); - controllers[ index ] = controller; + if ( parent === null ) { + + camera.matrixWorld.copy( camera.matrix ); + + } else { + + camera.matrixWorld.multiplyMatrices( parent.matrixWorld, camera.matrix ); + + } + + camera.matrixWorldInverse.copy( camera.matrixWorld ).invert(); + +} + +// Animation Loop + +let onAnimationFrameCallback = null; + +function onAnimationFrame( time, frame ) { + + pose = frame.getViewerPose( referenceSpace ); + + if ( pose !== null ) { + + const views = pose.views; + const baseLayer = session.renderState.baseLayer; + + state.bindXRFramebuffer( baseLayer.framebuffer ); + + let cameraVRNeedsUpdate = false; + + // check if it's necessary to rebuild cameraVR's camera list + + if ( views.length !== cameraVR.cameras.length ) { + + cameraVR.cameras.length = 0; + cameraVRNeedsUpdate = true; } - return controller.getHandSpace(); + for ( let i = 0; i < views.length; i ++ ) { - }; + const view = views[ i ]; + const viewport = baseLayer.getViewport( view ); - // + const camera = cameras[ i ]; + camera.matrix.fromArray( view.transform.matrix ); + camera.projectionMatrix.fromArray( view.projectionMatrix ); + camera.viewport.set( viewport.x, viewport.y, viewport.width, viewport.height ); - function onSessionEvent( event ) { + if ( i === 0 ) { - const controller = inputSourcesMap.get( event.inputSource ); + cameraVR.matrix.copy( camera.matrix ); - if ( controller ) { + } - controller.dispatchEvent( { type: event.type, data: event.inputSource } ); + if ( cameraVRNeedsUpdate === true ) { + + cameraVR.cameras.push( camera ); + + } } } - function onSessionEnd() { + // - inputSourcesMap.forEach( function ( controller, inputSource ) { + const inputSources = session.inputSources; - controller.disconnect( inputSource ); + for ( let i = 0; i < controllers.length; i ++ ) { - } ); + const controller = controllers[ i ]; + const inputSource = inputSources[ i ]; - inputSourcesMap.clear(); + controller.update( inputSource, frame, referenceSpace ); - _currentDepthNear = null; - _currentDepthFar = null; + } - // restore framebuffer/rendering state + if ( onAnimationFrameCallback ) onAnimationFrameCallback( time, frame ); - state.bindXRFramebuffer( null ); - renderer.setRenderTarget( renderer.getRenderTarget() ); +} + +// + +class WebXRManager extends EventDispatcher { + + constructor( _renderer, _gl ) { + + super(); + + scope = this; + + state = _renderer.state; + + renderer = _renderer; + gl = _gl; // - animation.stop(); + this.enabled = false; + + this.isPresenting = false; + + } + + getController( index ) { + + let controller = controllers[ index ]; + + if ( controller === undefined ) { + + controller = new WebXRController(); + controllers[ index ] = controller; + + } + + return controller.getTargetRaySpace(); + + } + + getControllerGrip( index ) { + + let controller = controllers[ index ]; + + if ( controller === undefined ) { - scope.isPresenting = false; + controller = new WebXRController(); + controllers[ index ] = controller; + + } - scope.dispatchEvent( { type: 'sessionend' } ); + return controller.getGripSpace(); } - this.setFramebufferScaleFactor = function ( value ) { + getHand( index ) { + + let controller = controllers[ index ]; + + if ( controller === undefined ) { + + controller = new WebXRController(); + controllers[ index ] = controller; + + } + + return controller.getHandSpace(); + + } + + setFramebufferScaleFactor( value ) { framebufferScaleFactor = value; @@ -145,9 +357,9 @@ function WebXRManager( renderer, gl ) { } - }; + } - this.setReferenceSpaceType = function ( value ) { + setReferenceSpaceType( value ) { referenceSpaceType = value; @@ -157,21 +369,21 @@ function WebXRManager( renderer, gl ) { } - }; + } - this.getReferenceSpace = function () { + getReferenceSpace() { return referenceSpace; - }; + } - this.getSession = function () { + getSession() { return session; - }; + } - this.setSession = async function ( value ) { + async setSession( value ) { session = value; @@ -218,130 +430,9 @@ function WebXRManager( renderer, gl ) { } - }; - - function onInputSourcesChange( event ) { - - const inputSources = session.inputSources; - - // Assign inputSources to available controllers - - for ( let i = 0; i < controllers.length; i ++ ) { - - inputSourcesMap.set( inputSources[ i ], controllers[ i ] ); - - } - - // Notify disconnected - - for ( let i = 0; i < event.removed.length; i ++ ) { - - const inputSource = event.removed[ i ]; - const controller = inputSourcesMap.get( inputSource ); - - if ( controller ) { - - controller.dispatchEvent( { type: 'disconnected', data: inputSource } ); - inputSourcesMap.delete( inputSource ); - - } - - } - - // Notify connected - - for ( let i = 0; i < event.added.length; i ++ ) { - - const inputSource = event.added[ i ]; - const controller = inputSourcesMap.get( inputSource ); - - if ( controller ) { - - controller.dispatchEvent( { type: 'connected', data: inputSource } ); - - } - - } - } - // - - const cameraLPos = new Vector3(); - const cameraRPos = new Vector3(); - - /** - * Assumes 2 cameras that are parallel and share an X-axis, and that - * the cameras' projection and world matrices have already been set. - * And that near and far planes are identical for both cameras. - * Visualization of this technique: https://computergraphics.stackexchange.com/a/4765 - */ - function setProjectionFromUnion( camera, cameraL, cameraR ) { - - cameraLPos.setFromMatrixPosition( cameraL.matrixWorld ); - cameraRPos.setFromMatrixPosition( cameraR.matrixWorld ); - - const ipd = cameraLPos.distanceTo( cameraRPos ); - - const projL = cameraL.projectionMatrix.elements; - const projR = cameraR.projectionMatrix.elements; - - // VR systems will have identical far and near planes, and - // most likely identical top and bottom frustum extents. - // Use the left camera for these values. - const near = projL[ 14 ] / ( projL[ 10 ] - 1 ); - const far = projL[ 14 ] / ( projL[ 10 ] + 1 ); - const topFov = ( projL[ 9 ] + 1 ) / projL[ 5 ]; - const bottomFov = ( projL[ 9 ] - 1 ) / projL[ 5 ]; - - const leftFov = ( projL[ 8 ] - 1 ) / projL[ 0 ]; - const rightFov = ( projR[ 8 ] + 1 ) / projR[ 0 ]; - const left = near * leftFov; - const right = near * rightFov; - - // Calculate the new camera's position offset from the - // left camera. xOffset should be roughly half `ipd`. - const zOffset = ipd / ( - leftFov + rightFov ); - const xOffset = zOffset * - leftFov; - - // TODO: Better way to apply this offset? - cameraL.matrixWorld.decompose( camera.position, camera.quaternion, camera.scale ); - camera.translateX( xOffset ); - camera.translateZ( zOffset ); - camera.matrixWorld.compose( camera.position, camera.quaternion, camera.scale ); - camera.matrixWorldInverse.copy( camera.matrixWorld ).invert(); - - // Find the union of the frustum values of the cameras and scale - // the values so that the near plane's position does not change in world space, - // although must now be relative to the new union camera. - const near2 = near + zOffset; - const far2 = far + zOffset; - const left2 = left - xOffset; - const right2 = right + ( ipd - xOffset ); - const top2 = topFov * far / far2 * near2; - const bottom2 = bottomFov * far / far2 * near2; - - camera.projectionMatrix.makePerspective( left2, right2, top2, bottom2, near2, far2 ); - - } - - function updateCamera( camera, parent ) { - - if ( parent === null ) { - - camera.matrixWorld.copy( camera.matrix ); - - } else { - - camera.matrixWorld.multiplyMatrices( parent.matrixWorld, camera.matrix ); - - } - - camera.matrixWorldInverse.copy( camera.matrixWorld ).invert(); - - } - - this.getCamera = function ( camera ) { + getCamera( camera ) { cameraVR.near = cameraR.near = cameraL.near = camera.near; cameraVR.far = cameraR.far = cameraL.far = camera.far; @@ -401,90 +492,16 @@ function WebXRManager( renderer, gl ) { return cameraVR; - }; - - // Animation Loop - - let onAnimationFrameCallback = null; - - function onAnimationFrame( time, frame ) { - - pose = frame.getViewerPose( referenceSpace ); - - if ( pose !== null ) { - - const views = pose.views; - const baseLayer = session.renderState.baseLayer; - - state.bindXRFramebuffer( baseLayer.framebuffer ); - - let cameraVRNeedsUpdate = false; - - // check if it's necessary to rebuild cameraVR's camera list - - if ( views.length !== cameraVR.cameras.length ) { - - cameraVR.cameras.length = 0; - cameraVRNeedsUpdate = true; - - } - - for ( let i = 0; i < views.length; i ++ ) { - - const view = views[ i ]; - const viewport = baseLayer.getViewport( view ); - - const camera = cameras[ i ]; - camera.matrix.fromArray( view.transform.matrix ); - camera.projectionMatrix.fromArray( view.projectionMatrix ); - camera.viewport.set( viewport.x, viewport.y, viewport.width, viewport.height ); - - if ( i === 0 ) { - - cameraVR.matrix.copy( camera.matrix ); - - } - - if ( cameraVRNeedsUpdate === true ) { - - cameraVR.cameras.push( camera ); - - } - - } - - } - - // - - const inputSources = session.inputSources; - - for ( let i = 0; i < controllers.length; i ++ ) { - - const controller = controllers[ i ]; - const inputSource = inputSources[ i ]; - - controller.update( inputSource, frame, referenceSpace ); - - } - - if ( onAnimationFrameCallback ) onAnimationFrameCallback( time, frame ); - } - const animation = new WebGLAnimation(); - animation.setAnimationLoop( onAnimationFrame ); - - this.setAnimationLoop = function ( callback ) { + setAnimationLoop( callback ) { onAnimationFrameCallback = callback; - }; + } - this.dispose = function () {}; + dispose() {} } -Object.assign( WebXRManager.prototype, EventDispatcher.prototype ); - export { WebXRManager };